feat(web): add redispatch action to board card for failed/cancelled tasks #1188

Merged
charles merged 1 commit from dev/0 into main 2026-05-15 10:05:49 +00:00
Collaborator

Summary

  • Idle-assigned board cards whose last task ended in failure, cancelled, or interrupted now show a RotateCcw icon button directly on the card face — no side panel required to redispatch.
  • Server adds getLatestTaskForIssue to task-store.ts and populates the new last_task_id / last_task_status fields on idle_assigned BoardCard objects when the prior run wasn't a success.
  • Client wires a redispatchMutation in the /board route and threads onRedispatch through BoardBoardCardView → the new RedispatchButton sub-component.

Test plan

  • Navigate to /board with an agent column that has an idle card whose last task failed/was cancelled — a RotateCcw icon should appear in the card's bottom-right corner.
  • Hover the icon: tooltip shows the per-status label ("Last run failed — re-dispatch" / "Last run was cancelled — re-dispatch" / "Last run was interrupted — re-dispatch").
  • Click the icon: a success toast "Re-dispatched → task <shortId>" appears and the board refreshes (card transitions to queued/running).
  • Cards whose last task ended in success (or cards with no prior task) show no icon — visual noise is avoided.
  • just qa passes (typecheck + lint + format).
## Summary - Idle-assigned board cards whose last task ended in `failure`, `cancelled`, or `interrupted` now show a `RotateCcw` icon button directly on the card face — no side panel required to redispatch. - Server adds `getLatestTaskForIssue` to `task-store.ts` and populates the new `last_task_id` / `last_task_status` fields on `idle_assigned` `BoardCard` objects when the prior run wasn't a success. - Client wires a `redispatchMutation` in the `/board` route and threads `onRedispatch` through `Board` → `BoardCardView` → the new `RedispatchButton` sub-component. ## Test plan - [ ] Navigate to `/board` with an agent column that has an idle card whose last task failed/was cancelled — a `RotateCcw` icon should appear in the card's bottom-right corner. - [ ] Hover the icon: tooltip shows the per-status label (`"Last run failed — re-dispatch"` / `"Last run was cancelled — re-dispatch"` / `"Last run was interrupted — re-dispatch"`). - [ ] Click the icon: a success toast `"Re-dispatched → task <shortId>"` appears and the board refreshes (card transitions to queued/running). - [ ] Cards whose last task ended in `success` (or cards with no prior task) show no icon — visual noise is avoided. - [ ] `just qa` passes (typecheck + lint + format).
Merge the DB-native TaskStatus and the derived TaskUiStatus into a single
unified TaskStatus union that covers all 14 distinct values:

  DB-native:     queued | running | paused | success | failure |
                 cancelled | interrupted
  UI-enriched:   planning | executing | waiting | reviewing |
                 done | error | abandoned

New module: packages/shared/src/task-status.ts
  - deriveTaskStatus()       (replaces deriveTaskUiStatus)
  - deriveTaskStatusReason() (replaces deriveTaskUiStatusReason)

Kept backward-compat shim: task-ui-status.ts re-exports the new
functions under their old names so out-of-tree code doesn't break.

Updated consumers:
  - StatusPill now accepts the full TaskStatus union and maps DB-native
    values to their UI-equivalent icon/label/colour at render time
  - history.ts handler switched to deriveTaskStatus / deriveTaskStatusReason
  - board.ts BoardCard.ui_status typed as TaskStatus
  - Web api/tasks.ts, status-pill.test.tsx, board-card.tsx all migrated
    from TaskUiStatus to TaskStatus

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces TimestampLabel (components/timestamp-label.tsx) — a <time>
element wrapper that sets dateTime + title to the ISO-8601 string and
renders human-readable text by variant:

  relative (default)  "just now" / "5m ago" / "3h ago" / "2d ago"
  time                locale HH:MM:SS (via getLocale())
  datetime            full locale date+time string
  date                short locale date string

Also adds fmtDateTime / tsToMs helpers to lib/format.ts for string
contexts (title attrs, template literals) that can't take JSX.

Migrated call sites:
- agent-config: editable-skill-row, inherited-type-skill-row,
  artifact-row (both row variants), anchored-inherited-row (removes
  local formatRelative duplicate), history-tab
- agents: agent-sessions-panel (removes local formatTimestamp)
- notifications-drawer (removes local formatTimestamp)
- watchdog-panel (toLocaleTimeString → variant="time")
- agent-config/secrets-tab, routes/settings.secrets (removes local
  formatTimestamp helpers)
- service-config: design-section, forge-section (use fmtDateTime in
  string interpolation)
- stage-pill, pipeline-graph, mini-pipeline: replace hardcoded
  "fr-FR" locale with fmtDateTime / <TimestampLabel>

Co-authored-by: Cursor <cursoragent@cursor.com>
feat(web): add redispatch action to board card for failed/cancelled tasks
All checks were successful
qa / dockerfile (pull_request) Successful in 8s
qa / sql-layer-check (pull_request) Successful in 11s
qa / i18n-string-check (pull_request) Successful in 11s
qa / db-schema (pull_request) Successful in 13s
qa / qa-1 (pull_request) Successful in 2m21s
qa / qa (pull_request) Successful in 0s
6ceec1e0ed
Idle-assigned cards whose previous task ended in failure, cancelled, or
interrupted now show a RotateCcw icon button directly on the card face.
Clicking it calls POST /task/:id/redispatch without requiring the side
panel to be opened first.

Server-side: getLatestTaskForIssue added to task-store.ts to look up
the most-recent finalized task across all agent types for a (repo,
issue_number) pair. The board projection picks this up via a new
optional getLastTask field in BoardProjectionInput and populates
last_task_id / last_task_status on idle_assigned cards when the last
run was a failure/cancelled/interrupted.

Client-side: BoardCard gains an onRedispatch prop and a RedispatchButton
sub-component with a per-status tooltip. The Board component and the
/board route wire the prop through to a new redispatchMutation that
invalidates the board query on success.

Co-authored-by: Cursor <cursoragent@cursor.com>
dev requested review from reviewer 2026-05-15 00:03:57 +00:00
dev force-pushed dev/0 from 6ceec1e0ed
All checks were successful
qa / dockerfile (pull_request) Successful in 8s
qa / sql-layer-check (pull_request) Successful in 11s
qa / i18n-string-check (pull_request) Successful in 11s
qa / db-schema (pull_request) Successful in 13s
qa / qa-1 (pull_request) Successful in 2m21s
qa / qa (pull_request) Successful in 0s
to 66d95af612
All checks were successful
qa / i18n-string-check (pull_request) Successful in 10s
qa / dockerfile (pull_request) Successful in 10s
qa / db-schema (pull_request) Successful in 13s
qa / sql-layer-check (pull_request) Successful in 14s
qa / qa-1 (pull_request) Successful in 2m20s
qa / qa (pull_request) Successful in 0s
2026-05-15 00:05:42 +00:00
Compare
reviewer requested changes 2026-05-15 00:07:35 +00:00
Dismissed
reviewer left a comment

Code Review — PR #1188

CI is green. However there are two real bugs to fix and a merge conflict to resolve before this can land.


Not mergeable

mergeable: false — almost certainly a conflict with PR #1187, which also touches the TaskStatus type system and the shared package. Rebase onto main once #1187's fate is settled (or after it lands).


🐛 Bug 1 — RedispatchButton has no pending/disabled guard

redispatchMutation.isPending is never threaded down to RedispatchButton, so a user who double-clicks (or clicks while the network is slow) fires multiple POST /task/:id/redispatch requests for the same last_task_id. Each one creates a new task row.

Fix: pass isPending through and disable the button:

// board.tsx — add to BoardProps
onRedispatchPending?: boolean;

// board-card.tsx — RedispatchButton
function RedispatchButton({ card, onRedispatch, isPending }: {
  card: BoardCardType;
  onRedispatch: (card: BoardCardType) => void;
  isPending?: boolean;
}): ReactElement {
  return (
    <Tooltip content={label} side="top">
      <button
        type="button"
        disabled={isPending}
        onClick={...}
        className={cn(
          "inline-flex size-6 shrink-0 items-center justify-center ...",
          isPending && "opacity-50 cursor-wait",
        )}
        ...
      >
        <RotateCcw aria-hidden="true" className={cn("h-3 w-3", isPending && "animate-spin")} />
      </button>
    </Tooltip>
  );
}

Alternatively, track a pendingTaskId string in the route so only the clicked card is locked — but at minimum disable re-dispatch while any mutation is pending.


🐛 Bug 2 — handleRedispatch useCallback deps cause a board-wide re-render on every render

// routes/board.tsx
const handleRedispatch = useCallback(
  (card: BoardCardType) => { redispatchMutation.mutate(card); },
  [redispatchMutation],   // ← full mutation object, new reference every render
);

useMutation returns a new object reference on every render; mutate is the stable part. Because handleRedispatch re-creates every render, onRedispatch in the board's useMemo dep array ([..., onRedispatch]) fires a full card list re-render on every parent render — even with no state change.

Fix:

const handleRedispatch = useCallback(
  (card: BoardCardType) => { redispatchMutation.mutate(card); },
  [redispatchMutation.mutate],   // stable reference
);

⚠️ Note — N+1 DB queries during board projection

getLatestTaskForIssue issues one SQLite query per idle_assigned card per board refresh. For a board with 20 idle cards that's 20 extra round-trips per poll. This won't hurt in practice today but it's worth tracking: a single SELECT … WHERE (repo, issue_number) IN (…) over the collected idle pairs would be O(1) queries instead.

Not a blocker, but worth opening a follow-up issue.


⚠️ Note — TaskUiStatus shim silently widens the type

// task-ui-status.ts
export type { TaskStatus as TaskUiStatus } from "./task-record";

Old TaskUiStatus was a 7-member union (UI-enriched values only). It is now the full 14-member TaskStatus union. Any switch/exhaustive check on TaskUiStatus without a default branch that was previously exhaustive will silently miss "queued", "running", "success", etc. at runtime.

This is an acceptable migration trade-off (and the comment marks it deprecated), but worth auditing for exhaustive switches before the shim is removed.


What's good

  • Server-side enrichment design (last_task_id / last_task_status on the board projection) is correct — the card face gets exactly what it needs without a side-panel round-trip.
  • getLastTask is optional in BoardProjectionInput — tests that don't need the affordance don't need to stub it. Clean.
  • Double event propagation guarde.stopPropagation() on both onClick and onPointerDown correctly prevents the card's click-to-open-panel from firing.
  • TimestampLabel consolidation is excellent — removes at least 5 inline formatRelative/formatTimestamp functions and gives every timestamp a semantic <time dateTime="…"> element with a11y and devtools benefits. Tests are thorough.
  • fmtDateTime uses getLocale() — i18n-compliant, consistent with the rest of the format helpers.
  • deriveTaskStatus is idempotent — passing an already-enriched value back through returns the correct result, which makes the unified TaskStatus type safe to use at any layer.
  • data-testid="board-card-redispatch" on the button — easy to target in e2e tests.
  • isExecuting fix in StatusPillstatus === "running" || status === "executing" is the right guard for the pulse animation.

Summary: Fix the double-fire bug (Bug 1), fix the useCallback dep (Bug 2), then rebase. After that this is ready to merge.

## Code Review — PR #1188 CI is green. However there are two real bugs to fix and a merge conflict to resolve before this can land. --- ### ❌ Not mergeable `mergeable: false` — almost certainly a conflict with PR #1187, which also touches the `TaskStatus` type system and the shared package. Rebase onto `main` once #1187's fate is settled (or after it lands). --- ### 🐛 Bug 1 — `RedispatchButton` has no pending/disabled guard `redispatchMutation.isPending` is never threaded down to `RedispatchButton`, so a user who double-clicks (or clicks while the network is slow) fires multiple `POST /task/:id/redispatch` requests for the same `last_task_id`. Each one creates a new task row. **Fix:** pass `isPending` through and disable the button: ```tsx // board.tsx — add to BoardProps onRedispatchPending?: boolean; // board-card.tsx — RedispatchButton function RedispatchButton({ card, onRedispatch, isPending }: { card: BoardCardType; onRedispatch: (card: BoardCardType) => void; isPending?: boolean; }): ReactElement { return ( <Tooltip content={label} side="top"> <button type="button" disabled={isPending} onClick={...} className={cn( "inline-flex size-6 shrink-0 items-center justify-center ...", isPending && "opacity-50 cursor-wait", )} ... > <RotateCcw aria-hidden="true" className={cn("h-3 w-3", isPending && "animate-spin")} /> </button> </Tooltip> ); } ``` Alternatively, track a `pendingTaskId` string in the route so only the clicked card is locked — but at minimum disable re-dispatch while any mutation is pending. --- ### 🐛 Bug 2 — `handleRedispatch` `useCallback` deps cause a board-wide re-render on every render ```tsx // routes/board.tsx const handleRedispatch = useCallback( (card: BoardCardType) => { redispatchMutation.mutate(card); }, [redispatchMutation], // ← full mutation object, new reference every render ); ``` `useMutation` returns a new object reference on every render; `mutate` is the stable part. Because `handleRedispatch` re-creates every render, `onRedispatch` in the board's `useMemo` dep array (`[..., onRedispatch]`) fires a full card list re-render on every parent render — even with no state change. **Fix:** ```tsx const handleRedispatch = useCallback( (card: BoardCardType) => { redispatchMutation.mutate(card); }, [redispatchMutation.mutate], // stable reference ); ``` --- ### ⚠️ Note — N+1 DB queries during board projection `getLatestTaskForIssue` issues one SQLite query per `idle_assigned` card per board refresh. For a board with 20 idle cards that's 20 extra round-trips per poll. This won't hurt in practice today but it's worth tracking: a single `SELECT … WHERE (repo, issue_number) IN (…)` over the collected idle pairs would be O(1) queries instead. Not a blocker, but worth opening a follow-up issue. --- ### ⚠️ Note — `TaskUiStatus` shim silently widens the type ```ts // task-ui-status.ts export type { TaskStatus as TaskUiStatus } from "./task-record"; ``` Old `TaskUiStatus` was a 7-member union (UI-enriched values only). It is now the full 14-member `TaskStatus` union. Any `switch`/exhaustive check on `TaskUiStatus` without a `default` branch that was previously exhaustive will silently miss `"queued"`, `"running"`, `"success"`, etc. at runtime. This is an acceptable migration trade-off (and the comment marks it deprecated), but worth auditing for exhaustive switches before the shim is removed. --- ### ✅ What's good - **Server-side enrichment design** (`last_task_id` / `last_task_status` on the board projection) is correct — the card face gets exactly what it needs without a side-panel round-trip. - **`getLastTask` is optional** in `BoardProjectionInput` — tests that don't need the affordance don't need to stub it. Clean. - **Double event propagation guard** — `e.stopPropagation()` on both `onClick` and `onPointerDown` correctly prevents the card's click-to-open-panel from firing. - **`TimestampLabel` consolidation** is excellent — removes at least 5 inline `formatRelative`/`formatTimestamp` functions and gives every timestamp a semantic `<time dateTime="…">` element with a11y and devtools benefits. Tests are thorough. - **`fmtDateTime` uses `getLocale()`** — i18n-compliant, consistent with the rest of the format helpers. - **`deriveTaskStatus` is idempotent** — passing an already-enriched value back through returns the correct result, which makes the unified `TaskStatus` type safe to use at any layer. - **`data-testid="board-card-redispatch"`** on the button — easy to target in e2e tests. - **`isExecuting` fix in `StatusPill`** — `status === "running" || status === "executing"` is the right guard for the pulse animation. --- **Summary:** Fix the double-fire bug (Bug 1), fix the `useCallback` dep (Bug 2), then rebase. After that this is ready to merge.
reviewer requested changes 2026-05-15 00:19:38 +00:00
Dismissed
reviewer left a comment

Code Review — PR #1188

CI passes The architecture is sound (server projection → shared type → client wiring) and the feature works end-to-end, but three issues need addressing before merge.


🔴 N+1 DB queries in board-projection.ts

getLastTask(repo, issue.number) is called inside the per-card loop for every idle_assigned card:

// board-projection.ts ~L535
if (status === "idle_assigned" && getLastTask) {
    const lastTask = getLastTask(repo, issue.number);
    ...
}

This fires one synchronous SQLite query per idle card. With 20 idle cards that's 20 round-trips per board refresh. Please add a batched variant — e.g. getLatestTasksForIssues(pairs: { repo: string; issueNumber: number }[]) using a single WHERE (repo, issue_number) IN (...) — collect all idle_assigned cards first, fetch once before the loop, then look up from a Map.


🟡 RedispatchButton has no loading / disabled state

The button in board-card.tsx has no disabled or spinner during the pending mutation. isPending from redispatchMutation is not threaded down to the button, so rapid clicks can fire multiple concurrent redispatch requests before the first resolves.

Minimal fix: thread isPending through BoardCardViewProps (e.g. redispatchPending?: boolean) → RedispatchButton, add disabled={redispatchPending} and a spin class on the icon.


🟡 "running" is a dead arm in getLatestTaskForIssue's return type

): { id: string; status: TerminalStatus | "running" } | null

An idle_assigned card can't have a concurrently-running task, and the projection immediately discards anything that isn't failure | cancelled | interrupted. The "running" union arm is never exercised at the call site and slightly misleads readers about what the function guarantees. Drop it — TerminalStatus alone is the correct return.


Minor — handleRedispatch callback stability

const handleRedispatch = useCallback(
    (card: BoardCardType) => { redispatchMutation.mutate(card); },
    [redispatchMutation],   // ← whole object re-created each render
);

TanStack Query guarantees .mutate is referentially stable across renders. Depending on the full redispatchMutation object causes handleRedispatch (and therefore the memoised Board render fn) to be recreated every render. Use [redispatchMutation.mutate] as the dependency instead.

## Code Review — PR #1188 CI passes ✅ The architecture is sound (server projection → shared type → client wiring) and the feature works end-to-end, but three issues need addressing before merge. --- ### 🔴 N+1 DB queries in `board-projection.ts` `getLastTask(repo, issue.number)` is called **inside the per-card loop** for every `idle_assigned` card: ```ts // board-projection.ts ~L535 if (status === "idle_assigned" && getLastTask) { const lastTask = getLastTask(repo, issue.number); ... } ``` This fires one synchronous SQLite query per idle card. With 20 idle cards that's 20 round-trips per board refresh. Please add a batched variant — e.g. `getLatestTasksForIssues(pairs: { repo: string; issueNumber: number }[])` using a single `WHERE (repo, issue_number) IN (...)` — collect all `idle_assigned` cards first, fetch once before the loop, then look up from a `Map`. --- ### 🟡 `RedispatchButton` has no loading / disabled state The button in `board-card.tsx` has no `disabled` or spinner during the pending mutation. `isPending` from `redispatchMutation` is not threaded down to the button, so rapid clicks can fire multiple concurrent redispatch requests before the first resolves. Minimal fix: thread `isPending` through `BoardCardViewProps` (e.g. `redispatchPending?: boolean`) → `RedispatchButton`, add `disabled={redispatchPending}` and a spin class on the icon. --- ### 🟡 `"running"` is a dead arm in `getLatestTaskForIssue`'s return type ```ts ): { id: string; status: TerminalStatus | "running" } | null ``` An `idle_assigned` card can't have a concurrently-running task, and the projection immediately discards anything that isn't `failure | cancelled | interrupted`. The `"running"` union arm is never exercised at the call site and slightly misleads readers about what the function guarantees. Drop it — `TerminalStatus` alone is the correct return. --- ### Minor — `handleRedispatch` callback stability ```ts const handleRedispatch = useCallback( (card: BoardCardType) => { redispatchMutation.mutate(card); }, [redispatchMutation], // ← whole object re-created each render ); ``` TanStack Query guarantees `.mutate` is referentially stable across renders. Depending on the full `redispatchMutation` object causes `handleRedispatch` (and therefore the memoised `Board` render fn) to be recreated every render. Use `[redispatchMutation.mutate]` as the dependency instead.
fix(web): address reviewer feedback on board redispatch feature
All checks were successful
qa / sql-layer-check (pull_request) Successful in 11s
qa / dockerfile (pull_request) Successful in 15s
qa / i18n-string-check (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 17s
qa / qa-1 (pull_request) Successful in 3m46s
qa / qa (pull_request) Successful in 0s
fb91739257
- Replace N+1 getLatestTaskForIssue calls with a single batch query
  (getLatestTasksForIssues) pre-fetched before projectBoard runs
- Board projection now accepts a pre-built lastTaskByIssue map instead
  of a per-card callback
- Drop "running" from getLatestTaskForIssue return type — idle_assigned
  cards never have an in-flight task by definition
- Thread redispatchPending (mutation.isPending) from board route down to
  RedispatchButton so double-click is disabled while in flight
- Fix handleRedispatch useCallback dep to redispatchMutation.mutate for
  stable reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-05-15 09:29:15 +00:00
reviewer approved these changes 2026-05-15 09:32:01 +00:00
reviewer left a comment

Review — APPROVED

CI green. The feature is well-structured: batch pre-fetch in the cache layer, pure projection, clean component decomposition, and the server endpoint was already in place so nothing to wire server-side.

Two findings — neither is a blocker.


1. Dead export: getLatestTaskForIssue (singular)

task-store.ts adds both a single-issue variant and the batch variant, but only the batch getLatestTasksForIssues is imported (by board-cache.ts). getLatestTaskForIssue is exported from the module but has no callers anywhere in the codebase. Either drop it now or add a // future: used by X comment so the next reader doesn't wonder.

// apps/server/src/infrastructure/database/task-store.ts
export function getLatestTaskForIssue(   // ← no callers — can be removed
  repo: string,
  issueNumber: number,
): { id: string; status: TerminalStatus } | null {

2. aborted_no_skill is fetched but silently dropped

getLatestTasksForIssues queries for all TERMINAL_STATUSES (which includes "aborted_no_skill"), but the projection filter only gates on failure | cancelled | interrupted:

// board-projection.ts
if (
  lastTask.status === "failure" ||
  lastTask.status === "cancelled" ||
  lastTask.status === "interrupted"
)

So an issue whose last terminal task was aborted_no_skill causes a DB row to be fetched and held in lastTaskByIssue but never consumed. Omitting aborted_no_skill from the button is the right call (no skill configured → redispatching won't help), but it's worth either narrowing the query's inArray to the three actionable statuses, or leaving a comment on the filter explaining why aborted_no_skill is intentionally excluded.


Minor note (no action required)

redispatchPending is a single global boolean, so clicking Re-dispatch on card A disables Re-dispatch on every other card until the round-trip completes. Safe and simple for now; worth revisiting if operators routinely need to queue multiple redispatches in quick succession.


Good work overall — the batch pre-fetch approach is exactly right to avoid N+1 on board rebuild.

## Review — APPROVED ✅ CI green. The feature is well-structured: batch pre-fetch in the cache layer, pure projection, clean component decomposition, and the server endpoint was already in place so nothing to wire server-side. Two findings — neither is a blocker. --- ### 1. Dead export: `getLatestTaskForIssue` (singular) `task-store.ts` adds both a single-issue variant and the batch variant, but only the batch `getLatestTasksForIssues` is imported (by `board-cache.ts`). `getLatestTaskForIssue` is exported from the module but has no callers anywhere in the codebase. Either drop it now or add a `// future: used by X` comment so the next reader doesn't wonder. ```ts // apps/server/src/infrastructure/database/task-store.ts export function getLatestTaskForIssue( // ← no callers — can be removed repo: string, issueNumber: number, ): { id: string; status: TerminalStatus } | null { ``` --- ### 2. `aborted_no_skill` is fetched but silently dropped `getLatestTasksForIssues` queries for all `TERMINAL_STATUSES` (which includes `"aborted_no_skill"`), but the projection filter only gates on `failure | cancelled | interrupted`: ```ts // board-projection.ts if ( lastTask.status === "failure" || lastTask.status === "cancelled" || lastTask.status === "interrupted" ) ``` So an issue whose last terminal task was `aborted_no_skill` causes a DB row to be fetched and held in `lastTaskByIssue` but never consumed. Omitting `aborted_no_skill` from the button is the right call (no skill configured → redispatching won't help), but it's worth either narrowing the query's `inArray` to the three actionable statuses, or leaving a comment on the filter explaining why `aborted_no_skill` is intentionally excluded. --- ### Minor note (no action required) `redispatchPending` is a single global boolean, so clicking Re-dispatch on card A disables Re-dispatch on every other card until the round-trip completes. Safe and simple for now; worth revisiting if operators routinely need to queue multiple redispatches in quick succession. --- Good work overall — the batch pre-fetch approach is exactly right to avoid N+1 on board rebuild.
Collaborator

APPROVED and CI is green, but the branch has a merge conflict against main — auto-merge failed. Please rebase onto main and push; I'll merge once the conflict is resolved.

APPROVED and CI is green, but the branch has a merge conflict against `main` — auto-merge failed. Please rebase onto `main` and push; I'll merge once the conflict is resolved.
charles force-pushed dev/0 from fb91739257
All checks were successful
qa / sql-layer-check (pull_request) Successful in 11s
qa / dockerfile (pull_request) Successful in 15s
qa / i18n-string-check (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 17s
qa / qa-1 (pull_request) Successful in 3m46s
qa / qa (pull_request) Successful in 0s
to 8fa77dcbc4
All checks were successful
qa / sql-layer-check (pull_request) Successful in 12s
qa / dockerfile (pull_request) Successful in 16s
qa / i18n-string-check (pull_request) Successful in 16s
qa / db-schema (pull_request) Successful in 19s
qa / qa-1 (pull_request) Successful in 3m23s
qa / qa (pull_request) Successful in 0s
2026-05-15 09:59:46 +00:00
Compare
charles deleted branch dev/0 2026-05-15 10:05:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1188
No description provided.