feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: Workflow Boards — kanban state machines that drive coding agents - #3135

Closed
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion
Closed

feat: Workflow Boards — kanban state machines that drive coding agents#3135
ccdwyer wants to merge 1 commit into
pingdotgg:mainfrom
ccdwyer:ft/hyperion

Conversation

@ccdwyer

@ccdwyerccdwyer commented Jun 18, 2026

Copy link
Copy Markdown

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)

  • Event-sourced core: append-only event store, projection pipeline, and read model, with durable sagas, lane-entry tokens, and durable approvals so in-flight work survives restarts. Startup is gated on workflowRecovery.recover() (retried with backoff) before mutating RPCs unblock.
  • Step executors: agent steps (drive coding agents in a worktree), approval gates, script steps, merge, and a GitHub pull-request loop.
  • Smart routing via predicate rules, WIP limits, retention/aging sweeps + digests, and a dry-run simulator for evaluating routing without side effects.
  • One consolidated migration, 033_WorkflowSchema, creates every workflow table in a single step.

RPC + contracts

  • Full WebSocket RPC surface for board/ticket CRUD, intake proposals, dry-run, diffs, metrics, webhooks, and work-source / outbound connections, with typed contracts in packages/contracts.

Web UI

  • Drag-and-drop board view and a fullscreen canvas editor (lane/step/routing editing, version history + revert, dry-run panel, self-improve dialog), plus board creation, rename, delete-with-cascade, route colors, lane drag, and per-ticket status.

Mobile

  • A "Needs You" inbox and ticket action-sheet screen, push notifications for attention events (per-device preference gating), and deep-link routing into the right ticket.

Integrations & ingest

  • One-way GitHub Issues / Asana → ticket sync plus a curated work-item import picker; external events/webhooks (SSRF-safe URL validation, Slack/generic formatters); collaborating agents (per-agent session resume + inter-agent handoff); markdown + editable ticket comments; design-board templates; and provider-aware ticket-description spill with an intake braindump guard so long inputs stay within provider limits.

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

imageimageimage

Checklist

  • This PR is small and focused (lol, oh no)
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
WorkflowsDemo.mov

Note

Add kanban Workflow Boards as a state-machine runtime driving coding agents

  • Introduces a full workflow board system: kanban-style boards with lanes, pipelines, agent/script steps, routing rules, WIP limits, and ticket lifecycle management backed by a SQL event store and projection pipeline.
  • Adds a WorkflowEngine that executes pipeline steps (agent turns, scripts, approvals, merge/PR ops), routes tickets across lanes on step outcomes or external events, enforces WIP limits, and persists all state as immutable events.
  • Adds work-source integrations (GitHub Issues, Jira, Asana) that sync external issues into tickets with auto-pull rules and reconciliation diffing.
  • Adds outbound webhook delivery (generic and Slack formatters) with SSRF validation, durable delivery outbox, and exponential backoff retry.
  • Adds a board editor UI with canvas view, drag-and-drop lane/step routing, version history with diffs, dry-run simulation, and a self-improvement proposal flow backed by provider text generation.
  • Adds mobile screens for a cross-environment "Needs You" inbox and per-ticket action sheet.
  • Adds relay support for board/ticket push notifications with signed proofs, preference gating, and APNs fan-out.
  • Adds workflow:read and workflow:operate auth scopes, a webhook ingress route, and wires the full runtime into server startup with a dedicated workflow recovery phase.
  • Risk: workflow recovery now runs at startup and blocks dependent background services (GitHub poller, notification dispatcher, work-source syncer, outbound dispatcher) if it fails, resulting in those subsystems being skipped rather than started.

Macroscope summarized dc4e660.

@coderabbitai

coderabbitaiBot commented Jun 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883cf0c7-6824-432c-b750-0216d06e6026

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 18, 2026
Comment threadapps/server/src/workflow/Layers/WorkflowFileLoader.ts
Comment threadapps/server/src/workflow/outbound/OutboundUrlValidator.ts
Comment threadapps/web/src/components/board/editor/canvas/RoutingEdges.tsx
Comment threadapps/server/src/workflow/Layers/SetupRunService.ts
Comment threadapps/mobile/src/features/board/NeedsYouInboxScreen.tsx Outdated
Comment on lines +153 to +160
const onRefresh = useCallback(() => {
setRefreshing(true);
void load().finally(() => {
if (mountedRef.current) {
setRefreshing(false);
}
});
}, [load]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumboard/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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

This comment was marked as off-topic.

Comment on lines +150 to +154
if (currentSnapshot !== null && currentSnapshot.status === "exited") {
yield* Deferred.succeed(done, { exitCode: currentSnapshot.exitCode ?? 1 }).pipe(
Effect.asVoid,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MediumLayers/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.

@macroscopeappmacroscopeappBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One Effect service convention issue found. See the inline comment. The rest of the new workflow service modules follow the repository's established Effect conventions.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import Effect modules from their subpaths as namespaces rather than as consolidated named imports from "effect", matching the rest of the workflow code and the wider server codebase (e.g. import * as Effect from "effect/Effect").

Suggested change
import{Data,Effect}from"effect";
import*asDatafrom"effect/Data";
import*asEffectfrom"effect/Effect";

The sibling test file OutboundUrlValidator.test.ts (import { Effect } from "effect") has the same issue and should be updated too.

Posted via Macroscope — Effect Service Conventions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsettings/WorkSourceConnectionsSettings.tsx:312

When environmentId changes, loadConnections re-runs without resetting connections, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add setConnections(null) at the start of the callback.

- setLoadError(null);+ setLoadError(null);+ setConnections(null);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx around line 312:
When `environmentId` changes, `loadConnections` re-runs without resetting `connections`, so the previous environment's list stays rendered during the next load and a late fetch resolution overwrites the new data. Add `setConnections(null)` at the start of the callback.
Evidence trail:
apps/web/src/components/settings/WorkSourceConnectionsSettings.tsx lines 302-323 (component definition, loadConnections callback, useEffect); line 414 (no key prop on WorkSourceConnectionsPanel); apps/web/src/workflow/useWorkflowApi.ts lines 29-117 (useMemo depends on [registry, environmentId], producing new reference on change).

@ccdwyer
ccdwyerforce-pushed the ft/hyperion branch 2 times, most recently from d665706 to 356c8a3CompareJune 21, 2026 03:00
…d for rebase)
Squash of the ft/hyperion stack (Jira provider + the upstream client-architecture
rebase/port) for a clean rebase onto the newer upstream main. Per-commit history
preserved on ft/hyperion-prerebase2-2026-06-20.
Claude-Session: https://claude.ai/code/session_012bpxnLcbEhVHJ4pjamRpUW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ccdwyer@mrpunyetaz-cloud