Commit Graph

3868 Commits

Author SHA1 Message Date
gsxdsm
60706ed5e4 fix: demote high-frequency TUI log lines to debug
Session setup, track bookkeeping, intentional skill exclusions, token-cache
metrics, zero-count recovery summaries, and expected-missing PROMPT seed reads
were flooding the default log pane. Gate them behind FUSION_DEBUG so only
state transitions and operator-actionable warnings remain visible.
2026-08-01 11:48:56 -07:00
gsxdsm
01d65805c3 FN-8693: refresh reused worktree bases before execution
Refresh reused execution worktrees against the current integration baseline.

- Rebase or reset clean reused worktrees before coding sessions while preserving task commits.
- Persist and audit refreshed base SHAs, and block unsafe refresh states before execution.
- Cover executor, graph, and heartbeat refresh paths with regression tests.

Files changed:
 .changeset/fn-8693-stale-worktree-base.md          |   7 +
 docs/architecture.md                               |   1 +
 .../src/__tests__/agent-heartbeat-worktree.test.ts |  28 ++++
 .../__tests__/ce-workflow-step-executor.test.ts    |  44 ++++++
 .../src/__tests__/worktree-base-refresh.test.ts    |  90 ++++++++++++
 packages/engine/src/agent-heartbeat.ts             |  35 ++++-
 packages/engine/src/executor.ts                    |  67 ++++++++-
 packages/engine/src/merger.ts                      |   5 +
 packages/engine/src/run-audit.ts                   |  11 ++
 packages/engine/src/workflow-graph-executor.ts     |  32 ++++-
 packages/engine/src/worktree-acquisition.ts        |  28 +++-
 packages/engine/src/worktree-base-refresh.ts       | 158 +++++++++++++++++++++
 12 files changed, 498 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8693
Fusion-Task-Lineage: e39a441f-39b5-4723-b503-753e921018f3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 10:03:28 -07:00
gsxdsm
e8ca86d5ee FN-8705: prioritize review and execution slot admission
Prioritize lifecycle-critical work whenever project capacity becomes available.

- Rank admission candidates as review/merge, execution, then planning.
- Coordinate scheduler handoffs with project and host capacity reservations.
- Cover lane priority and document the updated operator behavior.

Files changed: .changeset/fn-8705-slot-priority.md                |  7 ++
 docs/architecture.md                               |  2 +-
 docs/dashboard-guide.md                            |  2 +-
 packages/engine/src/__tests__/concurrency.test.ts  | 95 +++++++++++-----------
 .../engine/src/__tests__/project-engine.test.ts    |  4 +-
 .../starved-refinement-x-triage-poll.test.ts       | 11 ++-
 .../__tests__/triage-refinement-routing.test.ts    | 16 ++--
 .../workflow-continuation-capacity.test.ts         |  3 +-
 packages/engine/src/concurrency.ts                 | 37 +++++++--
 packages/engine/src/project-engine.ts              | 10 ++-
 packages/engine/src/runtimes/in-process-runtime.ts |  3 +-
 packages/engine/src/scheduler.ts                   | 40 ++++-----
 packages/engine/src/triage.ts                      |  7 +-
 13 files changed, 133 insertions(+), 104 deletions(-)

Fusion-Task-Id: FN-8705

Fusion-Task-Lineage: ef66360b-e504-4e3f-b25e-b032a719d8c0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 09:24:18 -07:00
gsxdsm
1aa1516023 FN-8692: harden mission validator assertion matching
Make validator assertion identity handling explicit and fail closed for ambiguous legacy responses.

- Include authoritative assertion IDs in validator prompts
- Recover only unique exact-count legacy results positionally
- Reject duplicate unknown and empty assertion IDs with regression coverage
- Document the assertion identity contract and add a patch changeset

Files changed:
 .changeset/fn-8692-validator-assertion-ids.md      |   7 +
 docs/missions.md                                   |   2 +-
 .../src/__tests__/mission-execution-loop.test.ts   | 142 ++++++++++++++++++++-
 packages/engine/src/mission-execution-loop.ts      |  70 ++++++++--
 4 files changed, 204 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-8692

Fusion-Task-Lineage: 973c65e9-962f-472b-8d42-aef1de830d70

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 09:18:49 -07:00
gsxdsm
ac8ce148d5 FN-8691: suppress repeated task wedge notifications
Prevent resolved-and-rewedged tasks from flooding operator notification channels.

- Persist per-reason notification timestamps with a six-hour cooldown.
- Apply cooldown claims consistently in durable storage and in-memory fallback paths.
- Cover reason transitions, legacy state, invalid timestamps, and cooldown expiry.

Files changed:
 .changeset/fn-8691-wedge-notification-cooldown.md  |   7 ++
 docs/architecture.md                               |   4 +-
 .../postgres/store-wedge-resolution.pg.test.ts     |  67 +++++++++++-
 packages/core/src/index.ts                         |   1 +
 packages/core/src/store.ts                         |  21 +++-
 packages/core/src/task-store/task-mutation-ops.ts  |   6 ++
 packages/core/src/types.ts                         |   2 +
 packages/core/src/types/task-core.ts               |  15 +++
 .../__tests__/task-wedge-notification.test.ts      | 117 ++++++++++++++++++++-
 .../src/notification/notification-service.ts       |  30 +++++-
 10 files changed, 261 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8691

Fusion-Task-Lineage: b7460508-f9b0-488b-828c-a51e9477304d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 08:58:49 -07:00
gsxdsm
04c2bb4707 FN-8654: rotate credential instances after provider limits
Retry provider-limit failures with eligible credential instances before falling back to existing pauses and backoff.

- Add a runtime-shared credential rotator with cooldown, exhaustion, and audit handling.
- Wire credential rotation into executor and heartbeat retry lanes while preserving user pause controls.
- Document the behavior and cover rotation, recovery, and retry paths.

Files changed:
 .changeset/fn-8654-credential-instance-rotation.md |   7 +
 AGENTS.md                                          |   1 +
 docs/architecture.md                               |   2 +-
 docs/settings-reference.md                         |   4 +
 .../__tests__/credential-instance-rotation.test.ts |  88 +++++++++++
 .../__tests__/credential-rotation-lanes.test.ts    |  20 +++
 .../__tests__/credential-rotation-recovery.test.ts |  19 +++
 .../__tests__/credential-rotation-wiring.test.ts   |  15 ++
 .../__tests__/rate-limit-retry-rotation.test.ts    |  50 ++++++
 .../src/__tests__/usage-limit-detector.test.ts     |  14 ++
 packages/engine/src/agent-heartbeat.ts             | 102 +++++++++++-
 .../engine/src/credential-instance-rotation.ts     | 175 +++++++++++++++++++++
 packages/engine/src/executor.ts                    | 141 +++++++++++++++--
 packages/engine/src/index.ts                       |   7 +
 packages/engine/src/project-engine.ts              |   5 +
 packages/engine/src/rate-limit-retry.ts            |  32 +++-
 packages/engine/src/runtimes/in-process-runtime.ts |  29 +++-
 packages/engine/src/usage-limit-detector.ts        |  18 ++-
 18 files changed, 699 insertions(+), 30 deletions(-)

Fusion-Task-Id: FN-8654
Fusion-Task-Lineage: 44d63441-270c-4949-8c34-47ec4c9992e4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 08:20:29 -07:00
gsxdsm
006cc40454 FN-8685: add durable cross-process task deletion consumers
Deliver durable, replay-safe cross-process task deletion observation.

- Add PostgreSQL lifecycle consumer cursors, leases, acknowledgements, retention, and recovery.
- Start named consumers in dashboard, serve, and engine runtime paths.
- Preserve delete integration metadata while suppressing replayed GitHub and GitLab side effects.
- Cover outbox identity, observed delivery, fencing, and reconciliation behavior.

Files changed:
 ...fn-8685-cross-process-task-deleted-observers.md |   7 +
 .../fn-8685-task-deleted-outbox-consumers.md       |   7 +
 docs/architecture.md                               |   8 +-
 ...tgres-cross-process-task-deleted-observation.md |   8 +-
 docs/storage.md                                    |  10 +-
 packages/cli/src/commands/dashboard.ts             |   9 +-
 packages/cli/src/commands/serve.ts                 |   9 +-
 packages/cli/src/project-context.ts                |   9 +-
 .../task-deleted-outbox-consumer.pg.test.ts        | 157 ++++++++
 ...-deleted-observed-dispatch-side-effects.test.ts |  36 ++
 .../task-lifecycle-consumer-identity.test.ts       |  22 ++
 packages/core/src/index.ts                         |  11 +
 .../0041_fn_8685_task_lifecycle_consumers.sql      |  88 +++++
 packages/core/src/postgres/schema-applier.ts       |  16 +-
 packages/core/src/postgres/schema/project.ts       |  45 +++
 packages/core/src/postgres/startup-factory.ts      |   4 +
 packages/core/src/store.ts                         |  54 ++-
 .../__tests__/lifecycle-outbox-writer.test.ts      |   4 +-
 .../core/src/task-store/archive-lifecycle-2.ts     |   1 +
 packages/core/src/task-store/lifecycle-ops.ts      |  13 +-
 packages/core/src/task-store/lifecycle-outbox.ts   |   2 +
 packages/core/src/task-store/project-store-ops.ts  |   4 +-
 .../src/task-store/task-deleted-outbox-consumer.ts | 333 +++++++++++++++++
 .../task-store/task-lifecycle-consumer-identity.ts |  32 ++
 .../task-store/task-lifecycle-consumer-registry.ts | 396 +++++++++++++++++++++
 .../task-store/task-lifecycle-event-retention.ts   | 104 ++++++
 packages/core/src/task-store/task-mutation-ops.ts  |   1 +
 packages/dashboard/src/github-tracking-state.ts    |  12 +-
 packages/dashboard/src/gitlab-delete-close.ts      |   3 +
 packages/dashboard/src/gitlab-split-close.ts       |   7 +-
 packages/dashboard/src/project-store-resolver.ts   |   9 +-
 packages/engine/src/project-manager.ts             |   4 +-
 packages/engine/src/project-runtime.ts             |   2 +-
 packages/engine/src/runtimes/in-process-runtime.ts |  17 +-
 packages/engine/src/self-healing.ts                |  27 ++
 35 files changed, 1439 insertions(+), 32 deletions(-)

Fusion-Task-Id: FN-8685
Fusion-Task-Lineage: 63eca9ac-d2af-44b0-ba79-388a950148d3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 06:09:04 -07:00
gsxdsm
4f09758dc8 FN-8681: retarget executor step credential instances
Enable executor step sessions to use selected and rotated credential instances.

- Pass task-selected credential instances into step-session execution.
- Re-resolve live credential targets after usage-limit retries using the effective agent runtime configuration.
- Retarget future sessions safely and cover retry behavior.
- Document the runtime behavior and add a patch changeset.

Files changed:
 .changeset/fn-8681-credential-instance-retarget.md |   7 +
 docs/settings-reference.md                         |   7 +-
 .../src/__tests__/step-session-executor.test.ts    | 211 +++++++++++++++++++++
 packages/engine/src/executor.ts                    |  21 ++
 packages/engine/src/step-session-executor.ts       |  94 ++++++++-
 5 files changed, 332 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8681

Fusion-Task-Lineage: 99724283-6f5d-4890-b7f8-65af1d88b12c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 03:45:59 -07:00
gsxdsm
4009eb34cb FN-8683: remove unreachable SQLite task polling replica
Document PostgreSQL task-deletion observation and remove the obsolete SQLite polling path.

- Remove polling state, replica emissions, and activity-log suppression from TaskStore.
- Retain backend-aware cache warming while documenting the transactional-outbox follow-up.
- Add tombstone and soft-delete abort coverage across core and engine lanes.

Files changed:
 docs/architecture.md                               |   3 +-
 ...tgres-cross-process-task-deleted-observation.md | 128 ++++++++++++++++
 docs/storage.md                                    |   3 +-
 .../task-delete-nonblocking-cleanup.test.ts        |  54 +++++++
 .../task-deleted-polling-replica-tombstone.test.ts |  57 +++++++
 .../task-updated-lanes-emit-surfaces.test.ts       |  26 ----
 packages/core/src/store.ts                         |  13 +-
 packages/core/src/task-store/lifecycle-ops.ts      | 168 ++-------------------
 packages/core/src/task-store/task-artifacts-ops.ts |   4 -
 .../__tests__/executor-soft-delete-abort.test.ts   |  15 ++
 .../src/__tests__/triage-soft-delete-abort.test.ts |  14 ++
 11 files changed, 284 insertions(+), 201 deletions(-)

Fusion-Task-Id: FN-8683
Fusion-Task-Lineage: a052db0c-b6fc-4b05-b6b2-f8217b56ded0
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 03:24:36 -07:00
gsxdsm
3793b576d8 FN-8673: comment on split source issue closures
Explain GitHub source-issue closures when imported work is split into subtasks.

- carry split closure context through task deletion and triage
- post one explanatory comment before closing source and tracking GitHub issues
- preserve exactly-one-comment behavior when close retries after transient failures
- document split closure behavior and cover source/tracking scenarios

Files changed:
 .changeset/fn-8673-split-close-issue-comment.md    |  7 ++
 docs/settings-reference.md                         |  2 +-
 docs/task-management.md                            |  1 +
 .../task-delete-caller-attribution.test.ts         | 32 +++++++
 packages/core/src/index.ts                         |  2 +-
 packages/core/src/store.ts                         | 10 +--
 packages/core/src/task-delete-attribution.ts       | 16 ++++
 .../core/src/task-store/archive-lifecycle-2.ts     | 14 ++--
 packages/core/src/task-store/archive-lifecycle.ts  |  6 +-
 packages/core/src/types.ts                         | 13 +++
 .../src/__tests__/github-tracking-state.test.ts    | 98 +++++++++++++++++++++-
 packages/dashboard/src/github-tracking-state.ts    | 82 +++++++++++++++---
 packages/engine/src/__tests__/triage.test.ts       |  4 +
 packages/engine/src/triage.ts                      | 10 +++
 14 files changed, 268 insertions(+), 29 deletions(-)

Fusion-Task-Id: FN-8673

Fusion-Task-Lineage: 904c2445-64d9-47b9-b706-f64b23c4e3a6

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 02:47:25 -07:00
gsxdsm
8a6949dd24 FN-8661: resolve selected credential instances for sessions
Resolve requested provider credential instances before creating agent sessions.

- Thread lane credential instance selections through planning, validation, execution, review, and merge sessions.
- Resolve selected instances into runtime credential stores while retaining provider-default fallback behavior.
- Preserve selected credentials for mission validation, executor retries, and spawned child agents.

Files changed:
 .../fn-8661-credential-instance-resolution.md      |  7 ++
 AGENTS.md                                          |  1 +
 docs/architecture.md                               |  2 +-
 docs/secrets.md                                    |  2 +
 docs/settings-reference.md                         |  1 +
 .../dashboard/src/__tests__/routes-auth.test.ts    | 80 +++++++++++++++++++
 .../dashboard/src/routes/register-model-routes.ts  | 78 +++++++++++++++++++
 .../src/__tests__/agent-session-helpers.test.ts    | 16 ++++
 .../credential-instance-resolution.test.ts         | 49 ++++++++++++
 packages/engine/src/agent-heartbeat.ts             |  1 +
 packages/engine/src/agent-runtime.ts               |  9 ++-
 packages/engine/src/agent-session-helpers.ts       | 67 +++++++++++-----
 packages/engine/src/auth-storage.ts                | 90 ++++++++++++++++++----
 packages/engine/src/executor.ts                    | 29 ++++++-
 packages/engine/src/merger-ai.ts                   |  2 +
 packages/engine/src/merger.ts                      |  5 ++
 packages/engine/src/mission-execution-loop.ts      |  4 +-
 packages/engine/src/pi.ts                          |  7 +-
 packages/engine/src/pr-response-run-ops.ts         |  1 +
 packages/engine/src/reviewer.ts                    |  7 ++
 packages/engine/src/triage.ts                      |  2 +
 21 files changed, 420 insertions(+), 40 deletions(-)

Fusion-Task-Id: FN-8661

Fusion-Task-Lineage: 1e34a3ce-0857-4619-9746-ce0dc12dc2ba

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 02:05:03 -07:00
gsxdsm
13f7266b67 FN-8670: reuse model runtime fixtures in engine tests
Share a warmed Pi model runtime across engine catalog tests.

- Add an isolated in-memory model registry fixture backed by a per-file shared runtime.
- Warm the runtime before catalog tests and verify custom-provider registry isolation.
- Document the required fixture pattern for real Pi SDK catalog tests.

Files changed:
 docs/testing.md                                    |  1 +
 .../engine/src/__tests__/_model-runtime-fixture.ts | 40 ++++++++++++++++++++++
 .../custom-providers-openai-completions.test.ts    | 15 +++-----
 .../custom-providers-openai-responses.test.ts      | 15 +++-----
 .../src/__tests__/provider-registration.test.ts    | 33 ++++++++++++------
 5 files changed, 73 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-8670

Fusion-Task-Lineage: 7410d114-1853-44cc-a09b-a997fbc7f119

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 01:34:43 -07:00
gsxdsm
ebe514c3e4 FN-8677: propagate task update lanes before conversion
Propagate cache-warmed workflow lanes through task updates so synchronous engine consumers support renamed boards.

- Add task lane cache and attach resolved lanes to task:updated metadata.
- Update scheduler, triage, and notification consumers to use carried lanes with bridge-safe fallbacks.
- Cover lane propagation and renamed-lane event behavior with core and engine tests.

Files changed:
 .changeset/fn-8677-manual-merge-hold-lanes.md      |   7 ++
 .changeset/task-updated-carries-lanes.md           |   7 ++
 ...orkflow-ir-readers-always-return-the-default.md |  22 +++++
 .../sync-workflow-ir-second-blocker.test.ts        |  43 +++-----
 .../core/src/__tests__/task-lane-cache.test.ts     |  30 ++++++
 .../task-updated-lanes-emit-surfaces.test.ts       |  92 ++++++++++++++++++
 .../__tests__/task-updated-lanes-payload.test.ts   |  42 ++++++++
 packages/core/src/index.ts                         |   1 +
 packages/core/src/store.ts                         |  36 ++++++-
 packages/core/src/task-lane-cache.ts               |  63 ++++++++++++
 .../core/src/task-store/archive-lifecycle-2.ts     |   3 +
 packages/core/src/task-store/moves.ts              |   1 +
 packages/core/src/task-store/task-artifacts-ops.ts |   1 +
 packages/core/src/task-store/task-update.ts        |   1 +
 packages/core/src/task-store/update-task-deps.ts   |   4 +-
 .../core/src/task-store/workflow-definitions.ts    |  71 +++++---------
 .../__tests__/scheduler-task-updated-lanes.test.ts | 108 +++++++++++++++++++++
 .../task-updated-lanes-bridge-compat.test.ts       |  94 ++++++++++++++++++
 ...task-updated-lanes-engine-emit-surfaces.test.ts | 101 +++++++++++++++++++
 .../src/__tests__/triage-pause-abort.test.ts       |  22 +++++
 .../src/__tests__/triage-planning-wake.test.ts     |  25 +++++
 .../notification-renamed-lifecycle-columns.test.ts |  84 +++++++++++++++-
 .../__tests__/task-wedge-notification.test.ts      |  19 ++++
 .../src/notification/notification-service.ts       |  56 ++++-------
 packages/engine/src/scheduler.ts                   |  62 +++---------
 packages/engine/src/triage.ts                      | 105 ++++++--------------
 scripts/lib/inert-sync-lane-baseline.json          |   5 +-
 27 files changed, 858 insertions(+), 247 deletions(-)

Fusion-Task-Id: FN-8677

Fusion-Task-Lineage: d8fef9db-0f88-4dfd-9813-be25e10e3588

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 01:15:10 -07:00
gsxdsm
5f12044168 FN-8652: add multiple provider credential instances
Enable operators to create, select, and remove named credentials for each supported provider.

- Add provider-auth instance discovery and credential mutation endpoints.
- Add dashboard authentication controls, status handling, and instance coverage.
- Document instance behavior and add a release changeset.

Files changed:
 .changeset/fn-8652-provider-credential-instances.md       |   7 +
 docs/secrets.md                                    |   2 +
 docs/settings-reference.md                         |   4 +
 packages/dashboard/app/api.ts                      |   1 +
 packages/dashboard/app/api/provider-status.ts      |  94 +++++-
 .../dashboard/app/components/SettingsModal.tsx     | 138 ++++-----
 .../AuthenticationSection.instances.test.tsx       |  75 +++++
 .../settings/sections/AuthenticationSection.css    |  13 +
 .../settings/sections/AuthenticationSection.tsx    | 199 +++++++-----
 .../dashboard/src/__tests__/routes-auth.test.ts    |  27 +-
 packages/dashboard/src/routes.ts                   |  12 +-
 .../dashboard/src/routes/register-auth-routes.ts   | 334 ++++++++++++++++++---
 .../src/__tests__/provider-auth-instances.test.ts  |  65 ++++
 packages/engine/src/provider-auth.ts               |  89 ++++++
 14 files changed, 845 insertions(+), 215 deletions(-)

Fusion-Task-Id: FN-8652

Fusion-Task-Lineage: 39568d58-7f57-4d17-97cb-1837323b3a94

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 01:07:59 -07:00
gsxdsm
1e83dcceec chore(release): v0.74.0-beta.6
Version bump via changesets.
2026-08-01 00:55:04 -07:00
gsxdsm
9b82ff29e1 fix: enforce active capacity and clarify queued cards
Refresh canonical live-task claims at serialized scheduler, planning, and merge admission boundaries, including workflow-step leases and reservation handoffs. Back off capacity-denied merges safely across abort and restart lifecycles. Render queued planning cards in the header badge family with compact reason-specific icons.
2026-08-01 00:32:25 -07:00
gsxdsm
dd569395e8 FN-8671: isolate triage admission state in tests
Prevent leaked singleton admission state from affecting later triage polling tests.

- Add test-only coordinator reset and inspection seams for all admission categories.
- Stop tracked triage processors before clearing shared reservation and pre-held-slot state.
- Cover teardown behavior and document singleton-state isolation guidance.

Files changed:
 docs/testing.md                                   |  6 ++
 packages/engine/src/__tests__/concurrency.test.ts | 56 ++++++++++++++++
 packages/engine/src/__tests__/triage.test.ts      | 82 ++++++++++++++++++++---
 packages/engine/src/concurrency.ts                | 30 +++++++++
 4 files changed, 166 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8671

Fusion-Task-Lineage: 41217db4-1fd6-45d6-a846-c57fc3c7052e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 00:14:32 -07:00
gsxdsm
713e9320b0 fix(engine): enforce capacity for workflow continuations
Route durable planning and review continuations through shared project admission, retain reservations through execution, and prevent duplicate continuations from releasing another run's slot.
2026-07-31 23:33:47 -07:00
gsxdsm
897cce9d94 FN-8656: resolve scheduler lanes for renamed holds
Resolve scheduler lane lookup against workflow-defined hold and terminal columns.

- Use asynchronous workflow lane resolution after the synchronous event prologue
- Preserve legacy lane fallback and recognize all terminal workflow columns
- Update scheduler regression coverage, sync-lane guardrails, and release notes

Files changed:
 .changeset/fn-8656-scheduler-renamed-hold-lanes.md |   7 ++
 .../sync-workflow-ir-callsite-allowlist.test.ts    |  10 +-
 .../scheduler-renamed-hold-events.test.ts          |  20 ++--
 ...ow-scheduler-parked-columns-live-e2e.pg.test.ts |  13 ++-
 ...-sync-role-conversion-inert-live-e2e.pg.test.ts |   8 +-
 packages/engine/src/scheduler.ts                   | 121 +++++++++------------
 scripts/check-inert-sync-lane-conversions.mjs      |   5 +
 scripts/lib/inert-sync-lane-baseline.json          |   3 +-
 8 files changed, 92 insertions(+), 95 deletions(-)

Fusion-Task-Id: FN-8656

Fusion-Task-Lineage: 389a95a1-289f-4dde-86b3-1e450f8d43db

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 22:19:32 -07:00
gsxdsm
dcf1b921a3 fix(engine): close active-slot reservation handoff gaps 2026-07-31 22:10:14 -07:00
gsxdsm
5a19d1da6e fix: count only actively running tasks against worktree capacity
Retained directories on queued, paused, blocked, or terminal tasks no longer
consume scheduler slots. Agent concurrency and worktree capacity now count the
same canonical live-task population through one project admission ceiling
(resolveActiveTaskCapacityLimit) with an atomic reserveIfAvailable claim, so
planning, execute, and merge lanes cannot each observe and claim the final
worktree slot independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 22:07:05 -07:00
gsxdsm
182e3fdbe8 FN-8651: add named provider credential instances
Add provider-instance identity and auth.json storage for multiple credentials per provider.

- Export provider-instance key parsing, validation, and reserved metadata helpers.
- Resolve, list, mutate, and select named credential instances atomically in auth storage.
- Preserve legacy bare-key hydration and document the auth.json instance contract.
- Cover provider instance parsing, storage behavior, and concurrent writes.

Files changed:
 .changeset/fn-8651-provider-instances.md           |   7 +
 docs/secrets.md                                    |   8 +
 .../src/__tests__/oauth-credential-interop.test.ts |  16 ++
 packages/core/src/index.ts                         |  16 ++
 packages/core/src/oauth-credential-interop.ts      |  15 +-
 packages/core/src/provider-instance.test.ts        |  24 +++
 packages/core/src/provider-instance.ts             |  65 +++++++
 .../src/__tests__/auth-storage-concurrency.test.ts |  13 ++
 .../src/__tests__/auth-storage-instances.test.ts   |  75 ++++++++
 packages/engine/src/auth-storage.ts                | 214 +++++++++++++--------
 10 files changed, 367 insertions(+), 86 deletions(-)

Fusion-Task-Id: FN-8651

Fusion-Task-Lineage: cfabe496-60f9-4166-a09f-87ea2028cfd9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 21:56:30 -07:00
gsxdsm
a3c0501822 fix(engine): unblock retained worktree dispatch
Allow queued tasks to reuse worktrees they already hold when the durable worktree ledger is at or above capacity. Preserve independent agent and semaphore limits, and avoid releasing a worktree slot that a rejected transfer never acquired.
2026-07-31 21:21:28 -07:00
gsxdsm
408e558f54 FN-8648: audit sync workflow resolver test fixtures
Audit workflow resolver test fixtures so they reflect authoritative async resolution and document intentional sync coverage.

- Remove redundant synchronous resolver fixtures from async workflow tests.
- Preserve and document the scheduler fixture that exposes the known synchronous listener defect.
- Clarify the deliberate default-workflow sync contrast and strengthen recovery fixture intent.

Files changed:
 .../executor-planner-lanes-resolved.test.ts        | 45 +++++-----------------
 .../src/__tests__/planner-lane-resolution.test.ts  | 11 +++---
 .../planner-lanes-async-resolution.test.ts         |  9 ++++-
 .../recover-approved-intake-post-u11.test.ts       | 11 +++---
 .../scheduler-renamed-hold-events.test.ts          |  9 +++++
 .../__tests__/triage-release-renamed-hold.test.ts  |  6 ++-
 .../triage-undeclared-column-rescue.test.ts        | 11 +++---
 packages/engine/src/__tests__/triage.test.ts       | 23 +++++------
 8 files changed, 56 insertions(+), 69 deletions(-)

Fusion-Task-Id: FN-8648

Fusion-Task-Lineage: c75fe9ca-30a3-467e-9268-f8d6d95c49e7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 19:26:23 -07:00
gsxdsm
7bdeaa8b0c fix(engine): the new worktree ledgers count terminal lanes by NAME — renamed boards stall (#3296)
## Census

**Before: `COLUMN guards (the backlog): 2`, `--strict` RED. After:
`BACKLOG ZERO`, all five gates green.**

Two commits from last night's `maxWorktrees` rollout copied the same
holder ledger, both with literals:

| commit | file | gate |
|---|---|---|
| `374956ef23` | `triage.ts` | planning admission |
| `6c7467a78d` | `executor.ts` | `fn_spawn_agent` |

```ts
t.column !== "done" && t.column !== "archived"
```

## What it costs

Both exclude terminal lanes because a finished card's worktree is
**cleanup-owned, not capacity**. On a renamed board neither literal
matches, so every finished card keeps counting as a live holder. The
count only grows, the gate reaches zero room on a board with free slots,
and planning admission is withheld forever / every spawn is refused.

That is the **mirror** of the breach these commits fixed, and strictly
worse: 8 planners on a 4-slot board is visible; a permanent stall is
silent. The recorded reason even names the worktree budget, which the
operator then checks and finds has room.

## The conversion

`resolveProjectColumnsForRoles(store, ["complete", "archived"])` —
project-level, because the ledger spans the whole board with no single
task to resolve against. Matches triage's existing use in
`sweepStalePlanningStatuses` and executor's at the wip gates.
Legacy-seeded, so a default board still excludes exactly `done` and
`archived` — byte-identical there.

## Both conversions were UNCOVERED when written

Measured with #3214's blinding procedure **before** writing tests:
reverting either to the literals left **all 19 tests in the capacity
suites green**. Nothing in the tree could tell the conversion from what
it replaced — which is how the literals got there in the first place.

Each now has a renamed-board case that fails when blinded:

```
triage    converted 2 passed  |  BLINDED 1 failed | 1 passed  |  restored 2 passed
executor  converted 8 passed  |  BLINDED 1 failed | 7 passed  |  restored 8 passed
```

## The pairing earned itself immediately

Both new cases assert an **absence** (no throttle / no refusal), so each
is paired with a positive proving the gate still fires on the same
renamed board when a card genuinely holds the last worktree.

That caught a real defect in my own fixture: the candidate scan resolves
each task's **own workflow selection**, not `listWorkflowDefinitions`,
so my first version fell back to the default board where `drafting`
isn't a hold lane. No card was eligible, nothing throttled, and the
absence assertion **passed for the wrong reason**. The positive failed
and exposed it. Recorded at the fixture so the next reader doesn't
reintroduce it.

## Verification

```
42 tests across 6 capacity suites             pass
check-fnxc-future-dates                       green
check-inert-sync-lane-conversions             green
check-lane-wiring                             green
check-sql-column-literals                     green
census --strict                               green   (BACKLOG ZERO restored)
```

No changeset: internal engine fix, no published-package surface change.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:16:07 -07:00
gsxdsm
69bc9fc7ab fix(engine): near-duplicate check runs before planning for imported issues
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:53:38 -07:00
gsxdsm
c19bec3752 fix(fnxc): two scheduler stamps to their authoring commits' real times (10 days and 1 day off) (#3294)
Last loose thread from the 2026-07-31 stamp-repointing wave. Two comment
lines.

```
line 1027  ConcurrencyAdmission      2026-07-31-09:00  ->  2026-07-21-22:30   (eef5eb751e)
line 1471  WorkflowLifecycleColumns  2026-07-31-05:00  ->  2026-07-30-20:55   (109204c590)
```

## Not a revert — neither value was ever right

| stamp | originally | after #3280 | authoring commit (UTC) |
|---|---|---|---|
| `ConcurrencyAdmission` | `2026-08-06-09:00` (16 days ahead) |
`2026-07-31-09:00` (10 days late) | **2026-07-21 22:30** |
| `WorkflowLifecycleColumns` | `2026-08-01-05:00` (~1.5 days ahead) |
`2026-07-31-05:00` (~1 day late) | **2026-07-30 20:55** |

Both were written **ahead of their own commits** to begin with. Three
lanes then repointed stamps to turn `main` green (#3261, #3269, #3280),
moving the **date** back a day while keeping the clock time — which
converts an hours-off stamp into a days-off one in the opposite
direction. #3282 reverted the batch it owned; these two were outside its
scope.

So restoring the originals would be wrong too. The defensible values are
the authoring commits' UTC timestamps, per the `date -u` rule #3281
settled.

## Why now

`scheduler.ts` is a hot file. This survived two successive claimants — I
flagged it on #3262 and again on #3288 rather than opening a conflicting
PR, and said I'd take it once the file was unclaimed.
`check-file-claimed` now reports UNCLAIMED, so here it is.

## Scope

The gate is **green either way** — #3277 fixed the comparison, so
nothing is blocked by this. It is purely about the FNXC trail recording
when the work actually happened, which is the only reason the trail
exists. A stamp that satisfies a check while misstating the date by ten
days is worse than no stamp.

## Verification

```
check-fnxc-future-dates           green
check-inert-sync-lane-conversions green
check-lane-wiring                 green
check-sql-column-literals         green
census --strict                   green
```

Diff is two comment lines — no executable change. No changeset.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 18:51:50 -07:00
gsxdsm
6c7467a78d fix(engine): spawned children gate on maxWorktrees too — the spawn note promised both dimensions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:43:37 -07:00
gsxdsm
374956ef23 fix(engine): planning admission respects maxWorktrees — the merge-drain freeze was masking the missing gate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:39:40 -07:00
gsxdsm
42a2699b9a test(scheduler): pin the worktree-capacity arithmetic — both failure directions (#3288)
## What

Pins the **worktree-capacity arithmetic** — the gap #3262 measured,
named, and explicitly left for someone to claim. Two commits: a
behaviour-preserving seam extraction, then the test.

#3262's own scope note:

> blinding this predicate to `false` leaves all 22 scheduler suites
green (365 tests). The capacity logic it feeds has no behavioural
coverage at all.

## Two live defects, opposite directions

Both came out of these few lines:

- **UNDER-COUNT admits work over the cap.** `maxWorktrees=4`, four
planning sessions each holding a worktree, and a replan dispatch
admitted as the **fifth** — the ledger counted WIP cards only and never
learned to count planners.
- **OVER-COUNT self-deadlocks.** A planned Ready card *retains* its
planning worktree for execution reuse, so counting it as a holder blocks
its own release: `2 wip + 3 idle-held = 5/4`, and the first unpause
released 2 of 4 slots' worth of work.

**Both are pinned, and the asymmetry is why.** Under-counting breaks the
cap and lets real work over it; over-counting only starves dispatch. A
test covering the "safe" direction alone would leave the expensive one
open.

## Mutation-tested — all four caught

| mutation | result |
|---|---|
| drop the terminal exclusion | 1 failed / 7 passed |
| count WIP cards twice | 2 failed / 6 passed |
| count cards holding no worktree | 1 failed / 7 passed |
| drop the self-slot subtraction | 1 failed / 7 passed |

```
clean:            8 passed (8)
scheduler suite:  16 files / 151 tests passed  (behaviour preserved by the extraction)
typecheck, lint:  clean
```

## Scope, stated rather than implied

The terminal predicate is **injected**, not resolved here. Which lanes
are terminal is #3262's test; resolving it in this file would make it
fail for that reason instead of this one. This pins the **set
arithmetic** — who is excluded, and how the total is formed.

Still not covered, and I am not claiming otherwise: the *stateful* half
of the ledger — the `+= 1` on dispatch and the `Math.max(0, … - 1)` on
failure inside `schedule()`'s loop. Extracting that would mean
restructuring dispatch itself, which is a different change from this
one.

## Process note

I claimed this on #3262 **before** starting rather than after, because
`scheduler.ts` is the hottest file in the tree and I produced three
duplicate PRs earlier tonight by picking up small shared-surface work
someone else already had in flight. Announcing first cost one comment;
the duplicates cost three PRs and two closes.


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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved scheduler capacity calculations for tasks that retain
existing worktrees.
  * Prevented WIP tasks from being counted twice.
  * Excluded completed and worktree-less tasks from reserved capacity.
* Corrected candidate capacity calculations when no worktree capacity is
reserved.

* **Tests**
* Added coverage for worktree reservation totals and candidate reuse
scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 18:33:41 -07:00
gsxdsm
6834ba35bd fix(engine): hung-pass watchdogs on scheduler, merge queue, and continuation drain
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:19:17 -07:00
gsxdsm
00769fad7c fix(engine): run merges outside the admission drain — every merge froze planning admission
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:13:32 -07:00
gsxdsm
e51ebff381 fix(engine): hung-poll watchdog — triage admission recovers loudly instead of dying silently
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:08:50 -07:00
gsxdsm
f2f6795010 FN-8635: keep worktree slider visible
Keep Command Center capacity controls visible and correctly editable across settings states.

- Render Max worktrees in the shared full-width range wrapper.
- Preserve capacity values after load failures and explain disabled worktree limits.
- Add control tests, browser geometry coverage, documentation, and a release changeset.

Files changed:
 .changeset/fn-8635-worktrees-slider.md             |   7 +
 docs/dashboard-guide.md                            |   2 +-
 .../command-center/CommandCenterControls.css       |   6 +
 .../command-center/CommandCenterControls.tsx       |  63 +++++---
 .../__tests__/CommandCenterControls.test.tsx       |  76 ++++++++-
 packages/engine/e2e/fn-8635-worktrees-slider.mjs   | 180 +++++++++++++++++++++
 6 files changed, 310 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-8635

Fusion-Task-Lineage: fb9c2a46-3c5c-4871-bab4-c43af20cb4de

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 18:03:13 -07:00
gsxdsm
94ed527bb1 refactor(scheduler): the capacity gate's terminal check uses the shared isTerminalColumnRole (#3262)
**Rebased. The census claim in my original title was overtaken — this is
now a correction, not a conversion.**

## Census: 0 before, 0 after

#3261 got there first, by **recording** the fallback rather than
converting it. Its `DELIBERATE` reasoning is correct and I kept it
verbatim.

## What this corrects

That note says:

> Recorded rather than converted because **there is nothing to convert
TO**.

There is. **`isTerminalColumnRole` in core is this predicate, term for
term** — verified against `column-roles.ts` rather than assumed:

| | hand-rolled | `isTerminalColumnRole` |
|---|---|---|
| flags present | `flags.complete === true \|\| flags.archived === true`
| same, via the two role helpers |
| flags undefined | `columnId === "done" \|\| columnId === "archived"` |
same, via `LEGACY_COMPLETE/ARCHIVED_COLUMN_ID` |

And the helper's own doc names this exact case — it exists *"because the
pattern `column !== \"done\" && column !== \"archived\"` is the single
most repeated shape in the backlog"* and *"keeps callers from
re-deriving it and from accidentally dropping one half."*

**The rest of #3261's argument stands and is preserved.** The
undefined-flags arm is a **live** path, and treating an unreadable
workflow as non-terminal would count a finished card's retained worktree
against live capacity. That reasoning is about the *fallback's
existence*, not about *where the predicate lives* — and the shared
helper carries the identical fallback.

Second time in this file: `isWipColumnTask` two lines up records that it
was itself once *"a hand-rolled copy of `isWipColumnRole`"*. That's an
argument for the helper being easy to miss, not for anyone being
careless.

## Coverage, stated rather than implied

**Blinding this predicate to `false` leaves all 22 scheduler suites
green (365 tests)** — the capacity logic it feeds has no behavioural
coverage at all.

The added test pins the **lane vocabulary** (both renamed terminal
lanes, the non-terminal lanes, the legacy fallback). It does **not** pin
the capacity arithmetic, which stays unguarded and belongs to that
gate's owner. Under-counting is the dangerous direction: the commit
adding the gate reports `maxWorktrees=4` with **a fifth worktree
admitted**.

## Verification

- 23 scheduler suites — **368 green**; `tsc` clean
- Census 0 → 0; DELIBERATE count unchanged at 148; inert ratchet green

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 17:52:09 -07:00
gsxdsm
c67aafde1c revert(fnxc): restore seven author stamps I falsified while chasing a gate bug (#3282)
Closes #3279. Undoes the damage my #3261 did, now that #3277 has landed
and made it safe.

## What went wrong

#3277 established that last night's "future-dated" stamps were
**correct** — the author's local date in a UTC+1 container, written
minutes before their commits. The gate compared against the runner's
local calendar (PDT) and called them tomorrow.

I diagnosed it as author error and repointed seven stamps to turn main
green. The values I wrote were **neither the author's local time nor
UTC** — invented times chosen to satisfy a broken check. The FNXC record
is this project's why-does-this-exist trail, so those stamps misstated
when the work happened.

## Restored verbatim

| file | mine (wrong) | restored |
|---|---|---|
| `workflow-column-boundary-capacity.test.ts` | `22:30` |
`2026-08-01-00:30` |
| `runtimes/in-process-runtime.ts` | `22:20` | `2026-08-01-00:20` |
| `scheduler.ts` (`MissionReconciliation`) | `22:00` |
`2026-08-01-00:00` |
| `workflow-column-boundary-hooks.ts` | `22:20` | `2026-08-01-00:20` |
| `workflow-column-boundary.ts` ×2 | `22:20` | `2026-08-01-00:20` |
| `workflow-graph-task-runner.ts` | `22:20` | `2026-08-01-00:20` |

## The check that mattered

Sequencing was deliberate — #3277 had to land first or this would have
re-reddened main. The real question is whether the gate now accepts the
**originals**, measured across the rollover boundary at local
`2026-07-31 17:23 PDT` / UTC `2026-08-01 00:23`:

```
America/Los_Angeles   exit 0        Europe/Paris   exit 0
UTC                   exit 0        Asia/Tokyo     exit 0
```

`123 known future-dated stamp(s), none added`. **No baseline change
needed** — #3278's pruning already re-recorded `scheduler.ts`, and these
are known stamps rather than new ones.

Stamps only: `git diff` shows **zero** non-FNXC lines, 7 insertions / 7
deletions across 6 files. `census --strict` 0, `pnpm test:gate` 0.

## The part worth keeping

I argued against exactly this on #3263 — *"it rewrites stamps whose
authors are not us"* — and then did it myself six lines later, because I
was confident about a cause I had not checked. The commits' timestamps
were available the entire time; I read the runner's clock and never
asked what timezone the **author** was in.

Four of last night's seven PRs were fixing something that was not
broken. This is the cleanup for my share of that.
2026-07-31 17:39:04 -07:00
gsxdsm
500f40e65b fix: descriptive waiting badges (Queued to revise / Queued behind FN-X) + dependency-free blocked exits replan calmly
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:37:38 -07:00
gsxdsm
5acd8e987b fix(fnxc): three future-dated scheduler stamps — main red through three closed fixes (#3280)
**`check-fnxc-future-dates` exits 1 on `origin/main`.**

```
packages/engine/src/scheduler.ts: 3 future-dated stamps, baseline allows 2
  FNXC:ConcurrencyAdmission     2026-08-06-09:00   (six days out)
  FNXC:WorkflowLifecycleColumns 2026-08-01-05:00
  FNXC:WorkflowScheduling       2026-08-01-01:05
```

All three repointed to `2026-07-31`, times preserved. Gate now exits 0.

## This red has outlived three owners

#3270, #3272 and #3274 were each opened against it and each **closed
without merging**. Main has been red on this gate for hours while three
fixes came and went.

Claimed with `check-file-claimed.mjs` before starting — only #3262
touches `scheduler.ts`, and it is a terminal-role refactor rather than a
stamp fix, so this was genuinely unowned.

## Why this keeps recurring

Seven incidents in roughly two hours. The mechanism, in one line: **the
date check runs only in CI** (`pr-checks.yml:66`, no pre-commit or
pre-push hook), so every PR is validated against main's baseline *at its
own CI time* and cannot see a concurrent or later change. Two PRs
stamping the same file both pass, then compose into a red main. One case
(#3273) was a stale branch **reverting** an already-merged fix.

Patching instances has not converged — this PR is the eighth attempt at
the same class. Two structural options, neither of which I am landing
unilaterally since the second changes the gate's contract:

- run the date check at **author time** (pre-push); it needs no baseline
for "is this date in the future", so it cannot be raced
- make the date rule **baseline-free** — a future-dated stamp is always
wrong, unlike a lifecycle literal that may be a deliberate fallback

`2026-08-06` being six days out also suggests these are not off-by-one
timezone slips but stamps written from an intended future date.

## Verification

- `check-fnxc-future-dates` — **exit 0** (was exit 1 on main)
- `scheduler` suites — **148 pass**
- `tsc --noEmit` (engine) — 0 errors
- comment-only diff, no behaviour change

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 17:28:14 -07:00
gsxdsm
911b7f1c31 test(engine): memoize identical full-repo census spawns in lifecycle-column-census
The lifecycle-column-census test file spawned the census CLI ~14 times, each
parsing every tracked source file to a TypeScript AST (~2s for ~1960 files).
Many spawns were byte-identical, deterministic, read-only real-repo scans:
the --json census 4x, the plain report 2x, plus a repeated identical
--update-baseline tree-sync across the ratchet cases. Memoize each distinct
read-only spawn's output (keyed by argv) and reuse the synced baseline JSON,
collapsing duplicate full-AST scans without changing any assertion.

File wall-time: 30.5s -> 18.6s (-39%). 57/57 tests still pass.

Fusion-Task-Id: FN-slow-test-census
2026-07-31 17:27:42 -07:00
gsxdsm
3f95c6d53e fix(engine): a Ready card's retained worktree transfers on release instead of blocking it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:40:41 -07:00
gsxdsm
8e6b0ad67e test(engine): main is red — the census preconditions were MET, repoint two guards that expired on success (#3260)
## main is red, and this is the second half of it

Running the full `engine-default` project on `origin/main` — 754 files,
10,538 tests — returns **3 files / 4 tests failing**. One is the
parked-seam audit counter, fixed in #3258. The other three are here.

All three assert that lifecycle debt **still exists**. It does not:

| assertion | expected | actual |
| --- | --- | --- |
| `finds >10 literal move targets, so the census is not vacuous` | > 10
| **0** |
| `both recoveryRehome groups are non-empty` | > 0 each | **0 / 0** |
| `still says ROSE when guards genuinely grew` | exit 1 | **exit 0**
(mutation was a no-op) |

Nothing regressed. The conversion program drove the engine's literal
move-target population to zero and the census baseline to zero entries.
**Each guard's premise was "the debt still exists", so each expired the
moment the work succeeded — and expired by failing, which reads as a
regression in the very thing it was guarding.**

The third is the sharpest: it manufactured a rise by finding a baseline
entry with more than one guard and zeroing it. With `byFile` empty there
was nothing to find, so it mutated nothing, the census correctly passed,
and the test asserted exit 1 against a correct pass.

## Repointed, not deleted

A vacuity guard must not depend on real debt existing. Two halves:

- **Vacuity now runs against a synthetic fixture** — a small in-memory
source with three `moveTask` literals (one with `recoveryRehome`, one
targeting an undeclared column). The collector is exercised forever
regardless of how much real debt remains. This is what keeps the rest
honest: at a real population of 0 the zero-assertions are trivially
true, and **only the fixture proves they would still fire**.
- **The real-tree assertions now assert zero**, so a reintroduced
literal move target fails them. Same guarantee as before, pointed at the
state the tree is actually in.

The ROSE case builds its own one-file tree through the
`FUSION_CENSUS_FILE_ROOT`/`FILE_LIST` seam from #3230 rather than
borrowing a baseline entry that no longer exists — a rise
**constructed** instead of borrowed. It also now asserts the failure is
*not* the reclassification wording, which is the distinction that file
exists to protect.

## Verification

| mutation | expected | result |
| --- | --- | --- |
| reintroduce a literal `moveTask(id, "in-review")` in the engine | fail
| exit 1 ✅ |
| blind the collector to return `[]` | fail | exit 1 ✅ |
| remove the `ROSE` wording from the census script | fail | exit 1 ✅ |
| clean tree | pass | exit 0 ✅ |

60 tests green across the three census suites. All eight ratchets exit
0. Test-only; no changeset.

**One honest note on my own method.** My first `ROSE` mutation replaced
1 of the 2 occurrences in the script and the test stayed green — which
looks exactly like a dead assertion. It was an ineffective mutation, not
a dead test; the manual run still printed `ROSE` from the other
occurrence. Re-run against both, it failed. A mutation that does not
actually change behaviour proves nothing, and it is worth checking that
the mutation landed before concluding the test is dead — the same trap
as reading a report-only ratchet's exit 0 as a pass.

## Not in scope

`defaultColumnIds()` has a pre-existing type error (`Property 'columns'
does not exist on WorkflowIrV1` — union narrowing). Untouched by this PR
and the test executes fine; flagging rather than fixing, since it is
unrelated to the red.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved census analysis reliability when evaluating guard-count
increases and reclassification messages.
* Improved detection and classification of literal move targets and
declared columns.
* Enhanced file path reporting for inputs outside the primary source
directory.

* **Tests**
* Added isolated test scenarios using temporary census data and
fixtures.
* Strengthened validation of recovery, plain, and declared-column
classifications.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 16:40:12 -07:00
gsxdsm
78d411cfe2 fix: main is RED on two gates — record the new fallback, repoint six future-dated stamps (#3261)
`9094d1640e` (globalPause gates every graph node entry) reddened **two**
lifecycle gates on main. Both are fixed here, in separate commits.

## 1. The census ratchet went 0 → 2

`isTerminalColumnTask` in `scheduler.ts`:

```ts
const flags = columnFlagsForTask(task);
if (flags) return flags.complete === true || flags.archived === true;
return task.column === "done" || task.column === "archived";   // ← counted
```

**The code is correct.** It resolves traits first and falls back only
when the workflow is unreadable. The census counts fallback literals on
purpose — *"a fallback literal is still a literal and should go when the
trait path becomes unconditional"* — and reports them beside the backlog
as already-converted. Its own remedy for a legitimate one is a
`DELIBERATE-LITERAL` marker at the site.

Recorded rather than converted because **there is nothing to convert
to**: a task whose workflow cannot be read has no resolved lane, and
treating it as non-terminal would count a finished card's retained
worktree against live capacity — the opposite of what the surrounding
fix does.

Marker sits in the declaration's **leading** comments; an inline one
attaches to the wrong node and is silently ignored, which cost a
miscount once before. Baseline re-recorded in the same commit, since the
census tracks deliberate counts and reports a marker addition as
`RECLASSIFIED`.

## 2. The stamp gate was red as well

Six files stamped `2026-08-01-00:2x` while UTC was `2026-07-31`:

```
workflow-column-boundary.ts             2      workflow-graph-task-runner.ts   1
workflow-column-boundary-hooks.ts       1      in-process-runtime.ts           5 (allows 4)
workflow-column-boundary-capacity.test  1
```

This checkout is UTC-7, so "just after midnight local" is tomorrow in
UTC — the case AGENTS.md documents, which passes `pnpm lint` locally
*because* the local clock agrees with what was written. Second
occurrence today; I fixed the same shape on #3208 for another worker.

Repointed to `2026-07-31-22:2x`, preserving relative order. **Zero
non-comment lines changed** — 8 lines across 6 files, verified by
diffing out FNXC lines.

## Measured

| check | before | after |
|---|---|---|
| `census --strict` | **1** | **0** |
| backlog | **2** | **0** (DELIBERATE-LITERAL 148 → 150) |
| `check-fnxc-future-dates` | **1** | **0** |
| `pnpm test:gate` | 0 | 0 |
| `census-reclassification-message` | 2 failed | **1 failed** |

That last row is deliberate: the remaining failure is the
expired-premise case #3260 fixes, and I have not touched it. The
capacity test from `9094d1640e` still passes 9/9.

## Why this landed at all

Both gates run in `pr-checks.yml`, so a PR carrying either would have
gone red. Worth someone checking how it merged — a stale merge base
would explain it, and if so the same hole is open for the next merge.
2026-07-31 16:29:37 -07:00
gsxdsm
9094d1640e fix(engine): globalPause gates every graph node entry; maxWorktrees counts planning/review holders
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:12:49 -07:00
gsxdsm
c8a6af13a0 test(census): pin file DISCOVERY, which every existing test was blind to (#3259)
Follow-through on the recommendation I made reviewing #3256: **a gate
needs a test for its file discovery, not only its matcher.**

## The gap

This suite pinned the matcher and never the scan. Every case either
feeds the classifier a source string or drives the CLI against the real
tree — so **the file list could return empty and all 53 tests would
still pass.**

Not hypothetical. `git ls-files` lists tracked files only, so a new file
with a plain `task.column === "in-review"` scored 0 until staged
(#3254). The identical bug then turned up in the move-target ratchet
**behind its own 12 matcher tests** (#3256) — I wrote those 12
specifically to stop that gate regressing, and they could not see it,
because they import the matcher and never run a scan.

## Four cases, on a synthetic tree

Driven through `FUSION_CENSUS_FILE_ROOT` + `FUSION_CENSUS_FILE_LIST`, so
discovery is testable without creating files inside a checkout the
operator writes to concurrently:

- a guard in a scanned file **reaches the classifier** and is counted
- `--strict` fails **for the right reason** (message names the file; not
an ENOENT fail-closed)
- **every** listed file is counted, not just the first
- files are read from the **scan root**, so a listed path and a read
path cannot diverge

## The second case earns its wording

Its first version asserted only `code === 1` — and **passed while
discovery was broken.** With the injected list ignored, paths come from
the real repo while reads resolve against the fixture root, every read
misses, and the gate fails closed with exit 1. Right code, unrelated
cause.

A test that cannot tell *"found a guard"* from *"could not read
anything"* is not testing the ratchet. Asserting the message is what
separates them.

I found that only by checking which cases the control actually failed —
3 of 4, not 4 of 4. Had I stopped at "the control fails, ship it", I
would have added a test that passes for the wrong reason to a suite
whose whole purpose is catching tests that pass for the wrong reason.

## Measured

| check | result |
|---|---|
| suite | **57 passed** (53 + 4) |
| anti-vacuity: `injectedList` forced undefined | **all 4 fail** (3/4
before strengthening case 2) |
| restored | 57/57 |
| `census --strict` / `check-fnxc-future-dates` | 0 / 0 |

Tests only — no gate or product change. The same four assertions port
directly to the other lifecycle gates once each grows the fixture seam;
the move-target ratchet is the obvious next one, and its `.mjs` is
currently claimed by #3256.
2026-07-31 16:03:46 -07:00
gsxdsm
f3d7b73741 test(engine): main is red — re-record the parked-seam audit at 4-of-6, split by resolution path (#3258)
## main is red

`workflow-optional-role-param-caller-audit-live-e2e.pg.test.ts` fails on
`origin/main` in `engine-default`:

```
AssertionError: expected 4 to be 2
  expect(parkedConverted.length).toBe(2);
```

Found by running the whole live-E2E corpus rather than trusting that it
passes — 27 files, 159 tests, 1 red. Not in the merge gate, so it has
been sitting there. The alarm fired **downward**, exactly as that file
was written to: two more call sites started passing `parkedColumns`, and
a counter fails when someone *closes* a gap as well as when someone
widens it.

## Why I did not just write 4

Re-recording it at 4 would have laundered an inert conversion through
the audit written to catch inert conversions. Walking all six sites
before touching the number:

| site | `parkedColumns` provenance | verdict |
| --- | --- | --- |
| `agent-heartbeat.ts:1267` | — | unconverted |
| `agent-heartbeat.ts:3796` | — | unconverted |
| `self-healing.ts:13184` | `await resolveProjectColumnsForRoles`
(:13170) | async-resolved |
| `self-healing.ts:13294` | `await resolveProjectColumnsForRoles`
(:13293) | async-resolved |
| `task-agent-sync.ts:243` | `await resolveLinkSyncColumnRoles` (:225) |
async-resolved |
| `scheduler.ts:1798` | `resolveTaskParkedColumnsSync` (:1797) | **SYNC
— INERT** |

`scheduler.ts:1798` resolves through `resolveTaskParkedColumnsSync` →
`getTaskWorkflowSelectionImpl`, which is `undefined` for every task
under PostgreSQL. The resolver then takes its `!workflowId` branch and
returns the **default builtin IR** — not `undefined` falling through to
a legacy arm, but a real IR resolving real traits, with full confidence.
It answers `hold`/`intake` as `todo`/`triage` on every board, exactly as
the literal did. Driven proof:
`workflow-scheduler-sync-role-conversion-inert-live-e2e.pg.test.ts`.

**The shape count (4) and the live count (3) are different numbers, and
only the second is about behaviour.** Both are now asserted, plus the
sync-resolved site by name so it cannot quietly become "just one of the
four".

## A mistake worth recording, because the test caught it

My first draft keyed on `await` appearing inside the call window. The
argument is nearly always a variable (`[...driftedParkedColumns]`,
`roles.parked`) and the `await` lives in that variable's **assignment**,
several lines above. That draft classified all four sites as inert — and
it **would have passed** had I written the expected number to match what
it measured. It failed only because I asserted 2 live from reading the
source first, and the mismatch exposed the detector.

Classification is now by provenance: take the root identifier, find
where the file assigns it, ask whether *that* is awaited. The same bug
recurred in my named-site check and failed the same way.

That is the whole hazard of this program in miniature — a source-text
audit that measures nothing looks exactly like one that measures
everything, and it is the *number you expected* that catches it, not the
green.

## Not asserted, deliberately

`self-healing.ts:13184` is inert for an unrelated reason: its gate is
`hasFreshRun || hasActiveExecution` and never reads
`shouldPreserveParkedLink`, so its correctly-resolved set decides
nothing today. Its own FNXC note says so. Resolution path is
mechanically checkable; "the gate never reads the answer" is not, and
asserting it on a string match would produce a number nobody could
maintain. Recorded as prose in the header.

I also did not touch `scheduler.ts`. It is a real defect, not a
deferral, but converting it is not this file's job — it is named in the
test so the next person converting it is sent here to move it from the
inert list to the live one.

## Verification

Mutation-verified — a passing audit proves nothing until it has been
seen to fail:

| mutation | expected | result |
| --- | --- | --- |
| convert the scheduler site to the async resolver | fail (3/1 → 4/0) |
exit 1 ✅ |
| drop `parkedColumns` from a converted site | fail (shape 4 → 3) | exit
1 ✅ |
| add a new unconverted caller | fail (calls 6 → 7) | exit 1 ✅ |
| clean tree | pass | exit 0 ✅ |

Full 27-file live-E2E corpus green (159 tests). Ratchets exit 0.
Test-only; no changeset.

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

## Summary by CodeRabbit

* **Tests**
* Expanded end-to-end audit coverage for workflows with optional role
parameters.
* Improved validation of caller resolution paths, including asynchronous
and synchronous scheduling scenarios.
* Updated expectations to reflect all supported conversion paths and
strengthened verification of scheduler behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 16:01:02 -07:00
gsxdsm
1e50b71255 fix(engine): reap leaked fn-verify verification worktrees in the temp-dir sweep
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 15:14:30 -07:00
gsxdsm
623581837a fix(engine): mock provider sends 0-based steps — test mode full-task runs complete again (#3231)
Found by a live browser E2E of the coding workflow in test mode: every
scripted full-task run failed at `steps#0:step-execute` with `Step 4 out
of range (task has 4 steps)`, rebounding through recovery forever.

**Root cause:** `fn_task_update.step` has been **0-based since FN-6607**
(executor.ts FNXC:StepNumbering — the old `step - 1` conversion made
Step 0 impossible to mark). `mock-provider.ts` still sent `index + 1`,
so test mode marked steps 1..N instead of 0..N-1: Step 0 (Preflight)
never completed and step N threw out-of-range. Test mode's full-task
path has been broken since June.

**Also fixes the test that pinned the bug:** `mock-provider.test.ts`
expected `{ step: 1 }` for a fixture whose first unfinished step is
index 0 — the expectation encoded the 1-based off-by-one.

Verified: 12/12 mock-provider tests; the live E2E instance completes the
task after this patch (see follow-up screenshot in the session).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 12:54:58 -07:00
gsxdsm
3f06d7201b test(engine): assert the evaluator's exact archived-lane set, not toContain (#3232)
## What

Follow-up to the #3224 review comment *"reject legacy archived
identifiers for renamed workflows."* Test-only.

That comment had two halves. **The half it got wrong** is already
answered on main: asserting an exact single-column set `["vaulted"]`
*fails*, because `resolveProjectColumnsForRoles` unions
`LEGACY_COLUMN_IDS_BY_ROLE` in as a documented floor — so a row whose
workflow cannot be resolved still classifies. Pinning `["vaulted"]`
would encode the opposite of the design.

**The half it got right was never addressed.** `toContain` also passes
when the set grows a lane nobody intended, and an over-broad archived
set silently classifies *live* rows as archived. So the reviewer's worry
was legitimate even though the proposed fix was not.

The exact set is assertable — it just is not the one the review
proposed:

| board | resolved set |
|---|---|
| default | `["archived"]` |
| renamed | `["archived", "vaulted"]` — legacy floor + the board's own
lane |

`[...].sort()` in the helper makes ordering stable, so these pin the
resolver's whole answer rather than a substring of it.

## Proven to catch what `toContain` missed

Giving the fixture a second `archived`-trait column fails both new
assertions:

```
AssertionError: expected [ 'archived', 'cold-store' ] to deeply equal [ 'archived' ]
AssertionError: expected [ 'archived', 'cold-store', 'vaulted' ] to deeply equal [ 'archived', 'vaulted' ]
Tests  2 failed | 1 passed (3)
```

The previous `toContain` assertions pass unchanged against that same
spurious lane. That is the whole justification for this PR — without the
injection test it would be a stylistic preference.

```
clean:            Tests 3 passed (3)
spurious lane:    Tests 2 failed | 1 passed (3)
lint clean
```

## Note

I said in the review thread I would tighten this, so this closes that
loop. I also checked before editing whether another worker had already
done it — main already carries the FNXC tag and the legacy-floor
explanation from the same review round, so this PR adds only the part
still missing rather than redoing settled work.
2026-07-31 12:47:08 -07:00
gsxdsm
d4c25384ae test(engine): record what the zero-backlog early return stops testing (#3228)
## A green case that stopped testing what its name says

#3226 fixed a red `main` correctly: `fileWithGuards()` now returns
`null` at zero backlog, and with nothing to inflate there is no rise to
manufacture. Asserting `totals.column === 0` and returning is the honest
response.

What went unrecorded is the cost. At zero, these two cases:

- *"exits 0 and REWRITES the baseline under `--update-baseline`, even
when the count rose"*
- *"exits 1 and LEAVES the baseline alone on a rise without
`--update-baseline`"*

no longer exercise the CLI's ordering or exit codes. They assert the
backlog is empty and return. **If the write-before-exit ordering
regressed — the exact bug those cases were written for — both would
still pass.**

That matters more than it would elsewhere, because **zero is not a state
to wait out.** It is this program's terminal state: the backlog went 126
→ 0 and is meant to stay there. So the vacuity is permanent, not
transitional.

This file already legislates against precisely this, two hundred lines
down:

> `/* Anti-vacuity: an empty exclusion list would make the assertion
below trivially true. */`

## What this PR does

Adds a comment on `fileWithGuards()` recording (a) which cases go
vacuous at zero and why, (b) that zero is terminal so it will not
resolve itself, and (c) the durable fix.

**Comment only. No behaviour change — suite stays 53/53.**

## The durable fix, recorded rather than done

Point the scan at a synthetic tree so the fixture stops being a function
of the real backlog — the same seam `FUSION_CENSUS_BASELINE_PATH`
already provides for the baseline, applied to the file list.

It needs one CLI correction to work, and that is a genuine bug in my own
code regardless of this suite: `triageFindings` and the sync-resolver
check read files via `join(REPO_ROOT, f.file)`, where `REPO_ROOT` is
derived from the **script's** location. An overridden file list
therefore changes which paths are *listed* without changing where they
are *read from*, and every read misses with `ENOENT`.

I verified that approach works (a three-file fixture yields a stable `1
backlog / 1 deliberate / 1 sync-resolved`) and got two of the six
failing cases green with it, then stopped rather than keep guessing in a
file being actively revised. Left as a comment so whoever takes it does
not re-derive the diagnosis.

## Why this is worth a PR at all

This program's recurring failure is instruments that report green while
measuring nothing — an inert conversion the census scored as a win, a
ratchet wired to nothing, a gate that could not fail. A test asserting
`0 === 0` under a name promising ordering coverage is the same shape at
the test layer. The suite cannot be fixed in this PR without re-opening
work someone else owns, but it can at least stop being silent about it.

## Census before / after

```
before:  COLUMN guards (the backlog):   0
after:   COLUMN guards (the backlog):   0
```

## Verification

`test:gate` exit 0 · `lifecycle-column-census.test.ts` **53 passed** ·
`fnxc-future-dates`, `lifecycle-columns`, `inert-sync-lanes`,
`quarantine-ledger`, `inert-flag-seams`, `lane-wiring`,
`sql-column-literals` — all exit 0.
2026-07-31 12:28:09 -07:00
gsxdsm
12c4ab5a6e test(engine): pin the evaluator's archived-lane read — the service had no test at all (#3224)
## What

Pins the **evaluator's archived-lane read**. Test-only — no product
change.

`HybridEvaluatorService.evaluateTask` resolves the board's archived
lanes and hands them to `collectDeterministicSignals`, which decides
which of a task's related rows count as archived when scoring a run.

**The service had no test anywhere in the repo.** Four test files import
the module; none construct or exercise it. So this conversion was
unobservable for the simplest possible reason — *nothing ran the code*.
That is a different failure from the ones this audit has been finding
(harnesses that run the code but cannot see the difference), and worth
distinguishing: no amount of fixture care helps when the entry point is
never called.

## Measured

| | default (control) | renamed | differential |
|---|---|---|---|
| converted | pass | pass | pass |
| blinded to `["archived"]` | pass | **FAIL** | **FAIL** |

```
converted: Test Files 1 passed (1) / Tests 3 passed (3)
blinded:   Test Files 1 failed (1) / Tests 2 failed | 1 passed (3)
the 4 files importing evaluator.ts, plus this one: 5 files/76 tests, all green
lint clean; fnxc-future-dates: none added; census unchanged
```

Per the rule I documented in #3223, the blind was confirmed applied with
`git diff --stat` **before** the run rather than trusting the tool's
exit code.

## What breaks without it

On a board whose archived lane is `vaulted`, the evaluator hands the
collector the legacy `{archived}` set. Rows resting in `vaulted` are not
recognised as archived, and the deterministic half of every evaluation
score is computed from a wrong picture of the task's history. **Nothing
errors, the run completes, the number is just wrong** — which is why it
survived unnoticed.

## Pinned without faking a provider response

The assertion is about what the collector *receives*, which is decided
before any model call. `collectDeterministicSignals` is mocked to record
its arguments and throw a sentinel; the test asserts the resolved lane
set and stops.

This is deliberate over the obvious alternative of feeding `runPrompt` a
canned AI payload: `deps.runPrompt` is injectable so either approach is
offline, but a canned payload has to satisfy `parseAiResponse` and every
`EVAL_SCORE_CATEGORIES` entry, and would silently rot into a maintenance
burden on a test whose subject is one `Set`. Reversible if someone later
wants full end-to-end evaluator coverage — that is a different test, not
this one.

## Completes the engine audit

With this, every `resolveProjectColumnsForRoles` call site in
`packages/engine` has been blinded:

| file | resolvers | result |
|---|---|---|
| `self-healing.ts` | 64 | 21 pinned, 1 recorded inert by construction,
remainder mapped |
| `executor.ts` | 2 | both already covered |
| `scheduler.ts` | 1 | uncovered → pinned (#3219, merged) |
| `triage.ts` | 1 | uncovered → pinned (#3221) |
| `restart-recovery-coordinator.ts` | 1 | already covered |
| `notification-service.ts` | 1 | already covered |
| `evaluator.ts` | 1 | uncovered → pinned (this PR) |

`project-engine.ts:5154` takes `roles` as a **parameter**, so it is a
generic wrapper with no fixed role set to blind — flagged rather than
guessed at; its callers are where the question belongs.

**`packages/core`'s 17 files remain entirely unaudited** and I am
claiming nothing about them.


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

* **Tests**
* Added regression coverage to verify reliable resolution of archived
workflow lanes.
* Covered both the default archived-lane name and custom renamed
configurations.
  * Confirmed compatibility with legacy archived-lane naming behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:20:08 -07:00