Commit Graph

12266 Commits

Author SHA1 Message Date
gsxdsm
71279ed042 fix(FN-8600): recover a duplicate verdict the planner reported in its reply
The prompt fix stops planners writing the verdict in prose, but it relies on
every model reading one sentence correctly. This closes the hole underneath it.

When the finalize read finds no spec at all, the planner's streamed reply is
searched for a line that is exactly `DUPLICATE: FN-NNNN`. If found, the engine
writes the canonical marker file and continues — so marker parsing, keep/delete
resolution, and the sourceMetadata.nearDuplicateOf that renders the operator's
decision all run on the unchanged file contract rather than a second code path
that could drift from it.

Deliberately narrow. The marker must occupy a whole line, only the first counts,
and recovery is gated on the plan being genuinely absent — a planner that wrote
a real spec is never overridden by something it said in passing. The text tail
is bounded because the verdict lands in the closing summary, and it tees off
onText rather than reading AgentLogger, whose buffer is flushed on a timer.

Verified both directions: the tests fail without the recovery block, and the
"wrote a real spec while mentioning a marker" case keeps its spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:23:00 -07:00
gsxdsm
a00f2633ce fix(engine): demote more TUI chatter across merger, self-heal, and ntfy
Route foreach/merger/worktree/self-healing skips, ntfy send bookkeeping, session-purpose runtime picks, planning using-model, and checkpoint rewind lines to debug so recoveries and failures stay visible in the operator log.
2026-07-26 10:15:18 -07:00
gsxdsm
f5163d8351 fix(dashboard): resync SSE state on reopen and bound the service-worker cache
Follow-up to f157bf7460, fixing the regressions an adversarial review found in
the mobile tab-discard work.

- SSE hidden-suspend dropped events silently: the per-task/run log streams are
  live-only with no replay, and many subscribers had no onReconnect, so a 60s+
  hidden window left invisible gaps in logs, a never-rendered approval banner,
  a diverged chat transcript, and a missed merge advance notice. Every
  subscriber now resyncs authoritative state on reopen.
- useAgentLogs refetches its authoritative page on reconnect and reports
  hasMore truthfully once paging reaches the first entry.
- Agent run logs are windowed rather than discarded, so the head of a long run
  stays reachable.
- lastFetchTimeMs is seeded from the cached envelope's savedAt, so a hydrated
  stale snapshot no longer renders every in-progress card as stuck.
- MAX_IMMUTABLE_CACHE_ENTRIES lands as 200; it was committed as Infinity, which
  left the cache-first bucket unbounded and the cap dead code.
- useAgentLogs.ts held a literal NUL byte that made git treat the file as binary
  and grep skip it; replaced with an escape sequence so it stays reviewable.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, pnpm check:changesets
clean, 25 scoped test files / 1337 tests passing. The xterm scrollback constants
and several components still lacking onReconnect remain untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:13:39 -07:00
gsxdsm
c7fa02f370 FN-8597: restore executor task-done invariant coverage
Restore the quarantined executor graph-completion invariant suite with real foreach projections.

- Exercise complete and partial expanded workflow-step projections at the merge boundary.
- Remove the rescued invariant suite from Vitest quarantine and clear its ledger entry.
- Extend the shared executor logger mock with the debug method required by the integration tip.

Files changed:
 .../__tests__/executor-task-done-invariant.test.ts | 267 +++++++++++++++++++--
 .../engine/src/__tests__/executor-test-helpers.ts  |   7 +
 packages/engine/vitest.config.ts                   |   7 -
 scripts/lib/test-quarantine.json                   |   8 +-
 4 files changed, 254 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8597

Fusion-Task-Lineage: 05a08e31-7da0-4c93-86a0-9baf8db7ce52

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 10:04:36 -07:00
gsxdsm
f157bf7460 fix(dashboard): harden visibility suspension, log caps, and mobile board UX
Suspend poll/SSE work when the tab is hidden, cap log buffers, restore board scroll more reliably, and improve list windowing/live tickers with related tests and a mobile-tab retention changeset.
2026-07-26 09:50:44 -07:00
gsxdsm
9bad0e1233 fix(engine): demote high-frequency TUI log spam to debug
Route process spawn/exit, verification success paths, MCP connect, skill info listings, createFnAgent/session bookkeeping, and executor dispatch chatter through FUSION_DEBUG so the operator log pane keeps real lifecycle outcomes.
2026-07-26 09:50:43 -07:00
gsxdsm
86c892b67c fix(FN-8600): make the duplicate-report instruction writable, not self-contradictory
The planning prompt said "do not write PROMPT.md" and, in the same breath,
"write DUPLICATE: {id} to the output file" — where the output file IS
PROMPT.md. A planner that took the first clause literally wrote no file and
reported the duplicate in prose.

The engine only ever reads the verdict from PROMPT.md's contents, so that
duplicate was invisible: the task failed deterministic validation as
"PROMPT.md file not found or empty", retried, terminalized to failed, emitted
a task-wedge mail, was recovered to todo by self-healing, and re-planned —
three full Opus planning cycles on FN-8600 before it was caught, with no
operator decision ever surfaced because sourceMetadata.nearDuplicateOf is only
set on the branch that parses the file.

Both prompt sites now say to write PROMPT.md with the marker as its entire
contents, and say why prose alone is not recorded.

Note the engine ordering is already correct — tryFinalizeExplicitDuplicateMarker
runs before validateGeneratedPrompt, and a worktree-local spec is recovered
first. Nothing to reorder; the file simply never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:42:24 -07:00
gsxdsm
ae512aec2b FN-8601: enforce foreach merge proof
Require complete foreach execution evidence before workflow merge review.

- Add reusable foreach instance coverage proof evaluation.
- Block checklist projection and merge admission on incomplete or failed node results.
- Cover core proof logic and PostgreSQL merge-boundary behavior.
- Add a patch changeset for the merge safeguard.

Files changed:
 .changeset/fn-8601-foreach-merge-proof.md          |   7 ++
 .../src/__tests__/workflow-merge-proof.test.ts     |  43 ++++++++
 packages/core/src/index.gate.ts                    |   2 +
 packages/core/src/index.ts                         |   2 +
 packages/core/src/workflow-merge-proof.ts          |  74 +++++++++++++
 ...xecutor-merge-boundary-foreach-proof.pg.test.ts | 111 +++++++++++++++++++
 packages/engine/src/executor.ts                    | 117 +++++++++++++--------
 7 files changed, 314 insertions(+), 42 deletions(-)

Fusion-Task-Id: FN-8601

Fusion-Task-Lineage: 40578171-0b13-4538-8f38-3948ed1e92c0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:40:37 -07:00
gsxdsm
a516c8b409 docs(FN-8600): capture the live-planning-worktree reclaim incident
Documents why self-healing force-removed a worktree a planning session was
using and parked the card branch-conflict-unrecoverable: planning gained a task
worktree but never took an active-session lease, so the reclaim sweep's liveness
guard had nothing to see, and a zero-commit branch classifies as
tip-already-merged by construction.

Captures the investigation's dead ends too — including reading maxConcurrent
from a multi-tenant config table without filtering by project_id, which produced
a confidently wrong root cause — and the three ways the first version of the fix
was itself wrong.

CONCEPTS.md: adds planning to the Active-session lease kinds (the entry had gone
stale), states the converse invariant that an unheld path reads as proof nothing
is running, and defines Top-level agent slot — the capacity concept whose
conflation with the worktree limit derailed the first hour of diagnosis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:22 -07:00
gsxdsm
2d263acc49 fix(FN-8600): keep self-healing from pausing live planners and unstick queued planning
Planning moved into the task's own worktree but never published that path to
activeSessionRegistry, so the self-owned-branch reclaim sweep's FN-4819 liveness
guard was blind to a live planner. A zero-commit task branch trivially reads as
tip-already-merged, so the sweep ran `git worktree remove --force` on the tree a
planning session was using, the removal failed, and the failure escalated to
branch-conflict-unrecoverable — parking a healthy card paused with no operator
action.

Planning now claims its worktree through acquireActiveSessionPath (new "planning"
session kind) and releases it only while it still owns the record, so a live
executor that took over the same path mid-teardown is never cleared.

Also fixes planning starvation and its diagnosability:
- admitOldest walks past candidates whose lane declines instead of ending the
  pass on candidates[0], unwinding each declined attempt's pre-held executor slot
  and reservation exactly so a decline cannot leak capacity past maxConcurrent.
- Withheld planning admission emits a deduped task:plan-admission-throttled
  run-audit event (ids/counts only), written fire-and-forget with the dedupe
  marker set only after the write lands. Previously the binding gate lived only
  in a log line that is persisted nowhere, so "why did this card sit queued to
  plan?" was unanswerable after the fact.

Reviewed by 8 review agents; every finding acted on or recorded. A proposed
STALE_SEMAPHORE_EXCESS_REPAIR_MS 600s->180s reduction was reverted under review —
nested runs are already excluded from the reclaim floor, so the window guards
uncounted top-level holders such as a merge body, and shortening it would trade a
bounded visible stall for an unbounded silent cap breach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:26:13 -07:00
gsxdsm
d4aa79b66c FN-8598: preserve legacy task cost badges
Restore cost badges for tasks with valid legacy token totals.

- Preserve usage records when optional timestamps and cache-write totals are absent
- Use task creation time to satisfy legacy usage timestamp requirements
- Cover card badge rendering, unpriced mixed usage, and mobile visibility
- Add a patch changeset for the restored badge behavior

Files changed:
 .changeset/fn-8598-cost-badge-fix.md               |   7 +
 .../task-token-usage-serialization.test.ts         |  45 +++++++
 packages/core/src/task-store/serialization.ts      |  16 ++-
 .../__tests__/TaskCard.cost-badge.test.tsx         | 146 +++++++++++++++++++++
 .../app/utils/__tests__/taskTokenCost.test.ts      |  11 ++
 5 files changed, 221 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8598

Fusion-Task-Lineage: 83fb4051-8e0f-4ee9-9f00-0e4d5cb8661e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:06:41 -07:00
gsxdsm
795a38c018 fix(engine): quiet graph review-entry audits and label engine aborts truthfully
Recognise workflow-graph moves into in-review so gate entry no longer emits handoff-invariant violations, and split pause-abort provenance so engine teardowns are engine-abort instead of hard-cancel.
2026-07-26 08:56:58 -07:00
gsxdsm
c76f276266 fix(dashboard): stop mobile board swipes jumping two columns at the edges
Columns are narrower than a phone viewport, so the first/last column's ideal
centered scrollLeft is outside the reachable scroll range. isColumnCentered
compared against that unreachable value, so an edge rest never read as
centered and commitDirectionalPage took its origin at release (already moved
onto the next column) instead of at gesture start — paging two columns.

Clamp the centering target to the reachable range, and clamp the mid-transit
origin against the gesture-start column so drag travel is never counted twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:48:36 -07:00
gsxdsm
9afd88dd9b FN-8599: fix task card size badge alignment
Keep the size badge aligned with the first task-card header row when status badges wrap.

- Anchor desktop and mobile size chips to the header start with fixed tokenized heights
- Cover size-badge geometry and paused/reviewing badge combinations
- Add a patch changeset for the task-card alignment fix

Files changed:
 .changeset/fn-8599-task-card-size-badge.md         |  7 +++
 packages/dashboard/app/components/TaskCard.css     | 25 +++++++--
 .../__tests__/TaskCard.badge-height.test.tsx       | 13 ++++-
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 62 +++++++++++++++++++--
 4 files changed, 98 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8599

Fusion-Task-Lineage: 062716b6-58ca-4f5b-8741-50856e933192

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 08:42:46 -07:00
gsxdsm
af897d9e3c FN-8596: isolate cross-root plugin MCP discovery
Prevent cross-root MCP discovery from unloading active plugin runtimes.

- Isolate discovery loader lifecycle and runtime-state persistence.
- Preserve shared plugin owners when non-owner loader participants stop.
- Cover core, dashboard, and engine cross-root discovery behavior.

Files changed:
 .changeset/fn-8596-plugin-discovery-isolation.md   |   7 ++
 .../plugin-loader-lifecycle-scope.test.ts          |  12 +++
 .../plugin-mcp-servers-discovery-isolation.test.ts | 115 +++++++++++++++++++++
 packages/core/src/plugin-loader.ts                 |  34 +++++-
 packages/core/src/plugin-mcp-servers.ts            |   8 +-
 .../context-plugin-mcp-discovery-isolation.test.ts |  48 +++++++++
 packages/dashboard/src/routes/context.ts           |  39 ++++++-
 ...-runtime-plugin-mcp-discovery-isolation.test.ts |  69 +++++++++++++
 packages/engine/src/runtimes/in-process-runtime.ts |  47 +++++++--
 9 files changed, 364 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-8596

Fusion-Task-Lineage: 231e53b6-a9a3-4a65-9732-3dabe44da198

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 08:35:04 -07:00
gsxdsm
d47d44c669 fix(engine): demote residual routine/peer-exchange poll chatter
Close log-spam skeptic gaps: routine-scheduler pause and re-entrance no-ops, and peer-exchange zero-work sync cycles, move to debug with contract-test locks.
2026-07-26 08:26:20 -07:00
gsxdsm
cfa84781d6 fix(engine): demote high-frequency TUI log spam to debug
Route steady-state chatter (maintenance batch, skill listings, activity heartbeats, stuck polls, SSE connect/disconnect, heartbeat timer skips, cron/routine de-dupe skips, hold-release capacity races) through FUSION_DEBUG so the operator log pane keeps real state changes and failures.
2026-07-26 08:20:37 -07:00
gsxdsm
beb83a1c1b fix(engine): close the unowned-card strand and harden the planning path
Second FN-8596 strand, found after the first fix shipped. Clearing the
stale `planning` status moved the card into a state owned by NOBODY:

  - planning excluded it: stale `firstExecutionAt` from its first pass made
    hasAdvancedPastPlanning true, and the previous fix only rescued cards
    that still carried a planning-stage status;
  - recoverAdvancedTriageTasks — the designated owner of that
    "stranded-advanced" class — also excluded it, because it bails on
    `workflowIrPinColumnId === "triage"`: it cannot resume a card into the
    column it already occupies (the pin was plan-replan, which lives in
    triage).

So the card sat indefinitely with no sweep, log, or audit event naming it.

hasAdvancedPastPlanning now decides on arrival order alone for any card in
the planner column: a stamp written BEFORE the card reached triage belongs
to a previous pass, whatever the status is now. A card that genuinely
advanced is still caught by the column check at the top, and one claimed by
execution AFTER landing here has a stamp newer than its arrival, so it
still reads advanced and stays with advanced-recovery. This flips one case
I added in the previous commit — production proved that classification
stranded the card.

Hardening, so this class cannot hide again:

  - detectStalledCards: a detect-only watchdog emitting
    `task:stall-watchdog-detected` for any non-terminal, unpaused card idle
    past 30m with no live session and no queued continuation. Deduped per
    shape. It deliberately does NOT mutate — a generic mutator racing the
    specialized sweeps is the bug class this file keeps re-fixing, so
    recovery stays with the sweep that owns each shape and this guarantees
    visibility.
  - The silent skips are now loud: runIfStillPlanningUnderTaskLock (all
    four callers inherit it), the planning handoff moveTaskIf, and the four
    requestPreMergeOptionalStepFix refusals now log why nothing was
    scheduled and that the card was left parked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:59:42 -07:00
gsxdsm
f005cee885 fix(engine): surface silent stalls and add stalled-card watchdog
Make planning-guard and remediation no-ops emit warnings, and detect idle non-terminal cards with no session or continuation so FN-8596-class strands show up in logs and run-audit.
2026-07-26 07:49:05 -07:00
gsxdsm
08f69745f9 fix(engine): quiet routine maintenance batch logs in TUI
Route per-step maintenance success/skip chatter to debug so the operator log pane keeps real recovery events instead of 50+ no-op lines every cycle.
2026-07-26 07:38:06 -07:00
Phil Larson
0643a64f0d fix(desktop): pin complete Pi runtime closure (#2439)
## Summary
- pin `pi-agent-core`, `pi-ai`, `pi-coding-agent`, and `pi-tui` to one
exact 0.82.0 workspace override set
- extend the Pi version policy guard to reject missing, ranged, or
mismatched desktop runtime closure overrides
- add a patch changeset for the legacy desktop packaging fix

## Test plan
- `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs` (5
passed)
- `node scripts/check-pi-versions-pinned.mjs`
- `corepack pnpm check:changesets --strict`
- focused engine fixtures: 4 files / 47 tests passed
- GitHub: Desktop packaging, Lint, Typecheck, Build, Gate, and Greptile
Review passed
2026-07-26 07:34:50 -07:00
gsxdsm
4633c6441b fix(engine): stop stranding replan cards on stale execution stamps
Root cause of the FN-8596 strand (card sat in Planning, doing nothing,
until an engine restart).

Plan Review returned REVISE, the graph rebounded the card to `triage` with
`needs-replan`, and triage claimed it — overwriting the status with the
TRANSIENT `planning`. `needs-replan` is a durable park that outranks the
execution timestamps, but `planning` is deliberately excluded from
REPLAN_PARK_STATUSES, so the card fell through to the stamp check. Those
stamps were written when it entered `in-progress` on its FIRST pass and are
never cleared, so the replanning card read as "advanced past planning" for
the rest of the session.

From there everything was a silent no-op:
updatePlanningStateIfStillCurrent returned false and its callers returned
with no log, no audit and no requeue. The revision session wrote the
revised PROMPT.md (via the store tool, which bypasses the guard) and the
finalize refused to hand the card off — "prompt written, then total
silence", status frozen at `planning`.

Stale stamps are now discriminated from a live claim by arrival order: a
stamp written BEFORE the card arrived in the planner column belongs to a
previous pass, while one written after arrival means execution genuinely
won the FN-8361 race and recovery must not clear the status out from under
it. A missing/unparseable columnMovedAt keeps the prior answer, so this can
only narrow the strand, never widen the race. The PR #2360
stranded-advanced class (stamps with no planning status) is untouched — all
30 pre-existing guard cases still pass.

Also warns when a planning finalize declines to hand off. That path was
completely silent, which is why this strand left nothing in any log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:33:37 -07:00
gsxdsm
581b7d0a49 fix(triage): clear stale planning statuses periodically, not only at startup
Observed on FN-8596: a plan-review REVISE routed to `plan-replan`, triage
claimed the card with `status:"planning"` and ran the revision session, and
the session wrote the revised PROMPT.md then died without finalizing. The
card sat in `triage` with `status:"planning"`, no live planner, and no
workflow continuation.

That status makes the card invisible to triage rediscovery (it looks
claimed), and the only sweep that cleared it ran at STARTUP — so the card
was unrecoverable short of an engine restart. The leaked-slot reaper then
reclaimed its concurrency slot, which made it look idle without making it
runnable.

Adds a periodic counterpart in the poll loop. Clearing the status is the
whole repair: the card is back in triage with a real spec, so ordinary
rediscovery re-picks it. It does not move, pause, or fail the card.

Guards against racing a healthy planner: the in-process `processing` set,
plus a 20-minute staleness floor that also covers a planner owned by
another node this process cannot see. Operator parks are never touched.

This fixes the recovery gap, not the trigger — why that session failed to
finalize is still under investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:23:44 -07:00
gsxdsm
26dcccb7c3 fix(workflow): harden review-gate lifecycle interactions in In review
Follow-ups to running the pre-merge review gates in `in-review`. Each was
verified against the code before being fixed; one reported issue was
refuted and is noted below.

1. Symbol locks (packages/core/src/task-store/moves.ts)
   FN-8306 made the lifecycle transition the symbol-lock RELEASE authority
   but wrote no counterpart. That was harmless while a task only left WIP
   at handoff/terminal; the gate crossing now releases the task's declared
   symbols and the remediation node re-enters `in-progress` to edit the
   same files in the same live worktree with its locks gone. Neither
   acquire site (scheduler dispatch, claimDueWorkflowWorkItem) is on the
   graph re-entry path. Adds a symmetric re-acquire on `!wip -> wip`.
   Best-effort by design: a contended symbol logs and proceeds, which is
   exactly the pre-fix posture, rather than parking the remediation behind
   another holder and re-creating the stranding this change set removed.

2. Premature merge (packages/engine/src/self-healing.ts)
   `recoverMergeableReviewTasks` was the only in-review sweep with no
   liveness gate. The graph commits the column crossing at node entry and
   writes the gate's pending lease two DB round trips later, and
   `getTaskMergeBlocker` has no notion of "enabled but resultless", so in
   that window the sweep could enqueue a merge with Code Review never run.
   Filters `executingIds`, matching recoverGhostReviewTasks.

3. Orphan sweep (packages/engine/src/self-healing.ts)
   The reported restart hazard is REFUTED: nothing re-attaches an in-review
   graph run, so those leases are genuinely dead and marking them failed is
   correct FN-8492 behavior. But the sweep also runs from periodic
   maintenance in the same live process, where a tick between the lease
   write and session registration could fail a gate that just started.
   Honors a within-floor `classifyReviewLease`, matching the semantics Plan
   Review already had. Cleanup of dead leases is delayed by the staleness
   floor, not defeated. The audit event gains `needsOperatorBypass` for
   `autoMerge:false` rows, which self-healing deliberately skips and only
   fn_task_bypass_review can clear — previously indistinguishable from an
   auto-recoverable rewrite.

4. Stall detection (packages/engine/src/planner-overseer.ts)
   The `reviewer` and `merger` stages had no time-based check at all and
   returned `progressing` unconditionally, so a hung gate produced no
   signal however long it sat. Adds gate-anchored detection on both (a
   plain in-review card with no reviewState resolves to `merger`, not
   `reviewer`), keyed on the pending lease's own `startedAt` rather than
   `columnMovedAt` so it cannot fire during a legitimate human merge-wait.

`cumulativeActiveMs` is documented, not changed: it now excludes gate
runtime, but adding the `timing` trait to `in-review` would count arbitrary
human merge-wait as active work — a worse distortion than the omission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:28:04 -07:00
gsxdsm
47d030215c feat(workflow): run pre-merge review gates in the In review column
Code Review and Browser Verification now run with the card in `in-review`
instead of `in-progress`, so the board shows the card under review with the
running step as a badge (matching the Coding (Ideas) preset). Their paired
remediation nodes stay in `in-progress`, so a changes-requested verdict
visibly sends the card back to implementation.

The column move IS the badge switch: the dashboard badge was already
lane-gated on `column === "in-review"`. Applied to the shared stepwise
coding IR, so it is inherited by builtin:coding (the default),
builtin:stepwise-coding, builtin:brainstorming and builtin:coding-ideas;
builtin:legacy-coding keeps its historical placement.

Two consequences handled:

- Capacity: `in-review` has no `wip` trait, so the slot is released during
  review and the remediation crossing back into `in-progress` can hit the
  non-bypassable in-transaction capacity check. The column boundary now
  PARKS the run on a `capacity-exhausted` rejection instead of failing it,
  preserving the failed gate result and worktree so the next graph run
  retries once a slot frees. Non-capacity rejections still propagate.

- Reopen clears: `applyReopenFieldClears` wiped `workflowStepResults` on
  every in-review -> in-progress move, which the remediation crossing now
  performs routinely. That destroyed the remediation input, made
  `routeRetryableRemediationGraphFailureToPreMergeFix` and
  `recoverFailedPreMergeWorkflowStep` silently no-op, and — worse — made
  both `getTaskMergeBlocker` branches vacuously false, so a card could
  return to `in-review` and be mergeable with its gate never re-run. Now
  exempted for graph-owned in-review -> in-progress crossings only;
  operator reopens, merge bounces and every -> todo/triage rebound still
  clear, so the executor's documented bounce invariant is unchanged.

Adds regression coverage for both (there was previously none for the
reopen clear in either direction), and annotates the unreachable legacy
scheduler dispatch block rather than mirroring the fix into dead code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:04:40 -07:00
gsxdsm
fd073e287f FN-8592: self-heal stranded hold continuations
Restore graph-owned plan-review continuations for eligible hold-column cards stranded after planning cancellation.

- Detect real-spec hold cards with no active workflow continuation and re-seed Plan Review safely.
- Serialize workflow continuation seeding, review-result writes, and lease claims to prevent duplicate recovery.
- Add recovery diagnostics, release warnings, regression coverage, and a patch changeset.

Files changed:
 .changeset/fn-8592-stranded-hold-continuation.md   |   7 +
 AGENTS.md                                          |   1 +
 docs/architecture.md                               |   4 +
 .../workflow-task-serialization-protocol.test.ts   | 119 +++++++++++++
 .../workflow-work-items-conditional-seed.test.ts   | 191 +++++++++++++++++++++
 packages/core/src/store.ts                         |   5 +-
 .../src/task-store/async-workflow-workitems.ts     | 123 +++++++++----
 packages/core/src/task-store/project-store-ops.ts  |  14 ++
 .../src/task-store/workflow-task-create-ops.ts     |  16 +-
 .../src/task-store/workflow-workitems-ops-2.ts     |  91 ++++++----
 .../src/__tests__/pre-release-plan-review.test.ts  |  17 ++
 ...self-healing-stranded-hold-continuation.test.ts | 171 ++++++++++++++++++
 packages/engine/src/hold-release.ts                |  57 +++++-
 packages/engine/src/plan-review-continuation.ts    |  94 ++++++++++
 packages/engine/src/runtimes/in-process-runtime.ts |  30 +---
 packages/engine/src/self-healing.ts                | 100 ++++++++++-
 16 files changed, 945 insertions(+), 95 deletions(-)

Fusion-Task-Id: FN-8592

Fusion-Task-Lineage: fe7ffd34-96e4-4418-a879-7418e6293d30

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 00:46:07 -07:00
gsxdsm
8b9cf3d4e8 fix(dashboard): remove right-edge dead space on landscape-tablet task pop-ups
The `.floating-window__body` resize-handle clearance gutter (FN-8015) was
carved out only for 769-1024px, so landscape iPads (1180-1366 CSS px) fell
through to the desktop contract and kept it — content stopped ~17px short of
the right edge while the left edge stayed flush.

Gate the carve-out on the input device instead of viewport width: the gutter
only protects resize hot zones a pointer can actually grab. `(pointer: coarse)`
is primary-input only, so a touchscreen laptop on a trackpad still reports
`fine` and keeps desktop clearance.

Measured at 1180px: header inset went from 1px/17px to 1px/1px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 00:00:19 -07:00
gsxdsm
106c61e6ee fix(agent-tools): close the fn_delegate_task Deny bypass and the store's window clamp
Follow-up to 13a2b2a9d, from a multi-agent review of that commit. Three of its
claims did not hold.

1. fn_delegate_task bypassed the gate entirely (P0). It reaches the same
   createAgentTask primitive, was registered unconditionally in both session
   lanes, and validated only that the TARGET agent is non-ephemeral — never the
   caller. Under Deny an ephemeral worker could enumerate agents and delegate
   unlimited tasks. It is now withheld under Deny, and also under
   upon_validation: delegation has no proposal channel, so leaving it available
   would launder a create past the operator review that policy requires.

2. The widened dedupe window was capped at 5 minutes. The store query in
   branch-and-pr-entities.ts carried its own independent `?? 60_000` /
   `min(300_000, …)` pair, so widening only duplicate-guard.ts under-delivered
   and made the new ceiling unreachable. Both sites now share
   FINGERPRINT_WINDOW_DEFAULT_MS / FINGERPRINT_WINDOW_MAX_MS.

3. The pi-extension gate does not fire at all. pi's ExtensionContext carries no
   agentId — the read is a speculative cast and only tests supply one, so every
   real call short-circuits as a human caller. The fail-closed direction is kept
   for the day an identity signal exists, but the limitation is now documented
   instead of implied to be enforcement.

Also: the session prompt now states when creation is disabled and names
fn_task_log as the fallback (the base prompt still taught fn_task_create, which
is the same instruction/capability mismatch that fed the retry storm);
suppression emits an `agent:task-create-withheld` run-audit event; and the two
source-text ratchet tests are replaced with behavioral assertions on the tool
list the executor actually hands the model — verified to fail when the guard is
broken, which the string assertions did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:44:56 -07:00
gsxdsm
0c85613313 fix(engine): address code-review findings on the planner/worktree recovery fixes
Review of 2dbfe3d31 + 05b704dc6 surfaced real defects in both fixes:

- The unusable-worktree probe composed two helpers across an unnecessary
  self-healing -> step-runner import edge, and the directory check added no
  discriminating power over the `.git` probe. Replaced with one canonical
  hasUsableWorktreeShape beside classifyTaskWorktree, which also applies the
  repo-root gate (FN-6861) when a rootDir is available; both call sites pass one.
  Its narrower guarantee vs the canonical classifier is now documented and
  pinned by tests, including the de-registered shape it cannot see.
- REPLAN_PARK_STATUSES is derived from PLANNING_STAGE_STATUSES instead of
  re-listed, so a new durable park status cannot be added to one set only.
- The preserve/clear decision no longer pretends to steer `worktree`: the rebound
  is a reopen move, which clears it regardless. Documented, and the test now
  asserts the durable row rather than only the updateTask argument.
- `branch` is cleared only when it is the re-derivable canonical fusion/<id>;
  a non-canonical branch survives so a card's only commit pointer is not dropped.
- The recovery log named the recorded worktree even when the session had targeted
  an AI-merge clean room. It now names the refused path and says whether the
  recorded worktree was gone too.
- Added task:auto-recover-worktree-session-metadata so the decision is legible to
  agents, not only in human log prose.
- isTaskStillInPlanningStage's parameter type now includes the execution stamps
  its implementation reads.
- Test hygiene: real-fs fixtures wrapped in try/finally; changeset dev note
  corrected; FN-8361 asserted at the discovery surface, not only in the guard
  table.

Also captures the shared bug class in docs/solutions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:24:10 -07:00
gsxdsm
351fba1d33 FN-8594: improve project overview mobile reflow
Make the project overview usable at small viewport widths while hiding its non-functional mobile task navigation.

- Reflow overview filters, stats, cards, and skeletons for 480px and 380px breakpoints.
- Hide the mobile navigation bar and clear its reserved height outside an active project task view.
- Add regression coverage and document responsive dashboard behavior.

Files changed:
 docs/dashboard-guide.md                            |  5 +-
 packages/dashboard/app/App.tsx                     |  8 +-
 .../mobile-feature-access-regression.test.tsx      | 22 ++++-
 .../project-overview-small-screen.test.ts          | 91 ++++++++++++++++++++
 packages/dashboard/app/components/MobileNavBar.tsx | 19 ++++-
 packages/dashboard/app/components/ProjectCard.css  | 58 +++++++++++++
 .../dashboard/app/components/ProjectOverview.css   | 99 ++++++++++++++++++++++
 7 files changed, 294 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8594

Fusion-Task-Lineage: 0999f6e4-ffe9-4ed6-a471-8e5ba3413029

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 23:12:41 -07:00
gsxdsm
13a2b2a9da fix(agent-tools): hide fn_task_create under Deny and widen the dedupe window
Operator report: with project policy "Ephemeral agent follow-up tasks = Deny",
an executing agent filed ten follow-up tasks — five parallel fn_task_create
calls it reported as timed out, then five sequential retries.

Two defects:

1. Deny was advisory. fn_task_create was registered for every session and only
   refused inside execute(), so the model still saw the tool, planned around it,
   and retried it. The pi extension's isEphemeralCallerAgent also failed OPEN
   whenever the caller id did not resolve to an agent row — which is the normal
   shape of an ephemeral task-worker — so on that lane Deny was a no-op.

2. The deterministic content-fingerprint duplicate window was 60s, which only
   covered concurrent in-flight creates. A retry two minutes later saw nothing
   and filed a second task.

Fixes: isAgentTaskCreateToolAvailable() withholds the tool from ephemeral
sessions under Deny in both engine lanes (outer execution session, per-step
workflow session); isEphemeralCallerAgent fails closed on an unresolvable
caller id; the fingerprint window goes 60s -> 10m (clamp ceiling 5m -> 1h).
upon_validation keeps the tool, and permanent-agent and human/chat callers are
unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:03:46 -07:00
gsxdsm
05b704dc60 fix(engine): stop requeueing review tasks into a worktree that no longer exists
Unusable-worktree recovery preserved task.worktree whenever the failing path
differed from it, treating the difference as proof the recorded worktree was
live. The reported strand had both gone: an AI-merge clean room refused as an
"incomplete worktree" while the task worktree had already been removed, so
every requeue re-dispatched into a missing directory ("Working directory does
not exist … Cannot execute bash commands") until the retry budget burned out
and the card parked failed in review. Preserve the recorded worktree only when
it is still a usable checkout; otherwise clear it so the next dispatch builds a
fresh one from the branch.

Also splits the planning-stage guard: only the DURABLE replan parks
(needs-replan, plan-review-unavailable) outrank sticky execution stamps.
"planning" is the transient planner claim, so a stamp landing on it still means
execution won the FN-8361 race.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:03:11 -07:00
gsxdsm
99b80ad748 feat(dashboard): add opt-in auto-update and harden restart supervision
Add the `autoUpdateAndRestart` global setting (default off, Settings ->
General next to Release channel). When enabled, the dashboard host installs
available updates on the selected channel by itself and requests the
supervised in-place restart. Supervised hosts only: without a parent to
respawn, installing would leave a running process whose code no longer
matches its own install.

Fix two ways the restart affordance could silently do nothing:

- The supervisor now stamps FUSION_SUPERVISOR_PID and supervision is only
  counted when that pid is the real parent. FUSION_RESTART_SUPERVISED is
  inherited by every process Fusion spawns, so `fn dashboard` launched from
  an agent terminal skipped its own supervisor while still advertising
  restart support -- a restart request then killed it for good.
- Settings and the update banner probe /system/info on mount and treat
  capability as advisory: the button always issues the request and shows the
  server's actual refusal instead of sitting disabled after a failed probe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:52:53 -07:00
gsxdsm
2dbfe3d312 fix(engine): re-plan cards that Plan Review sends back instead of stranding them
Plan Review REVISE rebounds a card to a planner lane with status
needs-replan, but hasAdvancedPastPlanning read the sticky
firstExecutionAt/executionStartedAt stamps as proof the card had left
planning. Triage discovery filters on that guard, so a rebounded card was
never re-admitted and sat in triage/needs-replan forever ("stuck in
planning"). An explicit planning-stage status now outranks the stamps in
both planner lanes; a triage card stamped with no planning status is still
excluded so self-healing's advanced recovery keeps owning it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:41:18 -07:00
gsxdsm
147398f2c8 FN-8595: add mobile project favorite controls
Group mobile project switcher favorites ahead of other projects.

- Reuse the shared bookmark store for mobile project rows and toggles.
- Add responsive favorite-control styling and coverage for grouping, empty states, and selection behavior.
- Add a release changeset for the mobile favorite-project experience.

Files changed:
 .changeset/mobile-project-favorites.md             |   7 ++
 packages/dashboard/app/components/Header.tsx       | 120 +++++++++++++++------
 .../dashboard/app/components/ProjectSelector.css   |  40 +++++++
 .../Header.mobile-project-favorites.test.tsx       | 116 ++++++++++++++++++++
 4 files changed, 253 insertions(+), 30 deletions(-)

Fusion-Task-Id: FN-8595

Fusion-Task-Lineage: 1f739ee9-1ff5-4b47-ac9e-7e4a5857f1a5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 21:57:15 -07:00
maren61513
f1a202916f chore(deps): add .github/dependabot.yml (#2435)
Adds `.github/dependabot.yml` so GitHub automatically opens PRs for
outdated dependencies on a weekly cadence. Covers both the primary
package ecosystem and GitHub Actions workflows.

The PR limit is set to 5 to avoid noise. Feel free to adjust or drop
either ecosystem.

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

## Summary by CodeRabbit

* **Chores**
* Added automated weekly checks for updates to GitHub Actions and npm
packages.
  * Limited open npm update requests to five at a time.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-25 21:14:35 -07:00
gsxdsm
bf317f6340 chore(release): v0.74.0-beta.3
Version bump via changesets.
2026-07-25 21:06:33 -07:00
gsxdsm
10ebd0e025 fix(dashboard): keep quick-entry portal menus attached when space is tight
Extract shared fixed-menu positioning so deps/agent/node/priority pickers
clamp max-height without detaching from the trigger when free space is
shorter than the preferred dropdown height.
2026-07-25 21:01:25 -07:00
gsxdsm
c8427f1c1e test(engine): repair Full Suite drifts after plan-worktree cutover
Re-pin the execSync allowlist after self-healing/executor line shifts, capture
implementation-session tools under the graph-owned pause harness, treat
worktree alone as not past planning for replan targets, and age starved-
refinement fixtures past the post-escalation cooldown window.
2026-07-25 21:01:21 -07:00
gsxdsm
9f6aaa933d fix(engine): save worktree-written plans to the project and the database
Planning sessions run in the task's own worktree with the coding tool
surface, but the spec path handed to the planner is relative
(.fusion/tasks/<id>/PROMPT.md) while finalization reads it against
rootDir. A planner using the generic write tool instead of
fn_task_prompt_write stranded the spec inside the worktree, where
finalization could not see it and worktree disposal destroyed it.

project.tasks also has no `prompt` column, so PROMPT.md was
filesystem-only and the project checkout was the sole durable copy of
every plan.

Add plan-artifact-writeback.ts:

- reconcileWorktreePlanArtifact copies a worktree-stranded PROMPT.md
  back into the main project .fusion folder through
  store.updateTask({ prompt }), keeping File Scope validation, the root
  write, and the task.json sync atomic. Empty, absent, and identical
  worktree copies are no-ops so a correct spec is never clobbered.
- mirrorPlanToProjectDb mirrors the authoritative plan into the `plan`
  task document, which triage already reads as a planning-draft
  fallback, making that recovery path DB-backed. Identical content is
  skipped so revisions do not churn.

Both are best-effort: a failure never turns a good planning pass into an
error. Wired at the reconcile-before-finalize-read seam, at finalization
with the post-hygiene accepted content, and inside fn_task_prompt_write.

Covers the invariant rather than the repro: tests assert worktree-
stranded, root-only, empty worktree file, absent file, identical
content, persistence failure, redundant-mirror skip, and mirror failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:46:07 -07:00
gsxdsm
20c5e1ff4e fix(dashboard): let the mobile settings footer scroll sideways by touch
The footer rail already had overflow-x: auto, so it scrolled with a mouse,
but the global mobile `* { touch-action: pan-y }` lock intersected horizontal
pans away from every element. Opt the rail and its inner touch targets (the
buttons receive the touchstart; touch-action is not inherited) back into
pan-x, contain overscroll so a fling does not chain out to the document, and
free the button groups from the mobile max-width: 100% reset that squeezed
them into overlap instead of widening the scrollable content.

Also align the footer's media query with MOBILE_MEDIA_QUERY (max-width 768px
OR max-height 480px), which SettingsModal.tsx uses to pick the mobile footer
markup: landscape phones were rendering mobile markup under desktop CSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:18:45 -07:00
gsxdsm
17b8bfecde fix(dashboard): stop small mobile board swipes from jumping several columns
Multi-column fling reach came from release velocity alone, so a quick short
thumb flick (~30px, several px/ms) bought 2-3 extra columns and the board flew
past the intended column. Extra pages now also have to be earned with travel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:02:20 -07:00
gsxdsm
b5707318b5 feat(engine): add force to fn_task_promote for agent-native override parity
fn_task_promote can now pass force:true to start execution when a task is still
waiting on planning or plan review, matching the dashboard's promote override.
The rejection message names the flag so a caller that hits the gate can decide,
and a forced release says the pending replan was cancelled rather than burying it.

Force stays opt-in per explicit promote request: the hold-release sweep and the
webhook event release have no force parameter, so FN-7648 still holds for every
automatic surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:02:14 -07:00
gsxdsm
82e0ce3132 fix(engine): reclaim task branches whose tip is an inherited foreign commit
The reclaim sweep's tip-already-merged arm vetoed on the branch tip's foreign
Fusion-Task-Id trailer alone. A task branch cut from the base that never
committed anything (planning aborted, card moved back to todo) points at the
PREVIOUS task's landed commit, so the veto fired on inherited metadata: the card
kept stale worktree/branch/baseCommitSha and re-logged
"already-merged rejected ... reason=foreign-task-tip" every sweep.

Hoist the merge-base diff proof already used by already-merged and
branch-misbound recovery into a shared foreignTipRejection helper and route all
three callers through it. Rejection still fires when the branch carries unique
content or the base already has this task's own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 20:01:50 -07:00
gsxdsm
41d60f0355 feat(board): explain the unplanned promote rejection and let operators force past it
Promote on a held card printed the raw i18n key `board.rejection.unplannedForExecution`:
FN-8471 added the server-side code without a client case or catalog entry, so
translateRejection fell through to `t(messageKey, messageKey)`.

- Add the explicit rejection case (both translate helpers) plus the en catalog
  entry and secondary-locale stubs.
- promoteHeldTask(..., { force }) waives ONLY the unplanned-for-execution gate;
  hold membership, capacity and slot reservation still arbitrate. It clears a
  needs-replan/plan-review-unavailable status so triage rediscovery cannot pull
  the card back into the waived replan, and emits task:promote-forced-unplanned.
- POST /tasks/:id/promote accepts { force: true }; the board asks for explicit
  confirmation first and only offers the override for this rejection.

Force stays operator-only — the sweep, the webhook release and fn_task_promote
never set it, so FN-7648 still holds for every automatic surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:56:54 -07:00
gsxdsm
1efff4e83c test(core): speed up schema-applier PG tests by dropping psql subprocess spawns
Route admin CREATE/DROP DATABASE through a short-lived postgres.js maintenance
connection instead of spawning psql via execSync per call, and remove the
redundant DROP-before-CREATE (db names are pid+random, never pre-exist).
Cuts ~2 of 3 subprocess forks per test across ~55 tests; the slowest core
test file drops from ~90s under full-suite contention (32.6s->27s standalone)
with all 75 tests still green.

Fusion-Task-Id: FN-SLOW-TEST
2026-07-25 17:20:18 -07:00
gsxdsm
0c93f5c41e test(dashboard): speed up SettingsModal close-flush tests
Replace whole-tree *ByRole("button", { name: "Close" }) dismissal queries
with scoped querySelector targets (.modal-close, .modal-actions-right button,
.settings-embedded-mobile-close) in the SettingsModal suites and shared harness.
The role+name query recomputes accessible names across the large Settings tree,
adding ~700ms per dismissal test. Assertions are unchanged.

- footer Close 1058ms -> 332ms; embedded mobile close 1003ms -> 344ms
- models-auth workflow-lane flush ~1s -> 235ms
- expectSettingPersists/assertProjectModelSavePayload harness helpers scoped too

422 SettingsModal tests pass; lint clean.
2026-07-25 17:17:24 -07:00
gsxdsm
2560944663 chore(release): v0.74.0-beta.2
Version bump via changesets.
2026-07-25 16:16:07 -07:00
gsxdsm
d10d91bae5 fix(engine): stop planning when a card is withdrawn; sweep stale pre-execution worktrees
Withdrawing a card from planning (todo -> Ideas) now stops the work:
- triage aborts and disposes the planning session through the same path
  pause/delete already use, and clears status:"planning" so the planning badge
  goes away and the card reads as a plain idea again;
- the executor aborts in-flight graph work on any backward move out of
  todo/triage, so a Plan Review does not keep streaming against a card the
  operator pulled back;
- moving it back to todo needs no new code: the existing column wake fires and,
  with the status cleared, the card is an ordinary planning candidate again.

Pre-execution worktrees (planning acquires one now) are reclaimed two ways: an
immediate release on an explicit withdrawal, and a self-healing sweep
`reconcile-pre-execution-worktrees`. The sweep is deliberately timid — 30 days
of complete inactivity, and it skips anything active or waiting (todo,
executing, in-review, done, paused, carrying any status, blocked, or scheduled
for recovery). Every real safety condition lives in the executor: never
executed, no live session, clean branch, nothing uncommitted.

hasAdvancedPastPlanning no longer reads a worktree as execution evidence.
Planning owns a worktree now, so that signal would have made every planning
write skip; execution timestamps carry the meaning instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:20:17 -07:00
gsxdsm
168819b35d fix(engine): run every lane in the task worktree; contention is a wait, not a failure
Contention prevention (why tasks shared a path at all):
- Planning ran `tools: "coding"` at the repo root, so every planner had write
  tools in the operator's checkout and all planners shared one path. Planning
  now acquires the task's own worktree (TriageProcessor.acquirePlanningWorktree
  -> TaskExecutor.ensureTaskWorktreeForPlanning).
- Graph nodes with no worktree acquired one instead of falling back to rootDir,
  so Plan Review / Code Review / custom gates all run isolated. Plan Review
  re-acquires when its recorded worktree is gone, replacing FN-7996's
  run-from-the-repo-root degrade. Workspace projects are unchanged.
- Registration goes through acquireActiveSessionPath, which reclaims a leaked
  entry whose holder is provably dead and aged past the FN-5256 floor. A live
  holder still contends — real serialization is never clobbered.

Classification (the reported symptom):
- A lease held by another task is no longer a provider failure. It carries
  SESSION_CONTENTION_HOLD_VALUE, classifies transient, is excluded from
  isNonPlanDefectPlanReviewFailure, and stops burning the node's fast retries.
- The executor waits it out on a 10-attempt 5s->60s ladder and then leaves the
  task cleanly queued. There is no terminal branch: contention always ends, so
  parking would only ask a human to press Retry on a condition that fixed
  itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:59:53 -07:00