## What
Plan Review, planning, and the replan loop move from the implementation
column into the **planning lane** (`todo`), so a task under
specification never holds a WIP slot. The card crosses into
`in-progress` exactly once, at `parse`, released by the scheduler.
Operators also finally see a **Plan Review** badge while the gate runs —
it was previously invisible on the default workflow.
## The part that made it possible
Moving the node is ten lines. It was attempted three times and reverted
each time, because a graph run with no durable continuation replayed
from `start` and dragged an in-progress card *backward* out of the WIP
column, firing `abort-on-exit` and stranding it in a pre-WIP column with
no releaser.
So this PR adds the graph **entry contract** —
`resolveColumnResumeNode`:
| Card is in | Resumes at |
|---|---|
| `triage` | `start` |
| `todo` | `plan` |
| `in-progress` | `parse` — never re-plans, never moves backward |
| `in-review` | first review node — gates are not skipped |
`ir.columns` is ordered and that order is the lifecycle order; rework
and failure edges are excluded so the entry point is always the main
path. The proof it's the right fix: **`executor-task-done-invariant`
passes unmodified** after failing every previous attempt.
## Also in here
- **Release gate narrowed twice.** `isUnplannedForExecution` applies its
pre-release plan-review gate only when the node's column equals the
card's column *and* the group is enabled for the task. The enablement
check fixes a real deadlock — a task with Plan Review toggled off was
held forever waiting for evidence nothing would ever write.
- **Badge cleanup.** Gate badge reads "Plan Review" instead of the
ambiguous "Reviewing" and no longer hides behind a lane restriction; the
status badge stops duplicating it; `planning` renders as "Planning"
instead of the raw engine token.
- **Coding (Ideas)** renames its planner column to "Planning" (id `todo`
unchanged) and loses its private planning-node re-home — the graph it
clones is already plan-in-place.
- **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose
column its workflow no longer declares. Written for a follow-up, kept
because it makes any column edit survivable.
## Test changes
Scheduler and release fixtures now model a card whose Plan Review passed
— the state every real card is in when the capacity sweep sees it. A
held unreviewed card is the gate working, and that path stays owned by
`pre-release-plan-review.test.ts`.
New `workflow-graph-entry-contract.test.ts` covers the invariant at
every lifecycle position, plus the gap-column and remediation-node
cases.
## Verification
Gate 299 + 70 + 10, dashboard badge suites 672, engine
workflow/entry/executor suites 147, core 122. Lint and typecheck clean.
Full engine suite sits at the pre-existing baseline (notifier /
plugin-runner / notification-service, untouched by this).
## Follow-up
Removing the Todo column entirely is a separate ~207-site
lifecycle-vocabulary refactor — planned in
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
(companion docs PR).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Plan Review now runs in the Planning lane before implementation
begins.
* Cards resume from their current workflow column without replaying
earlier steps.
* Added automatic recovery for cards stranded in outdated workflow
columns.
* **Improvements**
* Renamed the Coding (Ideas) planner column to “Planning.”
* Refined Plan Review gating to respect enabled settings and the card’s
current column.
* Updated planning and Plan Review badges for clearer, consistent labels
across cards and lists.
* **Bug Fixes**
* Improved workflow transitions and release behavior around planning,
review, and execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groundwork for FN-8603's remaining ~14-minute wait. Liveness for a pending
review gate is judged purely by a 15-minute staleness floor because a lease
records WHO took it (`leaseOwner` = run id) but not WHERE, and under multi-node
every engine sees every other engine's leases. A fresh-but-unknown lease might
be running on a peer, so the floor was the only safe test -- and a lease left by
this node's own crashed process is indistinguishable from it.
Adds `WorkflowStepResult.leaseNodeId` plus an optional `LocalNodeLeaseIdentity`
argument to `classifyReviewLease`. One narrow new case: a lease stamped with the
caller's OWN node id whose `startedAt` predates the caller's process boot is
provably dead -- the process that could have owned it is gone -- so it
classifies as `reclaim` immediately rather than aging out. Deliberately narrow,
because widening it is a double-dispatch risk: absent (legacy) or peer node ids
keep the floor, and a lease taken by this process after boot is still adopted.
InProcessRuntime.start() resolves the local node id from CentralCore (fail-soft;
on error it stays undefined and floor-only semantics apply) and passes it to
SelfHealingManager. The graph executor stamps the field when deps.localNodeId is
set.
NOT YET WIRED, so this is inert in production and behavior is unchanged end to
end: `localNodeId` is not threaded from WorkflowGraphTaskRunner /
WorkflowTaskRuntime down into the executor deps, so no lease actually carries a
`leaseNodeId` yet. The reader is ready; the writer needs that pass-through
(WorkflowGraphTaskRunnerDeps gains the field, the runner forwards it, and the
runtime supplies this.localNodeId). Stopping here rather than half-threading it.
Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green
(299 + 70), core workflow-step-results suite green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contention prevention (why tasks shared a path at all):
- Planning ran `tools: "coding"` at the repo root, so every planner had write
tools in the operator's checkout and all planners shared one path. Planning
now acquires the task's own worktree (TriageProcessor.acquirePlanningWorktree
-> TaskExecutor.ensureTaskWorktreeForPlanning).
- Graph nodes with no worktree acquired one instead of falling back to rootDir,
so Plan Review / Code Review / custom gates all run isolated. Plan Review
re-acquires when its recorded worktree is gone, replacing FN-7996's
run-from-the-repo-root degrade. Workspace projects are unchanged.
- Registration goes through acquireActiveSessionPath, which reclaims a leaked
entry whose holder is provably dead and aged past the FN-5256 floor. A live
holder still contends — real serialization is never clobbered.
Classification (the reported symptom):
- A lease held by another task is no longer a provider failure. It carries
SESSION_CONTENTION_HOLD_VALUE, classifies transient, is excluded from
isNonPlanDefectPlanReviewFailure, and stops burning the node's fast retries.
- The executor waits it out on a 10-attempt 5s->60s ladder and then leaves the
task cleanly queued. There is no terminal branch: contention always ends, so
parking would only ask a human to press Retry on a condition that fixed
itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Workflows could reach Plan Review without an authoritative PROMPT.md,
producing misleading approvals or stranding the task. Planning now
verifies durable prompt persistence before releasing the card, and every
workflow entry/review surface fails closed when its required plan is
absent. Confirmed absence triggers bounded automatic replanning;
TaskStore read outages retry in place; exhausted recovery parks visibly
without consuming review-fix budget or overriding pause, manual-review,
terminal, or merge-confirmed state.
Related: FN-8455
## Validation
- Focused workflow-artifact, graph-recovery, review, writer, and triage
regression suites pass.
- @fusion/engine typecheck passes.
- Repository lint, changeset validation, and diff checks pass.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Plan Review now fails closed when `PROMPT.md` is missing or blank,
returning a revision request with a typed `failureValue`.
* Required workflow artifacts are treated as missing unless they exist
with non-empty content; read failures are handled separately.
* Recovery now deterministically chooses replan vs “park-failed” with
bounded retries, and records a `task:required-artifact-missing` audit
event.
* **Workflow Improvements**
* Triage and approval now persist `PROMPT.md` through the dedicated
prompt-write flow and verify it was stored exactly.
* Optional-group remediation preserves typed required-artifact missing
failures for pre-merge fixes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
The Coding (Ideas) workflow now behaves like the board it presents:
Ideas stays inert, Todo owns planning and plan review, In progress owns
implementation, and In review owns code review and merge. The restored
preset is intentionally limited to that five-stage path, while the
existing Coding workflow remains unchanged.
Workflow execution now suspends at Todo→In progress instead of running
the implementation node early. A durable, single-owner continuation
records the exact resume node and survives process restarts; the
scheduler remains the only component allowed to admit the task into WIP.
Disabled optional review groups traverse the same boundary without
invoking a reviewer, avoiding the prior stuck-task behavior.
Workflow validation also rejects capacity holds with no reachable WIP
destination, so deterministic lifecycle deadlocks fail at authoring time
rather than after a task is running.
Session-settled decisions carried from planning: columns are execution
invariants, scheduler-owned WIP admission is preserved, the existing
Coding (Ideas) preset is restored and simplified, and invalid release
topology is rejected (user-approved).
## Validation
- `pnpm lint`
- `pnpm verify:fast`
- `pnpm test:gate` (296 engine, 128 PostgreSQL core, and 63 CI-shape
tests)
- Focused workflow lifecycle tests (106 assertions)
- PostgreSQL regression coverage proves atomic continuation replacement
and database rejection of a second active owner
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added durable, resumable workflow execution across capacity boundaries
(including explicit suspend/resume at the correct node).
* Introduced Todo “plan review” workflow continuations and automated
planning/capacity draining.
* Restored Coding (Ideas) as a selectable built-in and updated its lane
placement; improved optional-step group enablement support.
* **Bug Fixes**
* User moves back to Todo now cancels active workflow continuations.
* Rejected workflow boundary transitions now surface as errors (instead
of silently continuing).
* Workflows with undriveable capacity-hold configurations are now
rejected.
* **Tests / Data**
* Expanded coverage for workflow suspension, continuations, and
continuation replacement; updated database schema to persist
continuation metadata and enforce single active continuation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Part **2 of 3** of the IR-driven lifecycle cutover (stacked on #2341;
top is #2335).
**Scope (80 files, packages/engine + cli/pi skill docs +
AGENTS/architecture):** graph-driven column moves via the
column-boundary controller (R1), single-mover scheduler/hold-release
trait cutover (KTD-2/KTD-9), trait re-keyed self-healing + merger with
the R7b confirmed-merge-must-finalize guarantee, graph-exclusive Plan
Review with leased dedup (R4/R5), the executeCore body-lift — zero
legacy re-entry — with fn_review_step + interceptor machinery deleted
and tombstone-ratcheted (R9), builtin workflow runtime fixes (missing
hold handler, unseamed-node column inheritance, no-merge completion
mover), the 6-column benchmark acceptance suite (11 tests) + 12-builtin
lifecycle sweep (94 assertions), and the executor test-harness
modernization. Also retires core's interpreter-cutover scaffolding whose
last consumer (the authoritative driver) dies here.
**Merge order:** #2341 → this → #2335. After #2341 merges, retarget this
to main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes the code-review/plan-review/CE gate workflow node failing with a blank "(no feedback captured)" message when a dispatch or infra exception (not a reviewer verdict) causes the step to fail.
- WorkflowGraphExecutor now synthesizes a non-blank WorkflowStepResult.output when an enabled optional-group (code-review, plan-review, browser-verification) or CE source:"node" skill-gate template node fails via dispatch/infra exception
- Diagnostic output is derived from the node:<id>:error context-patch key, falling back to the failure value, then a stable sentinel
- status, verdict extraction, edge routing, and self-healing's latestFailedPreMergeStep selection are unchanged
- Added regression test coverage: workflow-graph-optional-group-no-feedback.test.ts
- Added changeset (patch) documenting the fix for Runfusion/Fusion#1946
Files changed:
.changeset/fn-7642-code-review-no-feedback-diagnostic.md | 7 +
packages/engine/src/__tests__/workflow-graph-optional-group-no-feedback.test.ts | 246 +++++++++++++++++++++
packages/engine/src/workflow-graph-executor.ts | 104 ++++++++-
3 files changed, 355 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7642
Fusion-Task-Lineage: 1329e907-652f-4230-a945-5a9d7040ae69
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Two distinct v0.52.0 regressions reported in issue #1863.
1. Triage loop (engine): the best-effort completion-summary graph node is
wired into every built-in workflow with a success-only edge. A thrown
handler exception or a failed summary projection write bypassed the
advisory `!blocking -> success` coercion, terminated the graph at
'completion-summary', and routeGraphFailureToExecutionResume bounced the
in-review task back to todo forever (token usage 0, execution NOT STARTED).
The graph executor now degrades a completion-summary node failure to
success (ensureWorkflowCompletionSummary still backfills task.summary), with
a routeGraphFailureToExecutionResume backstop. Shared isCompletionSummaryNode
predicate exported from @fusion/core.
2. i18n object-key crashes (dashboard): three views called t() with keys that
resolve to nested objects (taskDetail.executionMode, routing.source,
nodes.dockerHost), so i18next returned "returned an object instead of
string" and crashed the render. Added leaf label keys across all locales and
switched the callers.
Tests: engine non-fatal completion-summary regression (fails without the fix),
dashboard invariant guard scanning t("literal") callers against real en/app.json,
and a Stats-panel reproduction against the real bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record skill-backed workflow nodes into workflowStepResults and teach task progress surfaces to include those graph-node records without re-showing disabled optional workflow checks.
Classify write-capable graph nodes before handler dispatch and let executor adapters fulfill the declared worktree requirement, keeping custom-node execution out of lifecycle decision making.
Fusion-Task-Id: FN-7224
Move completed-step replay and proven-merge finalization into workflow-owned execution paths so graph restarts continue from live task projection instead of failing stale step nodes.
Fusion-Task-Id: FN-7224
- workflow-graph-executor: wrap each post-merge walk() in try/catch so a malformed
post-merge IR / traversal error is logged and skipped, never flipping an already-
merged task to failed (non-blocking post-merge contract) [T9, real bug].
- Refresh stale FNXC comments now that graphNativePostMerge is default-ON and the
legacy merger post-merge path was removed (experimental-features, workflow-graph-
executor, workflow-graph-post-merge.test) [T6/T7/T8].
- Normalize FNXC timestamps to yyyy-MM-dd-hh:mm (TaskCard.test, taskProgress.test) [T2/T3].
- Changeset: category fix → feature to match the minor bump [T0].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds graph-native post-merge step execution behind experimentalFeatures.graphNativePostMerge
(default OFF — byte-identical behavior until enabled). After a successful merge-attempt
(the merge seam awaits the merge Promise), the graph runs post-merge optional-group nodes
and records phase:"post-merge" results, non-blocking. The merge-region traversal hop is
inert when the flag is off, and empty for builtin:coding even flag-on (its merge exits only
reach merge-region nodes or end), so the parity oracle holds. Optional-group recording now
derives phase + log prefix from config.phase (defaults pre-merge). Adds postMergeOptionalGroupNode
factory for migration/custom workflows. Legacy merger post-merge path untouched (U7b cutover).
Plan U7a.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enabled optional-group nodes now upsert their outcome into the existing
task.workflowStepResults field keyed by node id, and emit [pre-merge] logs at
parity with the legacy runWorkflowSteps path. Disabled groups stay byte-inert.
Reuses the existing WorkflowStepResult type + store.updateTask path (no new
table/type/store method). Unblocks the unified progress bar for graph-run steps.
Plan U2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reject failure-condition edges inside optional-group templates (the single-pass
walk surfaces template failures as the group's outcome, so an internal failure
edge was silently dead) — Greptile P2.
- flowToIr: a container/group node (foreach/loop/optional-group) is v2-only — its
presence now forces v2 serialization (an inserted optional-group on a plain
workflow no longer serializes as invalid v1) — CodeRabbit.
- Disabled optional-group bypass routes a plain success with no distinguishing
value, so an outcome:* edge can't preempt success routing (inertness) — CodeRabbit.
- Downgrade heuristic: presence of a legacy optionalSteps key (incl. []) keeps v2.
- Resolver docblock corrected (config-less groups resolve to a fallback entry).
- Strengthen tests: assert both inserted groups + v2 round-trip; failure-edge
rejection case.
- Changeset: bump to major (removed exported WorkflowOptionalStep type).
- Plan: record U7a as delivered in this cohort; only the workflow-step seam
infra removal remains deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review found that enabling a built-in optional-group whose node id collides
with a WORKFLOW_STEP_TEMPLATES id (browser-verification) was silently bypassed:
resolveEnabledWorkflowSteps materialized a WorkflowStep row and stored its id
instead of the group node id, so the executor's enabledWorkflowSteps.includes(
node.id) check never matched. Pass the task's workflow optional-group ids through
untouched on both create-with-enable and update/toggle paths (new
resolveAllOptionalGroupIds + optionalGroupIdSet). Add colliding-id regression
tests. Also (review P3) record the group's own outcome on the disabled-bypass
path so a disabled group is fully context-inert, matching the enabled path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional-group branch to runNodeAndTraverse beside foreach/loop: when
the group id is in the task's enabledWorkflowSteps it runs the template subgraph
exactly once (new single-pass runOptionalGroup helper, no rework/iteration);
otherwise it passes through to the group's children without executing the body.
Covered by a two-task divergence execution test plus single-pass, inertness,
and template-failure-routing cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalizes the foreach-only bounded-rework mechanism to the top-level
graph walk so the PR review loop (await-review -> pr-respond -> rework
back) is a legal bounded cycle: a kind:rework back-edge to a stacked node
returns a ReworkSignal the loop-head re-runs up to maxReworkCycles, then
routes outcome:rework-exhausted. Non-rework cycles still throw 'Cycle
detected' (safety preserved); foreach rework unchanged (shared core
constants). Adds createAutoMergeGateHandler (live entity re-fetch +
isPrEntityAutoMergeReady -> auto-on/auto-off). Pins R14: graph-executed
PR tasks merge through pr-merge, never the legacy queue. 122 graph tests
green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the three first-class PR workflow node kinds and their handlers via
createPrNodeHandlers(deps), registered in createDefaultNodeHandlers
(fail-closed pr-nodes-unwired when absent). GitHub ops are injected as
callbacks (PrNodeGithubOps) at all three CLI sites (daemon/serve/dashboard)
so the engine never imports the dashboard client (FN-3049). pr-create
routes open/failed as outcomes; pr-merge passes expectedHeadOid and never
writes 'merged' (reconcile corroborates); pr-respond delegates to an
injected respond callback (U5 fills the body). 10 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>