Commit Graph

3538 Commits

Author SHA1 Message Date
gsxdsm
8b039a543e fix(desktop): advance Pi runtime pin to 0.82.1 for packaging PR lane (#2465)
## Summary
- Advance the matched Pi runtime pin (`pi-ai`, `pi-coding-agent`,
`pi-agent-core`, `pi-tui`) from **0.82.0 → 0.82.1** so
electron-builder's production-dependency walk accepts `pi-agent-core`'s
`pi-ai@^0.82.1` requirement.
- Fixes the Desktop packaging PR-lane failure:
`Production dependency @earendil-works/pi-ai not found for package
@earendil-works/pi-agent-core` (required `^0.82.1`).
- Keep the workspace override guard; update pin-policy fixtures and CLI
package-config expectations.
- Tighten the advisory packaging step-order test so it asserts against
the real `electron-builder --dir` step (not a missing release-only step
name that previously passed via `indexOf === -1`).
- Run `pnpm dedupe` so the packaging lane's lockfile dedupe
early-warning is clean.

## Context
#2439 pinned the full Pi closure at 0.82.0 and made recent main-based
packaging runs green. This advances to the current upstream patch so
deploy + electron-builder stay aligned with `pi-agent-core@0.82.1`'s
declared dependency range.

## Test plan
- [x] `node scripts/check-pi-versions-pinned.mjs`
- [x] `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs`
- [x] `pnpm --filter @runfusion/fusion exec vitest run
src/__tests__/package-config.test.ts`
- [x] `pnpm --filter @fusion/desktop exec vitest run
src/__tests__/release-workflow.test.ts`
- [x] `pnpm dedupe --check`
- [ ] GitHub: Desktop packaging (should run full packaging walk —
lockfile/package.json touched)
- [ ] GitHub: PR Checks (Lint, Typecheck, Build, Gate)
2026-07-26 23:47:49 -07:00
gsxdsm
99c9f14ee0 feat: run Plan Review in the planning lane with a Plan Review badge (#2462)
## What

Plan Review, planning, and the replan loop move from the implementation
column into the **planning lane** (`todo`), so a task under
specification never holds a WIP slot. The card crosses into
`in-progress` exactly once, at `parse`, released by the scheduler.

Operators also finally see a **Plan Review** badge while the gate runs —
it was previously invisible on the default workflow.

## The part that made it possible

Moving the node is ten lines. It was attempted three times and reverted
each time, because a graph run with no durable continuation replayed
from `start` and dragged an in-progress card *backward* out of the WIP
column, firing `abort-on-exit` and stranding it in a pre-WIP column with
no releaser.

So this PR adds the graph **entry contract** —
`resolveColumnResumeNode`:

| Card is in | Resumes at |
|---|---|
| `triage` | `start` |
| `todo` | `plan` |
| `in-progress` | `parse` — never re-plans, never moves backward |
| `in-review` | first review node — gates are not skipped |

`ir.columns` is ordered and that order is the lifecycle order; rework
and failure edges are excluded so the entry point is always the main
path. The proof it's the right fix: **`executor-task-done-invariant`
passes unmodified** after failing every previous attempt.

## Also in here

- **Release gate narrowed twice.** `isUnplannedForExecution` applies its
pre-release plan-review gate only when the node's column equals the
card's column *and* the group is enabled for the task. The enablement
check fixes a real deadlock — a task with Plan Review toggled off was
held forever waiting for evidence nothing would ever write.
- **Badge cleanup.** Gate badge reads "Plan Review" instead of the
ambiguous "Reviewing" and no longer hides behind a lane restriction; the
status badge stops duplicating it; `planning` renders as "Planning"
instead of the raw engine token.
- **Coding (Ideas)** renames its planner column to "Planning" (id `todo`
unchanged) and loses its private planning-node re-home — the graph it
clones is already plan-in-place.
- **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose
column its workflow no longer declares. Written for a follow-up, kept
because it makes any column edit survivable.

## Test changes

Scheduler and release fixtures now model a card whose Plan Review passed
— the state every real card is in when the capacity sweep sees it. A
held unreviewed card is the gate working, and that path stays owned by
`pre-release-plan-review.test.ts`.

New `workflow-graph-entry-contract.test.ts` covers the invariant at
every lifecycle position, plus the gap-column and remediation-node
cases.

## Verification

Gate 299 + 70 + 10, dashboard badge suites 672, engine
workflow/entry/executor suites 147, core 122. Lint and typecheck clean.
Full engine suite sits at the pre-existing baseline (notifier /
plugin-runner / notification-service, untouched by this).

## Follow-up

Removing the Todo column entirely is a separate ~207-site
lifecycle-vocabulary refactor — planned in
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
(companion docs PR).

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

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

## Summary by CodeRabbit

* **New Features**
* Plan Review now runs in the Planning lane before implementation
begins.
* Cards resume from their current workflow column without replaying
earlier steps.
* Added automatic recovery for cards stranded in outdated workflow
columns.
* **Improvements**
  * Renamed the Coding (Ideas) planner column to “Planning.”
* Refined Plan Review gating to respect enabled settings and the card’s
current column.
* Updated planning and Plan Review badges for clearer, consistent labels
across cards and lists.
* **Bug Fixes**
* Improved workflow transitions and release behavior around planning,
review, and execution.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:42:46 -07:00
gsxdsm
5ae6332563 refactor: collapse dead SQLite dual-path code; keep migration-only readers (#2454)
# Remove dead SQLite dual-path code; keep migration-only readers

## Summary
PostgreSQL cutover left hundreds of production dual-path branches
(`backendMode ? PG : SQLite/store.db`) whose SQLite arms only hit
throwing `Database`/`ArchiveDatabase`/`CentralDatabase` stubs. This
change mechanically collapses those unreachable arms so production
authority is AsyncDataLayer/PostgreSQL only, while preserving the six
authorized read-only migration/recovery `DatabaseSync` seams.

## Dual-path mass removed
| Metric | Before | After |
|---|---|---|
| `if (…backendMode)` (non-test) | ~328 | ~70 |
| `store.db` / `this.db` refs in core (non-test) | ~570+ | ~375 (mostly
pure legacy MissionStore/eval/insight SQLite classes + thin getters) |
| Net diff | — | **~6.7k lines removed** across 41 files |

Remaining `backendMode` checks are intentional (incomplete-PG sync
safe-defaults, settings-sync disabled-on-PG, symbol-lock PG-only gates,
“requires PostgreSQL” config versioning throws), not live SQLite
authority.

## Subsystems cleaned
- **Core TaskStore / task-store/***: collapsed if/else and early-return
dual-path across reads, moves, lifecycle, mutations, workflow, archive,
branch/PR, artifacts, comments, audit, project ops, etc. `initImpl` is
PostgreSQL-only (SQLite startup tail deleted).
- **Satellite stores**: automation, agent, routine, plugin, secrets,
approval-request, central-core dual-path arms collapsed.
- **Plugins**: reports async methods, compound-engineering pipeline +
session stores, CLI Printing Press store — SQLite fallbacks removed; PG
required.
- **Engine**: no functional dual-path change beyond whitespace
(settings-sync / peer-exchange PG-disabled behavior kept).

## Six migration-only readers retained (allowlist unchanged)
1. `packages/core/src/postgres/sqlite-migrator.ts`
2. `packages/core/src/project-identity.ts`
3. `packages/core/src/sqlite-validation.ts`
4. `packages/core/src/postgres/startup-factory.ts`
5. `packages/cli/src/commands/db.ts`
6. `scripts/lib/start-local-project.mjs`

Plus low-level `sqlite-adapter` and migrator/startup-import tests.
Inventory ratchet still requires exactly these six `new DatabaseSync(`
production sites, all `readOnly: true`.

## Not treated as SQLite
- `.fusion/project.json`, `task.json`, `agent-log.jsonl` file storage
- AsyncDataLayer / Drizzle PG paths
- Incomplete-PG sync safe-default stubs (still return empty/false/null
under backend without consulting SQLite)

## Verification
- `sqlite-production-reader-inventory.test.ts` — 15/15 pass
- `incomplete-pg-ports.pg.test.ts` — 6/6 pass
- Targeted PG tests (create-task, move, handoff, runtime-persistence,
agent, mission, insight, central-core) — green
- `tsc --noEmit` for `@fusion/core`, `@fusion/engine`,
`@fusion/dashboard` — green
- `scripts/check-no-getdatabase.mjs` — clean

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

* **Improvements**
* Improved end-to-end consistency by making PostgreSQL/async persistence
the standard across core task/workflow, automation, agents, plugins,
routines, secrets, approvals, central operations, and session storage.
* Unified scheduling, settings, configuration revision writes,
run/workflow selection, queues/leases/transitions, and audit/lifecycle
updates around consistent async transaction behavior.
* **Bug Fixes**
* Fixed edge cases for archived/deleted reads, unarchive/recovery flows,
not-found handling, and task/artifact/document/log/comment operations,
including more reliable emissions and hydration across search/list and
lifecycle operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-26 23:28:42 -07:00
Phil Larson
52d64fa66e fix(engine): project CE steps after review handoff (#2464)
## Summary

- reconcile successful graph-native workflow results with pending task
checklist steps even when review handoff already moved the card into the
merge column
- preserve terminal, paused, and no-redundant-move behavior
- cover the real Compound Engineering post-review-handoff state with a
regression test

## Root cause

Compound Engineering runs `review-handoff` before `merge`. Review
handoff moves the task to `in-review`, which is also the merge column.
`ensureWorkflowMergeBoundaryTask()` returned immediately for cards
already in that column, before projecting successful
`workflowStepResults` onto legacy `Task.steps[]`. The merger then saw
`0/N` and rejected approved work with `task has incomplete steps`.

## Verification

- RED: regression test failed before the fix because `store.updateTask`
was never called
- GREEN: `executor-graph-boundary.test.ts` — 6 passed
- relevant non-PostgreSQL set — 31 passed, 5 PostgreSQL tests explicitly
skipped
- `@fusion/engine` typecheck passed
- changeset format passed
- `git diff --check` passed

## Baseline note

`ce-workflow-step-executor.test.ts` currently has three failures on
clean `origin/main` after FN-8601 foreach-proof hardening. The same
failures reproduce without this patch and are not regressions from this
change.


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

* **Bug Fixes**
* Improved reconciliation after review handoff by projecting completed
step results onto the legacy checklist when reaching the merge column.
* Prevented tasks from being marked approved with incomplete step counts
(including “0/N” style states).
* Reduced unnecessary merge failures and deadlock/pause scenarios when
merge-column progress was already recorded.
* **Tests**
* Added coverage for execute-and-merge workflows, ensuring
merge-boundary resolution updates pending steps without moving the task.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-26 23:13:21 -07:00
gsxdsm
0e3d2a2265 refactor: delete meta-task auto-archive and automated recovery follow-ups (#2461)
Deletes two pieces of automated "meta" machinery that filed and
garbage-collected cards restating state already on the task that failed.
Net **-1015 lines**.

## Why

**Automated recovery follow-ups.** `createAutomatedFollowup` and its
dedup engine (289 lines of signature matching, 1h recurrence
rate-limiting, 24h supersedes windows) existed to file recovery cards
for verification-cap and merge-conflict give-ups. In both cases the
parent is *already* parked `failed` with a descriptive `error` and a log
entry carrying the failing command, branch, and output — the card was a
second copy of that.

**Meta-task auto-archive.** The sweeps that garbage-collected those
cards were worse than redundant: the regex classifier matched ordinary
feature work, and its positional fallback bound cards to unrelated
tasks, so **live work could be archived**.

They are removed together, because the auto-archive sweeps only existed
to clean up after the follow-up engine.

## What changed

### Deleted
- `packages/engine/src/verification-followup-dedup.ts` in full —
`createAutomatedFollowup`, `decideAutomatedFollowup`,
`AutomatedFollowupKind`, `computeVerificationFailureSignature`,
`extractFailingTestFiles`.
- `findActiveRecoveryFollowUp` — dead code, defined and never called
(`tsc` independently flagged it `6133 declared but its value is never
read`).
- The meta-task auto-archive sweeps `autoArchiveResolvedMetaTasks` /
`autoArchiveStalledMetaTasks` and helpers `classifyMetaTask` /
`resolveMetaTargetTaskId` / `computeMetaChainDepth` / `archiveMetaTask`
/ `evaluateMetaAutoArchiveGuards`, plus settings
`metaTaskStallAutoCloseMs` and `metaTaskActiveExecutionGraceMs`.
- Run-audit types `task:auto-archived-meta-resolved`,
`task:auto-archived-meta-stalled`,
`task:auto-archive-meta-resolved-skipped`,
`task:auto-archive-meta-stalled-skipped`,
`verification:followup-created`, `verification:followup-deduped`.

The two signature helpers were **deleted rather than relocated** — once
the three call sites went they were provably unreachable:
`buildVerificationFailureSignature` had exactly one caller, and it was
the only caller of `extractFailingTestFiles`.

### Call sites 1 and 2 — park kept, card dropped
Verification-cap and merge-conflict give-ups keep their park, audit
event, operator comment, and log entry. Site 1's `error` string was
reworded off `"See follow-up task for investigation."` (no follow-up
will exist) to carry the guidance itself. `autoResolveDisabled` was
**kept** — it still drives the outer park guard and the `reason` string;
only the inner branch that guarded card creation is gone.

### Call site 3 — autostash orphan, replaced not deleted
This one is a genuine data-loss guard, so it keeps a durable trail. A
`live`-classified orphan is a merger stash holding **real uncommitted
work**, and unlike sites 1–2 there is no parked parent — the parent may
already be `done` and merged, so nothing else on the board would ever
mention the stash.

The card is replaced by a `logEntry` **and** an `addTaskComment` on the
parent, preserving every fact the old description carried: the sha,
`record.label` (the handle `git stash` recovery needs),
`record.detectedByTaskId`, and `sourcePhase`. New truthful run-audit
event `task:autostash-orphan-live-detected` replaces the borrowed
`verification:followup-*` name, with ids/outcomes-only metadata per
AGENTS.md.

### Kept unchanged: the two real product features
Eval follow-ups (`eval-followups.ts`) and PR-comment follow-ups
(`pr-comment-handler.ts`) only borrowed the shared engine for its dedup
pass. Both keep their exact behavior, column, priority, `sourceType`,
and log lines, with dedup inlined as a `listTasks` scan on
`suggestionId` / `prNumber` respectively. Both fail open (create) if the
listing throws, matching the old engine.

## Test changes — read this one

Two tests asserted the *deleted* engine's rate-limited `"[verification
recurrence]"` logEntry. Those assertions were removed, **not loosened**:
both tests still assert no duplicate card is created, and the eval test
still asserts the existing id is reported back. No coverage of surviving
behavior was weakened. The three `meta-*` test files were deleted along
with the sweeps they covered.

## Verification

```
$ pnpm test:gate
 Test Files  2 passed (2)     Tests   10 passed (10)    # core
 Test Files  16 passed (16)   Tests  299 passed (299)   # engine-core
 Test Files  1 passed (1)     Tests   70 passed (70)    # ci-shape
GATE_EXIT=0

$ pnpm --filter @fusion/engine --filter @fusion/core exec tsc --noEmit -p tsconfig.json
TSC_EXIT=0   (no output)
```

Plus a file-scoped run over the touched surfaces (`eval-followups`,
`pr-comment-handler`, `merger-autostash-orphan-surface`,
`merger-autostash-cleanup`, `run-audit`, `run-audit-secret-taxonomy`,
`project-engine`, `project-engine-manager`): **213/213 passed**.

A repo-wide grep confirms no surviving references to any deleted symbol,
module, or audit event.

🤖 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**
* Failed tasks now retain recovery and verification details directly on
the original task instead of generating separate follow-up cards.
* Live autostash issues now preserve stash information in task comments
and activity logs.
* Existing evaluation and pull-request follow-ups continue to be reused
when appropriate.

* **Changes**
  * Removed automatic archival of meta-tasks.
  * Removed obsolete meta-task timing settings.

* **Documentation**
* Updated architecture and settings documentation to reflect these
workflow changes.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:38:58 -07:00
Phil Larson
232e17d1cb test(engine): complete runtime logger mock (#2458)
## Summary
- add the missing `debug` method to the runtime-resolution logger mock
- prevent logger calls from short-circuiting runtime selection
assertions

## Test plan
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/runtime-resolution.test.ts`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm check:changesets`

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

* **Tests**
* Updated the runtime-resolution test suite’s mocked logger to also
support debug-level messages, alongside existing log, warn, and error
handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-26 22:35:28 -07:00
Phil Larson
00778cb10f test(engine): refresh shellout allowlist (#2451)
## Summary
- refresh the engine synchronous-shellout allowlist after recent
self-healing and executor source additions shifted audited call sites
- keep the guard's path, primitive, and signature checks unchanged

## Test plan
- `corepack pnpm --filter @fusion/engine exec vitest run
src/__tests__/engine-no-blocking-shellout.test.ts --silent=passed-only
--reporter=dot`


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

* **Tests**
* Updated static validation allowlists for audited synchronous shell
command operations so matching stays accurate with the latest call-site
locations.
* Kept safeguards that prevent unapproved blocking shell commands from
passing validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-26 22:33:21 -07:00
gsxdsm
256c64a7bd chore(release): v0.74.0-beta.5
Version bump via changesets.
2026-07-26 18:11:47 -07:00
gsxdsm
beebd270bd fix: make Queued to plan / Ready badges agree with the planning lane
TaskCard inferred "unplanned" from steps.length === 0 while triage's
todo-discovery and the scheduler's dispatch filter both decide from
PROMPT.md seed-ness, so the badges disagreed with the engine in both
directions: a real spec that parsed to zero steps read as "Queued to
plan" while the scheduler already treated it as a WIP-slot candidate, and
a re-seeded card still carrying old steps read as "Ready" while triage
was about to plan it. Either way the badge sent operators to the wrong
cap.

Adds the shared isTaskAwaitingPlanning predicate (replan park, missing
spec, seed-vs-real content) used by both triage's discovery and a new
best-effort `awaitingPlanning` enrichment on GET /api/tasks. TaskCard
derives both badges from that one value — strict complements — and keeps
the step count only as a fallback for SSE payloads and older servers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:48:46 -07:00
gsxdsm
5ea98f7d4b fix: release admission claims when triage evicts a hung planner
evictStaleProcessing cleared `processing` but left the task in
`coordinatorAdmittedTaskIds`, which is only cleared by specifyTask's
finally — the path a hung promise never reaches. The card stayed
eligible (so the throttle branch never logged or emitted
`task:plan-admission-throttled`) while admitOldest's refresh filtered it
out, leaving it on the "Queued to plan" badge with free slots and no
diagnostic until engine restart. Also drop an untransferred pre-held host
slot, which otherwise waits out the 600s stale-excess valve.

Regression tests assert the invariant on the real production candidate
source: an evicted card is re-offered and its host slot returned, while a
still-live stale task keeps both claims.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:33:35 -07:00
gsxdsm
0022621d22 chore(release): v0.74.0-beta.4
Version bump via changesets.
2026-07-26 17:00:59 -07:00
gsxdsm
2bb8537352 FN-8616: make agent tool-output limits configurable
Expose the shared agent tool-output budget as a scoped operator setting with an explicit no-limit option.

- Resolve global and project output caps with a safe finite default and zero sentinel.
- Propagate configured budgets through PI and plugin runtime tool wrappers.
- Add settings controls, localized labels, documentation, and regression coverage.

Files changed:
 .changeset/fn-8616-tool-output-budget-setting.md   |  7 ++++
 docs/agents.md                                     |  4 +-
 docs/settings-reference.md                         |  1 +
 .../core/src/__tests__/tool-output-budget.test.ts  | 23 ++++++++---
 packages/core/src/index.gate.ts                    |  2 +
 packages/core/src/index.ts                         |  2 +
 packages/core/src/settings-schema.ts               | 12 ++++++
 packages/core/src/tool-output-budget.ts            | 31 +++++++++++++--
 packages/core/src/types/settings-scope.ts          |  8 ++++
 .../app/components/settings/save-split.ts          |  1 +
 .../sections/GlobalGeneralSection.search.ts        | 20 ++++++++++
 .../settings/sections/GlobalGeneralSection.tsx     | 26 ++++++++++++
 ...lobalGeneralSection.tool-output-budget.test.tsx | 46 ++++++++++++++++++++++
 .../settings-default-descriptions.test.tsx         |  1 +
 .../src/__tests__/agent-session-helpers.test.ts    | 20 ++++++++++
 .../src/__tests__/runtime-resolution.test.ts       | 15 +++++++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 45 ++++++++++++++++-----
 packages/engine/src/agent-runtime.ts               |  2 +
 packages/engine/src/agent-session-helpers.ts       | 18 +++++++--
 packages/engine/src/pi.ts                          | 29 ++++++++++----
 packages/engine/src/runtime-resolution.ts          | 10 ++++-
 packages/i18n/locales/en/app.json                  |  4 ++
 packages/i18n/locales/es/app.json                  |  6 ++-
 packages/i18n/locales/fr/app.json                  |  6 ++-
 packages/i18n/locales/ko/app.json                  |  6 ++-
 packages/i18n/locales/zh-CN/app.json               |  6 ++-
 packages/i18n/locales/zh-TW/app.json               |  6 ++-
 packages/i18n/src/resources.d.ts                   |  4 ++
 28 files changed, 323 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8616

Fusion-Task-Lineage: 3ca99a61-d6ae-48ff-98d2-f14a153aa2b7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 16:16:27 -07:00
gsxdsm
30f81ac0cd fix(engine): compact verification failure output
Keep successful verification responses quiet and return bounded, high-signal diagnostics for failures without hiding zero-work or green-while-red warnings.
2026-07-26 15:36:33 -07:00
gsxdsm
07c8c95b10 FN-8614: cap agent tool output
Bound every engine-injected tool result to preserve agent context capacity.

- Add shared 16,000-character total text budgets with deterministic truncation markers and validated overrides.
- Apply outermost output clamps to Pi and non-Pi plugin tool paths, with semantic caps for high-volume reads.
- Cover budget behavior and document the operator-facing configuration contract.

Files changed:
 .changeset/fn-8614-tool-output-budget.md           |  7 ++
 docs/agents.md                                     |  8 ++
 .../core/src/__tests__/tool-output-budget.test.ts  | 58 +++++++++++++
 packages/core/src/index.gate.ts                    |  7 ++
 packages/core/src/index.ts                         |  7 ++
 packages/core/src/tool-output-budget.ts            | 97 ++++++++++++++++++++++
 .../src/__tests__/agent-artifact-tools.test.ts     | 10 +++
 .../src/__tests__/agent-document-tools.test.ts     | 10 +++
 .../__tests__/agent-task-logs-read-tools.test.ts   |  8 ++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 67 +++++++++++++++
 packages/engine/src/agent-session-helpers.ts       |  7 +-
 packages/engine/src/agent-tools.ts                 | 43 ++++++++--
 packages/engine/src/pi.ts                          | 54 +++++++++++-
 13 files changed, 374 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8614

Fusion-Task-Lineage: b6a76ccd-d7b4-4b43-af7e-cfd16ffb7fc8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:42:06 -07:00
gsxdsm
15a2fb18cc Merge branch 'fix/incomplete-pg-ports'
Wire incomplete PostgreSQL ports for archive, reconcile, health, settings
cache, agent cache, and async prompt overrides.
2026-07-26 13:16:13 -07:00
gsxdsm
ab87d0d803 fix(api): return 404 for missing tasks, and make task deletions attributable
Three related fixes, all originating from a `[api:error] Request failed`
log line showing a 500 on `GET /api/tasks/FN-8610/runtime-fallback`.

1. Missing/deleted tasks now return 404 instead of 500.
   `getTaskImpl` signalled a miss with a bare `Error`, and route catches
   only mapped errno `ENOENT` to 404 — a leftover from the file-backed
   storage era. In Postgres mode nothing sets an errno code, so every
   unknown/missing/soft-deleted/wrong-project read returned 500. Adds a
   typed `TaskNotFoundError` (message byte-identical) plus a shared
   `task-lookup-error` mapper applied across the task, session-diff,
   git/GitHub, workflow and file-workspace route registrars. The same
   bare throw existed on both archive-lifecycle delete paths, so
   `DELETE /tasks/:id` was affected too.

2. 5xx logs now carry the origin stack.
   `rethrowAsApiError` constructed a fresh `ApiError` from the message
   and discarded the original, so the `FNXC:ApiErrorDiagnostics`
   contract logged the rethrow site rather than the throw site — the
   reported log entry had no stack at all. Threads `cause` through the
   error factories and walks the chain (bounded, cycle-guarded).

3. Task deletions are attributable, and non-operator deletes notify.
   `task:deleted` audit rows recorded `agentId: "system"` for every HTTP
   delete, making an operator click indistinguishable from a script or
   an agent; the calling agent's task id was accepted by the store and
   then never persisted. Adds a `callerKind` union recorded in audit
   metadata, tags every delete call site, and stamps a self-reported
   `x-fusion-client` header from the dashboard client. When the caller
   is `agent-tool` or `api-unattributed`, a best-effort notice is sent
   to the operator mailbox; operator and engine deletes stay silent.

`x-fusion-client` is attribution, not authentication — anything can send
it. No delete-blocking, gating or permission logic is added here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:15:36 -07:00
gsxdsm
2b55077546 fix: wire incomplete PostgreSQL ports for archive, reconcile, health
Replace empty backendMode stubs with real AsyncDataLayer paths: archive ID
reservation and isTaskArchivedAsync, orphaned task.json re-import, health
snapshots via checkPostgresHealth, settings/agent memory caches for sync
readers, async builtin prompt overrides, and self-healing audit/health
callers that previously used dead sync SQLite fallbacks.
2026-07-26 13:10:20 -07:00
gsxdsm
f01461a70e feat(engine): thread the node id onto review-gate leases, activating pre-boot reclaim
Completes 3b83282273. The classifier and the self-healing reader landed there but
nothing stamped `leaseNodeId`, so the pre-boot reclaim path was unreachable.

Wiring: InProcessRuntime -> TaskExecutor -> WorkflowGraphTaskRunner ->
WorkflowGraphExecutor, which writes the field onto the pending lease.

The executor takes `getLocalNodeId`, a GETTER rather than a value, because the
runtime resolves the node id asynchronously (a CentralCore read) partway through
start() while `executorOptions` is built earlier in the same method. A snapshot
taken at construction would freeze `undefined` and silently disable attribution
forever -- the failure mode where the feature looks wired, typechecks, and never
fires. Reading it at runner-construction time picks up the resolved id.

With this, a review gate whose session dies to an engine restart is reclaimed on
the next self-healing pass instead of waiting out the 15-minute staleness floor.
Peer-owned and legacy unattributed leases still take the floor, so the
double-dispatch protection multi-node depends on is unchanged.

Adds five classifier cases: own-node pre-boot reclaims; peer-node, unattributed,
own-node-post-boot, and no-identity-supplied all still adopt. Verified the first
is not vacuous -- disabling the branch fails exactly that case (1 failed / 13
passed) and no other.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green
(299 + 10 + 70), plan-review-lease + plan-review-single-owner +
self-healing-orphaned-pending-step-results green (27).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:29:54 -07:00
gsxdsm
3b83282273 feat(engine): attribute review-gate leases to a node so dead local leases reclaim fast
Groundwork for FN-8603's remaining ~14-minute wait. Liveness for a pending
review gate is judged purely by a 15-minute staleness floor because a lease
records WHO took it (`leaseOwner` = run id) but not WHERE, and under multi-node
every engine sees every other engine's leases. A fresh-but-unknown lease might
be running on a peer, so the floor was the only safe test -- and a lease left by
this node's own crashed process is indistinguishable from it.

Adds `WorkflowStepResult.leaseNodeId` plus an optional `LocalNodeLeaseIdentity`
argument to `classifyReviewLease`. One narrow new case: a lease stamped with the
caller's OWN node id whose `startedAt` predates the caller's process boot is
provably dead -- the process that could have owned it is gone -- so it
classifies as `reclaim` immediately rather than aging out. Deliberately narrow,
because widening it is a double-dispatch risk: absent (legacy) or peer node ids
keep the floor, and a lease taken by this process after boot is still adopted.

InProcessRuntime.start() resolves the local node id from CentralCore (fail-soft;
on error it stays undefined and floor-only semantics apply) and passes it to
SelfHealingManager. The graph executor stamps the field when deps.localNodeId is
set.

NOT YET WIRED, so this is inert in production and behavior is unchanged end to
end: `localNodeId` is not threaded from WorkflowGraphTaskRunner /
WorkflowTaskRuntime down into the executor deps, so no lease actually carries a
`leaseNodeId` yet. The reader is ready; the writer needs that pass-through
(WorkflowGraphTaskRunnerDeps gains the field, the runner forwards it, and the
runtime supplies this.localNodeId). Stopping here rather than half-threading it.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green
(299 + 70), core workflow-step-results suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:24:29 -07:00
gsxdsm
00011b0113 fix(engine): recover restart-orphaned review steps in one cycle, raise fix budget
FN-8603 sat in-review for ~36 minutes after an engine restart killed its Code
Review session 34 seconds in. It did recover on its own; the cost was latency,
not a terminal park.

Sweep ordering. reconcile-orphaned-pending-step-results PRODUCES the failed
results that recover-failed-pre-merge-steps CONSUMES, but in the periodic
maintenance list it ran ~15 entries after it. A step orphaned in cycle N was
therefore rewritten to failed only after recovery had already scanned, so
nothing re-ran it until cycle N+1. Moved it immediately before its consumer and
removed the now-duplicated later entry. Startup recovery already ordered the two
correctly.

Post-review fix budget. Default raised 3 -> 10 per operator request. Three
passes is below the observed convergence length for the gates this fallback
actually governs -- Browser Verification and custom optional gates -- since Plan
Review and Code Review already resolve to "unbounded" when unset, and exhausting
the budget parks the card for a human. The declaration default and five inline
`settings.maxPostReviewFixes ?? 3` call sites in executor.ts/self-healing.ts had
drifted into separate literals, so raising one alone would have left every
unset-settings path on the old value; they now share the exported
DEFAULT_MAX_POST_REVIEW_FIXES.

Not done, and why. Re-dispatching a restart-orphaned lease immediately at
startup is the change that would close the remaining ~14-minute wait, but it is
unsound as specified: liveness is judged by a 15-minute lease-staleness floor
because leases carry no node attribution, so treating a pre-boot lease as dead
would let one node orphan another node's genuinely running review. Needs a node
id on the lease record first. Left the floor intact.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green,
self-healing orphaned-pending-step-results and optional-step-revision suites
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:08:03 -07:00
gsxdsm
cca13737b6 FN-8603: reduce steady-state diagnostic log noise
Route routine core, engine, and dashboard diagnostics through debug-gated shared loggers.

- Demote steady-state diagnostic sites while preserving warnings and errors for actionable failures.
- Add cross-package severity contracts and manifest coverage for demoted log sites.
- Document logging severity guidance and add a patch changeset.

Files changed:
 .changeset/fn-8603-log-severity.md                 |  7 ++
 docs/diagnostics.md                                | 20 ++++--
 .../__tests__/log-severity-spam-contract.test.ts   | 71 ++++++++++++++++++
 packages/core/src/activity-analytics.ts            |  5 +-
 packages/core/src/ai-summarize.ts                  | 61 +++++++---------
 packages/core/src/async-mission-store.ts           |  5 +-
 packages/core/src/async-secrets-store.ts           |  7 +-
 packages/core/src/central-core.ts                  | 17 ++---
 packages/core/src/docker-provisioning.ts           | 13 ++--
 packages/core/src/index.ts                         |  1 +
 packages/core/src/master-key.ts                    |  9 ++-
 packages/core/src/memory-compaction.ts             | 29 ++++----
 packages/core/src/memory-insights.ts               |  7 +-
 packages/core/src/migration-orchestrator.ts        |  7 +-
 packages/core/src/mission-store.ts                 |  5 +-
 packages/core/src/node-discovery.ts                |  7 +-
 packages/core/src/notification/dispatcher.ts       |  9 ++-
 .../core/src/plugins/bundled-plugin-install.ts     | 11 +--
 packages/core/src/reflection-store.ts              |  5 +-
 packages/core/src/secrets-store.ts                 |  7 +-
 packages/core/src/task-store/agent-logs.ts         | 21 +++---
 packages/core/src/task-store/async-events.ts       |  5 +-
 packages/core/src/task-store/async-maintenance.ts  |  7 +-
 packages/core/src/task-store/comments-ops.ts       |  7 +-
 packages/core/src/task-store/task-mutation-ops.ts  | 11 +--
 packages/core/src/task-store/workflow-integrity.ts |  9 ++-
 packages/core/src/types/merge-policy.ts            |  5 +-
 packages/core/src/usage-events.ts                  |  5 +-
 .../__tests__/log-severity-spam-contract.test.ts   | 48 +++++++++++++
 packages/dashboard/src/ai-refine.ts                |  5 +-
 packages/dashboard/src/ai-session-diagnostics.ts   | 10 +--
 packages/dashboard/src/chat.ts                     |  8 ++-
 packages/dashboard/src/devserver-manager.ts        |  9 ++-
 packages/dashboard/src/file-service.ts             |  5 +-
 packages/dashboard/src/github-tracking-comments.ts |  7 +-
 .../dashboard/src/github-tracking-reconciler.ts    |  5 +-
 packages/dashboard/src/github-tracking-state.ts    |  5 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |  5 +-
 packages/dashboard/src/insights-routes.ts          |  9 ++-
 packages/dashboard/src/issue-image-attachments.ts  |  5 +-
 packages/dashboard/src/knowledge-index.ts          |  5 +-
 packages/dashboard/src/plugin-routes.ts            |  7 +-
 packages/dashboard/src/routes/board-workflows.ts   |  5 +-
 packages/dashboard/src/routes/context.ts           |  5 +-
 .../dashboard/src/routes/register-auth-routes.ts   | 13 ++--
 .../routes/register-docker-provisioning-routes.ts  |  7 +-
 .../dashboard/src/routes/register-git-github.ts    | 21 +++---
 packages/dashboard/src/routes/register-gitlab.ts   |  7 +-
 .../src/routes/register-session-diff-routes.ts     |  9 ++-
 .../src/routes/register-settings-memory-routes.ts  |  7 +-
 .../src/routes/register-setup-activity-routes.ts   |  7 +-
 .../dashboard/src/routes/register-signal-routes.ts |  5 +-
 .../src/routes/register-task-workflow-routes.ts    | 11 +--
 packages/dashboard/src/runtime-logger.ts           | 11 +--
 packages/dashboard/src/server.ts                   |  7 +-
 packages/dashboard/src/sse.ts                      |  8 ++-
 packages/dashboard/src/terminal-service.ts         | 34 ++++-----
 packages/dashboard/src/view-chunk-manifest.ts      |  5 +-
 .../engine/src/__tests__/log-severity-manifest.ts  | 83 ++++++++++++++++++++++
 .../__tests__/log-severity-spam-contract.test.ts   | 40 ++++++++++-
 .../src/__tests__/logger-debug-gating.test.ts      |  7 +-
 packages/engine/src/goal-anchoring-audit.ts        |  5 +-
 packages/engine/src/plugin-runner.ts               | 44 ++++++------
 packages/engine/src/pty-native.ts                  |  9 ++-
 .../engine/src/runtimes/child-process-worker.ts    |  4 +-
 packages/engine/src/self-healing.ts                | 12 ++--
 packages/engine/src/worktree-hooks.ts              | 10 ++-
 67 files changed, 632 insertions(+), 250 deletions(-)

Fusion-Task-Id: FN-8603

Fusion-Task-Lineage: 53901db6-1af2-4bd7-b5ea-49507e048ef2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:01:19 -07:00
gsxdsm
9ff1587b84 fix(engine): keep the task worktree across replan bounces
`moveTask`'s reopen-to-todo/triage block clears `task.worktree` but leaves
`task.branch` intact, and `moveTaskToReplanColumn` called it with no options.
A replan bounce therefore left the row split-brained: no worktree pointer, but
still owning `fusion/<id>`, which was still checked out in the worktree it had
just orphaned. The next planning acquisition skipped its resume branch (gated on
`task.worktree`), re-created the same branch, collided, and fell into
`cleanupConflictingWorktree` — force-remove + `git branch -D` + fresh
`git worktree add` + init command, on every bounce. Observed on FN-8603: two
Plan Review REVISE bounces burned two full teardown/rebuild cycles for nothing,
since planning writes its spec to the task store, not the worktree.

Pass `preserveWorktree: true` at the shared seam, so this covers every replan
mover — Plan Review REVISE, required-artifact recovery, and the executor and
scheduler spec-staleness and filesystem-validation rebounds. Acquisition still
re-validates, so a preserved pointer to a removed checkout self-heals as before;
the rest of the replan contract (steps reset, status/error cleared) is unchanged.

Regression coverage asserts the invariant across both replan-column shapes
(triage and plan-in-place todo) and all three reopen origins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:53:04 -07:00
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
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
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
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
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
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
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
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
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
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
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
bf317f6340 chore(release): v0.74.0-beta.3
Version bump via changesets.
2026-07-25 21:06:33 -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
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