feat: Workflow Boards — kanban state machines that drive coding agents#3135
feat: Workflow Boards — kanban state machines that drive coding agents#3135ccdwyer wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Per-project boards as state machines: lanes hold pipelines of agent/approval/ script/PR steps; one git worktree per ticket; event-sourced engine with durable sagas, lane-entry tokens, and durable approvals. Includes: board creation + visual editor (form/canvas/history+revert), script steps, smart routing, WIP enforcement, retention, delete-board cascade, route colors, lane drag, per-ticket status, rename, ticket collaboration; external events/webhooks, aging+digest, dry-run simulator; GitHub PR loop; board notifications + mobile; one-way GitHub Issues / Asana sync + curated import picker; flexible work sources; collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; provider-aware ticket-description spill + intake braindump guard. Single consolidated migration (033_WorkflowSchema).
| const onRefresh = useCallback(() => { | ||
| setRefreshing(true); | ||
| void load().finally(() => { | ||
| if (mountedRef.current) { | ||
| setRefreshing(false); | ||
| } | ||
| }); | ||
| }, [load]); |
There was a problem hiding this comment.
🟡 Medium board/NeedsYouInboxScreen.tsx:153
During pull-to-refresh from the error state, load() clears error at line 91 before the async work completes, and onRefresh() never sets loading to true. This produces the state {loading: false, refreshing: true, rows: [], error: null}, which causes deriveInboxViewState to return {kind: "empty"} — so the screen briefly flashes "You're all caught up" instead of showing a loading indicator or preserving the error display until new data arrives.
const onRefresh = useCallback(() => {
setRefreshing(true);
+ setLoading(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/board/NeedsYouInboxScreen.tsx around lines 153-160:
During pull-to-refresh from the error state, `load()` clears `error` at line 91 before the async work completes, and `onRefresh()` never sets `loading` to `true`. This produces the state `{loading: false, refreshing: true, rows: [], error: null}`, which causes `deriveInboxViewState` to return `{kind: "empty"}` — so the screen briefly flashes "You're all caught up" instead of showing a loading indicator or preserving the error display until new data arrives.
Evidence trail:
apps/mobile/src/features/board/NeedsYouInboxScreen.tsx lines 83-93 (load() clears error before await), lines 153-160 (onRefresh never sets loading to true), line 168 (deriveInboxViewState call). apps/mobile/src/features/board/inboxViewState.ts lines 33-45 (deriveInboxViewState does not check refreshing in the empty case at line 43, causing it to return {kind: 'empty'} when refreshing=true, loading=false, rows=[], error=null).
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| if (currentSnapshot !== null && currentSnapshot.status === "exited") { | ||
| yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe( | ||
| Effect.asVoid, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟡 Medium Layers/SetupRunService.ts:150
When terminals.getSnapshot returns null (no session exists, e.g., terminal was cleaned up), the code proceeds to Deferred.await(done) and waits for events that will never arrive. With timeoutMs set, this waits the full timeout; without it, the effect hangs indefinitely. Consider treating null the same as the exited case—resolve immediately with an error since the terminal no longer exists.
- if (currentSnapshot !== null && currentSnapshot.status === "exited") {
+ if (currentSnapshot === null) {
+ yield* Deferred.succeed(done, { exitCode: 1 }).pipe(Effect.asVoid);
+ } else if (currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/workflow/Layers/SetupRunService.ts around lines 150-154:
When `terminals.getSnapshot` returns `null` (no session exists, e.g., terminal was cleaned up), the code proceeds to `Deferred.await(done)` and waits for events that will never arrive. With `timeoutMs` set, this waits the full timeout; without it, the effect hangs indefinitely. Consider treating `null` the same as the exited case—resolve immediately with an error since the terminal no longer exists.
Evidence trail:
apps/server/src/workflow/Layers/SetupRunService.ts lines 146-155 (REVIEWED_COMMIT): null check condition on line 150, fallthrough to Deferred.await on line 155. apps/server/src/terminal/Services/Manager.ts lines 138-144: getSnapshot documentation 'Returns null if no session exists for the given ids' and return type `TerminalSessionSnapshot | null`. apps/server/src/terminal/Layers/Manager.ts lines 2218-2221: getSnapshot implementation returns null when session is None.
Per-project boards as state machines: lanes hold pipelines of agent/approval/ script/PR steps; one git worktree per ticket; event-sourced engine with durable sagas, lane-entry tokens, and durable approvals.
Includes: board creation + visual editor (form/canvas/history+revert), script steps, smart routing, WIP enforcement, retention, delete-board cascade, route colors, lane drag, per-ticket status, rename, ticket collaboration; external events/webhooks, aging+digest, dry-run simulator; GitHub PR loop; board notifications + mobile; one-way GitHub Issues / Asana sync + curated import picker; flexible work sources; collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; provider-aware ticket-description spill + intake braindump guard.
Single consolidated migration (033_WorkflowSchema).
What Changed
Adds Workflow Boards — per-project kanban boards that are executable state machines. Each lane holds a pipeline of typed steps (agent / approval / script / merge / pull-request), tickets flow lane-to-lane under JSON-Logic routing rules, and every ticket runs in its own isolated git worktree.
Engine (server)
workflowRecovery.recover()(retried with backoff) before mutating RPCs unblock.033_WorkflowSchema, creates every workflow table in a single step.RPC + contracts
packages/contracts.Web UI
Mobile
Integrations & ingest
Why
Driving coding agents by hand doesn't scale past a couple of tickets: there's no durable record of where each piece of work is, no enforced gates between "agent proposed" and "merged," and no isolation between concurrent tasks. Modeling a project as a state machine makes the pipeline explicit and inspectable — each lane is a stage, each step is a typed unit of work, and routing rules decide what moves next.
The engine is event-sourced rather than mutating rows in place so that the full history is replayable and crashes mid-pipeline recover cleanly (durable sagas + lane-entry tokens), which is essential when steps spawn long-running agents and external side effects (PRs, webhooks, merges). Each ticket gets its own worktree so parallel agents never clobber each other's working tree. Routing, WIP, and approvals are first-class instead of conventions, so the board enforces the process rather than relying on the operator to remember it.
UI Changes
Checklist
WorkflowsDemo.mov
Note
Add kanban workflow boards with agent-driven pipelines, RPC surface, and mobile inbox
WorkflowEnginerequires successful recovery (workflowRecovery.recover()) on startup before mutating RPCs are unblocked; recovery failure gates all workflow operations and retries with exponential backoff up to 3 times📊 Macroscope summarized 5fe0ecf. 122 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.