Fixes silent runtime fallback visibility (dashboard never read wasConfigured
or session:runtime-resolved) and threads the real FallbackReason
(not_found vs factory_error) through resolveRuntime()/logRuntimeFallback
instead of hardcoding "not_found" for every fallback.
- packages/engine/src/runtime-resolution.ts: resolvePluginRuntime() now
returns a tagged miss result distinguishing not_found from factory_error;
resolveRuntime() threads the real reason through and returns it as
ResolvedRuntime.fallbackReason
- packages/engine/src/agent-session-helpers.ts: includes fallbackReason in
the session:runtime-resolved audit event metadata
- packages/dashboard/src/routes/register-task-workflow-routes.ts: new
GET /api/tasks/:id/runtime-fallback endpoint
- packages/dashboard/app/hooks/useRuntimeFallbackStatus.ts +
packages/dashboard/app/components/RuntimeFallbackBadge.tsx: new polling
hook + badge/toast component wired into TaskCard, ActiveAgentsPanel, and
AgentsView
Ref: Fusion task FUX-022, investigations/FUX-017-hermes-runtime-fallback.md
recommendation #1
Add a path-containment check to fn_task_attach so it can no longer read files outside the task's worktree via traversal or absolute paths.
- Resolve the requested path and confine it to ctx.cwd (the task worktree) before any readFile call, rejecting "../" traversal, absolute paths, and other boundary-escaping inputs
- Add regression tests in extension.test.ts covering traversal/absolute-path attack vectors
- Add changeset (patch, category: security) documenting the fix
Files changed:
.changeset/fn-7619-attach-boundary.md | 7 ++
packages/cli/src/__tests__/extension.test.ts | 133 ++++++++++++++++++++++++++-
packages/cli/src/extension.ts | 23 ++++-
3 files changed, 161 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7619
Fusion-Task-Lineage: d35d9218-d678-4989-945d-6c1e1a322c5c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Executors could previously treat a pending approval as a normal turn end and go hunt for ungated workarounds instead of stopping. This change makes wait-for-approval a hard suspend point.
- wait-for-approval now suspends the in-flight executor session via awaitAbortInFlightTaskWork
- Dedupe identical pending approvals so repeated waits don't pile up
- Executor prompts now carve out awaiting-approval as a legitimate turn end (agent-prompts.ts)
- Extend provisioning-gate and agent-action-gate coverage for the new suspend/carveout behavior
- Add changeset (patch) documenting the fix for release notes
- Update docs/agents.md and docs/architecture.md to describe the new blocking behavior
Files changed:
.changeset/fn-7608-awaiting-approval-blocking.md | 7 ++
docs/agents.md | 1 +
docs/architecture.md | 1 +
packages/core/src/agent-prompts.ts | 5 +
.../engine/src/__tests__/agent-action-gate.test.ts | 82 +++++++++++++
.../executor-approval-gate-suspend.test.ts | 128 +++++++++++++++++++++
.../executor-approval-prompt-carveout.test.ts | 61 ++++++++++
packages/engine/src/agent-heartbeat.ts | 13 +++
packages/engine/src/executor.ts | 28 +++++
packages/engine/src/pi.ts | 22 +++-
.../sandbox/__tests__/provisioning-gate.test.ts | 29 +++++
packages/engine/src/sandbox/provisioning-gate.ts | 11 ++
12 files changed, 384 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7608
Fusion-Task-Lineage: 9e42d8ee-bda7-4ef1-b159-46c2100bbc48
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Task creation surfaces stopped hardcoding column:"triage", so new tasks now land in the selected-or-default workflow's resolved intake column instead of always jumping to Planning/triage.
- Removed hardcoded column:"triage" override in engine's createTaskCreateTool (fn_task_create), letting TaskStore.createTask resolve the landing column from the workflow's intake-trait column.
- Removed the equivalent hardcoded override in the pi extension's fn_task_create, and updated its response text to echo the actual landing column instead of a fixed "Column: triage" string.
- Fixed signal-route, GitHub-import, and planning-subtask-route task creation to stop forcing column when no workflowId is given (or, for planning subtask routes, even when one is provided).
- Custom workflows with a non-triage intake column (e.g. Inbox) now correctly capture new cards inert until released, while the default builtin:coding workflow still resolves to "triage" byte-identically.
- Added regression coverage (agent-tools-intake-column.test.ts, extension-workflow-tools.test.ts) and a patch changeset documenting the fix.
Files changed:
.changeset/fn-7611-intake-column.md | 7 ++
.../src/__tests__/extension-workflow-tools.test.ts | 70 +++++++++++
packages/cli/src/extension.ts | 10 +-
.../src/__tests__/register-signal-routes.test.ts | 8 +-
.../dashboard/src/__tests__/routes-github.test.ts | 2 -
.../dashboard/src/routes/register-git-github.ts | 16 ++-
.../src/routes/register-planning-subtask-routes.ts | 24 +++-
.../dashboard/src/routes/register-signal-routes.ts | 8 +-
.../__tests__/agent-tools-intake-column.test.ts | 138 +++++++++++++++++++++
packages/engine/src/__tests__/agent-tools.test.ts | 1 -
packages/engine/src/agent-tools.ts | 21 +++-
11 files changed, 288 insertions(+), 17 deletions(-)
Fusion-Task-Id: FN-7611
Fusion-Task-Lineage: daf7f755-b1c7-4859-b74f-f15593d5e79e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Replace the fixed generic "Would you like to go deeper?" theme buckets with AI-proposed, plan-specific topics, falling back to the existing regex-derived themes when the AI supplies none.
- Add optional PlanningSummary.deepeningThemes (id/label/description) to the completion payload contract in @fusion/core.
- Instruct the planning AI prompt to propose 2-5 concrete, plan-aligned deepening themes tied to the plan's actual title/description/deliverables.
- Add normalizeDeepeningThemes to validate/sanitize untrusted AI-supplied theme data (dedupe, cap at 6, trim, drop malformed entries) and omit the field entirely when nothing valid remains.
- Update buildDeepeningCheckpointOptions to prefer AI-supplied deepeningThemes over the generic CHECKPOINT_THEME_CANDIDATES regex fallback, keeping the reserved 'Proceed to final plan' option first and deterministic in both branches.
- Update docs/dashboard-guide.md to describe the new plan-specific deepening behavior and its generic fallback.
- Add unit test coverage for the new normalization and checkpoint-option-building logic.
- Add a minor changeset for @runfusion/fusion.
Files changed:
.changeset/fn-7616-planning-deepening-themes.md | 7 +
docs/dashboard-guide.md | 3 +-
packages/core/src/types.ts | 11 ++
.../planning-interview-formatters.test.ts | 176 +++++++++++++++++++++
.../src/__tests__/routes-planning.test.ts | 61 +++++++
packages/dashboard/src/planning.ts | 86 +++++++++-
6 files changed, 341 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7616
Fusion-Task-Lineage: f6c1d0d7-f0ca-45c9-85cd-d57958ad94c7
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the Task Detail Oversight dropdown trigger rendering shorter than the Priority and Execution-mode controls on non-mobile viewports.
- Made `.detail-oversight-menu-dropdown` an `inline-flex` with `align-items: stretch` so the popover-positioning wrapper participates in `.detail-meta-inline-controls`'s stretch behavior instead of only sizing to its own content.
- Added `align-self: stretch` to `.detail-oversight-menu-trigger` so it fills the now-stretched wrapper, matching Priority/Execution-mode's direct-child stretch.
- Added a regression test asserting the wrapper/trigger stretch declarations exist, apply at every viewport, and don't leak into the absolutely-positioned popover.
- Added a patch changeset documenting the fix.
Files changed:
.changeset/fn-7618-oversight-trigger-height.md | 7 ++++
.../dashboard/app/components/TaskDetailModal.css | 26 ++++++++++++
...etailModal.responsive-and-dependencies.test.tsx | 48 ++++++++++++++++++++++
3 files changed, 81 insertions(+)
Fusion-Task-Id: FN-7618
Fusion-Task-Lineage: f918c2aa-5677-446c-a7dc-17c72adb9c27
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fix Planning Mode's Back button incorrectly rendering the AI generation/loading view instead of returning directly to the previous question.
- handleBack no longer sets view to "loading" during the deterministic rewindPlanningSession call; that view is reserved for real model-generation turns.
- Added isBackPending state to drive a lightweight inline pending indicator on the Back button (spinner icon, disabled Back/Continue) while the rewind request is in flight, keeping the QuestionForm mounted throughout.
- On success, the rewound history/question is applied the same as before; on failure, the error message is surfaced while remaining on the question view (never loading).
- Added a changeset for the fix.
- Expanded PlanningModeModal.planning-flow.test.tsx coverage for the Back button's success/error/pending behavior.
Files changed:
.changeset/FN-7615-planning-back-no-generation.md | 7 +
.../dashboard/app/components/PlanningModeModal.tsx | 34 +++-
.../PlanningModeModal.planning-flow.test.tsx | 182 ++++++++++++++++++++-
3 files changed, 217 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7615
Fusion-Task-Lineage: 27e9c541-ce2f-42de-b83a-777af1e56858
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds bounded internal retry to the desktop embedded-start path so a transient first-attempt failure self-heals before the operator ever sees the "Couldn't start local Fusion" error screen.
- LocalRuntimeManager.startEmbedded() now retries startEmbeddedAttempt() up to startupRetries (default 3) total attempts with a startupRetryDelayMs (default 150ms) delay between attempts, both overridable via constructor options for deterministic zero-delay tests.
- status.state stays "starting" across retried attempts; only the final attempt's real error sets state "error" and is thrown/surfaced, so genuine failures still report their real message unchanged.
- Only affects the embedded-start path (never external-cli or already-running paths).
- Adds regression tests covering retry-then-success, exhausted-retries-surfaces-final-error, and status transitions across attempts.
Files changed:
.../desktop/src/__tests__/local-runtime.test.ts | 158 ++++++++++++++++++++-
packages/desktop/src/local-runtime.ts | 76 +++++++++-
2 files changed, 228 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7617
Fusion-Task-Lineage: 2baf90f0-052c-42ff-a5b1-bc0b802d0ddc
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Introduces a single canonical step-number helper so the task-dialog step indicators agree with the Activity tab for the same underlying step.
- Add `getCanonicalStepNumber()` in `packages/dashboard/app/lib/step-display.ts`, returning the raw 0-based, PROMPT.md-numbered step index (Step 0 = Preflight), clamped to a valid range.
- Update `ActiveAgentsPanel` to derive its step/total-steps display from the new helper instead of adding its own +1 to `task.currentStep`.
- Update `TaskTokenStatsPanel`'s step-progress row to use the same canonical helper instead of its own +1 math.
- Add regression tests (`step-number-alignment.test.tsx`) asserting the task-dialog and Activity-tab step numbers stay in sync across surfaces.
Files changed:
.../dashboard/app/components/ActiveAgentsPanel.tsx | 8 +-
.../app/components/TaskTokenStatsPanel.tsx | 7 +-
.../__tests__/step-number-alignment.test.tsx | 162 +++++++++++++++++++++
packages/dashboard/app/lib/step-display.ts | 57 ++++++++
4 files changed, 229 insertions(+), 5 deletions(-)
Fusion-Task-Id: FN-7612
Fusion-Task-Lineage: a33703f5-32cb-4d10-9153-399821517ea3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes workspace-mode (workspaceWorktrees) tasks failing auto-merge under mergeStrategy=pull-request, where processPullRequestMergeTask threw "could not determine repository" because the workspace root is a container of independent git sub-repos, not itself a git repo.
- Hoist an isWorkspaceTask check in ProjectEngine's merge dispatch (project-engine.ts) before the mergeStrategy branch, so workspace tasks always fall through to the existing direct/landWorkspaceTask path regardless of configured mergeStrategy.
- Add processPullRequestMergeTask and syncGroupPrCallback defense-in-depth guards (task-lifecycle.ts) that throw the new named WorkspaceTaskMergeError if a workspace task ever reaches the PR-merge path.
- Add engine tests covering multi-repo, single-repo, and zero-commit no-op workspace tasks under mergeStrategy=pull-request, plus a non-regression test for the legacy single-worktree PR path.
- Add CLI tests asserting the new guards throw WorkspaceTaskMergeError.
- Add a patch changeset describing the fix.
Files changed:
.changeset/fn-7610-workspace-pr-merge-routing.md | 7 ++
.../src/commands/__tests__/task-lifecycle.test.ts | 56 +++++++++
packages/cli/src/commands/task-lifecycle.ts | 33 ++++-
.../engine/src/__tests__/project-engine.test.ts | 140 +++++++++++++++++++++
packages/engine/src/project-engine.ts | 18 ++-
5 files changed, 252 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7610
Fusion-Task-Lineage: 31768b77-d9a9-4a79-a055-bbc6b228a1c4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Two engine test files fail on current `main` (17c4007). Both are stale
**test-mock** breakages — no production code is touched.
### 1. `restart.integration.test.ts`
Its `vi.mock("../pi.js", …)` factory replaces the module wholesale but
omits `ModelFallbackExhaustedError`. `triage.ts` guards its catch block
with `err instanceof ModelFallbackExhaustedError` (imported from
`pi.js`), so evaluating that guard throws *"No
ModelFallbackExhaustedError export is defined on the mock"*.
Fix: export a plain `Error`-subclass stub from the factory. No restart
test enters the fallback-exhausted branch, so `instanceof` simply
returns `false` — a faithful stub.
### 2. `reliability-interactions/mission-validation-trigger-gap.test.ts`
Two recovery-path `missionStore` mocks omit `getMission`. #1910's
mission-active gate now walks `getSlice → getMilestone → getMission`
inside `resolveFeatureMission`. The resulting throw is swallowed by
`processTaskOutcome`'s `catch`, aborting recovery before it can ensure
assertions / start the validator run — surfacing as
`ensureFeatureAssertionLinked` asserted called-once but seen 0 times.
Fix: add `getMission` returning an active mission to both mocks.
### Verification
```
npx vitest run src/__tests__/reliability-interactions/mission-validation-trigger-gap.test.ts src/__tests__/restart.integration.test.ts
Test Files 2 passed (2)
Tests 54 passed (54)
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Improved coverage for mission recovery and restart flows, making
validation scenarios more reliable.
* Fixed test mocks so recovery and triage paths can run without
unexpected errors during assertions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
The mission validator runs its read-only judge session with `cwd:
this.rootDir` — the engine's **main working copy**. When a task's merge
landed on the remote (or in another worktree) and `rootDir` was never
fetched/reset to that commit, the judge reads **pre-merge files** and
returns a spurious `fail`.
#1917's premerge column guard does not catch this case: by the time
validation runs the task column is already `done`, so execution falls
through to `handleValidationFail` and mints a **bogus Fix Feature** for
code that is actually correct and merged.
## Fix
A symmetric second guard in the `fail` branch, placed **after** the
#1917 premerge column check:
- `isValidationWorkspaceStale(feature)` resolves the task's integration
SHA and runs `git merge-base --is-ancestor <sha> HEAD` in `rootDir`.
- **Only affirmative staleness evidence defers.** `--is-ancestor` exit
`1` (the SHA is *not* an ancestor of HEAD → the workspace predates the
merge) → defer the fail to **inconclusive**, so a later validation
judges the merged code.
- Every other outcome trusts the fail: exit `0` (ancestor → workspace is
fresh), no integration SHA available, or a bad/unknown object (exit
`128`).
Fail-open doctrine, matching #1917: a guard may only ever **defer** a
fail, never **suppress** one on missing or unreadable data.
## Tests
Four real-git cases in `mission-execution-loop.test.ts` (skipped when
`git` is unavailable):
1. Judged checkout predates the merged commit → fail deferred to
inconclusive, no Fix Feature minted, emits `validation:inconclusive`.
2. Merged commit is an ancestor of HEAD (fresh workspace) → normal fail
path, Fix Feature minted, emits `validation:failed`.
3. Task carries no integration SHA → fail open (normal fail).
4. Integration SHA is an unknown object (exit 128) → fail open (normal
fail).
Verified with stash-red/restore-green discipline: with the production
guard stashed, case (1) goes red while the three fail-open guardrails
stay green — proving case (1) exercises the fix. Full file: 62 passed.
`tsc --noEmit`: clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Validation failures are now deferred when the workspace appears to be
out of date with merged changes, reducing incorrect failure reports.
* Tasks with linked work continue to use the usual failure path when the
current workspace is up to date.
* Staleness checks now avoid masking real validation failures when no
merge reference is available or when the reference can’t be verified.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Supersedes stale generated fix features when a mission feature is
repaired by a fresh generated fix.
- Records supersede events in mission store state and prevents stale
generated fixes from staying active.
- Adds mission-store and mission-execution-loop coverage for stale
generated fix supersession.
## Test Plan
- corepack pnpm --filter @fusion/core vitest
packages/core/src/__tests__/mission-store.test.ts
- corepack pnpm --filter @fusion/engine vitest
packages/engine/src/__tests__/mission-execution-loop.test.ts
- corepack pnpm build
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Prevented stale generated fix chains from incorrectly remaining active
after their source feature has passed validation, ensuring generated fix
features and linked tasks are correctly finalized.
* Improved active mission recovery by reconciling superseded generated
fix features per slice and skipping them during recovery iteration.
* **New Features**
* Added slice-level reconciliation for superseded generated fix
features, automatically completing them when a passed ancestor is
detected.
* **Tests**
* Added/extended unit tests covering validator-driven reconciliation and
recovery behavior for stale generated fixes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The mission validator runs its read-only judge session with cwd:
this.rootDir — the engine's main working copy. When a task's merge landed
on the remote or in another worktree and rootDir was never fetched/reset to
it, the judge reads PRE-merge files and returns a spurious `fail`. #1917's
premerge column guard doesn't catch this: the task column is already `done`,
so it falls through to handleValidationFail and mints a bogus Fix Feature.
Add a symmetric second guard in the fail branch, after the premerge column
check: isValidationWorkspaceStale resolves the task's integration SHA and
runs `git merge-base --is-ancestor <sha> HEAD` in rootDir. Only affirmative
staleness evidence (exit 1 = NOT an ancestor) defers the fail to
inconclusive; exit 0 (ancestor/fresh), a missing SHA, or a bad object
(exit 128) all trust the fail. Fail-open: a guard may DEFER a fail, never
SUPPRESS one on missing or unreadable data.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
When a coding agent produces **zero commits** relative to base, the
AI-merge path wedges the card terminally:
1. `runAiMerge` → `landOneRepo` builds a clean-room worktree and runs a
dependency install.
2. On a non-workspace land the dep-install step throws hard (`if
(!ctx.nonFatalDependencySync) throw depsErr;`).
3. The throw is transient-classified and retried up to
`MAX_AUTO_MERGE_TRANSIENT_RETRIES` → `Auto-merge transient retries
exhausted (3/3)`.
4. The card is parked `failed` (→ archived), even though the correct
outcome for an empty branch is a no-op finalize.
The truly-empty branch *would* reach `outcome: "empty"` anyway via
`mergeAndReview` producing no `squashSha` — but only **after** the
throw-prone churn that fails first.
The canonical `aiMergeTask`/`classifyOwnedLandedEvidence` path already
has an early empty-own-diff fast-path; the `runAiMerge` → `landOneRepo`
path did not.
## Fix
Short-circuit inside `landOneRepo`, right after the `tipSha` computation
and **before** the clean-room build, when the branch is a confident zero
commits ahead of the integration tip:
```ts
const aheadRaw = await git(["rev-list", "--count", `${integrationBranch}..${branch}`], repoRootDir).catch(() => "");
if (Number.parseInt(aheadRaw.trim(), 10) === 0) {
await audit.git({ type: "merge:ai-empty", target: integrationBranch, metadata: { taskId, tipSha } });
return { outcome: "empty", tipSha, integrationBranch };
}
```
- Returns the **identical** `{ outcome: "empty", tipSha,
integrationBranch }` shape and the same `merge:ai-empty` audit event the
downstream already handles, so `runAiMerge`'s empty-outcome handling
(block-to-todo / no-op finalize) is unchanged.
- **Only** short-circuits on a confident `0`: a git failure yields `""`
→ `parseInt` → `NaN` (≠ 0) and falls through to the normal path — no
behavior change on error.
- Placed in `landOneRepo` (not `runAiMerge`) because it is shared by
both the single-repo and workspace per-repo callers.
## Test
New test in `merger-ai.test.ts` asserts the merge agent is **never
invoked** for a 0-ahead branch, the result is a no-op, `main` is
unmoved, and the card moves to `done` with `preserveProgress`. Without
the fix the branch reaches the clean room and `mergeAndReview` invokes
the merge agent, so the assertion fails — it genuinely guards the
short-circuit.
Full engine merge-suite regression run is green (merger-ai,
workspace-merger and lease variants, cleanup, dependency-sync,
group-merge, classify-owned-landed-evidence).
🤖 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 merge handling for branches with no new commits ahead of the
target branch.
* Empty or already-synced branches now finish as a no-op instead of
triggering merge work.
* Tasks in this scenario still move to done, while the main branch
remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
`MissionExecutionLoop.runFeatureValidation` treats a validator "fail"
verdict as authoritative regardless of whether the linked task's code
has actually landed. When validation fires while the task is still
mid-pipeline — an in-review PR, an external merge train, a deferred base
sync — the validator judges a checkout that predates the merge,
concludes the feature "is not present", and `handleValidationFail` mints
a Fix feature for work that is already done.
We hit this in production (2026-07-05): a recovery-path validation ran
against four features whose implementing tasks were in-review in an
external merge pipeline. All four "failed" → four duplicate Fix tasks
were created one minute after the real work merged. Worse, the Fix
tasks' planned file scopes included hot shared files, so their
file-scope leases serialized the entire board until they were manually
archived.
## Fix
Before dispatching a `fail` verdict, resolve the linked task's column.
If it affirmatively shows the task has **not** completed (any column
other than `done`/`archived`), route the outcome to
`handleValidationInconclusive` (R21 — completes the run as `blocked`,
logs `verification_inconclusive`, notifies autopilot, **spawns no Fix
feature**) with a "code not merged yet — validation deferred" reason. A
later validation (post-merge recovery pass) judges the real merged code.
**Fails open by design** — the guard may only ever *defer* a fail, never
suppress one on missing data. Missing `taskId`, missing task, unreadable
store, or unknown column all fall through to the normal
`handleValidationFail` path:
```ts
private async getPremergeTaskColumn(taskId: string | undefined): Promise<string | null> {
if (!taskId) return null;
const linkedTask = await this.taskStore.getTask(taskId).catch(() => null);
const column = linkedTask?.column;
if (!column || column === "done" || column === "archived") return null;
return column;
}
```
The vanilla flow is unaffected: the scheduler triggers validation on
`toColumn === "done"`, so by the time a normally-triggered validation
runs the task is already `done` and the guard is a no-op. Only
recovery-path / re-validation runs that race an unmerged task are
deferred.
## Tests
Three new tests in `mission-execution-loop.test.ts` (`premerge guard`
describe):
1. fail verdict + linked task `in-review` → routes to inconclusive: no
Fix feature, run completed as `blocked`, `validation:inconclusive`
emitted (not `validation:failed`), `verification_inconclusive` mission
event logged
2. fail verdict + linked task `done` → normal fail path: Fix feature
created, `validation:failed` emitted
3. fail verdict + `taskStore.getTask` rejects → fails open to the normal
fail path
`npx vitest run src/__tests__/mission-execution-loop.test.ts`: 58/58
green. `npx tsc --noEmit`: clean. Full engine suite: the 46 failures
across 24 files present on my branch fail **identically on clean
`17c4007`** (verified by re-running the same files on a detached
checkout of upstream main) — all pre-existing/environment-dependent,
none related to this change.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Validation failures now account for whether the linked task is
actually merged. If the task is still in progress, the result is marked
as inconclusive instead of creating a fix flow.
* Added clearer handling when task details can’t be read, so normal
failure behavior still applies.
* Improved validation status reporting and event logging for merged vs.
unmerged task states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
A long-running agent session holds its OAuth access token in memory.
When the token rotates mid-run (Claude Max access tokens have an ~8 h
lifetime), the next API call fails with `401
{"type":"error","error":{"type":"authentication_error","message":"Invalid
authentication credentials"}}`. `withRateLimitRetry` only retries
usage-limit errors and re-throws everything else immediately, so the
task is marked **failed** and the operator is alerted — even though the
credentials file has already been refreshed and the very next call would
succeed.
We run Fusion continuously on a server against a Claude Max subscription
and see this at essentially every ~8 h token boundary: whatever task or
heartbeat happens to be in flight at rotation time fails with a spurious
401, then self-heals on retry (in one case the "failed" task had
actually already completed and merged 11 minutes later). The engine's
`notification/oauth-*` modules added in 0.55 alert on upcoming expiry,
but nothing retries the in-flight call itself.
## Fix
Extend `withRateLimitRetry` with a transient-auth branch:
- `isTransientAuthError` matches `"type": "authentication_error"`,
`invalid authentication credentials`, `token_expired` / `token expired`,
and OAuth-scope errors.
- Auth errors get their **own small budget**: 2 retries at a flat ~5 s
delay (±10 % jitter). Credential refresh completes within seconds, so
the rate-limit backoff curve (30 s → 2 min) would just prolong the
outage.
- Auth retries decrement the loop counter, so they never consume
rate-limit attempts; the existing usage-limit path is unchanged.
- Genuinely bad credentials still propagate after ~10 s (initial + 2
quick retries), so real auth failures are not masked.
- Abort-signal handling matches the existing path (no sleep when already
aborted).
## Testing
- 4 new tests in `rate-limit-retry.test.ts`: retry-then-succeed on
rotation, budget exhaustion (initial + 2), auth retries not consuming
rate-limit attempts (`maxRetries: 1` + auth error + 429 still succeeds),
and pattern classification.
- `vitest run src/__tests__/rate-limit-retry.test.ts` — 15/15 pass; `tsc
--noEmit` clean.
- We have been running this change (as a patch on the distributed
bundle) in production since 2026-06-29 across 0.50 → 0.54 → 0.55; the ~8
h spurious-failure alerts stopped while real failures still surface.
🤖 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 retry behavior for authentication-related failures, including
credential rotation and expired/invalid tokens.
* Transient authentication errors now use a short, consistent delay with
a dedicated retry budget, without impacting existing rate-limit backoff
behavior.
* OAuth scope/permission failures are excluded and now surface
immediately for re-authorization rather than being retried.
* **Tests**
* Added coverage for transient-auth retry timing, budget exhaustion, and
abort-signal cancellation.
* **Documentation**
* Updated release notes to reflect the revised OAuth token-rotation
retry behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
A transient error at merge time can flag an in-review card `failed` even
when its PR actually squash-merged on the remote (human merge,
merge-train, etc.). `recoverAlreadyMergedReviewTasks` runs the
already-merged **evidence detector** only against the **local** base
ref. If this process never fetched the merge, the owned commit is absent
locally → the detector returns `null` → `landed` is null → the card
never finalizes and **holds its file-scope lease forever**, wedging
every other task that touches the same files.
## Fix — fetch-then-prove
When a `failed` in-review candidate has a recorded PR
(`getPrimaryPrInfo`) and the local base yields no owned commit:
1. best-effort `git fetch origin <base>` (new `refreshRemoteBaseRef`
helper), then
2. re-run the **same** evidence detector against `origin/<base>`.
The detector's owned-commit proof and every foreign-ownership guard
inside it remain the **sole** finalize gate, so this only un-wedges a
genuinely-merged task — it never phantom-finalizes on unproven state.
**Safety:**
- Gated on a recorded PR — no PR ⇒ nothing could have merged remotely ⇒
no fetch.
- Fail-closed — a fetch error (offline / auth / no remote) is swallowed;
if `origin/<base>` can't be resolved the card is left untouched.
- No new dependency, no github client seam — direct git only.
## Tests
Two real-git tests in `self-healing-already-merged.real-git.test.ts`,
both verified to **fail without the prod change**:
- **fetch-then-prove positive:** a PR squash-merges on a bare remote
while the local base stays stale → recovery fetches, proves the owned
SHA against `origin/main`, and finalizes the card to `done`
(`mergeConfirmed: true`, worktree removed).
- **phantom-finalize guard:** remote base advances with a commit owned
by a *different* task → the fetch still runs, but the detector proves
nothing → the card is left `failed`/`in-review`, never healed.
Full self-heal suite: 594 passed. Engine typecheck clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved recovery for review tasks that appear already merged when the
local base branch is stale.
* The app now refreshes the remote base branch before re-checking merge
status, helping finalize tasks correctly and clean up completed
worktrees.
* Added coverage for cases where the remote base has moved forward with
either the merged commit or unrelated changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Use the project-scoped plugin runner for project chat routes so
runtime hints resolve plugin runtimes correctly.
- Expose the runtime plugin runner through
ProjectEngine/InProcessRuntime.
- Preserve Hermes runtime session message state and accept session_id
emitted on stderr without mixing stderr into the assistant body.
## Test Plan
- corepack pnpm --filter @fusion-plugin-examples/hermes-runtime test
- corepack pnpm --filter @fusion-plugin-examples/hermes-runtime
typecheck
- corepack pnpm --filter @fusion/engine typecheck
- corepack pnpm --filter @fusion/dashboard typecheck
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved project-scoped chat routing so plugins resolve more
consistently with the engine’s runtime.
* Hermes chat sessions now refresh cached plugin-runner usage when
needed and preserve conversation history (user and assistant messages).
* **Bug Fixes**
* More robust Hermes output parsing: session IDs are extracted reliably
even when emitted on stderr, and stderr is no longer treated as
assistant text.
* Hermes session state now retains an error message when the CLI fails,
improving troubleshooting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Fixes TaskDetailModal so manual PR affordances stay visible based on the live global auto-merge setting rather than the per-task effective override, and repairs a pre-existing test regression from the FN-7510 oversight default change.
- isManualPrFlow now checks mergeStrategy === "pull-request" && !autoMergeEnabled (live global setting) instead of the per-task effective auto-merge override, fixing a regression from FN-7255 that stranded users without manual PR controls when a task's auto-merge override was true but global auto-merge was off.
- Pinned plannerOversightLevel: "off" on the Chat-first default-routing test fixture so the FN-7510 autonomous-oversight default doesn't add an extra Activity-view option and break the test's actual intent (asserting Chat-first tab routing).
- Added changeset documenting the fix.
Files changed:
.changeset/fn-7607-manual-pr-flow.md | 7 +++++++
packages/dashboard/app/components/TaskDetailModal.tsx | 14 +++++++++++++-
.../TaskDetailModal.attachments-and-tabs.test.tsx | 12 +++++++++++-
3 files changed, 31 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7607
Fusion-Task-Lineage: f0b077d4-792f-4e43-8e40-43d325920be5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Unifies the previously mobile-only Oversight overflow menu into a single, always-present dropdown that replaces the scattered desktop oversight buttons and the separate mobile affordance.
- Replace discrete desktop oversight action buttons in TaskDetailModal's footer with one universal "Oversight actions" dropdown trigger, reusing the menu across desktop and mobile breakpoints.
- Simplify TaskDetailModal.tsx footer rendering logic, removing now-redundant responsive branching for oversight controls.
- Update TaskDetailModal.css to drop the old mobile-only oversight-overflow styles and support the unified dropdown across breakpoints.
- Update definition-actions, oversight-controls, oversight-mobile, rendering, and responsive-and-dependencies tests to assert the single dropdown behavior and disambiguate the exact "Actions" button query from the new "Oversight actions" aria-label.
- Refresh docs/dashboard-guide.md to describe the unified oversight dropdown UX.
Files changed:
docs/dashboard-guide.md | 14 +-
packages/dashboard/app/components/TaskDetailModal.css | 89 +++++------
packages/dashboard/app/components/TaskDetailModal.tsx | 177 +++------------------
packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx | 70 ++++----
packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx | 130 ++++++++++-----
packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx | 42 +++--
packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx | 27 ++--
packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 57 ++++---
8 files changed, 278 insertions(+), 328 deletions(-)
Fusion-Task-Id: FN-7604
Fusion-Task-Lineage: afbb7573-d654-48db-a9ac-aecbd8e22e46
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Give the TaskDetailModal priority chip distinct tinted borders/backgrounds per level so low/high/urgent/normal are visually distinguishable at a glance.
- Add per-level border-color and stronger background overrides for .detail-priority-chip.card-priority-badge--{low,high,urgent}, using the matching semantic color token (info/warning/error) with higher specificity than the shared base rule.
- Leave the FN-7585 shared base chip rule and FN-7597 neutral 'normal' treatment untouched; scope changes strictly to .detail-priority-chip so read-only TaskCard badge tints are unaffected.
- Add a regression test asserting each level has distinct, non-var(--border) border-colors and backgrounds, mutually distinct across levels, while the read-only TaskCard badge selectors remain unchanged.
Files changed:
.../dashboard/app/components/TaskDetailModal.css | 33 ++++++++++
...etailModal.responsive-and-dependencies.test.tsx | 71 ++++++++++++++++++++++
2 files changed, 104 insertions(+)
Fusion-Task-Id: FN-7601
Fusion-Task-Lineage: 1e2fc574-835c-43b0-8689-02884ad5c2d6
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Moving a card out of a non-legacy workflow column — e.g. Coding (Ideas)
"ideas" → "todo" — was rejected with "Invalid transition: 'ideas' → 'todo'.
Valid targets: none".
Workflow columns graduated to always-on but moveTaskInternal's compat-flag
legacy branch (the default path, since no experimental flag is emitted)
validated every move against the legacy VALID_TRANSITIONS table, which is
keyed only by the built-in column ids. Default-workflow moves survived by
coincidence (its ids ARE the legacy ids); a task in a custom column had no
key so every move was rejected.
The legacy branch now resolves a non-legacy source column's targets from the
task's own workflow adjacency (resolveAllowedColumns), while keeping the
legacy bare-Error contract intact for legacy columns (transition-parity /
characterization suites unchanged). Adds a regression test covering the
ideas -> todo -> in-progress -> in-review chain and non-adjacent rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A present, unexpired Anthropic subscription OAuth token that lacks an
inference scope (e.g. a profile-only grant) authenticates identity but
403s on every model call. /auth/status previously validated only token
presence + expiry, so it reported such a token as connected while all
inference failed. It now treats an inference-incapable Anthropic OAuth
token as not-connected (authenticated:false, expired:true so the
re-login banner fires) with a scope-specific loginError. Gated to
Anthropic providers only; tokens with no recorded scopes are treated as
usable to avoid false negatives on fresh logins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes overlapping Record and Clear buttons on the Keyboard Shortcuts settings rows by replacing the icon-only button class with a text button class and locking layout with flex-shrink.
- Swap ShortcutCaptureInput Record/Clear buttons off the icon-only `btn-icon` class (which forced line-height:0 and a 36px mobile square, clipping labels) onto a text-button class
- Add `.shortcut-capture` row CSS with `flex-shrink:0` on controls so the input and buttons never overlap and stack cleanly on mobile
- Add regression tests covering the Keyboard Shortcuts section layout
- Add changeset documenting the fix
Files changed:
.changeset/fn-7602-shortcut-row-layout.md | 7 ++
.../dashboard/app/components/SettingsModal.css | 17 ++++
.../settings/sections/ShortcutCaptureInput.tsx | 14 +++-
.../__tests__/KeyboardShortcutsSection.test.tsx | 95 ++++++++++++++++++++++
4 files changed, 131 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7602
Fusion-Task-Lineage: 50cf6975-f0fb-42dd-87b0-50578977a0f4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
The Anthropic OAuth refresh request sent `scope: user:profile`, which under
RFC 6749 §6 re-issues the access token with exactly that scope — stripping
`user:inference` and 403-ing every model call while the account still read
as "logged in via OAuth". Stop sending `scope` on refresh (Anthropic then
preserves the originally-granted scopes, matching pi-ai), and widen
ANTHROPIC_DEFAULT_SCOPES to mirror pi-ai's full granted Claude Code scope
set so any fallback describes a usable token.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attach the transient plannerOverseerState snapshot to the single-task detail route so the Nudge control reflects live overseer observation instead of always showing the periodic-observation message.
- GET /api/tasks/:id now best-effort attaches plannerOverseerState (mirrors the list route), never throwing on enrichment failure.
- TaskDetailModal reads overseerSnapshot from workingTask (merged full-detail object) instead of the raw task prop, so detail refetches via fetchTaskDetail (dependency chips, Documents view, logs, post-open refetch) no longer drop the snapshot.
- Added regression tests for the detail-route enrichment and the modal's Nudge-availability behavior.
- Added a patch changeset documenting the fix.
Files changed:
.changeset/fn-7600-oversight-nudge-detail-snapshot.md | 7 ++
packages/dashboard/app/components/TaskDetailModal.tsx | 14 ++-
.../TaskDetailModal.oversight-controls.test.tsx | 131 +++++++++++++++++++++
.../__tests__/tasks-planner-overseer-state.test.ts | 95 +++++++++++++++
packages/dashboard/src/routes/register-task-workflow-routes.ts | 25 +++-
5 files changed, 269 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7600
Fusion-Task-Lineage: 500614d0-091a-461c-8e7b-329a7b791502
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Aligns the task-detail Priority dropdown's size, border, and typography with the Oversight dropdown so both controls read as one consistent style.
- Give the untinted `normal` priority level a neutral, token-based chip background (scoped to `.detail-priority-chip.card-priority-badge--normal`) instead of an empty bordered shell, matching the Oversight `--off` chip treatment.
- Remove the Priority-only forced uppercase text-transform on the select/option so it relies on the ancestor label's uppercase transform like the Oversight select does.
- Add regression coverage asserting shared box-size/border tokens across the Priority chip, Oversight chip, and mobile Oversight overflow trigger, no duplicated text-transform overrides, preserved low/high/urgent semantic tints, and unaffected --saving state.
- Add a patch changeset documenting the fix.
Files changed:
.changeset/fn-7597-priority-dropdown-matches-oversight.md | 7 +++
packages/dashboard/app/components/TaskDetailModal.css | 27 ++++++++++--
packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 51 ++++++++++++++++++++++
3 files changed, 82 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7597
Fusion-Task-Lineage: d703e59a-35d8-4788-9ad2-1462d6f3c588
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Docs-only change adding front-door discovery for the already-shipped planner-oversight feature (FN-7508 → FN-7583), which previously had no entry point outside internal reference docs.
- Add a README.md feature table row and a new "Planner oversight" section describing oversight levels (off/observe/steer/autonomous) and the always-on human-confirmation gate for merge/PR and destructive actions, linking to Settings Reference and Dashboard Guide
- Add a README.md capabilities bullet cross-linking the new section
- Add a docs/README.md hub row pointing to Settings Reference, Dashboard Guide, and Architecture for planner oversight, and extend the 'power user' reading path
- Add a one-line pointer in docs/getting-started.md workflow section noting per-task/workflow oversight controls
Files changed:
README.md | 11 +++++++++++
docs/README.md | 9 ++++++---
docs/getting-started.md | 3 +++
3 files changed, 20 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7598
Fusion-Task-Lineage: 8141b44c-f007-45c0-a057-f4eeb34ae8d4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Tasks added to a workflow whose intake column differs from the default
(e.g. Coding (Ideas) -> "ideas") disappeared from the board until a manual
reload. The board resolves a card's lane from the board-workflows
taskWorkflowIds map, which only refetches on mount/focus/workflow-CRUD SSE
-- never on task creation. A freshly created card was absent from that map,
fell back to the default workflow (no "ideas" column), and was dropped from
every lane.
- Board.tsx: force one board-workflows refetch (deferred a tick,
signature-guarded) whenever a rendered task is missing from taskWorkflowIds,
so its real workflow + intake column resolve for any create surface.
- Board.tsx: re-home a selected-workflow task whose column the workflow no
longer declares into the intake lane instead of a phantom bucket.
- useBoardWorkflows.ts: widen refreshBoardWorkflows type to accept forceFresh.
- Add regression tests for tasks arriving via the tasks prop (SSE / non-board
create surfaces) and the orphan-column safety net.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorders task-definition prompt templates so the Before -> After Transformation section appears before other sections, making the expected change visible first.
- Move the Before -> After Transformation section ahead of other sections in agent-prompts.ts task-definition templates
- Update docs/task-management.md to reflect the new section order
- Add/extend tests in agent-prompts.test.ts and triage.test.ts covering the new ordering
- Add changeset fn-7593-before-after-top.md documenting the change
Files changed:
.changeset/fn-7593-before-after-top.md | 7 +++++++
docs/task-management.md | 2 +-
packages/core/src/__tests__/agent-prompts.test.ts | 20 ++++++++++++++++++++
packages/core/src/agent-prompts.ts | 20 +++++++++++++-------
packages/engine/src/__tests__/triage.test.ts | 17 +++++++++++++++++
5 files changed, 58 insertions(+), 8 deletions(-)
Fusion-Task-Id: FN-7593
Fusion-Task-Lineage: d0d5eb4d-2fe0-456c-b061-5c078b78911b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>