## Summary
- Full Suite run
[29641723814](https://github.com/Runfusion/Fusion/actions/runs/29641723814)
left shards 1–3 green; shard 4 (`@fusion/core`) failed after FN-8265
landed.
- `settings-parity`: `PROJECT_SETTINGS_KEYS` now includes
`NON_DEFAULT_PROJECT_SETTINGS_KEYS` (`ephemeralAgentTaskCreationPolicy`
has no default; resolver owns fallback).
- `schema-applier`: expected applied-migration lists and identity suite
include `TASK_PROPOSAL_CLAIM_VERSION` (`0020`).
## Test plan
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/settings-parity.test.ts
src/__tests__/postgres/schema-applier.test.ts` — 136 passed
- [ ] Full Suite all 4 shards green on main after merge
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Updated migration validation tests to include the latest task proposal
claim migration.
* Expanded settings parity checks to cover non-default project settings.
* Added coverage confirming migration ordering and immutable migration
identities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- uninstaller: taskkill only the first, digits-only postmaster.pid line
(the for /f loop ran taskkill on the port/epoch lines — potential
unrelated-process kill)
- git-missing dialogs use new ConfirmOptions.alwaysAsk so global
skip-confirmations cannot silently pick an unseen choice
- Windows quit prompt: embedded-local runtimes only, skipped during OS
session end (sync dialog blocked Windows shutdown)
- 'leave it running' detaches the embedded lifecycle (disarms its
process shutdown hook) so Electron exit cannot kill the postmaster
the operator chose to keep (new detachKeepingEmbedded)
- wizard: ref-based double-submit guard around the async git preflight
- clone route: ENOENT invalidate-and-retry matching runGitCommand
- openExternalUrl: drop the async window.open fallback (always
popup-blocked); log bridge failures instead
- DirectoryPicker: close the panel when listing the created folder
fails so Select cannot re-commit the parent
- git status probe bounded to two spawns (PATH + first candidate)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registering a project on a git-less host used to fail after submission
with a raw spawn error. The setup wizard now probes gitCli up front and
offers: open the Git downloads (picked up without restart thanks to the
stale-PATH resolver), create the project anyway without a git repo
(new skipGitInit passthrough, rejected for clone mode), or cancel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A git installed DURING first-run setup stayed invisible (spawn git
ENOENT on project creation; onboarding indicator stuck on missing)
because the server's PATH snapshot predates the install. New resolver
probes well-known install locations on PATH ENOENT with cache
invalidation, wired into project git init/workspace detection, the
onboarding git status probe, and the dashboard clone route.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Users whose embedded cluster was initdb'd with an OS-locale encoding by
a pre-fix version now self-heal with zero manual steps: on the
encoding-conversion schema failure the startup factory proves the
cluster is non-UTF-8 AND empty (the baseline transaction never applied,
so no schema or migrated data can exist) and that this process owns the
postmaster, then deletes the data dir and reboots once with the UTF-8
initdb defaults. Joined instances and unproven states keep the manual
re-init hint; one retry ever, so no loops.
Verified on the elevated windows-latest runner: CI seeds a real WIN1252
cluster via initdb and proves a stock 'fn serve' auto-recovers it to a
healthy /api/health (run 29633351848, all jobs green). Also caps the
desktop-windows embedded-PG smoke at 30 min and adds a skip input.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Squash of feature/win-elevated-no-user, verified end-to-end on the
elevated windows-latest runner (restricted-token double boot + full
'fn serve' /api/health smoke, both green).
- Elevated Windows boots embedded PostgreSQL via pg_ctl's built-in
restricted-token re-exec instead of creating a 'fusion-pg' local user
(operator requirement: Fusion must never create accounts). Removes
the credential launcher, icacls grants, and cmd/PowerShell wrapper —
and with them the 'directory name is invalid' and wrapper-log EBUSY
field failures. Leftover fusion-pg accounts are deleted on start.
- Embedded clusters are always initdb'd --encoding=UTF8 --locale=C
(GitHub issue #2286: OS-locale WIN1252/WIN1254 clusters could not
store the UTF-8 schema and crash-looped the dashboard). Existing
non-UTF-8 clusters get an actionable re-init hint at boot.
- Schema-backend boot failures now surface the full error cause chain
(DrizzleQueryError hid the real PostgresError behind the SQL text).
- Elevated stop() waits until the port closes and postmaster.pid is
gone before resolving.
- CI: branch verification workflow (restricted-token proof + elevated
boot smoke + account-absence assertions); boot-smoke stderr tail
widened for diagnosability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Start-Process -Credential (CreateProcessWithLogonW) validates the working
directory as the TARGET user. The launcher inherited the desktop app's cwd
(admin profile / install dir), which the dedicated fusion-pg user cannot
read, so elevated desktop boots died with "The directory name is invalid"
before postgres ever started. launch.ps1 now pins -WorkingDirectory to the
.pgrunner run dir inside the data dir the user was just granted full
control on. CI never caught it because runner cwds are world-traversable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-time SQLite→PostgreSQL migration runs inside createTaskStoreForBackend
before any HTTP server listens, so browsers saw "connection refused" and open
tabs failed silently for minutes. Now:
- CLI: a temporary holding server binds the dashboard port for the boot window,
serving an auto-reloading "Database migration in progress" page and an
/api/health payload with status "migrating" + structured progress; the port
is handed off (awaited) to the real app.listen().
- Dashboard SPA: already-open tabs render the new MigrationInProgressBanner
from the 15s health poll when status is "migrating".
- Desktop: LocalRuntimeManager publishes migration progress on
DesktopRuntimeStatus via the new core onMigrationProgress option;
DesktopLaunchGate shows the live label and extends its 30s startup timeout
while progress advances (2min stall cap), in both boot and first-run flows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Legacy SQLite databases can hold U+0000 in TEXT cells and inside stored
JSON, which PostgreSQL rejects in text and jsonb columns and which
aborted the first-boot auto-migration. Strip NUL from plain text cells,
JSON string values and object keys, malformed-JSON scalars, and opaque
legacy-preservation cells; content-checksum verification compares the
sanitized source against the sanitized target so migrations still verify.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shared_memory_type=mmap (defaulted 2026-07-16 for SysV shm exhaustion)
is invalid on Windows — PostgreSQL only accepts "windows" there and
dies with FATAL invalid value for parameter before opening the port.
Every Windows embedded start broke, failing the Windows release smoke
in both the v0.70.0 and v0.70.1 tag runs. Default flags now come from
defaultEmbeddedPostgresFlagsFor(platform): empty on win32 (no override
needed; SysV exhaustion cannot occur there), mmap elsewhere. Regression
test asserts the per-platform flag invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bun-compiled exe has been unbootable since the PG cutover: bun
standalone binaries do no node_modules resolution, so the deliberately
out-of-graph require("embedded-postgres") failed from /$bunfs, and
readFile'd migration .sql files were never embedded, so even external
DATABASE_URL mode died at schema init.
- schema-applier: resolveMigrationsDir() — FUSION_MIGRATIONS_DIR env >
module-relative dist/migrations (npm/desktop, unchanged) >
execPath-relative migrations/ (standalone exe), probe-based.
- embedded-lifecycle: require("embedded-postgres") first (npm/desktop
untouched), falling back to a self-contained staged bundle at
<execDir>/runtime/<platform>/embedded-postgres/dist/index.cjs
(FUSION_EMBEDDED_PG_RUNTIME_DIR override) with the native
initdb/pg_ctl/postgres payload beside it.
- build.ts: stage dist/migrations plus the per-target embedded-postgres
bundle + native payload (warn when a cross-target payload is absent on
the host, mirroring desktop's verifyEmbeddedPostgresPayloads).
- release.yml: package fn-cli-<os>-<arch>.tar.gz (binary + migrations +
runtime + client) with sha256 per leg; prune staged payload files from
the release-collection globs; bare fn-cli-* binaries still uploaded.
E2E-verified on the compiled binary: embedded mode initdb→/api/health
200 database healthy; DATABASE_URL mode applied migrations 0000–0019
(109 tables). Core typecheck clean; schema-applier 58/58 and
embedded-lifecycle 44/44 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
An audit of the SQLite→PostgreSQL store migration found data-store paths
still reaching the removed SQLite stub in backend (PG) mode. In backend
mode `store.db`/`getDatabase()` throw the removed-SQLite error, so each
of these either threw on every run or — worse — had the throw swallowed
into a silent wrong result. This PR routes all of them through the
`AsyncDataLayer` (and removes one dead primitive).
## The 6 live bugs fixed
| Fix | Was |
|-----|-----|
| `executor.ts` authoritative assigned-agent fallback now inherits the
TaskStore `asyncLayer` | silently returned `null` → model drift to the
pi built-in (the exact thing its comment guards) |
| `pruneAgentLogFilesAsync` replaces the sync self-healing prune call |
threw `SQLite Database is not available` every maintenance sweep →
agent-log pruning never ran |
| `cleanupOrphanedMaterializedSteps` deletes PG `workflow_steps` rows on
a failed create | swallowed the throw → leaked rows |
| `deleteTaskBackendImpl` now runs the async mission feature/task-link
unlink | PG hard delete left orphaned mission links |
| `getWorkflowSettingsProjectId` returns `rootDir` in backend mode
without touching the stub | swallowed throw for unscoped backend stores
|
| `fn plugin` unregistered-project fallback bootstraps a `CentralCore`
`AsyncDataLayer` | layerless `PluginStore` threw in PG |
## The 4 latent traps, fixed properly
- **`cleanupArchivedTasks`** — real async port (enumerate archived
soft-deleted rows, guarantee cold snapshot, hard-delete project row +
purge selection rows + rm dir).
- **`deleteWorkflowStep`** — real async port (delete `workflow_steps`
via the layer with `.returning()` to preserve the not-found contract).
- **`applyTaskPatch`** — **removed** (zero-caller SQLite column-patch
primitive with no backend analogue; impl + facade + import deleted).
- **`AgentStore.importLegacyFileRuns`** — clean backend no-op (no legacy
SQLite run-files exist in a PG deployment; its only `init()` caller
early-returns in backend mode).
## Symptom Verification
New PG regression suite
`packages/core/src/__tests__/postgres/store-sqlite-residue-fixes.pg.test.ts`
reproduces the original failures against real embedded Postgres and
asserts they're gone:
- orphaned `workflow_steps` are actually deleted (no swallowed throw)
- `pruneAgentLogFilesAsync` resolves and prunes inactive-task log files
- hard delete unlinks the mission feature from the task
- `deleteWorkflowStep` removes the row / reports not-found
- `cleanupArchivedTasks` hard-deletes the project row while retaining
the cold snapshot
## Verification
- `@fusion/core`, `@fusion/engine`, `@runfusion/fusion` typecheck clean
- ~50 existing + 5 new PG tests pass; lint clean; changeset validates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Prevented PostgreSQL backend maintenance from hitting removed legacy
SQLite code paths, avoiding datastore failures and residue cleanup
issues.
* Fixed workflow-step deletion and “not found” behavior in backend mode.
* Ensured backend hard-deletes correctly unlink related mission
feature/task links and clean orphaned materialized steps.
* Prevented legacy file-run imports from incorrectly reporting success
in backend mode.
* **New Features**
* Added async agent-log pruning for inactive tasks and updated
maintenance to use it.
* **Tests**
* Added PostgreSQL regression coverage for residue fixes and
archive/workflow cleanup.
* **Refactor**
* Removed an unused task patch operation and updated task-store cleanup
methods to be async where needed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## fix(postgres): scope the cross-process merge guard to the project
The guard's own comment (`project-engine.ts`) says it checks whether
another process is merging a task **"for this project"** — and in SQLite
mode the per-project DB file made that scoping implicit.
`getActiveMergingTaskImpl`'s `backendMode` branch queries the shared PG
`tasks` table with **no `project_id` filter**, so one merging task
anywhere serializes merges across **all** projects.
### Production evidence
6-project embedded-PG deployment: **697 cross-project `Merge deferred …
is already merging (cross-process guard)` retries in 10 minutes** — six
independent repos waiting on each other's serialized merger, collapsing
merge throughput ~6x and letting `in-review` pile up to 95 tasks.
### Fix
Add the existing `taskProjectScope(layer)` filter to the query's
conditions (one line + import). It is a no-op when the layer carries no
`projectId`, so single-project deployments and the SQLite path are
unchanged. Same pattern as the other project-scoped task queries.
Deployed on the affected instance: cross-project merges now proceed in
parallel; per-project serialization (the guard's documented intent) is
preserved.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved merge task handling so activity in one project no longer
unnecessarily blocks merge operations in other projects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: TrinaryCompute <fusion-merge@trinarycompute.dev>
PR #2260 added project.tasks.bulk_completion_refusal_at to the Drizzle model
and the 0000 baseline but shipped no forward migration. Databases created
before #2260 already carry the 0000 marker, so the applier skips the baseline
and they never gained the column — every such cluster crashed on the first
TaskStore SELECT ("column bulk_completion_refusal_at does not exist"), taking
down dashboard/app boot.
Adds forward migration 0018 (wired via BULK_COMPLETION_REFUSAL_AT_VERSION;
SCHEMA_BASELINE_VERSION -> "0018") so existing clusters heal on next startup.
Prevention:
- Per-column upgrade regression test reproducing the exact existing-DB failure.
- Migration-wiring-integrity guard (no PostgreSQL): SCHEMA_BASELINE_VERSION must
equal the highest migration file, and every .sql must be registered in the
applier so none silently never runs.
- Repairs 6 pre-existing schema-applier tests left stale by the 0017 addition
(baseline-marker identity + version-list enumerations).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Eliminates the remaining backend/PostgreSQL-mode sync-SQLite
(`store.db`) call sites — both the crashing ones and the
try/catch-masked ones that silently degraded features. Found via a full
audit of `store.db`/`archiveDb` residue after the PG cutover's per-site
routing missed them.
**Crashes fixed:**
1. **refineTask / duplicateTask** threw `TaskStore.db: SQLite Database
is not available in backend mode`. Both create rows through
`createTaskWithId` callbacks calling `store.atomicCreateTaskJson()`
directly, bypassing `_createTaskInternal`'s backend routing. The shared
helper now routes itself (soft-delete conflict check + non-destructive
insert in one AsyncDataLayer transaction).
2. **Merger verification cache**: `getVerificationCacheHit` ran sync
SQLite unguarded *outside* any try/catch in
`runDeterministicVerification`; `recordVerificationCachePass` was
swallowed so the cache never warmed. Both are now async with a PG
branch.
**Silent degradations fixed (features that were dead on PG):**
- Workflow run-branch + foreach step-instance persistence
(`saveWorkflowRunBranch`, `loadWorkflowRunBranches`,
`clearWorkflowRunBranches`, `saveWorkflowRunStepInstance`,
`loadWorkflowRunStepInstances`, `clearWorkflowRunStepInstances`) —
executor crash-resume checkpoints were silently never persisted.
- `getBranchProgressByTask` — returned an empty map, dropping
`branchProgress` from task payloads.
- `runPluginColumnTransitionHooks` — plugin `onEnter`/`onExit`
column-transition hooks never fired (marker bookkeeping + non-locking
task read now async).
- `getTaskColumns` — dashboard treated all agent-linked tasks as
non-terminal.
- `getWorkflowStep` / `listWorkflowSteps` — stored workflow-step rows
now read from `project.workflow_steps` (listing previously returned
plugin steps only); `getLegacyWorkflowStepSnapshot` returns `undefined`
on PG (legacy snapshot exists only in pre-migration SQLite).
- `readRawProjectSettings` / `listWorkflowPromptOverridesForProject` —
now read via the async layer.
These store methods became **async**; engine/dashboard callers await
them (the workflow persistence interfaces already accepted
`Promise`-returning impls).
**PG gotcha encoded in the fixes:** migration `0006_project_ownership`
rebuilds every project-schema PK to lead with `project_id`, so
column-list `ON CONFLICT` inference fails (42P10) — upserts target the
PK by constraint name.
## Surface Enumeration
- Creators through `atomicCreateTaskJson`: `refineTaskImpl`,
`duplicateTaskImpl` (fixed); `_createTaskInternalImpl` unaffected
(already routed).
- Verification-cache callers (all merger, all 3 sites now awaited).
- Run-branch/step-instance callers: executor persistence adapters,
parse-steps foreach probe, integration-queue flip, crash-resume
reconcile, graph-reset cleanup; triage replan cleanup; dashboard
spec-rebuild pin clears; agent-reflection rework summing — all awaited.
- Audit classified everything else as guarded or sync-mode-only (dead in
production — every entry point constructs stores via
`createTaskStoreForBackend`).
## Symptom Verification
- **Original symptoms:** refinement/duplicate creation threw; merge
verification threw; workflow checkpoints/branch progress/plugin
hooks/task-column lookups silently no-oped on PostgreSQL.
- **Exact reproduction:** `refine-duplicate-task.pg.test.ts`,
`verification-cache.pg.test.ts`, and
`sync-db-residue-backend.pg.test.ts` exercise each surface against
embedded-PostgreSQL backend-mode TaskStores.
- **Assertion it is gone:** all suites pass (14 + 5 tests), plus
`transition-pending-and-status-clear.pg.test.ts`,
`create-task-reserved-id.pg.test.ts`, dashboard `routes-github.test.ts`
(123), engine `triage.test.ts` (221) and `agent-reflection.test.ts`
(31). Core/engine/dashboard typecheck fully clean: the 13 errors from
the FN-8142 pi SDK migration are fixed by bumping
@earendil-works/pi-ai/pi-coding-agent to ^0.80.10 (FN-8142 used APIs
absent from the previously locked 0.80.6). Locally green: `pnpm
verify:fast` (scoped typecheck + build + CLI build + boot smoke), `pnpm
test:gate`, and `pnpm lint`.
🤖 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**
* Fixed refinement/duplication task creation in PostgreSQL-backed
backend mode.
* Improved backend-mode persistence for workflow checkpoints,
foreach-step instances, branch progress, and cleanup flows (including
retries/resets/transitions), so stored data reliably round-trips.
* Hardened backend-mode reads for workflow steps, task columns, project
settings, and prompt overrides.
* Made verification-cache reads/writes complete reliably, including
command-specific cache behavior.
* **Tests**
* Added PostgreSQL integration/regression coverage for
refinement/duplication, sync residue, and verification caching.
* **Chores**
* Bumped `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent`
to `^0.80.10`.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Applying the full schema baseline on every fresh test database cost ~530ms
per file and dominated PG gate wall-time under fork contention. Apply the
schema once per worker process into a pid-keyed template database, then
create each test DB via CREATE DATABASE ... TEMPLATE (fast server-side copy).
Dead-pid templates from crashed/prior runs are swept before creating a new
one; template copies are serialized since CREATE DATABASE ... TEMPLATE forbids
concurrent access to the source. Removes the throwaway probe2 timing test.
Fusion-Task-Id: FN-8146
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## What
FN-8141 follow-up 3. Tightens `deriveExecutorSignalMemory`
(packages/engine/src/overseer-noop-finalize-veto.ts) so a mid-execution
`progressing` overseer observation can no longer clear the executor
no-op-finalize veto.
## Why
The prior derivation took the **newest** executor `observe` entry and
cleared `incompleteWork` whenever it was anything but the canonical
failed reason. But the planner overseer emits a `progressing`
observation ("Task is actively executing in-progress work") the
**moment** a task re-enters execution — long before that execution
finishes. The defeating shape:
> task parks failed-incomplete → requeued → re-execution starts
(overseer observes `progressing`) → execution dies or reverts again
**without** a newer failed observation → newest observation is
`progressing` → `incompleteWork:false` → an empty no-op finalize is
**not** vetoed → the reverted branch launders into `done`.
`progressing` is not "completed green" — the veto's own contract says
the failure must be superseded by a green completion.
## Change
The executor stage in `planner-overseer.ts` emits only
`progressing`/`failed`/`stuck`/`blocked` — **no** green-completion
observation — so the timeline alone cannot distinguish progressing from
completed. Per the follow-up spec, the derivation now:
1. Scans the executor `observe` timeline for the newest
**failed-with-incomplete-work** observation.
2. Keeps `incompleteWork` TRUE unless a durable **clean-completion
task-log marker** is **strictly newer** than that failure park. Reuses
the shared `CLEAN_COMPLETION_MARKERS` set (now exported from
`@fusion/core`, single-sourced with
`evaluateCompletedPromotionFailureProvenance`) so it automatically
tracks sibling follow-up F2's removal of the promotion-output marker.
3. Fails safe on a malformed failure timestamp (stays vetoed).
4. `merger-ai.ts` threads `task.log` into the derivation.
All existing precedence rules are preserved: non-empty merges are never
vetoed; human-control deferral (user-paused / autoMerge:false) still
defers; a missing task fails open.
## Test evidence
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/overseer-noop-finalize-veto.test.ts
src/__tests__/merger-ai.test.ts` → **59 passed**. New/updated cases:
failed→progressing (no completion) ⇒ still vetoed (the regression this
fixes); progressing between two failed parks (FN-8141 timeline) ⇒
vetoed; clean-completion marker newer than failure ⇒ not vetoed; older
completion marker ⇒ still vetoed; no failure park ⇒ not vetoed; bounded
tail-scan preserved. Integration: empty lane with
failed-then-progressing timeline blocks the finalize; genuinely
re-executed green task finalizes to done.
- `pnpm --filter @fusion/core exec vitest run
src/__tests__/completed-promotion-failure-provenance.test.ts` → 9
passed.
- `pnpm --filter @fusion/engine exec tsc --noEmit` → clean. `pnpm
--filter @fusion/core exec tsc --noEmit` → clean.
- `pnpm verify:fast` → PASS (3 steps green).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved empty-merge finalization safeguards so an in-progress task
cannot incorrectly clear a previously detected incomplete-work failure.
* Finalization can now proceed when a newer clean-completion record
confirms successful completion.
* Added bounded task-history evaluation to ensure completion records are
interpreted safely and consistently.
* **Tests**
* Expanded coverage for progressing, failed, and clean-completion task
timelines.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>
## What
FN-8141 follow-up 2. Removes `"Auto-recovered: task work was complete
but stranded"` from `CLEAN_COMPLETION_MARKERS` in
`packages/core/src/completed-promotion-failure-provenance.ts`.
Clean-completion evidence is now **execution outcomes only**: `"Task
marked done by agent"` (accepted explicit fn_task_done, also covers the
PREMISE STALE skip-then-done flow) and `"All steps complete — implicit
fn_task_done"` (implicit-completion success).
## Why
That string is the PROMOTER'S OWN OUTPUT — self-healing's
`recoverCompletedTasks` (executor.ts:4594) narrating "I promoted this
task" — not evidence of a clean execution outcome. Any task whose
durable log contains a promotion written by the pre-#2257 buggy sweep
(the real FN-8141 row, or any pre-guard history) carried a permanent
"clean" marker: the tail scan hit the promotion line before the older
failure park and returned not-blocked, re-enabling the exact laundering
the guard exists to stop.
Audit confirmed no other genuine execution-outcome success markers are
missing — the PREMISE STALE accepted `fn_task_done` writes the
already-listed `"Task marked done by agent"` line (executor.ts:14939),
and the honest-blocked exit (`BLOCKED: ...`) is correctly NOT counted.
`grep` confirmed the removed string has only one other consumer: its
writer at executor.ts:4594. A task already promoted to in-review/done is
out of the promoters' todo/in-progress scan scope, so
legitimately-recovered old tasks are not wedged (verified by test rather
than assumed).
## Test evidence
- Core `completed-promotion-failure-provenance.test.ts`: **11 passed** —
added pre-fix-history shape (failure park → promoter recovery line →
blocked), promoter-line-alone → blocked, and positive coverage of each
remaining marker.
- Engine `self-healing.test.ts`: **405 passed** — added promoter
withholds on the pre-fix-history shape and emits the existing
`task:reconcile-stranded-completed-no-action` (reason
`failure-provenance`) event.
- `pnpm --filter @fusion/engine exec tsc --noEmit`: clean.
- `pnpm verify:fast`: PASS (3 steps green, no tests run).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Prevented failed tasks with prior failure history from being
automatically promoted.
- Ensured recovery messages cannot override authoritative failure
records or be mistaken for successful completion.
- Preserved the existing no-action behavior and audit event when
promotion is blocked by failure provenance.
- **Tests**
- Added regression coverage for failed-task promotion and stranded-task
recovery scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus <noreply@anthropic.com>