Commit Graph

3313 Commits

Author SHA1 Message Date
gsxdsm
a136535f15 fix(engine): taint steps skipped after a bulk-completion refusal so they cannot auto-promote (#2260)
## What & why

**FN-8141 laundered a failed task into `done` with zero net changes and
no sign-off.** After the executor's
`bulk-step-completion-without-review` refusal fired (steps had no
APPROVE verdicts), the agent used the sanctioned skip affordance
(`fn_task_update status="skipped"`) on the remaining unreviewed steps.
Because every completion check counts `skipped` as complete, the task
then satisfied the exact condition the refusal was protecting, and
downstream **automatic** promotion (implicit `fn_task_done`,
self-healing `recoverStrandedCompletedTodoTasks`) moved it to in-review
— where the AI merger found an empty diff and finalized it as a no-op
`done`.

This PR restores the invariant: **steps skipped while a
bulk-step-completion refusal marker is active on the task are "tainted"
and cannot carry the task to review through any automatic path.** The
taint clears on an honest exit — an accepted `fn_task_done` (explicit or
non-tainted implicit) or an operator manual retry — so the legitimate
`PREMISE STALE` skip-then-done flow is unaffected.

## Design

- **Persisted marker**: new nullable `Task.bulkCompletionRefusalAt` (ISO
timestamp), stamped when the `bulk-step-completion-without-review`
refusal fires (explicit `fn_task_done` handler + implicit
`handleImplicitTaskDoneRefusal`). Survives requeue so a refusal on
attempt N taints attempt N+1's promotion. Full store plumbing (types,
descriptors, serialization, SQLite/PG schema + health self-heal).
- **Pure evaluator** `evaluateSkipBypassTaint(task)` in `@fusion/core`
(next to `evaluateNoCommitsNoOpFinalize`): `blocked` iff the marker is
set AND ≥1 step is `skipped`. Single rule every AUTO-promotion check
calls.
- **Clearing**: accepted explicit `fn_task_done`, accepted
implicit/retry completion (the success-reset `updateTask`s), and
`buildManualRetryResetPatch` (operator retry). A fresh lifecycle that
genuinely re-does the work leaves zero skipped steps, so it is never
blocked even if a marker lingers.

## Surface enumeration (every consumer of "all steps done/skipped" that
gates AUTO-promotion)

- **executor.ts**: `getCompletedTaskFinalizationDecision` (gated on the
`isTaskWorkComplete` branch only, never on an accepted `taskDone`);
`recoverCompletedTask` (shared chokepoint for unpause resume,
completed-task watchdog, orphan resume);
`evaluateImplicitCompletionRefusal` (both implicit-completion loops);
`isTaskAlreadyCompleteForNonContinuableSession`; graph merge-boundary
`getWorkflowMergeImplementationProofFailure`.
- **self-healing.ts**: `recoverCompletedTasks` (stuck in-progress) and
`recoverStrandedCompletedTodoTasks` (the exact FN-8141 promoter).
- **Verified-safe, left as-is**: per-step graph node projections
(executor ~6274/6298) and progress-render checks — they don't gate
whole-task auto-promotion.

## Test evidence

Scoped runs (all green):

```
CORE:   pnpm --filter @fusion/core exec vitest run \
          src/__tests__/skip-bypass-taint-guard.test.ts \
          src/__tests__/skip-bypass-taint-persistence.test.ts \
          src/__tests__/manual-retry-reset.test.ts
        → 17 passed

ENGINE: pnpm --filter @fusion/engine exec vitest run \
          src/__tests__/executor-skip-bypass-taint.test.ts \
          src/__tests__/self-healing.test.ts
        → 401 passed
```

Coverage: pure-evaluator (skip-before-refusal counts, skip-after-refusal
doesn't, taint-clearing, empty-marker/empty-steps edges); store
round-trip of the marker (set→read→clear); executor white-box (implicit
completion refused when tainted, allowed when clean or fully re-done,
graph merge-boundary reports missing proof, and the **explicit
`fn_task_done` PREMISE-STALE honest exit stays accepted**); self-healing
(FN-8141 sequence does not promote from either recovery path; a clean
legitimately-skipped task still promotes); manual-retry clears the
marker.

## Note on `pnpm verify:fast`

`verify:fast` currently fails at the workspace-artifact bootstrap on
**pre-existing** pi-SDK type errors in
`packages/engine/src/{auth-storage,pi,provider-registration}.ts` — the
FN-8145 upstream migration breakage (pi 0.80.x removed
`AuthStorage`/`ModelRegistry.create`). **None of those files are in this
diff.** `@fusion/core` builds clean (`packages/core build: Done`), and
`@fusion/engine` `tsc` reports **no errors in the files this PR
touches** (`executor.ts`, `self-healing.ts`); the only engine build
errors are the FN-8145 files. This base failure is the same condition
FN-8141 describes and is out of scope for this task.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 20:37:05 -07:00
gsxdsm
29543a0aac FN-8157: add PostgreSQL workflow step-instance persistence
Persist workflow foreach step-instance state through async PostgreSQL store APIs.

- Add async save, load, and stale-run pruning operations backed by Drizzle.
- Route executor persistence, recovery, and integration projection through async APIs.
- Cover PostgreSQL persistence and migrate foreach wiring coverage to the PG harness.
- Quarantine unrelated flaky route and triage tests per the test ledger.

Files changed:
 .../workflow-run-step-instances.pg.test.ts         | 100 +++++++++++++++++++
 packages/core/src/store.ts                         |  14 ++-
 packages/core/src/task-store/remaining-ops-6.ts    | 109 ++++++++++++++++++++-
 .../dashboard/src/__tests__/routes-github.test.ts  |  14 +--
 .../src/routes/register-task-workflow-routes.ts    |  18 ++--
 packages/engine/src/__tests__/triage.test.ts       |   6 +-
 .../src/__tests__/workflow-foreach-wiring.test.ts  |  59 +++++------
 packages/engine/src/executor.ts                    |  57 ++++++++---
 packages/engine/src/triage.ts                      |   4 +-
 packages/engine/vitest.config.ts                   |   2 +-
 scripts/lib/test-quarantine.json                   |   7 +-
 11 files changed, 315 insertions(+), 75 deletions(-)

Fusion-Task-Id: FN-8157

Fusion-Task-Lineage: c359f0d3-9191-4d27-aaed-9912419c5c27

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 20:36:45 -07:00
gsxdsm
aa1e250dd3 fix(engine): overseer vetoes no-op merge finalize when the last executor signal was failed-with-incomplete-work (#2258)
## What & why

FN-8141 (\"Update pi SDK … verify Kimi K3 end to end\") was **laundered
into `done`** despite producing zero net changes. The executor reverted
the impossible work 5 times; the planner overseer emitted
`stage=executor signal=failed` (\"Executor stage parked failed with work
incomplete\") **twice**, then an hour later — because the overseer is
**stage-scoped and memoryless** — classified the same task `stage=merger
signal=progressing` and let the AI merger's **empty no-op finalize**
promote it to `done`. No reviewer ever saw it (skipped steps request no
review; the merge-review pass reviews an empty diff).

This restores the invariant: **a task whose most-recent executor signal
is failed-with-incomplete-work, with no subsequent green completion,
must not reach `done` via a zero-diff no-op merge finalize.**

## Change

Two pure, unit-testable, never-throw functions
(`packages/engine/src/overseer-noop-finalize-veto.ts`), following the
FN-7514 `evaluateOverseerHumanControl` precedent (pure predicate +
ids/outcomes-only audit metadata):

- **`deriveExecutorSignalMemory`** — reconstructs the most-recent
executor signal from the **durable `overseer:intervention` timeline the
overseer already writes** (no new persisted column / migration; \"the
existing oversight state storage the controller uses\"). A later green
executor observation supersedes an earlier failure, which is how \"no
subsequent execution completed green\" is derived. Keys on the
newly-exported `EXECUTOR_FAILED_INCOMPLETE_REASON` constant (already
load-bearing for FN-7577 feed dedup) as the single source of truth.
- **`evaluateNoOpFinalizeExecutorVeto`** — the veto decision.

Wired into the AI **empty-merge lane** (`merger-ai.ts`), composed with
(and independent of) the FN-6461 no-commits guard: on veto it sets
`error`, writes a durable log entry, emits
`overseer:no-op-finalize-vetoed-failed-executor`, and moves the task
back to `todo` with progress preserved — mirroring the FN-6461 blocked
lane. The move-to-todo transition takes the task out of the merge lane,
so the event isn't re-emitted every poll (equivalent to the
`overseer:oversight-withheld-human-control` per-(taskId, reason) dedup).

Independent of the sibling Task 2 merger-layer lineage guard: both can
fire; **either alone stops FN-8141**.

## Surface enumeration

- **Empty (zero-diff) no-op merge** — vetoed when memory is
failed-incomplete. ✅
- **Non-empty (real squash landed) merge** — **never** vetoed
(reviewers/merge review cover real diffs). ✅
- **failed-incomplete then later green execution** — memory superseded →
no veto. ✅
- **No executor memory / store lacks the async reader** — fail open (no
veto); FN-6461 + sibling guard remain the safety net. ✅
- **user-paused / autoMerge:false / approval-blocked** — defers to
FN-7514 human-control; never fights those semantics. ✅
- **Timeline entry filtering** — only `stage=executor` +
`action=observe` entries count as signals (retry/escalate messages
ignored). ✅

## Test evidence

`pnpm --filter @fusion/engine exec vitest run
src/__tests__/overseer-noop-finalize-veto.test.ts
src/__tests__/merger-ai.test.ts --silent=passed-only --reporter=dot`

```
Test Files  2 passed (2)
     Tests  48 passed (48)
```

Covers: derivation (failed→veto, failed-then-green→no-veto,
non-executor/non-observe ignored, empty→null); evaluator (veto, green,
no-memory, non-empty never-vetoed, user-paused defer, autoMerge:false
defer, missing-task fail-open); and an engine integration test driving
an **FN-8141-shaped** empty merge through `runAiMerge` → asserts
move-to-todo + `overseer:no-op-finalize-vetoed-failed-executor` audit
event + main untouched, plus the later-green case finalizing done.

`@fusion/core` builds clean (`pnpm --filter @fusion/core build`).

**Note on `pnpm verify:fast`:** it currently fails to build
`@fusion/engine`, but **only** in `auth-storage.ts` / `pi.ts` /
`provider-registration.ts` — the pre-existing pi-SDK breakage that *is*
this incident (pi 0.80.x removed
`AuthStorage`/`ModelRuntime`/`ModelRegistry`; tracked as FN-8145).
Verified identical errors with my changes stashed; **my diff touches
none of those files and adds zero new type errors** (tsc reports all
program errors before failing — none were in my files).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Prevented zero-change tasks from being incorrectly finalized when the
latest executor attempt failed with unfinished work.
- Preserved task progress and returned affected tasks to **Todo** for
continued processing.
- Allowed finalization to proceed after a subsequent successful executor
result.
  - Maintained existing human-control and non-empty merge behavior.
  - Added audit visibility for blocked finalization events.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 20:29:46 -07:00
gsxdsm
f079245acc fix(engine): defensive getAuth guard on session routing seam + realign pi tests to ModelRuntime seam (#2261)
## What / Why

**Reworked after FN-8179 (`fd43a57a4`) landed on `main`.** FN-8179 did
most of what the original PR #2261 did — pinned `@earendil-works/pi-ai`
/ `pi-coding-agent` to `^0.80.10` everywhere, added the
`createSessionOptions` `NonNullable` typing in `pi.ts`, and regenerated
`pnpm-lock.yaml`. This PR was rebased onto current `origin/main` and
reduced to **only the unique residual not covered by FN-8179**.

### Residual change 1 — defensive `getAuth` guard

The FN-8142 migration rewrote `attachSessionRoutingHeaders` from the
`ModelRegistry.getApiKeyAndHeaders` seam to `ModelRuntime.getAuth`, but
dropped the pre-migration defensive invariant: a missing resolution
method must **not** break session creation. On `main` the function now
calls `modelRuntime.getAuth.bind(...)` unguarded, which throws if
`getAuth` is ever absent.

This restores the guard: no-op (warn) when `getAuth` is missing, so a
future pi rename degrades to un-tagged requests instead of a hard
failure at every agent start.

### Residual change 2 — test realignment (needed against current main)

FN-8179 aligned the SDK but did **not** update the two `#1675`
routing-header test suites, which still asserted the old
`getApiKeyAndHeaders` seam. **Verified RED on current `main` before
touching them:**

- `pi-create-fn-agent.test.ts` — **60 / 104 failing** (mock had no
`ModelRuntime` export; `createAgentSession` now receives
`modelRuntime`).
- `pi-session-routing-headers.test.ts` — **4 / 5 failing**
(`attachSessionRoutingHeaders` signature is `getAuth`, not
`getApiKeyAndHeaders`).

Both are realigned to the `ModelRuntime.getAuth` seam (the mock gains
the `ModelRuntime` export) → **109 / 109 green**. Assertions were
strengthened to the new behavior, not weakened; the #1675 precedence
invariant (taskId > pi session id > no-wrap), header merge,
apiKey/provider-header passthrough, failed/undefined passthrough, and
absent-method no-op are all still asserted.

## Surface enumeration

- **Routing-header seam**: both the `createFnAgent` path and the
`attachSessionRoutingHeaders` unit (taskId / pi-session-id / no-id
precedence; header merge; apiKey + provider-header passthrough;
failed/undefined resolution; absent-method no-op).
- **Both mock forms** in `pi-create-fn-agent.test.ts`: the top-level
`vi.mock` and all three `vi.doMock` skill-selection blocks now export
`ModelRuntime`.

## Test evidence

- `pnpm --filter @fusion/engine exec tsc --noEmit` → **exit 0**.
- `pnpm --filter @fusion/engine exec vitest run` on both suites →
**109/109 pass** (was 64 failing on main).
- `pnpm verify:fast` → **PASS** (typecheck + build scoped to changed
packages + CLI build + boot smoke `GET /api/health 200`; no tests run).

Do not merge without CI. No release performed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved session creation reliability when authentication support is
unavailable.
* Preserved existing authentication details while adding session-routing
headers when supported.
* Prevented session creation from failing when authentication
information cannot be resolved.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 20:20:59 -07:00
gsxdsm
ced0e84f5d FN-8184: unify heartbeat multiplier timing
Keep scheduler repair and reports health aligned with the effective heartbeat cadence.

- Apply heartbeatMultiplier once through shared interval calculations.
- Align scheduler repair and reports-health stale thresholds with scaled cadence.
- Add multiplier regression coverage and document the effective timing rules.

Files changed:
 .../fn-8184-heartbeat-multiplier-consistency.md    |   7 ++
 docs/agents.md                                     |   8 +-
 .../src/__tests__/heartbeat-executor.test.ts       |  43 +++++++++
 .../src/__tests__/heartbeat-scheduler.test.ts      |  46 ++++++++++
 packages/engine/src/agent-heartbeat.ts             | 101 ++++++++++++++++-----
 5 files changed, 176 insertions(+), 29 deletions(-)

Fusion-Task-Id: FN-8184

Fusion-Task-Lineage: 05b79c5e-444a-42c3-9073-9c8c05df5def

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 20:16:36 -07:00
gsxdsm
26cb0cc2e1 FN-8180: add Kimi K3 catalog and pricing support
Expose Kimi K3 throughout the native pi catalog and token-cost surfaces.

- Add K3 pricing and normalized lookup coverage for Dashboard cost calculations.
- Verify native K3 registry, API model route, and repeat-stop behavior end to end.
- Add a minor release changeset for Kimi K3 model selection support.

Files changed:
 .changeset/fn-8180-pi-sdk-kimi-k3.md               |  7 +++
 packages/core/src/__tests__/model-pricing.test.ts  |  7 ++-
 packages/core/src/model-pricing.ts                 | 17 +++++-
 ...ister-model-routes-kimi-k3-supplemental.test.ts | 70 ++++++++++++++++++++++
 .../pi-prompt-session-and-check-recursion.test.ts  | 15 +++++
 .../src/__tests__/provider-registration.test.ts    | 17 ++++++
 6 files changed, 130 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8180
Fusion-Task-Lineage: 07598d73-c866-4329-b5cf-3aebc1c0a180
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 19:48:11 -07:00
gsxdsm
136958fc60 fix(engine): stranded-completed promoters withhold tasks whose last execution ended in a failure park (#2257)
## What & why

FN-8141 laundered a failed task into `done`. The executor correctly
parked the task `failed` ("task parked failed during no-fn_task_done
retry" / "fn_task_done refusal retry budget exhausted"), the pause-abort
machinery bounced it to `todo`, and ~12 minutes later
`recoverStrandedCompletedTodoTasks` promoted it to `in-review` because
every step was done/skipped — overriding the honest failure park. From
there the AI merger found an empty diff and finalized it as a no-op
`done`, with no reviewer ever seeing it.

Existing exclusions (`task.error`,
`evaluateNoCommitsNoOpFinalize().blocked`, active statuses, refreshing
review state) all missed it because the failure provenance lived **only
in the durable task log** by the time the promoter ran — status/error
had been cleared by the pause-abort bounce.

This PR restores the invariant: **a stranded-completed promoter must not
promote a task whose most recent execution lifecycle ended in a
failure/refusal park.**

## Change

- New pure, unit-testable evaluator
`evaluateCompletedPromotionFailureProvenance(task)` in `@fusion/core`
(next to `no-commits-finalize-guard.ts`). It scans the task-log **tail**
(bounded to 250 entries) and lets the **most-recent execution-outcome
marker** decide: a failure/refusal park → `{ blocked: true, reason:
"failure-provenance" }`; a fresh clean completion (`Task marked done by
agent` / `All steps complete — implicit fn_task_done`) that appears more
recently supersedes an earlier park; zero failure markers → not blocked.
Recency is by construction, so a failure that predates a newer clean
execution is never reached.
- Both self-healing sweeps (`recoverCompletedTasks` stuck-in-progress
**and** `recoverStrandedCompletedTodoTasks` stranded-todo) fetch the
full task for candidates that already cleared the cheap slim filters
(slim listings strip `log`) and skip when blocked, emitting a
**deduped** `task:reconcile-stranded-completed-no-action` run-audit
event (ids/outcomes-only: `taskId`, `reason`, `sweep`, `marker?`).
- Defense-in-depth: the shared executor `recoverCompletedTask`
chokepoint — which the sweeps AND the executor's own
unpause/`resumeOrphaned` fast-paths all funnel through — also refuses a
provenance-blocked promotion, so no route can launder a failed park.

**Escape hatch (documented in FNXC comments):** an operator
retrying/moving the task starts a fresh execution whose clean-completion
marker supersedes the failure park, clearing the block with no code
change.

## Surface enumeration

- `recoverCompletedTasks` (stuck-in-progress sweep, self-healing.ts) —
guarded + audited.
- `recoverStrandedCompletedTodoTasks` (stranded-todo sweep,
self-healing.ts) — guarded + audited. FN-8141 shows both columns can
launder.
- `recoverCompletedTask` executor callback (the route both sweeps +
unpause + `resumeOrphaned` share) — verified it did **not** check
log-based provenance; added the guard there as the final chokepoint.

## Test evidence

Pure-evaluator unit tests (`@fusion/core`) — marker detection,
most-recent-outcome recency, supersede-by-clean-completion,
empty/missing log, tail-scan bound:
```
pnpm --filter @fusion/core exec vitest run src/__tests__/completed-promotion-failure-provenance.test.ts
  Test Files  1 passed (1)   Tests  9 passed (9)
```

Self-healing integration tests (`@fusion/engine`) — FN-8141-shaped todo
(3 done + 2 skipped + refusal-exhaust/park marker) is NOT promoted and
emits the no-action event exactly once (deduped across a second cycle);
same task after a fresh clean execution IS promoted; stuck-in-progress
variant covered:
```
pnpm --filter @fusion/engine exec vitest run src/__tests__/self-healing.test.ts -t "recoverCompletedTasks|recoverStrandedCompletedTodoTasks|FN-8141"
  Test Files  1 passed (1)   Tests  14 passed | 382 skipped (396)
```

`@fusion/core` builds clean. My engine changes add **zero** new type
errors (verified: all 13 engine build errors are the pre-existing pi-SDK
cluster in `auth-storage.ts`/`pi.ts`/`provider-registration.ts`, none in
`self-healing.ts`/`run-audit.ts`/`executor.ts`/the new file).

## Known environmental blocker

`pnpm verify:fast` cannot go green on this branch: the `@fusion/engine`
build is **already broken at baseline** (confirmed by stashing all my
changes) by the pi 0.80.x SDK migration errors
(`ModelRegistry`/`AuthStorage`/`ModelRuntime`) — the exact FN-8145
upstream breakage described in the FN-8141 incident. That is out of
scope for this task and independent of this diff. Likewise, the 22
pre-existing
`restart.integration.test.ts`/`executor-fast-mode-workflows.test.ts`
failures are identical with and without my changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 19:36:57 -07:00
gsxdsm
1c4fdb7b59 fix(engine): require already-landed proof before finalizing an empty AI merge as done (#2259)
## What & why

Task FN-8141 laundered a failed task into `done`: its branch had no net
changes vs `main` **only because the executor reverted its own work five
times**, and the AI empty-merge lane took the "empty means
already-landed or nothing-to-do → finalize as no-op done" path, stamping
`mergeConfirmed: true` with no reviewer or operator sign-off.

This restores the invariant: **a commit-expected task that reaches the
empty AI-merge outcome must not finalize `done` without positive
evidence the work already landed.**

`packages/engine/src/merger-ai.ts` empty-outcome lane now, for a
commit-expected task (`noCommitsExpected !== true`), requires one of:
1. Durable recorded landing on the task
(`mergeDetails.mergeConfirmed`/`commitSha`).
2. A prior AI no-op finalization proof pair in the task log
(`hasPriorAiNoOpFinalizationProof`, FN-7261 shape).
3. The branch tip is an **ancestor of the integration branch**
(fast-forwarded / zero-ahead / already-integrated).
4. The already-on-main classifier (`detectAlreadyLandedOnMain`) finds a
distinct landing commit via a **strong** strategy
(`trailer`/`ancestry`/`patch-id`).

The classifier's weak `tree-equal`/`no-diff` strategies are
**deliberately rejected**: a branch that committed work then reverted it
back to base has a tree equal to `main` (main never advanced), which is
exactly the FN-8141 shape and would false-positive. Absent proof, the
task gets `task.error` set, emits run-audit
`task:empty-merge-finalize-blocked-no-landed-proof`
(ids/counts/outcomes-only), and is moved back to `todo` with progress
preserved — mirroring the existing FN-6461 blocked lane.

`noCommitsExpected === true` tasks are untouched (hardened separately in
the sibling Task 1). The non-empty landed path, group/PR routing, and
push-after-merge behavior are unchanged.

## Surface enumeration
- **Single-repo empty-outcome finalize (primary lane)** — guarded in
`runAiMerge`.
- **Workspace/multi-repo caller** — `landWorkspaceTask`'s all-empty
finalize is a second route. Already-landed sub-repos are proven up front
by `findProvenLandedCommit` and marked `status:"landed"`; when
`landedCount === 0` the guard re-checks each empty sub-repo's branch and
blocks the FN-8141 reverted shape (tip not an ancestor / branch
vanished) identically. (Note: the genuinely-integrated all-empty
workspace case already throws `missing-merge-confirmation` on
`mergeConfirmed:false`, so it never reached `done`; that pre-existing
path is left intact.)
- **Re-promotion ping-pong** — the blocked path sets `task.error`, and
`recoverStrandedCompletedTodoTasks` excludes any task with `task.error`,
so the promoter cannot re-promote the unchanged blocked task.
Regression-tested.

## Test evidence
Scoped tests (all green):
```
vitest run merger-ai.test.ts workspace-merger.test.ts  → 46 passed
vitest run self-healing.test.ts -t recoverStrandedCompletedTodoTasks → 4 passed
vitest run merger.test.ts merger-finalize-unproven.real-git → 20 passed
vitest run self-healing-workspace + workspace-merger-lease + workspace-merger-deps-resilient → 26 passed
```
New tests:
- merger-ai.test.ts: commit-expected empty (reverted) → blocked to todo
+ error + audit event, NOT done; empty + prior no-op proof → still no-op
done; empty + branch-ancestor-of-main → still no-op done;
noCommitsExpected empty → unchanged done path.
- workspace-merger.test.ts: all-empty (reverted) workspace → blocked to
todo + error, not done / not `task:merged`.
- self-healing.test.ts: a task blocked by this guard (all steps
done/skipped, `task.error` set) is NOT re-promoted by
`recoverStrandedCompletedTodoTasks`.

**`pnpm verify:fast` is red on this branch due to the pre-existing pi
SDK breakage** (`auth-storage.ts`/`pi.ts`/`provider-registration.ts` —
the FN-8142/FN-8145 `AuthStorage`/`ModelRegistry` removal that is the
root of the FN-8141 incident). Verified those identical build errors
reproduce with my changes stashed; this PR adds **zero** new type errors
(no build error is in `merger-ai.ts` or `run-audit.ts`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 19:36:41 -07:00
gsxdsm
19eb179473 fix(core): block empty-diff finalize of tasks with skipped steps — generalized FN-6461 guard (#2254)
## What & why

FN-8141 (\"Update pi SDK to latest and verify Kimi K3 end to end\") was
**laundered into `done` despite producing zero net changes**. The pi SDK
bump kept breaking `verify:fast`, the work was reverted 5×, and the
agent used the sanctioned skip affordance to mark **Testing &
Verification** and **Documentation & Delivery** `skipped`.
`isTaskComplete()` counts `skipped` as complete, so:

1. self-healing `recoverStrandedCompletedTodoTasks` promoted the todo
task to in-review (all steps done/skipped),
2. the AI merger saw an empty diff vs main → \"finalizing as no-op\" →
`done` with `mergeConfirmed:true`,
3. no reviewer ever saw it (skipped steps request no review; the
merge-review pass reviews an empty diff).

The only existing guard, `evaluateNoCommitsNoOpFinalize` (FN-6461),
missed it **twice**: it only fired when `noCommitsExpected === true`
(FN-8141 was commit-expected — the branch was empty because work was
*reverted*), and even then only blocked when `incomplete >= done`
(FN-8141 had 3 done vs 2 skipped).

## The fix

Generalize the guard (same exported name/shape — every finalize lane
keeps working) so a **zero-diff/no-op finalize is blocked whenever ANY
step is `skipped`**:

- a **verification-flavored** skipped step (name matching
`/test|verif|qa|review/i`) blocks **unconditionally**;
- any **other** skipped step blocks **unless** every non-skipped step is
`done` **AND** the task is the legacy `noCommitsExpected` ops shape;
- the legacy FN-6461 ratio rule (`noCommitsExpected` + `incomplete >=
done`) is preserved for pending/in-progress incomplete work;
- blocked evaluations return a precise `reason` naming the skipped
steps.

Legitimate shapes still pass: all-done no-skip empty diffs (left to the
lineage-proof work), zero-step tasks, and `noCommitsExpected` ops tasks
with a minor non-verification skipped tail.

## Surface enumeration

The guard is the single chokepoint used at every zero-diff finalize
lane; all already honor `.blocked`/`.reason`, so the core change fixes
each surface:

- `packages/engine/src/merger-ai.ts` ~1116 — AI empty-merge lane
- `packages/engine/src/merger.ts` ~6261 / ~7354 / ~7658 — merger
empty-own-diff + no-op lanes
- `packages/engine/src/self-healing.ts` ~2851 — stranded-todo promoter
pre-check; ~6335 — no-op review finalize

Behavior on block is unchanged (error set, durable log entry,
`task:no-commits-finalize-blocked-incomplete-steps` run-audit event,
move back to todo with progress preserved).

## Test evidence

- **Core** `pnpm --filter @fusion/core exec vitest run
src/__tests__/no-commits-finalize-guard.test.ts` → **9 passed**. Covers
FN-8141 shape (3 done + 2 skipped, not noCommitsExpected → blocked),
verification-skip blocks regardless of ratio/`noCommitsExpected`, legacy
`noCommitsExpected` shapes, all-done no-skip → not blocked, zero steps →
not blocked.
- **Engine lanes** — one test per finalize-lane family, all green:
- `merger-ai.test.ts` (AI empty lane, incl. new FN-8141
reverted-commit-expected case) → **36 passed**
  - `merger-finalize-unproven.real-git.test.ts` (merger lanes) → passing
- `self-healing.test.ts` (stranded-todo promoter + no-op review
finalize, incl. new FN-8141 promoter case) → **394 passed**

### `pnpm verify:fast` — pre-existing engine build breakage (not this
PR)

`verify:fast` fails at the workspace-dist bootstrap because
`@fusion/engine` does **not** typecheck on `main`:
`src/auth-storage.ts`, `src/pi.ts`, `src/provider-registration.ts`
reference `ModelRuntime` / `AuthInteraction` / `CredentialInfo` /
private `ModelRegistry` members removed by pi 0.80.9/0.80.10 (the
FN-8142 migration that motivated this incident; upstream fix is
FN-8145). Verified this failure reproduces with my changes **stashed**
(13 identical tsc errors at clean HEAD). This PR touches only
`@fusion/core` (builds clean, `tsc` exit 0) and engine **test** files —
no engine source — so it neither causes nor can resolve that breakage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Prevented empty or no-op finalization when verification, QA, review,
or other required steps are skipped.
* Ensured tasks with skipped work are not incorrectly marked complete,
merged, or promoted during recovery.
* Improved error messages to identify skipped verification steps
blocking completion.
* **Tests**
* Added regression coverage across finalization, merge, and self-healing
workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 19:36:02 -07:00
gsxdsm
9a37415887 fix(engine): add honest blocked exit to fn_task_done so impossible tasks park failed instead of laundering to done (#2256)
## What & why

FN-8141 ("Update pi SDK to latest and verify Kimi K3 end to end") was
impossible as specced — pi 0.80.x removed `AuthStorage`/`ModelRegistry`
APIs, so every SDK bump broke the build. The executor correctly reverted
its work and filed follow-up FN-8145 — but had **no sanctioned way to
end the task in a blocked state**. `fn_task_done` only expressed
success: the bulk-completion gate refused it, the requeue budget re-ran
the doomed task 5 times, and the only remaining affordance (mark every
step `skipped`, then complete) made `isTaskComplete()` return true.
Self-healing then promoted the "complete" todo to in-review and the AI
merger finalized the empty diff as `done`. **The honest path must be
cheaper than the laundering path.**

This adds a first-class **blocked** outcome to the executor's
`fn_task_done` tool.

## Change

- `fn_task_done` gains `outcome: "completed" | "blocked"` (default
`"completed"`), optional `blockedBy: string[]`, and `reason` (required
when blocked).
- `outcome="blocked"` runs **before** every completion gate (completion
blocker, verdict providers, worktree invariants, bulk-completion
refusal) — blocked is not a completion claim, so none of those gates
apply.
- Parks the task `failed` with `error = "BLOCKED: <reason>"`, following
the FN-7863 `EXECUTION_DISPATCH_LOOP_EXHAUSTED` park convention: **steps
keep their true statuses** (no auto-done, no auto-skip), worktree/branch
preserved. It does **not** call `onDone()`, so the executor's existing
`status === "failed"` post-loop branch honors the park instead of
handing off to review.
- `blockedBy` is recorded as real `task.dependencies` edges (unioned
with existing) so the task requeues behind the blocker.
- Emits run-audit `task:execution-blocked-parked` with ids/outcomes-only
metadata (`taskId`, `blockedBy` ids, `hasReason` boolean — **never** the
reason prose).
- Executor + core prompt guidance and the
`bulk-step-completion-without-review` refusal message now name the
blocked exit as **the** correct action when work cannot proceed,
replacing skip-and-done. `PREMISE STALE:` skip guidance is preserved for
genuinely-stale premises.

## Surface enumeration

- **fn_task_done tool schema + handler**
(`packages/engine/src/executor.ts`): blocked branch added at the top of
`execute`, before all gates.
- **Refusal/requeue machinery**: `formatTaskDoneRefusal` for
`bulk-step-completion-without-review` now points at the blocked exit;
the requeue-budget path is untouched (blocked never enters it).
- **Executor prompt text**: turn-ending rules, the "Cannot proceed"
section, the preflight/stale-premise escape hatch (now explicitly
distinguishes stale-premise skip from blocked).
- **Core prompt mirror** (`packages/core/src/agent-prompts.ts`): same
turn-ending + cannot-proceed guidance.
- **Tool reference doc**
(`packages/cli/skill/fusion/references/engine-tools.md`): `fn_task_done`
params updated. (grep for `fn_task_done` confirmed the only executable
tool schema is in executor.ts; CLI/pi surfaces re-export it, no separate
schema copy.)
- **Self-healing**: verified a blocked-parked row is NOT auto-recovered
by `recoverStrandedCompletedTodoTasks` — its steps are not all
done/skipped and `task.error` is set (both are hard filters in the
sweep).
- **Run Audit inventory** (`AGENTS.md`): documented the new event.

## Test evidence

New `packages/engine/src/__tests__/executor-task-done-blocked.test.ts`
(8 tests) asserts the invariant across surfaces:

```
pnpm --filter @fusion/engine exec vitest run \
  src/__tests__/executor-task-done-blocked.test.ts \
  src/__tests__/executor-task-done-invariant.test.ts \
  src/__tests__/gating-classifications.test.ts \
  src/__tests__/reliability-interactions/execute-requeue-loop-guard.test.ts --reporter=dot
→ Test Files 3 passed | Tests 138 passed (0 failed)
```

Coverage: blocked parks failed with `BLOCKED:` error and does **not**
trip the bulk-completion refusal or requeue to todo; `blockedBy` unioned
into `dependencies`; `task:execution-blocked-parked` emitted with
metadata that excludes the reason prose; steps left untouched; empty
`reason` rejected without parking; `completed` outcome unchanged (still
marks steps done, no blocked audit); and
`recoverStrandedCompletedTodoTasks` never promotes a blocked-parked row.

### Note on `pnpm verify:fast`

`verify:fast` currently fails at the workspace build step due to
**pre-existing** type errors in `packages/engine/src/auth-storage.ts`,
`pi.ts`, and `provider-registration.ts` — the exact FN-8142 pi SDK API
break that FN-8145 will fix. These are present on the base branch and
untouched by this PR. Verified instead that this change introduces
**zero** new type errors (`tsc` diff before/after, engine and core both
clean) and that all scoped tests are green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus <noreply@anthropic.com>
2026-07-16 19:35:37 -07:00
gsxdsm
fd43a57a41 FN-8179: align pi SDK versions with ModelRuntime API
Align workspace pi SDK dependencies with the ModelRuntime API required by the engine.

- Pin pi AI and coding-agent packages to 0.80.10 across workspace consumers.
- Keep session option typing compatible with the updated SDK contract.
- Add a patch changeset and regenerate the dependency lockfile.

Files changed:
 .changeset/fn-8179-pi-sdk-align.md                 |   7 +
 packages/cli/package.json                          |   4 +-
 packages/core/package.json                         |   2 +-
 packages/dashboard/package.json                    |   2 +-
 packages/engine/package.json                       |   4 +-
 packages/engine/src/pi.ts                          |   8 +-
 packages/pi-claude-cli/package.json                |   8 +-
 .../src/thinking-config.ts                         |   9 +-
 pnpm-lock.yaml                                     | 947 +++++++++++----------
 pnpm-workspace.yaml                                |   5 +
 10 files changed, 520 insertions(+), 476 deletions(-)

Fusion-Task-Id: FN-8179

Fusion-Task-Lineage: aef45c2e-f353-4014-93de-44be91f43293

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 19:01:03 -07:00
gsxdsm
b687cc994e FN-8174: preserve live triage planning sessions
Keep active planning sessions protected from stale recovery while reclaiming genuinely hung triage work.

- Retain stale processing entries that still have a live, non-aborted triage session.
- Continue evicting no-session and stuck-aborted tasks so recovery can proceed.
- Add triage and self-healing regression coverage, architecture guidance, and a patch changeset.

Files changed:
 .changeset/fn-8174-planning-premature-todo.md      |   7 ++
 docs/architecture.md                               |   1 +
 packages/engine/src/__tests__/self-healing.test.ts | 102 +++++++++++++++++++++
 packages/engine/src/__tests__/triage.test.ts       |  37 +++++++-
 packages/engine/src/triage.ts                      |  48 +++++-----
 5 files changed, 168 insertions(+), 27 deletions(-)

Fusion-Task-Id: FN-8174

Fusion-Task-Lineage: f6811d72-95b4-4b5f-a71f-212f50e3ecdd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 18:54:19 -07:00
gsxdsm
5a60643c0a FN-8142: migrate auth storage and model runtime to pi SDK
Migrate Fusion's credential and model integrations to pi SDK 0.80.8+.

- Replace legacy AuthStorage initialization with a locked Fusion credential store and ModelRuntime-backed registry.
- Wire asynchronous model initialization and refresh through CLI, desktop, dashboard, executor, and provider paths.
- Update provider, routing, and registry tests for the new SDK contracts.

Files changed:
 packages/cli/src/commands/__tests__/daemon.test.ts |   2 +-
 .../cli/src/commands/__tests__/dashboard.test.ts   |   9 +-
 .../cli/src/commands/__tests__/onboard.test.ts     |   1 +
 packages/cli/src/commands/__tests__/serve.test.ts  |   2 +-
 packages/cli/src/commands/daemon.ts                |  19 +-
 packages/cli/src/commands/dashboard.ts             |  20 +-
 packages/cli/src/commands/onboard.ts               |   6 +-
 packages/cli/src/commands/serve.ts                 |  19 +-
 packages/cli/src/commands/startup-model-sync.ts    |   4 +-
 packages/core/src/__tests__/openai-models.test.ts  |  17 +-
 ...-model-routes-openai-codex-supplemental.test.ts |  17 +-
 ...register-model-routes-zai-real-registry.test.ts |  15 +-
 packages/dashboard/src/routes.ts                   |  12 +-
 .../dashboard/src/routes/register-model-routes.ts  |   2 +-
 packages/desktop/src/local-runtime.ts              |   2 +-
 packages/desktop/src/local-server.ts               |   2 +-
 .../custom-providers-openai-completions.test.ts    |  16 +-
 .../custom-providers-openai-responses.test.ts      |  16 +-
 .../engine/src/__tests__/executor-test-helpers.ts  |   2 +-
 .../src/__tests__/pi-create-fn-agent.test.ts       |   8 +-
 .../engine/src/__tests__/pi-layers-wiring.test.ts  |   2 +-
 packages/engine/src/__tests__/pi.test.ts           |  47 ++---
 .../src/__tests__/provider-registration.test.ts    |  17 +-
 packages/engine/src/auth-storage.ts                | 218 ++++++++++++++++++---
 packages/engine/src/custom-provider-registry.ts    |  14 +-
 packages/engine/src/executor.ts                    |  15 +-
 packages/engine/src/pi.ts                          |  50 +++--
 packages/engine/src/provider-auth.ts               |  58 +++---
 packages/engine/src/provider-registration.ts       |  16 +-
 29 files changed, 421 insertions(+), 207 deletions(-)

Fusion-Task-Id: FN-8142

Fusion-Task-Lineage: 8ae79064-7820-4976-9645-9431b5a3129e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 18:13:37 -07:00
gsxdsm
ca7a5a7106 FN-8144: remove workspace worktrees on archive
Archive workspace task worktrees synchronously and safely across archive entry points.

- Add store-scoped workspace disposal planning, reservations, and quarantine handling.
- Install baseline and executor disposers that remove per-repository worktrees and branches without shell interpolation.
- Cover disposal-plan deduplication and document the archive cleanup behavior.

Files changed:
 .../fn-8144-archive-removes-workspace-worktrees.md |   7 ++
 AGENTS.md                                          |   1 +
 docs/task-management.md                            |   4 +
 .../archive-removes-workspace-worktrees.test.ts    |  59 +++++++++++
 packages/core/src/archive-worktree-disposer.ts     |  52 ++++++++++
 packages/core/src/index.gate.ts                    |   8 ++
 packages/core/src/index.ts                         |   8 ++
 .../core/src/task-store/archive-lifecycle-2.ts     |  29 ++++--
 packages/core/src/task-store/archive-lifecycle.ts  | 114 ++++++++++++++++++++-
 .../src/archive-worktree-disposer-install.ts       |  27 ++++-
 packages/engine/src/executor.ts                    |  25 ++++-
 11 files changed, 319 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-8144

Fusion-Task-Lineage: 1c4b65f3-a1d2-4a5c-a4b6-c263f9e6f61d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 17:05:29 -07:00
gsxdsm
ee28f5e45e FN-8155: test triage retry title handling
Cover blank-title behavior across retryable triage failures.

- Verify deterministic validation retries retain blank task titles
- Verify transient failures retain blank task titles while retries remain

Files changed:
 packages/engine/src/__tests__/triage.test.ts | 81 ++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)

Fusion-Task-Id: FN-8155

Fusion-Task-Lineage: d8b4414d-b105-4251-bc42-d02a6d84bf3a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 16:38:25 -07:00
gsxdsm
c6be0b158b FN-8129: centralize database backup settings
Move database backup policy and scheduling to shared global configuration.

- Split project memory backups from cluster-wide database backup settings.
- Migrate legacy backup values and routines safely into central global storage.
- Schedule and dispatch one shared PostgreSQL backup routine across project engines.

Files changed:
 .changeset/fn-8129-backup-settings-scope-split.md  |   7 +
 docs/dashboard-guide.md                            |   2 +
 docs/settings-reference.md                         |  10 +-
 packages/cli/src/commands/backup.ts                |   3 +-
 .../__tests__/backup-settings-migration.test.ts    |  50 ++++++
 .../src/__tests__/backup-settings-scope.test.ts    |  27 +++
 packages/core/src/backup-settings-migration.ts     | 188 +++++++++++++++++++++
 packages/core/src/backup.ts                        |  77 +++++----
 packages/core/src/global-routine-store.ts          | 104 ++++++++++++
 packages/core/src/index.gate.ts                    |   6 +-
 packages/core/src/index.ts                         |   6 +-
 .../core/src/postgres/migrations/0000_initial.sql  |  19 +++
 .../postgres/migrations/0015_global_routines.sql   |  19 +++
 packages/core/src/postgres/schema-applier.ts       |  19 ++-
 packages/core/src/postgres/schema/central.ts       |  21 ++-
 packages/core/src/postgres/startup-factory.ts      |  11 ++
 packages/core/src/settings-schema.ts               |  14 +-
 packages/core/src/types.ts                         |  31 +++-
 .../dashboard/app/components/SettingsModal.tsx     |  10 +-
 .../settings/__tests__/section-keys.test.ts        |   1 +
 .../app/components/settings/save-split.ts          |   2 +
 .../search/__tests__/settings-search-index.test.ts |   1 +
 .../settings/search/entries.ts                     |   2 +
 .../app/components/settings/section-keys.ts        |   4 -
 .../settings/sections/BackupsSection.search.ts     |  40 -----
 .../settings/sections/BackupsSection.tsx           | 112 +-----------
 .../sections/DatabaseBackupsSection.search.ts      |  51 ++++++
 .../settings/sections/DatabaseBackupsSection.tsx   | 142 ++++++++++++++++
 .../settings-default-descriptions.test.tsx         |   1 +
 packages/dashboard/src/routes.ts                   |  12 +-
 .../src/routes/register-settings-memory-routes.ts  |  41 ++---
 .../engine/src/__tests__/routine-scheduler.test.ts |  55 +++++-
 packages/engine/src/cron-runner.ts                 |   4 +-
 packages/engine/src/routine-runner.ts              |  67 +++++---
 packages/engine/src/routine-scheduler.ts           |  35 +++-
 35 files changed, 929 insertions(+), 265 deletions(-)

Fusion-Task-Id: FN-8129

Fusion-Task-Lineage: af17f39a-7f1c-40ff-8a4a-cd63895cd532

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 16:34:09 -07:00
gsxdsm
478f226a54 test: green full-suite CI after main drift (#2229)
## Summary
Restores green **Full Suite (non-blocking)** runs on `main`. Recent main
merges left i18n key parity, schema baseline bookkeeping (0011→0012),
heartbeat tool inventory (FN-8058 `fn_task_logs_read`), and merger
whitespace-classification mocks (execFile `git diff -p -w :2: :3:`) out
of date, so all four test shards failed.

## Root causes observed on main
- **Shard 4 / `@fusion/i18n`**: missing `skipConfirmationDialogs*` +
`reviewBudgetExhausted` in non-en locales; orphan
`awaitingApprovalPlanReviewReplanCap`
- **Shard 3 / `@fusion/core`**: `SCHEMA_BASELINE_VERSION` advanced to
`0012` while tests still equated it with
`OWNER_PROJECT_ID_SPLIT_VERSION` (`0011`) and omitted `0012` from
applied-migration lists
- **Shards 1–2 / `@fusion/engine`**: tool count/snapshot drift for
`fn_task_logs_read`; merger tests still mocked `git diff-tree` for
trivial classification after the execFile `:2:`/`:3:` cutover; mock
provider `updateTask` arity drift

## Changes
- Locale catalogs: add missing keys, drop orphan key
- Schema applier tests: immutable 0011 identity + baseline 0012 lists
- Heartbeat + gating snapshots: include `fn_task_logs_read`
- Merger unit mocks: recognize `git diff -p -w :2:path :3:path`
- Mock provider: accept optional third `updateTask` arg

## Test plan
- [x] `pnpm --filter @fusion/i18n exec vitest run` — 23/23
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/postgres/schema-applier.test.ts` (immutable + automation
upgrade) — pass
- [x] `pnpm --filter @fusion/core exec vitest run` project-identity +
satellite-fusiondir — pass
- [x] Engine suites from failed CI shards (file-scoped,
hermes/openclaw/paperclip/grok, reliability post-finalize/mission,
heartbeat, gating, merger recovery/prompt, mock-provider, etc.) — pass
- [ ] Full Suite workflow green on merge to main

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Improved project data isolation across backend operations.
- Added safer optional toast handling when UI components render outside
the full application shell.
  - Added support for reading task logs during agent heartbeat sessions.

- **Bug Fixes**
- Prevented runtime probes from hanging and avoided scanning large
binary files.
  - Improved path handling for workspaces with missing descendants.
- Corrected task retry state resets and GitHub import/issue-close
behavior.

- **Style**
  - Improved chat, terminal, and settings spacing.
  - Added clearer accessibility labeling for the auto-merge control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 16:01:29 -07:00
gsxdsm
06a5fd813d refactor: package code organization waves 6–7 (#2166)
## Summary

Waves 6–7 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`),
after #2148.

### Wave 6
| New module | Parent |
|---|---|
| `merger-autostash-labels.ts` | `merger.ts` |
| `types/agent-state.ts` | `types.ts` |
| `app/api/tasks-lifecycle.ts` | `legacy.ts` |
| `task-store/task-row-mappers.ts` | `remaining-ops-3.ts` (rename) |

### Wave 7
| New module | Parent |
|---|---|
| `self-healing-optional-step-revision.ts` | `self-healing.ts` |
| `self-healing-path-utils.ts` | `self-healing.ts` |
| `merger-git-parse` (+ `quoteArg`, `getBranchChangedFiles`) |
`merger.ts` |
| `app/api/settings.ts` | `legacy.ts` |

Public import paths stay stable via re-exports.

## Test plan
- [x] engine + dashboard typecheck (incl. app)
- [x] eslint on touched modules
- [x] merger-autostash / parse-porcelain / getBranchChanged / api-tasks
- [ ] CI merge gate

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated task lifecycle API client (task promotion, merge,
retry/reset/duplicate, pause/unpause, archive/unarchive, revert, plan
approve/reject) plus branch-group and planner oversight actions.
* Added a settings/config API service (effective task settings, update
check/refresh/install).
* Introduced standardized agent lifecycle states with identity/ephemeral
detection helpers.
* **Bug Fixes**
* Improved autostash label compatibility and NUL-delimited changed-file
detection for branch diffs.
* **Refactor**
* Modularized merger labeling/parsing, self-healing helpers, and
lifecycle/type wiring while keeping behavior consistent.
* **Tests**
  * Updated merger verification tests for `git diff -z` output handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 15:42:22 -07:00
gsxdsm
edc64138e0 feat(worktrees): task-pinned worktrees under worktreeNaming "task-id" (#2233)
## Summary

Adds **task-pinned worktrees** for `worktreeNaming: "task-id"`. Under
task-id naming, a task is pinned to exactly one derivable directory
`<worktreesDir>/<lowercased-task-id>` (e.g. `.worktrees/fn-7996`) for
its entire lifecycle — removing the ambiguity that let stale/foreign
`task.worktree` pointers strand a task (the FN-7996 shape).

`recycleWorktrees` stays fully functional and is **mutually exclusive**
with task-id pinning: the two can't be enabled together.

## Behavior

- **Pinned acquisition (`worktreeNaming: "task-id"`, recycling off):**
`acquireTaskWorktree` runs **derive → validate → reuse-or-recreate** at
the derived path — warm-reuse when the dir is a registered, usable
worktree on the task's own branch; otherwise reclaim-in-place
(`removeWorktree` + recreate at the SAME path, never a sibling name). A
disagreeing `task.worktree` cache self-corrects and emits a new
`worktree:pin-rederived` audit event, without consuming worktree-session
retries. The recycle pool is never consulted in pinned mode.
- **Mutual exclusivity:** enabling both `recycleWorktrees` and
`worktreeNaming: "task-id"` is rejected at the settings-write boundary —
HTTP 400 at `PUT /settings`, and an `Error` backstop in
`store.updateSettings` covering the CLI and every other writer
(`assertWorktreeNamingRecycleExclusive`). The runtime also gates pinned
mode on `!recycleWorktrees`, so a legacy on-disk config carrying both
degrades safely to recycling.
- **Settings UI:** the Settings → Worktrees panel enforces the
exclusivity bidirectionally — the *Recycle worktrees* toggle is disabled
while naming is *Task ID*, and the naming select is disabled while
recycling is on — so the conflicting state is unreachable, with help
text explaining why.
- **Byte-inert for the rest:** `random`/`task-title` naming and the
recycle pool (incl. `merger.ts` release) are unchanged;
worktrunk-managed layouts bypass pinning.

## Acceptance criteria (from the plan)

1. ✅ Pinned task dispatched N times only ever touches
`<worktreesDir>/<task-id>` on its own branch
2. ✅ No code path can hand task A's dir to task B (pool bypassed; path
derived from task id)
3. ✅ FN-7996 stale/foreign `task.worktree` self-corrects at next
dispatch (`worktree:pin-rederived`) without consuming session retries
4. ✅ Non-pinned modes with `recycleWorktrees: true|false` are
byte-identical (existing pool tests pass unchanged)
5. ✅ Stale same-name dir (crash leftover / archive→restore) reclaimed in
place, never suffixed
6. ✅ Docs updated (settings-reference, architecture, `worktreeNaming`
type doc); changeset (`minor`, `feature`); FNXC comments encode the
invariant

## Files

- `packages/engine/src/worktree-pinning.ts` — new pure helpers
(`isTaskPinnedWorktreeNaming`, `pinnedWorktreePathForTask`)
- `packages/engine/src/worktree-acquisition.ts` — pinned branch +
branch-match reclaim-in-place
- `packages/engine/src/run-audit.ts` — `worktree:pin-rederived` audit
type
- `packages/core/src/settings-validation.ts` (+ `index.ts`,
`task-store/settings-ops.ts`) — mutual-exclusion validator + wiring
- `packages/dashboard/src/routes/register-settings-memory-routes.ts` —
400 on conflict
-
`packages/dashboard/app/components/settings/sections/WorktreesSection.tsx`
(+ `packages/i18n/locales/en/app.json`) — bidirectional UI exclusivity
- `packages/core/src/types.ts`, `docs/*`, `.changeset/*`

## Verification

- New tests: engine `worktree-pinning` (5) +
`worktree-acquisition-pinned` (7); core
`worktree-naming-recycle-exclusive` (2); dashboard settings-route 400
(3) + WorktreesSection UI exclusivity (3)
- Regression sweep green: 194 engine
worktree/acquisition/pool/executor/merger-release tests, core settings
tests, dashboard i18n/settings-section tests
- `tsc --noEmit` clean for `@fusion/core` and `@fusion/engine`; changed
source files clean; eslint clean
- `pnpm verify:fast` PASS (build + scoped typecheck + boot smoke)

🤖 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**
* Added Task ID worktree naming, providing each task with a stable,
deterministic worktree directory.
* Automatically reuses valid pinned worktrees and recreates stale or
conflicting ones at the same path.
* Added clear settings controls and validation for incompatible Task ID
naming and worktree recycling options.

* **Documentation**
* Updated worktree architecture, settings reference, and in-app guidance
to explain pinned worktrees and configuration constraints.

* **Bug Fixes**
* Improved recovery from stale or incorrect worktree assignments without
consuming session retries.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:37:42 -07:00
gsxdsm
f57dfc03b6 FN-8105: remove archived task worktrees safely
Archive task worktrees through a store-scoped, race-safe disposal lifecycle.

- Reserve pinned worktree paths during archive cleanup and successor creation.
- Reconcile quarantined removals before reusing a pinned path.
- Gate PostgreSQL archival before destructive worktree disposal and wire CLI cleanup.

Files changed:
 .changeset/fn-8105-archive-removes-worktree.md     |   7 +
 docs/task-management.md                            |   4 +
 .../extension-experiment-finalize.test.ts          |   1 +
 .../src/__tests__/extension-fn-secret-get.test.ts  |   1 +
 .../extension-gitlab-tracking.test.ts              |   1 +
 .../cli/src/__tests__/extension-web-fetch.test.ts  |   1 +
 .../task-command-github-import-tracking.test.ts    |   1 +
 packages/cli/src/commands/__tests__/task.test.ts   |   1 +
 packages/cli/src/commands/task.ts                  |   8 +-
 packages/cli/src/extension.ts                      |   4 +
 .../__tests__/worktree-path-reservation.test.ts    |  58 ++++++++
 packages/core/src/archive-worktree-disposer.ts     |  21 +++
 packages/core/src/index.gate.ts                    |  13 ++
 packages/core/src/index.ts                         |  13 ++
 .../core/src/task-store/archive-lifecycle-2.ts     |   8 ++
 packages/core/src/task-store/archive-lifecycle.ts  |  37 +++++
 packages/core/src/worktree-path-reservation.ts     | 149 +++++++++++++++++++++
 .../src/archive-worktree-disposer-install.ts       |  18 +++
 packages/engine/src/executor.ts                    |  16 +++
 packages/engine/src/index.ts                       |   2 +
 packages/engine/src/runtimes/in-process-runtime.ts |   1 +
 packages/engine/src/worktree-acquisition.ts        |  27 +++-
 22 files changed, 388 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8105
Fusion-Task-Lineage: cabb8f52-093f-4986-bfda-2c7601a72579
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 15:27:03 -07:00
gsxdsm
4e4b6be1b4 Plan-approval mailbox notification + triage Plan Review convergence (#2237)
## Summary

Two related changes around the plan-approval flow, plus a fix for
triage's plan-review loop that was stranding tasks at the replan cap.

### 1. Post a mailbox message when a plan needs approval (`feat`)
The ntfy push on `awaiting-approval` already existed end-to-end. This
adds the missing **durable, in-dashboard record**: a `system`-typed
mailbox message linking to the task, written whenever a task enters
`awaiting-approval`.
- Fires **before** the notifications-enabled gate, so a dashboard-only
operator (no ntfy/webhook configured) still gets the record — the whole
point of the mailbox channel.
- `system` type avoids re-triggering the `message:agent-to-user` ntfy
pipeline (no double-notify); idempotent via `sendMessageOnce` (key
`plan-approval:<taskId>`).

### 2. Help triage Plan Review converge before the replan cap (`fix`)
Investigation of three tasks that burned all 8 plan-review replans
without converging (**FN-7996, FN-8105, FN-8108**) found the reviewer
surfaced a *new, deeper* issue each cycle instead of confirming its
prior ones were fixed (goalpost movement), and reviewed specs at
implementation altitude. This addresses the root causes:
- **Feed the spec reviewer its own prior REVISE feedback + the 1-based
replan attempt** so it verifies prior issues rather than moving
goalposts. Gated to `reviewType === "spec"` and `attempt > 1` — **code
review and normal plan review are byte-for-byte unaffected**
(double-verified).
- **Reviewer prompt:** converge-on-re-review rule (don't REVISE for your
own earlier miss), severity ratchet (critical-only at attempt ≥ 3), and
a **Spec Altitude** guard so exact SQL/lock/CAS protocol design is
deferred to code review.
- **Planner prompt:** front-load exhaustive surface enumeration before
writing File Scope, and a storage-architecture ground-truth note
(Postgres-only store, composite PK `(project_id, id)`, `schema-applier`
migrations) to stop the repeated stale-fact REVISE rounds.

## Testing
- `@fusion/core` + `@fusion/engine` typecheck clean.
- Added coverage: reviewer spec-convergence wiring (attempt gating +
code/plan exclusion + severity ratchet), triage prior-feedback
derivation (incl. empty-output→notes fallback), mailbox decoupling
(fires when push disabled) + rejection safety, and assertions for all
new prompt sections.
- Affected suites green: notification-service, reviewer,
triage-plan-review-replan-cap, triage-replan-feedback, agent-prompts.
- `pnpm check:changesets` passes (2 changesets: `@runfusion/fusion`
minor + patch).

## Review
Ran a 6-persona `ce-code-review` (correctness + adversarial on Opus;
maintainability, testing, project-standards, api-contract). Guards
verified unbreakable; no P0/P1 correctness or security issues. Applied
the resulting fixes: decoupled the mailbox write from the push gate
(P2), `??`→`||` in the feedback derivation (P3), de-duplicated the
`specConvergence` ternary (P3), and closed the test-coverage gap the
review flagged.

🤖 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**
- Plans transitioning to **awaiting approval** now write an idempotent,
task-linked **dashboard mailbox** message (approval reason + direct task
link), even if push/notifications are disabled.
- **Bug Fixes**
- Plan Review **replan** behavior now better **converges** on prior
REVISE feedback (including notes fallback) and stops looping at the
replan cap.
- At later attempts (attempt 3+), **REVISE** is applied to **critical**
issues while lower-severity items shift to suggestions.
- **Tests**
- Added/expanded coverage for mailbox messaging and spec-convergence
prompt wiring.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:44:07 -07:00
gsxdsm
d870878a23 FN-7998: add executor alternate model escalation
Add opt-in executor escalation after same-model tool-failure retries are exhausted.

- Persist escalation settings and one-shot task state across SQLite and PostgreSQL stores.
- Retry once on a configured alternate model or scheduler node and audit escalation outcomes.
- Expose escalation controls, documentation, translations, migration, and regression coverage.

Files changed:
 .changeset/fn-7998-executor-escalation.md          |   7 ++
 AGENTS.md                                          |   1 +
 docs/settings-reference.md                         |  13 ++-
 .../core/src/__tests__/settings-defaults.test.ts   |  23 ++++-
 packages/core/src/in-review-stall.ts               |  29 ++++++
 packages/core/src/index.gate.ts                    |   3 +-
 packages/core/src/index.ts                         |   3 +-
 packages/core/src/manual-retry-reset.ts            |   1 +
 .../0014_executor_escalation_attempt.sql           |   2 +
 packages/core/src/postgres/schema-applier.ts       |  17 ++++
 packages/core/src/postgres/schema/project.ts       |   1 +
 packages/core/src/settings-schema.ts               |   4 +
 packages/core/src/store.ts                         |   2 +-
 packages/core/src/task-store/persistence.ts        |   2 +
 packages/core/src/task-store/remaining-ops-2.ts    |   2 +-
 packages/core/src/task-store/remaining-ops-3.ts    |   2 +-
 packages/core/src/task-store/remaining-ops-6.ts    |   2 +-
 packages/core/src/task-store/serialization.ts      |   1 +
 packages/core/src/task-store/task-update.ts        |   2 +
 packages/core/src/types.ts                         |  13 +++
 .../dashboard/app/components/SettingsModal.tsx     |  12 +++
 .../app/components/settings/section-keys.ts        |   4 +
 .../settings/sections/SchedulingSection.search.ts  |  36 +++++++
 .../settings/sections/SchedulingSection.tsx        |   6 ++
 .../settings-default-descriptions.test.tsx         |   4 +
 .../__tests__/executor-tool-failure-retry.test.ts  |  91 +++++++++++++++++-
 packages/engine/src/executor.ts                    | 104 +++++++++++++++++++--
 packages/i18n/locales/en/app.json                  |   8 ++
 28 files changed, 376 insertions(+), 19 deletions(-)

Fusion-Task-Id: FN-7998

Fusion-Task-Lineage: bbce767d-c61a-4667-be62-abc0cc54d8be

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 14:31:59 -07:00
gsxdsm
2c3a777bca FN-8132: recover bare worktree branch collisions
Recover safe worktree creation when a dangling task branch already exists.

- Classify bare branch collisions and preserve foreign or mixed commit history.
- Reuse task-owned branches or recreate merged branches from the pinned start point.
- Audit recovery outcomes and cover native, fallback, and workspace acquisition paths.

Files changed:
 .../fn-8132-worktree-branch-collision-recovery.md  |   7 ++
 docs/architecture.md                               |   1 +
 .../__tests__/worktree-acquisition-backend.test.ts |  69 +++++++++++
 .../worktree-acquisition-workspace.test.ts         |  21 ++++
 .../worktree-backend-branch-collision.test.ts      | 132 +++++++++++++++++++++
 packages/engine/src/branch-conflicts.ts            | 121 +++++++++++++++++++
 packages/engine/src/run-audit.ts                   |   1 +
 packages/engine/src/worktree-acquisition.ts        |   3 +-
 packages/engine/src/worktree-backend.ts            |  94 +++++++++++++++-
 9 files changed, 447 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8132

Fusion-Task-Lineage: f09d140f-4fcd-48a6-99b1-a351630f37bd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 13:48:00 -07:00
gsxdsm
60b6e3e048 FN-7996: add configurable executor tool-failure retries
Add bounded, durable same-model retry handling for qualifying consecutive executor tool errors.
- Persist retry claims, cursors, and audit markers with PostgreSQL migrations.
- Expose project retry count, backoff, and failure threshold settings in the dashboard.
- Cover retry, exhaustion, reset, and stale-run safety behavior with tests.

Files changed:
 .changeset/fn-7996-executor-tool-failure-retry.md  |   7 +
 AGENTS.md                                          |   1 +
 docs/architecture.md                               |   1 +
 docs/settings-reference.md                         |  10 ++
 .../executor-tool-failure-retry-claim.test.ts      |  17 +++
 .../core/src/__tests__/manual-retry-reset.test.ts  |   3 +
 .../core/src/__tests__/settings-defaults.test.ts   |  15 +-
 packages/core/src/in-review-stall.ts               |  20 +++
 packages/core/src/index.gate.ts                    |   6 +
 packages/core/src/index.ts                         |   6 +
 packages/core/src/manual-retry-reset.ts            |   3 +
 .../0013_executor_tool_failure_retry.sql           |   4 +
 packages/core/src/postgres/schema-applier.ts       |  17 +++
 packages/core/src/postgres/schema/project.ts       |   3 +
 packages/core/src/settings-schema.ts               |   3 +
 packages/core/src/store.ts                         |  10 +-
 packages/core/src/task-store/persistence.ts        |   7 +
 packages/core/src/task-store/remaining-ops-2.ts    |   2 +-
 packages/core/src/task-store/remaining-ops-3.ts    |   2 +-
 packages/core/src/task-store/remaining-ops-6.ts    |  65 ++++++++-
 packages/core/src/task-store/serialization.ts      |   3 +
 packages/core/src/task-store/task-update.ts        |   6 +
 packages/core/src/types.ts                         |  16 +++
 .../dashboard/app/components/SettingsModal.tsx     |  15 ++
 .../app/components/settings/section-keys.ts        |   3 +
 .../settings/sections/SchedulingSection.search.ts  |  27 ++++
 .../settings/sections/SchedulingSection.tsx        |   4 +
 .../settings-default-descriptions.test.tsx         |   3 +
 .../__tests__/executor-tool-failure-retry.test.ts  | 160 +++++++++++++++++++++
 packages/engine/src/executor.ts                    |  87 ++++++++++-
 packages/i18n/locales/en/app.json                  |   6 +
 31 files changed, 523 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-7996
Fusion-Task-Lineage: d1682ef8-534c-410e-b74c-1f2cf176eac2
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 13:41:14 -07:00
gsxdsm
bd4857a44e FN-8136: update cron-runner backup error expectations
Align cron-runner backup failure assertions with the current PostgreSQL project-state error wording.

- Update legacy command backup failure expectation
- Update command-step backup failure expectation

Files changed:
 packages/engine/src/__tests__/cron-runner.test.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8136

Fusion-Task-Lineage: 8f182491-1ded-43bb-b4ca-02485dce656a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 13:17:00 -07:00
gsxdsm
fdea8a4294 FN-8118: document continuation rescue test coverage
Document why the verified post-done continuation rescue suite remains unquarantined.

- Record the serialized in-memory reliability coverage and its exclusion rationale.
- Preserve the engine-default reliability partition exclusion.

Files changed:
 packages/engine/vitest.config.ts | 2 ++
 1 file changed, 2 insertions(+)

Fusion-Task-Id: FN-8118

Fusion-Task-Lineage: 0ac7f24a-888c-470c-b175-98b4e0411061

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 12:42:17 -07:00
gsxdsm
2142841f0c FN-8111: restore reliability test coverage
Restore PostgreSQL-compatible reliability coverage and prevent completed tasks from wedging on stale continuation recovery.

- Update reliability fixtures and audit assertions for PostgreSQL-backed stores
- Prioritize completed-task handling before stale assistant-continuation retries
- Unquarantine the restored meta-archive and continuation reliability suites

Files changed:
 .../explicit-duplicate-marker-sweep.test.ts        |  4 ++++
 .../meta-archive-guard-composition.test.ts         | 26 +++++++++++++++++-----
 .../post-done-continuation-no-wedge.test.ts        |  3 ++-
 packages/engine/src/executor.ts                    |  7 ++++++
 packages/engine/vitest.config.ts                   |  4 ++--
 scripts/lib/test-quarantine.json                   | 10 ---------
 6 files changed, 36 insertions(+), 18 deletions(-)

Fusion-Task-Id: FN-8111

Fusion-Task-Lineage: 8b30b5cb-c160-44e1-8e8c-dd58f4877edc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 12:09:55 -07:00
gsxdsm
4a797d3804 FN-8117: restore explicit duplicate marker sweep coverage
Configure duplicate-marker PG fixtures with canonical FN task IDs so the sweep coverage exercises real deletion paths.

- Set taskPrefix to FN for duplicate-marker reliability fixtures.
- Remove the corrected test from the PG quarantine ledger and Vitest exclusions.
- Document why valid marker IDs are required for this coverage.

Files changed:
 .../explicit-duplicate-marker-sweep.test.ts          | 20 +++++++++++++-------
 packages/engine/vitest.config.ts                     |  4 +++-
 scripts/lib/test-quarantine.json                     |  5 -----
 3 files changed, 16 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-8117

Fusion-Task-Lineage: 3b09cbbe-924c-4e3c-849b-cf7643b0ac0e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 11:36:32 -07:00
gsxdsm
0b488523c2 FN-8104: retire legacy SQLite database fallbacks
Retire legacy SQLite database calls from PostgreSQL-only startup and self-healing paths.

- Route plugin schema initialization exclusively through the PostgreSQL executor.
- Delegate soft-delete column repair to the PostgreSQL reconciliation seam.
- Remove temporary getDatabase allowlist entries and add no-SQLite regression coverage.

Files changed:
 .../postgres/store-safe-defaults.pg.test.ts        | 14 +++++-
 packages/core/src/store.ts                         | 24 ++++------
 .../engine/src/__tests__/plugin-runner.test.ts     |  6 ---
 .../self-healing-fake-overlap-seam.test.ts         | 44 +++++++++++++++++++
 packages/engine/src/self-healing.ts                | 51 +++++-----------------
 scripts/lib/getdatabase-allowlist.json             | 17 +-------
 6 files changed, 77 insertions(+), 79 deletions(-)

Fusion-Task-Id: FN-8104

Fusion-Task-Lineage: 88dee027-51c3-4c68-94dc-88191fe20330

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 10:33:47 -07:00
gsxdsm
aa07a78f18 fix: recover graph-node missing-worktree failures instead of terminal-parking (FN-7996) (#2231)
## Why

FN-7996 sat in a dispatch→park loop **all day** (06:06→16:35): its
`worktree` metadata pointed at recycled pool worktrees (`coral-badger`,
`grand-ridge` — the latter actually belonged to FN-8069), Plan Review
refused to start in the missing directory, and the task terminal-parked
`failed` every cycle while the planner overseer blindly retried.

Root-cause chain:

1. **`graphFailureValue()` couldn't read optional-group results.**
`runOptionalGroup` publishes context under the group id
(`node:plan-review:value`) and the unqualified template id, but the
failed node is recorded as the materialized
`plan-review::plan-review-step` — the lookup only understood `#` foreach
ids. FN-7977's provider-failure hold *did* classify this failure, but
its hold value was invisible to routing.
2. **No graph-failure router handled the `assertValidWorktreeSession`
refusal**, so it fell to the terminal sink, which parked the task and
*overwrote* `task.error` with a generic message — erasing the signature
the missing-worktree self-healing sweep (in-review-only anyway)
classifies on.
3. **Plan Review didn't need the worktree at all** — its spec is
store-injected (FN-7561) — yet it launched its reviewer in whatever
stale `task.worktree` said.

## What

- `handleGraphFailure` routes unusable-worktree node failures (any node,
any error key, `::`/`#` materialized ids) into the existing bounded
worktree-session recovery: clear stale worktree/branch/session metadata,
requeue to todo, budgeted by `worktreeSessionRetryCount`. An exhausted
budget still falls through to the visible terminal park for human
inspection.
- `graphFailureValue` resolves `group::template` ids (group value first
— it carries post-classification routing intent — then the unqualified
template value). Foreach `#` behavior unchanged.
- Plan Review falls back to the repo root when its recorded worktree is
missing on disk; other read-only gates intentionally keep failing fast
into the new recovery (silently retargeting them to root would review
the wrong tree).
- `recoverMissingWorktreeSessionStartFailure` returns its outcome so the
graph router can distinguish requeue from escalate-exhausted; existing
truthy callers unchanged.

## Symptom Verification

- **Original symptom:** graph-node session-start refusal → `Workflow
graph terminated with failure at node 'plan-review::plan-review-step'`,
task parked failed with stale metadata intact, no recovery.
- **Reproduction:** `graph-node-missing-worktree-recovery.test.ts`
drives `handleGraphFailure` with the exact FN-7996 result shape
(optional-group materialized id + `Refusing to start coding agent in
missing worktree` node error).
- **Assertion it is gone:** the task is requeued to `todo` with
`worktree`/`branch`/`sessionFile` cleared and retry budget incremented —
and is *not* marked `failed`; budget exhaustion still parks visibly.

## Surface Enumeration

- Optional-group template nodes (Plan Review — the repro), write-capable
review gates, and any custom graph node: covered by the
`handleGraphFailure` router (scans exact/materialized/unqualified
`:error` keys).
- Execute-seam session start: already covered by the pre-existing
recovery (unchanged, still passes).
- In-review / merge-active columns: already covered by self-healing
sweeps (unchanged).
- Paused / user-paused / deleted / done tasks: explicitly left to their
owning machinery (guard tests).
- Budget exhaustion: falls through to the visible terminal park (test).

## Testing

- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/reliability-interactions/graph-node-missing-worktree-recovery.test.ts`
— 13 passed
- Adjacent suites (`worktree-incomplete-session-start`,
`executor-graph-requeue-gate`, `workflow-graph-optional-group`,
`executor-paused-abort-todo-benign`) — 78 passed
- `tsc --noEmit` on `@fusion/engine` — clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved recovery when workflow tasks encounter missing or recycled
worktrees.
* Automatically retries affected tasks with stale worktree details
cleared, up to the configured retry limit.
  * Escalates tasks after recovery attempts are exhausted.
* Improved failure routing for optional workflow groups and template
instances.
* Plan Review now falls back to the repository root when its recorded
worktree is unavailable.
* **Tests**
* Added regression coverage for recovery, routing, retry limits, and
repository-root fallback behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:28:11 -07:00
gsxdsm
1a337df5e9 FN-8098: add executor model fallback
Add workflow-specific executor fallback configuration and bounded recovery.

- Add executor fallback provider, model, and thinking settings across core schemas and settings UI.
- Route executor, heartbeat, child, and workflow-step sessions through the executor fallback resolver.
- Retry the primary model after fallback failure before reporting terminal exhaustion.
Files changed:

 .changeset/fn-8098-model-fallback.md               |  7 +++
 docs/settings-reference.md                         |  7 ++-
 .../core/src/__tests__/model-resolution.test.ts    | 13 ++++
 .../core/src/__tests__/settings-parity.test.ts     |  5 ++
 packages/core/src/builtin-workflow-settings.ts     | 24 +++++++
 packages/core/src/index.gate.ts                    |  1 +
 packages/core/src/index.ts                         |  1 +
 packages/core/src/model-resolution.ts              | 21 +++++++
 packages/core/src/settings-schema.ts               |  3 +
 packages/core/src/types.ts                         | 11 ++++
 .../app/components/WorkflowSettingsPanel.tsx       |  8 +++
 .../settings/sections/ProjectModelsSection.tsx     | 10 ++-
 packages/engine/src/__tests__/pi.test.ts           | 14 ++++-
 packages/engine/src/agent-session-helpers.ts       |  7 ++-
 packages/engine/src/executor.ts                    | 48 +++++++-------
 packages/engine/src/pi.ts                          | 73 ++++++++--------------
 packages/engine/src/step-session-executor.ts       |  8 ++-
 17 files changed, 180 insertions(+), 81 deletions(-)

Fusion-Task-Id: FN-8098

Fusion-Task-Lineage: 61b3103b-357b-431a-8d58-411e7806b87b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 08:33:57 -07:00
gsxdsm
14953f39e2 FN-8075: restore PostgreSQL self-healing test coverage
Restore self-healing coverage for PostgreSQL-backed maintenance.

- Update self-healing mocks and assertions for asynchronous audit APIs and PostgreSQL WAL behavior
- Align git command expectations and transient recovery budget coverage with current implementation
- Remove the repaired self-healing suite from the quarantine ledger and gate exclusion

Files changed:
 packages/engine/src/__tests__/self-healing.test.ts | 70 ++++++++++++----------
 packages/engine/vitest.config.ts                   |  1 -
 scripts/lib/test-quarantine.json                   |  5 --
 3 files changed, 39 insertions(+), 37 deletions(-)

Fusion-Task-Id: FN-8075

Fusion-Task-Lineage: 7dfce9b0-9d10-4d70-8e92-8100f6595fce

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 08:11:24 -07:00
gsxdsm
40687cb0be FN-8050: cover cached dependency reconciliation seeding
Ensure raw dependency-reconciliation fixtures invalidate warmed task caches.

- Warm slim task-list caches before corrupt-row seeding in cycle reconciliation tests.
- Document the PostgreSQL-only fixture seam and cache invalidation rationale.

Files changed:
 .../engine/src/__tests__/reliability-interactions/_helpers.ts | 11 ++++++-----
 .../dependency-cycle-reconcile.test.ts                        |  7 +++++++
 2 files changed, 13 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8050

Fusion-Task-Lineage: 5e708d58-faed-4271-a075-24c36eaa5878

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 07:48:12 -07:00
gsxdsm
50ed689379 FN-8044: restore dependency reconcile reliability tests
Restore dependency reconciliation coverage using a PostgreSQL raw-seeding seam.

- Add a cache-invalidating raw task-column seeding helper for corrupt fixture states.
- Update dependency-cycle and self-defeating reconciliation tests to use the PG seam.
- Remove restored suites from the reliability quarantine configuration and ledger.

Files changed:
 .../__tests__/reliability-interactions/_helpers.ts | 29 ++++++++
 .../dependency-cycle-reconcile.test.ts             | 86 +++++++++++-----------
 .../self-defeating-dep-reconcile.test.ts           |  7 +-
 packages/engine/vitest.config.ts                   |  7 +-
 scripts/lib/test-quarantine.json                   | 10 ---
 5 files changed, 82 insertions(+), 57 deletions(-)

Fusion-Task-Id: FN-8044

Fusion-Task-Lineage: 213f606c-1951-4d25-93b4-2ceb97460ace

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 07:14:01 -07:00
gsxdsm
969fce7aa1 FN-8047: migrate AgentStore multi-node tests to PostgreSQL
Migrate multi-node AgentStore coverage to shared PostgreSQL-backed fixtures.

- Make concurrent central claim insertion resolve unique-key races as checkout conflicts.
- Rework claim and owning-node handoff tests to use shared async PostgreSQL layers.
- Restore PostgreSQL-compatible tests from the quarantine ledger.

Files changed:
 packages/core/src/async-central-db.ts              |  9 ++-
 .../cross-node-claim-mutex.integration.test.ts     | 72 ++++++++++---------
 .../distributed-claim-mutex.integration.test.ts    | 27 +++----
 .../owning-node-handoff.integration.test.ts        | 41 +++++------
 .../__tests__/reliability-interactions/_helpers.ts | 83 ++++++++++++++++++++--
 .../multi-node-claim-mutex-interactions.test.ts    | 28 +++-----
 .../owning-node-unavailable-interactions.test.ts   | 36 +++++-----
 packages/engine/vitest.config.ts                   |  8 +--
 scripts/lib/test-quarantine.json                   | 25 -------
 9 files changed, 180 insertions(+), 149 deletions(-)

Fusion-Task-Id: FN-8047

Fusion-Task-Lineage: 3b7ee21e-0190-4364-a0cb-88aac5e2e1a3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 05:33:55 -07:00
gsxdsm
19ab7a9e93 FN-8068: add input-language task definitions
Add an opt-in setting that localizes planner-authored task-definition prose.

- Detect confident Spanish, French, Korean, and Chinese input before instructing triage localization.
- Expose and persist the project-level task-definition language toggle with translations and search support.
- Preserve English structural markers, code, and unsupported or uncertain input for deterministic parsing.

Files changed:
 .../fn-8068-task-definition-input-language.md      |  7 +++
 docs/settings-reference.md                         |  1 +
 packages/core/src/settings-schema.ts               |  7 +++
 packages/core/src/types.ts                         | 10 ++++
 .../app/__tests__/settings-sections.test.tsx       | 18 ++++++
 .../dashboard/app/components/SettingsModal.tsx     |  4 ++
 .../__tests__/SettingsModal.mobileClose.test.tsx   | 12 ++++
 .../__tests__/SettingsModal.models-auth.test.tsx   | 11 ++++
 .../app/components/settings/section-keys.ts        |  1 +
 .../sections/ProjectModelsSection.search.ts        | 10 ++++
 .../settings/sections/ProjectModelsSection.tsx     | 17 ++++++
 .../settings-default-descriptions.test.tsx         |  1 +
 packages/engine/src/__tests__/triage.test.ts       | 64 ++++++++++++++++++++++
 packages/engine/src/triage.ts                      | 29 +++++++++-
 packages/i18n/locales/en/app.json                  |  2 +
 packages/i18n/locales/es/app.json                  |  2 +
 packages/i18n/locales/fr/app.json                  |  2 +
 packages/i18n/locales/ko/app.json                  |  2 +
 packages/i18n/locales/zh-CN/app.json               |  2 +
 packages/i18n/locales/zh-TW/app.json               |  2 +
 packages/i18n/src/resources.d.ts                   |  2 +
 21 files changed, 205 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8068

Fusion-Task-Lineage: fe0102f5-68cb-443c-8512-2ffa5fe0e2f8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 05:18:30 -07:00
gsxdsm
5b4ec4c08f FN-8042: add merger fallback model lane
Add configurable project fallback models for AI merger retries.

- Resolve complete project merger fallback pairs before the shared global fallback.
- Expose merger fallback model and thinking controls in Project Models with reset-aware persistence.
- Apply the fallback lane across merger, review, PR-response, and recovery sessions with tests and documentation.

Files changed:
 .changeset/fn-8042-merger-fallback-model.md        |  7 +++
 docs/settings-reference.md                         |  4 +-
 .../core/src/__tests__/model-resolution.test.ts    | 24 ++++++++++
 .../core/src/__tests__/settings-parity.test.ts     |  3 ++
 packages/core/src/index.gate.ts                    |  1 +
 packages/core/src/index.ts                         |  1 +
 packages/core/src/model-resolution.ts              | 22 +++++++++
 packages/core/src/settings-schema.ts               |  4 ++
 packages/core/src/types.ts                         | 13 ++++++
 .../app/__tests__/settings-save-split.test.ts      | 41 +++++++++++++++++
 .../app/__tests__/settings-sections.test.tsx       | 36 +++++++++++++++
 .../app/components/settings/save-split.ts          |  7 +--
 .../settings/sections/ProjectModelsSection.tsx     | 53 +++++++++++++++++++++-
 .../settings-default-descriptions.test.tsx         |  3 ++
 .../src/__tests__/agent-session-helpers.test.ts    |  1 +
 .../__tests__/mcp-pr-response-forwarding.test.ts   |  1 +
 packages/engine/src/agent-session-helpers.ts       | 10 +++-
 packages/engine/src/merger-ai.ts                   | 17 +++++--
 packages/engine/src/merger.ts                      | 43 +++++++++++++-----
 packages/engine/src/pr-response-run-ops.ts         | 11 +++--
 20 files changed, 276 insertions(+), 26 deletions(-)

Fusion-Task-Id: FN-8042

Fusion-Task-Lineage: 31762a72-461e-438c-a12d-2816580283fd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 04:29:17 -07:00
gsxdsm
3f133e0b13 FN-8058: add task agent log reader
Expose paginated, filterable persisted agent logs to task-scoped and chat agent sessions.

- Add the read-only fn_task_logs_read tool across engine, dashboard chat/planning, heartbeat, step, and CLI extension surfaces.
- Filter agent-log entries before pagination, report matching totals, and render complete persisted rows for diagnosis.
- Document the tool, add release metadata, regression coverage, and complete affected engine mocks.

Files changed:
 .changeset/fn-8058-task-logs-read.md               |  7 ++
 docs/agents.md                                     |  4 +-
 packages/cli/skill/fusion/SKILL.md                 |  2 +-
 .../cli/skill/fusion/references/extension-tools.md | 11 +++
 .../skill/fusion/references/fusion-capabilities.md |  1 +
 .../extension-experiment-finalize.test.ts          |  2 +
 .../src/__tests__/extension-fn-secret-get.test.ts  |  2 +
 .../__tests__/extension-gitlab-tracking.test.ts    |  2 +
 .../src/__tests__/extension-integration.test.ts    |  1 +
 .../cli/src/__tests__/extension-web-fetch.test.ts  |  2 +
 packages/cli/src/__tests__/extension.test.ts       |  1 +
 packages/cli/src/extension.ts                      | 34 ++++++++
 .../src/__tests__/agent-logs-backend-mode.test.ts  | 28 +++++-
 packages/core/src/store.ts                         | 11 ++-
 packages/core/src/task-store/remaining-ops-7.ts    | 16 +++-
 packages/core/src/types.ts                         |  1 +
 packages/dashboard/src/__tests__/chat.test.ts      |  1 +
 .../planning-answered-question-reemit.test.ts      |  1 +
 .../planning-generation-cancellation.test.ts       |  1 +
 packages/dashboard/src/chat.ts                     |  5 ++
 packages/dashboard/src/planning.ts                 |  3 +
 .../__tests__/agent-task-logs-read-tools.test.ts   | 72 ++++++++++++++++
 packages/engine/src/agent-heartbeat.ts             |  4 +-
 packages/engine/src/agent-tools.ts                 | 99 +++++++++++++++++++++-
 packages/engine/src/executor.ts                    |  6 ++
 packages/engine/src/gating-classifications.ts      |  2 +
 packages/engine/src/index.ts                       |  6 ++
 packages/engine/src/step-session-executor.ts       |  6 +-
 28 files changed, 316 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-8058
Fusion-Task-Lineage: 74f198b2-f538-4b39-973f-431f22e68f29
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 04:03:43 -07:00
gsxdsm
c2475b012d FN-8064: add proactive task chat status updates
Task-detail chat now narrates engine progress and review outcomes in real time.

- Emit bounded, redacted status rows for step lifecycle and review paths.
- Present status entries with a distinct task-chat treatment.
- Cover status narration and diagnostic sanitization with engine tests.

Files changed:
 .changeset/fn-8064-proactive-chat.md               |   7 +
 docs/architecture.md                               |   1 +
 packages/dashboard/app/components/TaskChatTab.css  |  16 ++
 packages/dashboard/app/components/TaskChatTab.tsx  |   9 +-
 .../engine/src/__tests__/executor-prompt.test.ts   |  28 +++-
 .../engine/src/__tests__/proactive-status.test.ts  |  54 +++++++
 packages/engine/src/executor.ts                    | 176 ++++++++++++++++-----
 packages/engine/src/proactive-status.ts            | 117 ++++++++++++++
 8 files changed, 365 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-8064

Fusion-Task-Lineage: c6d0a9b5-0946-4bf4-8338-e982e1cbfd53

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:47:59 -07:00
gsxdsm
274318aebf FN-8056: enforce task token budgets
Enforce configured task token budgets whenever session usage is persisted.

- Apply soft alerts and hard pauses atomically from all executor persistence paths.
- Exclude cache-read tokens from budget usage and dispatch budget notifications once.
- Document budget semantics and add regression coverage.

Files changed: .changeset/fn-8056-token-budget-enforcement.md     |   7 ++
 docs/settings-reference.md                         |   2 +
 packages/core/src/types.ts                         |   4 +-
 .../src/__tests__/session-token-usage.test.ts      | 101 ++++++++++++++++++++-
 .../src/__tests__/token-budget-enforcer.test.ts    |  81 ++++++++++-------
 packages/engine/src/executor.ts                    |  22 ++++-
 packages/engine/src/session-token-usage.ts         |   8 +-
 packages/engine/src/token-budget-enforcer.ts       |  98 +++++++++++++++++---
 8 files changed, 262 insertions(+), 61 deletions(-)

Fusion-Task-Id: FN-8056

Fusion-Task-Lineage: 5f5ed522-f950-42ce-b4fd-e0b1d45b5815

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:52:10 -07:00
gsxdsm
e46ffebde1 FN-7985: surface review budget exhaustion and configure replan cap
Expose exhausted Plan Review replan budgets for operator approval and allow workflows to configure the cap.

- Add validated numeric workflow setting support and a Plan Review replan-cap setting.
- Route configured cap exhaustion with a distinct approval reason and preserve fallback behavior.
- Display the budget-exhaustion state across task cards, lists, and details.
- Add tests, localized copy, documentation, and a minor changeset.

Files changed:
 .changeset/fn-7985-review-budget-approval.md       |  7 ++++
 docs/settings-reference.md                         |  9 +++--
 docs/workflow-steps.md                             |  2 +-
 .../builtin-workflow-settings-triage.test.ts       | 27 +++++++++++++--
 packages/core/src/builtin-workflow-settings.ts     | 17 ++++++++++
 packages/core/src/index.gate.ts                    |  1 +
 packages/core/src/index.ts                         |  1 +
 packages/core/src/workflow-ir-types.ts             |  4 +++
 packages/core/src/workflow-ir.ts                   | 27 +++++++++++++++
 packages/core/src/workflow-settings-resolver.ts    |  1 +
 packages/core/src/workflow-settings.ts             |  6 ++++
 packages/dashboard/app/components/ListView.css     | 19 +++++++++++
 packages/dashboard/app/components/ListView.tsx     | 22 +++++++++---
 packages/dashboard/app/components/TaskCard.css     | 18 ++++++++++
 packages/dashboard/app/components/TaskCard.tsx     |  6 ++--
 .../dashboard/app/components/TaskDetailModal.tsx   |  4 +--
 .../app/components/__tests__/ListView.test.tsx     | 30 +++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 17 ++++++++--
 .../app/components/workflow-setting-display.ts     | 11 ++++++
 .../dashboard/app/utils/reviewBudgetApproval.ts    | 11 ++++++
 .../triage-plan-review-replan-cap.test.ts          | 39 +++++++++++++++++++---
 packages/engine/src/triage.ts                      | 21 ++++++++----
 packages/i18n/locales/en/app.json                  |  2 +-
 packages/i18n/src/resources.d.ts                   | 19 ++++++++---
 24 files changed, 288 insertions(+), 33 deletions(-)

Fusion-Task-Id: FN-7985

Fusion-Task-Lineage: 125f101c-caca-45c2-8b40-996b2a31c019

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:32:23 -07:00
gsxdsm
a31c370375 FN-8045: add transactional handoff failure-injection seam
Ensure PostgreSQL review handoffs roll back all dependent writes after an injected late failure.

- Add a test-only failure injector after transactional handoff writes.
- Include workflow work in same-column retry transactions.
- Restore PG-backed handoff atomicity coverage and remove its quarantine.

Files changed:
 packages/core/src/store.ts                         |  24 +++
 packages/core/src/task-store/moves.ts              |  21 ++-
 .../in-review-handoff-atomic.test.ts               | 172 +++++++++++++--------
 packages/engine/vitest.config.ts                   |   1 -
 scripts/lib/test-quarantine.json                   |   5 -
 5 files changed, 151 insertions(+), 72 deletions(-)

Fusion-Task-Id: FN-8045

Fusion-Task-Lineage: 517e3000-9b88-4b0d-9b25-1a585eb8f322

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 00:44:35 -07:00
gsxdsm
9a34862586 refactor: package code organization waves 3–5 (#2148)
## Summary

Waves 3–5 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`).
Behavior-preserving peels after #2139 and #2143.

### Wave 3 — Merger + heartbeat recovery
- **`merger-errors.ts`** — verification/abort error classes
- **`merger-owned-landed.ts`** — ownership classification +
`Fusion-Task-Id` trailer
- **`merger-conflict-resolution.ts`** — conflict classify/auto-resolve
- **`agent-heartbeat-error-recovery.ts`** — durable error-recovery
budget helpers

### Wave 4 — Self-healing + dashboard API
- **`self-healing-constants.ts`** — public timing/budget constants
- **`self-healing-branch.ts`** — `isBranchAheadOfBase`
- **`app/api/client.ts`** — `api` / `ApiRequestError` / `buildApiUrl` /
`proxyApi`
- **`app/api/health.ts`** — health, engine status, updates +
`withProjectId`

### Wave 5 — Types tracking + merger parse + task CRUD
- **`types/task-tracking.ts`** — PR/issue/GitHub/GitLab tracking
contracts
- **`merger-git-parse.ts`** — `parseFailingFilesFromOutput`,
`parsePorcelainZ`, `parseShortstatSummary`
- **`app/api/tasks.ts`** — task list/detail/create/update/move client
surface
- Line-count baselines ratcheted down for `merger.ts`, `types.ts`,
`legacy.ts`

Public import paths stay on parent modules / `legacy.ts` / package
barrels via re-exports.

## Test plan
- [x] core/engine/dashboard typecheck (including `tsconfig.app.json`)
- [x] eslint on touched modules
- [x] `parse-porcelain-z` + merger parseFailing/getBranchChanged tests
- [x] dashboard `api-tasks` + legacy-prinfo/pr-types (69)
- [ ] CI merge gate

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added dashboard API support for task listing/detail, archiving,
creation, review updates, duplicate detection, bulk model updates,
moving tasks, and overlap repair.
- Added health/engine status and refresh/start controls, plus update
checking.

- **Bug Fixes**
- Improved dashboard API handling for non-JSON/HTML responses with
clearer errors, better URL routing for remote nodes, and project-scoped
queries.
- Strengthened automated recovery for heartbeat error/model-unavailable
scenarios and safer merge-conflict classification/auto-resolution.

- **Tests**
- Updated merge-conflict resolution and lifecycle test mocks to match
the updated git command behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 00:01:17 -07:00
gsxdsm
1d6a0449ce fix(engine): keep replan cards plannable when they carry prior steps
Tasks piled up in the Planning column and never moved. hasAdvancedPastPlanning
counted steps.length > 0 as proof a card had advanced past planning, but a
replan card legitimately retains the steps its previous planning pass
materialized. The still-in-planning guard therefore failed for every card
Plan Review sent back, so triage's specifyTask claim silently skipped its
status:"planning" write and re-claimed the same cards every poll — never
planning them, and starving healthy cards out of the maxTriageConcurrent
slots they held.

Steps are no longer advancement evidence while a card sits in a planner lane:
the "triage" column, and the merged "todo" planner lane used by plan-in-place
workflows when the card carries a planning status. Worktrees and
execution/terminal columns remain durable advancement evidence, preserving
FN-7977's protection against a recovery write clobbering a card that raced
ahead into execution.

The primary claim path now warns instead of returning silently; recovery-write
skips stay silent by design. The silence is why this stalled the planner for
hours undiagnosed.

Regression coverage asserts the invariant across both planner surfaces rather
than the reported repro alone: triage cards with and without an explicit
needs-replan status, plan-in-place todo replans, every parked-for-planning
status, and the advancement signals that must still fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:53:49 -07:00
gsxdsm
7b31d54b98 fix(FN-8004): make AI merge rejections actionable and stranded merges retryable (#2160)
Two follow-ups to FN-8004. Both were found by watching FN-8004's *own*
merge livelock for 40 minutes — it turned out to be blocked by the very
class of bug it was filed to fix.

## 1. AI merge rejections lost their reasons

The reviewer prompt said **both** of these:

> "**End with a single decision line**: `REVIEW_VERDICT:
approve|reject`"
> "**Then list each concrete reason as a bullet.**"

Those are impossible to satisfy at once. Reviewers obeyed "End with" and
wrote their reasoning *above* the verdict — but `extractRejectReasons`
only scanned lines *after* it. So every such rejection collapsed to the
placeholder `reviewer rejected the merge without a stated reason`, and
that placeholder was then handed to the corrective re-merge pass **as
its instruction**. The pass got no actionable feedback and just
re-rolled the merge.

The evidence, from FN-8004's own merge — the pattern repeated across
*both* attempts:

| | Attempt A | Attempt B |
|---|---|---|
| review pass 1 | rejected, no reason (03:46) | rejected, no reason
(03:57) |
| corrective pass | 1/3 | 1/3 |
| review pass 2 | **approved** `a3a3cc6a8` (03:49) | approved |

A reviewer that rejects and then approves identical content isn't
objecting — the reason was being thrown away. Each wasted cycle cost ~7
minutes, stretching the merge past main's ~8-minute churn window so
every attempt lost to a concurrent advance and rebuilt. **The livelock
was caused by the lost-reason bug.**

Fix: the parser recovers reasons from either side of the verdict (inline
→ after → before, nearest-first so the closing argument leads, capped at
8 so a long transcript can't flood the corrective prompt), skipping
severity/verdict/markdown scaffolding. The prompt ordering is now
unambiguous — reasons first, verdict last, nothing after it.

## 2. An orphaned merge-active stamp was un-retryable by hand

The Retry gate refused **every** merge-active status (`Task is not in a
retryable state (current status: landing)`), while self-healing cleared
stale stamps automatically minutes later. So a merger killed mid-flight
— crash, engine restart, operator SIGTERM — blocked the operator's own
escape hatch at exactly the moment they'd reach for it. FN-8004 hit
this: a killed merge left `landing` stamped and Retry 400'd for the full
sweep delay.

`isStaleMergeActiveStatus` now lives in the leaf
`merge-active-status.ts`, shared by `recoverStaleMergingStatus` and the
Retry gate — so **the manual path can never be stricter than the
automatic one**. This is the same one-concept-two-definitions bug as
FN-8004's transient classifier, which is why it's worth fixing
structurally rather than adding another special case.

A live merge stays protected by two independent signals: it holds the
in-process lease **and** refreshes `updatedAt` each phase. Staleness
fails closed on an unparseable timestamp.

One subtlety worth reviewing: the bypass feeds `isInReviewRetry` rather
than only the gate. A bare gate bypass would fall through to the generic
branch and move fully-executed work to `todo`, **re-running finished
work** — a bug this fix could easily have introduced.

## Verification

- Gate green (294 + 122 + 63) · lint clean · engine + dashboard
typecheck clean · `verify:fast` PASS
- 70 merger-suite tests green; all 7 pre-existing verdict-parser tests
still pass (backward compatible — none of them covered the verdict-last
layout, which is exactly why this shipped)
- **The route regression test was confirmed non-vacuous**: neutralizing
the fix fails the two "now retryable" cases while the three
live-merge-protection cases still pass, proving they guard real behavior
rather than the new code.
- Regression tests assert the invariant across every surface per *Fix
the Invariant, Not the Repro*: all five `ACTIVE_MERGE_STATUSES` (a
merger can die in any phase, not just the reported `landing`), both
live-merge signals, boundary conditions, fail-closed paths, and that
pre-existing retry paths are unchanged. Test files carry the required
`## Symptom Verification` and `## Surface Enumeration` sections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* AI merge rejections now reliably include concrete, correctly ordered
reasons, even when provided before the verdict line.
* Manual retry can recover tasks stuck in stale merge-processing states.
* Retry is still blocked for tasks tied to active merge activity or
recently updated/advancing merges.
  * Existing failed-merge retry behavior remains unchanged.

* **Reliability**
* Improved shared handling of “orphaned” merge-active detection across
the engine and dashboard.

* **Tests**
* Added/expanded coverage for merge-active staleness, retry eligibility,
and verdict/reason parsing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:43:24 -07:00
gsxdsm
959a7877c8 FN-8020: harden agent heartbeat health classification
Validate the four-interval heartbeat grace window and classify invalid persisted heartbeats safely.

- Cover reported field heartbeat ages in dashboard and engine health checks.
- Mark unparseable heartbeat timestamps as unresponsive and clamp future timestamps to fresh.
- Align dashboard health documentation with the existing four-interval grace window.

Files changed:
 .../app/utils/__tests__/agentHealth.test.tsx       | 48 +++++++++++++++++++++-
 packages/dashboard/app/utils/agentHealth.tsx       | 26 ++++++++++--
 .../src/__tests__/heartbeat-executor.test.ts       | 31 ++++++++++++++
 3 files changed, 100 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8020

Fusion-Task-Lineage: 2bc0df78-d68c-489b-8bfb-9b09da10cdfa

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 22:11:26 -07:00
gsxdsm
9d791b3bf1 FN-8009: preserve approved plans through prompt hygiene
Keep manually approved plans idempotent when deterministic prompt hygiene is applied.

- Document normalized fingerprint comparison at the approval gate
- Cover approval reuse after Original Description and Frontend UX injection

Files changed:
 packages/engine/src/__tests__/triage.test.ts | 37 ++++++++++++++++++++++++++++
 packages/engine/src/triage.ts                |  8 ++++++
 2 files changed, 45 insertions(+)

Fusion-Task-Id: FN-8009

Fusion-Task-Lineage: 8474bc13-61e0-421a-8e99-080f99382285

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 21:52:06 -07:00
gsxdsm
3dcb62f40f FN-8008: normalize plan approval fingerprints
Keep approval recovery idempotent when deterministic prompt hygiene is injected.

- Normalize plan approval fingerprints around Original Description and Frontend UX sections.
- Preserve re-approval for operator-authored plan changes and cover recovery behavior.
- Document the normalization contract and add a patch changeset.

Files changed:
 .changeset/fn-8008-plan-approval-fingerprint.md   |  7 +++
 docs/workflow-steps.md                            |  2 +-
 packages/core/src/__tests__/plan-approval.test.ts | 53 +++++++++++++++-
 packages/core/src/plan-approval.ts                | 73 ++++++++++++++++++++++-
 packages/engine/src/__tests__/triage.test.ts      | 45 ++++++--------
 packages/engine/src/triage.ts                     | 40 ++-----------
 6 files changed, 153 insertions(+), 67 deletions(-)

Fusion-Task-Id: FN-8008

Fusion-Task-Lineage: 9c0f415d-662a-455a-a4bd-b873307e53bc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 21:33:11 -07:00
gsxdsm
402b3a91fa fix(FN-8004): treat heartbeat soft-delete races as benign instead of stranding agents
A task soft-deleted concurrently with a heartbeat-driven moveTask raised
TaskDeletedError from the engine's own board path, leaving the agent in `error`
with a non-empty lastError and requiring a stop/start cycle to recover.

The race is benign by construction: the task is gone, so the move is a no-op.
The heartbeat now classifies it via isConcurrentSoftDeleteRaceError (matching the
canonical message and serialized/typed forms), keeps the agent active, clears
stale error/recovery state, and emits agent:heartbeat-move-skipped-soft-delete
with ids/counts-only metadata. Concurrent operator pauses are preserved.

Squash-merged by hand from fusion/fn-8004. The engine's AI merge approved this
content twice (squash a3a3cc6a8) but could not land it: main advances every ~8
minutes and each merge cycle took ~10, so every attempt lost to a concurrent
advance and rebuilt. Each cycle also burned a corrective pass on a first-pass
review rejection with no stated reason — the issue #1946 class of bug that this
task's own report cites as a sibling.

Reconciled against #2157, which refactored transient-error-detector.ts: the new
classifier coexists with the extracted transient-error-patterns.ts leaf. Verified
on the merged tree — 123 tests green across FN-8004's suites and #2157's,
engine typecheck clean.

Fusion-Task-Id: FN-8004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:00:45 -07:00
gsxdsm
08a10bf486 fix(FN-8006): back off and pause Plan Review on provider rate limits
A rate-limited Plan Review re-ran every 30s for hours (~1,900 requests
per 5h window, reviewerFallbackRetryCount observed past 100), which is
the request volume that trips a provider's low-interactivity throttle —
so the retry storm prolonged the very outage it was retrying.

Root cause: runPlanReviewBeforeExecution catches every reviewStep throw
inline to keep triage alive, which converts them all to an UNAVAILABLE
verdict. That laundering had two consequences the earlier fixes missed:
FN-8006 terminalized RetryStormError and the reviewer started throwing
ReviewerProviderError for 429s, but a ReviewerProviderError still landed
in the UNAVAILABLE park — a FIXED 30s nextRecoveryAt with no attempt
counter and no cap. The reviewer's own escalation contract ("escalate so
UsageLimitPauser pauses every lane") held only on the executor path,
because the inline catch hid the error from triage's usage-limit handler
in specifyTask.

- triage: fire usageLimitPauser.onUsageLimitHit for usage-limit reviewer
  failures, so a 429 pauses every lane instead of re-parking one task.
- triage: re-park via computeRecoveryDecision (60s/120s/240s, ±10%
  jitter) and terminalize at MAX_RECOVERY_RETRIES. A reviewer that never
  yields a verdict is a real failure and must surface, not spin.
- triage: clear the borrowed recoveryRetryCount budget on any real
  verdict, so surviving an outage cannot shorten the executor's later
  transient budget.
- core: RetryStormError takes an optional cause, surfaced as
  underlyingError in serializeRetryStormError and folded into the
  message, so a cap no longer masks the real error. recordRetry threads
  it from the reviewer's error path.

Surface enumeration: the park is driven by a thrown provider error, a
thrown generic error, and a plain UNAVAILABLE verdict with no throw.
All three are covered — a repro pinned only to the reported 429 would
leave the other two spinning on the old fixed timer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:53:05 -07:00