Commit Graph

2960 Commits

Author SHA1 Message Date
gsxdsm
fe5a595984 FN-7622: unify desktop and CLI provider seeding to fix truncated provider list
The Electron desktop app's in-process dashboard server skipped the CLI's provider seeding sequence, so /api/providers and /api/models returned a truncated catalog (missing built-in API-key providers and user customProviders[]) compared to the identical config on the web build.

- Move provider-auth.ts and custom-provider-registry.ts from @fusion/cli into @fusion/engine as the single shared implementation
- Add engine/src/provider-registration.ts exposing seedDashboardProviders(), mirroring the CLI's exact startup order (built-in Zai provider registration -> wrapAuthStorageWithApiKeyProviders -> model merge/refresh -> registerCustomProviders -> settings:updated resubscription)
- Update desktop/src/local-runtime.ts and local-server.ts to call the shared seedDashboardProviders() helper instead of constructing a raw authStorage/modelRegistry
- Convert packages/cli/src/commands/provider-auth.ts and custom-provider-registry.ts into re-export shims preserving unchanged observable behavior
- Add engine/src/__tests__/provider-registration.test.ts and expand desktop local-runtime/local-server tests to cover the shared seeding path
- Add changeset for @runfusion/fusion (patch)

Files changed:
 .changeset/fn-7622-desktop-provider-parity.md      |   7 +
 .../cli/src/commands/custom-provider-registry.ts   | 122 +----
 packages/cli/src/commands/provider-auth.ts         | 517 +--------------------
 .../desktop/src/__tests__/local-runtime.test.ts    |  93 ++++
 .../desktop/src/__tests__/local-server.test.ts     |  62 ++-
 packages/desktop/src/local-runtime.ts              |  33 +-
 packages/desktop/src/local-server.ts               |  21 +-
 .../src/__tests__/provider-registration.test.ts    | 192 ++++++++
 packages/engine/src/custom-provider-registry.ts    | 117 +++++
 packages/engine/src/index.ts                       |  18 +
 packages/engine/src/provider-auth.ts               | 513 ++++++++++++++++++++
 packages/engine/src/provider-registration.ts       | 105 +++++
 12 files changed, 1172 insertions(+), 628 deletions(-)

Fusion-Task-Id: FN-7622
Fusion-Task-Lineage: fb6fbbf3-745e-4623-b7af-11471e13f138
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-07 22:04:51 -07:00
gsxdsm
1add12d703 fix: resolve full-suite CI failures across engine + dashboard (shards 1-4) (#1947)
## Summary

Fixes the failing **full-suite** CI run on `main` ([run
28874651861](https://github.com/Runfusion/Fusion/actions/runs/28874651861))
— all 4 test shards were red. ~32 test files failing across engine +
dashboard (src + app), rooted in ~13 distinct causes from recent main
commits. All resolved; the merge gate and full engine/dashboard suites
are green locally.

## Root causes & fixes

### Engine (shards 1 & 2)
- **`appendAgentLog` 6th timing arg (FN-7503, `2797803c0`)** —
`agent-logger.ts` now passes an optional
`{durationMs,timeToFirstTokenMs}` 6th arg; many
executor/heartbeat/merger tests asserted the old 5-arg form. Added a
shared timing-tolerant helper `agent-log-assertions.ts` (asserts
`taskId/text/type`, tolerant of the timing object) and applied it across
affected files — so future timing fields won't re-break every executor
test.
- **`reconcileSupersededGeneratedFixFeatures` (mission)** —
`mission-execution-loop.ts` calls a method the test's missionStore mock
lacked; added a no-op stub (the real `MissionStore` already implements
it).
- **`ModelFallbackExhaustedError` / `proseSignalsClearApproval` /
`extractJsonObjectCandidates` missing from `vi.mock`** — converted stale
hand-written mocks (`../pi.js`, `../reviewer.js` in
`executor-test-helpers.ts`) to `importOriginal`-spread so real exports
carry through.
- **Workspace product fixes (2):**
- `merger-ai.ts` — `landWorkspaceTask` now recovers the integration-tip
sha as `landedSha` when the A1 trailer-fallback proved a sub-repo landed
but its sha was never persisted, so `finalizeWorkspaceTask` can build
merge proof (was stranding partial-land retries in-review).
- `worktree-acquisition.ts` — `acquireWorkspaceRepoWorktree` strips the
shared project `integrationBranch/baseBranch` overrides before
forwarding to `acquireTaskWorktree` (FN-7360's `freshStartPoint` was
resolving an absent shared branch).
- **FN-7360 extra `git symbolic-ref` exec** — updated worktree
exec-count assertions for the new `resolveIntegrationBranch` call.
- **Planner-overseer / stepwise-workflow / workflow-graph /
workflow-prompt / executor-step-session / liveness-gate / checkout /
ce-workflow / triage-split** — test-alignments for intentional behavior
changes (FN-7229 retry-cap, FN-7265 review-node removal, FN-7335
pause-abort logging, FN-7577 recovery-budget, FN-7577 overseer denial
loop, specifyTask single promptWithFallback call, FN-4944
already-on-main noop log, FN-7486 ownership short-circuit).

### Dashboard API (shard 3)
- **`store.on('task:moved')` (FN-7337)** — `createServer` now registers
the listener; backed the 4 affected MockStores with EventEmitter (shared
root cause across chat-routes.rooms, register-git-github,
routes-run-cited-goals, routes-sandbox-audit).
- **`routes-agent-import`** — core mock converted to
`importOriginal`-spread (was missing FN-7444 planning-deepening
constants).
- **`session-resume-history`** — engine mock missing
`resolveMcpServersForStore`.
- **`task-create-workflow-route`** — `builtin:legacy-coding`
defaultSteps now include `plan-review` (FN-7224/7226).
- **GitLab parity** — added the missing `[GitLab Parity Inventory]`
cross-link in `docs/signals-connectors.md`.

### Dashboard app (shard 4)
- Test-alignments for intentional product changes: FN-7057 (workflow
selection preservation), FN-7340 (footer concurrency geometry), FN-7156
(Missions overview default), FN-7342/FN-6825 (board scroll + workflow
switcher), FN-7352 (openDetailTask 3rd arg), FN-7261 (backdrop dismiss
default-off), FN-7234 (non-authoritative fetch failures), plus a missing
`fetchWorkflowOptionalSteps` mock.

### MCP coverage
- `mcp-surface-coverage` forwarding needle updated for FN-7446's
`resolvePlanningMcpServers` helper.

## Approach notes
- Each fix is the **minimal** change at the correct source (test-update
where a recent commit intentionally changed behavior; product-fix for
the 2 real regressions). No assertion was loosened/deleted to force a
pass; no timeout appeasement.
- Coordination: work was partitioned by package across parallel
subagents (engine / dashboard-src / dashboard-app) with Main as the sole
git committer (path-scoped commits) after an early shared-index reset
wiped in-progress edits — process was tightened mid-flight.

## Verification
- **Full engine suite**: green (9231 passed; the lone local-only
`custom-providers-openai-completions` import error is stale local
`pi-ai@0.79.9` vs the lockfile's `0.80.3` — CI's fresh install resolves
`/compat`; it passed in the original CI run).
- **Dashboard API** (`dashboard-api-quality-backfill`): 242 files / 3185
tests / 0 failures.
- **Dashboard app** (`dashboard-app-quality-backfill`): all targeted
files green (37 + 95 tests).
- **Merge gate** (`pnpm test:gate`): engine-core 326 + ci-shape 63, plus
nohup/4040/appeasement/changeset-format checks — all pass.
- 2 changesets added for the published-`@runfusion/fusion` behavior
fixes (workspace landedSha, sub-repo worktree branch-strip).

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

* **Bug Fixes**
* Improved reliability for partial workspace land retries by recovering
the exact proven landed commit so durable merge proofs can complete.
* Fixed per-sub-repo worktree creation by removing invalid branch
override settings, preventing worktree-add failures.
* Dashboard stability updates: preserve mobile board scroll during
stabilization/restore, correct task filtering when workflows are
missing, ensure the Chat tab appears for done tasks, and refine
modal-dismiss and responsive popover behavior.
* **Documentation**
* Expanded the GitLab connector section with GitLab parity context and a
GitLab Parity Inventory reference.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-07 22:04:17 -07:00
gsxdsm
203c734340 fix(engine): require exact trailer line, not substring, for proven landed commit (Greptile P1)
findProvenLandedCommit now keeps --grep as a prefilter but verifies each
candidate carries an actual 'Fusion-Task-Id: <taskId>' trailer line via
git show -s --format=%B, so a later commit that merely mentions the trailer
text in its body cannot be selected. Regression covers a body-mention
intervening commit.
2026-07-07 10:25:11 -07:00
gsxdsm
518c5420f2 fix(engine): recover exact proven landed commit, not current tip (Greptile P1)
findProvenLandedCommit returns the task's own trailer commit (or recorded
landedSha when still an ancestor) instead of rev-parse on the integration
tip, so an intervening sub-repo land can't attribute a later unrelated
commit. Regression: intervening commit after lost persist recovers tipAfterFirst.
2026-07-07 10:09:28 -07:00
gsxdsm
93d1f702f0 test(engine): update MCP forwarding coverage needle for FN-7446 resolvePlanningMcpServers helper 2026-07-07 09:50:34 -07:00
gsxdsm
b7aa8b38e4 test(engine): fix pi-create-fn-agent RTK/pauseForApproval + step-session-executor logging/terminal-activity assertions 2026-07-07 09:46:00 -07:00
gsxdsm
f93ce52689 test(engine): fix ModelFallbackExhaustedError pi mock + FN-7360 worktree exec counts + sync conflict mapping 2026-07-07 09:37:18 -07:00
gsxdsm
b6a8f6430f test(engine): simulate fallback split-close in single promptWithFallback call (specifyTask no longer loops) 2026-07-07 09:02:28 -07:00
gsxdsm
38406c3442 test(engine): use shared appendAgentLog timing-tolerant helper in heartbeat/triage tests (FN-7503) 2026-07-07 08:55:27 -07:00
gsxdsm
58d085efab test(engine): spread real reviewer exports in executor-test-helpers mock (3167dbc83); align stepwise/graph/prompt-override tests (FN-7265/7335) 2026-07-07 08:54:49 -07:00
gsxdsm
82e06e37c8 test(engine): fix executor step-session/liveness-gate/checkout/ce-workflow mocks (FN-7229 retry-cap + workflow verdict wiring) 2026-07-07 08:43:58 -07:00
gsxdsm
90a6b569c4 test(engine): wire planner-overseer intervention denial loop to failed-signal snapshot (FN-7577) 2026-07-07 08:42:47 -07:00
gsxdsm
f2202619e0 fix(engine): recover workspace landedSha + strip shared branch overrides for sub-repo worktrees (FN-7360) 2026-07-07 08:42:47 -07:00
gsxdsm
dacbead012 test(engine): stub readCommitTaskOwnership in worktrunk-self-healing to isolate git worktree prune plumbing 2026-07-07 08:40:44 -07:00
gsxdsm
76014898e8 test(engine): add shared appendAgentLog timing-tolerant helper; fix merger-merge-details assertions (FN-7503) 2026-07-07 08:39:25 -07:00
gsxdsm
4ae71b2a15 test(engine): pin FN-4944 already-on-main fast-path noop log in post-finalize test 2026-07-07 08:37:04 -07:00
gsxdsm
443005d9b2 test(engine): repair project-engine mocks (OAuthRefreshScheduler + WS fail-closed getTask) 2026-07-07 08:37:04 -07:00
gsxdsm
f3c50de7e7 test(engine): stub reconcileSupersededGeneratedFixFeatures on mission-validation-trigger-gap mocks
recoverActiveMissions (mission-execution-loop.ts:263) calls
missionStore.reconcileSupersededGeneratedFixFeatures per slice; the 5
MissionExecutionLoop-backed mocks here omitted it, so recovery threw
(TypeError) at the slice loop and aborted before processTaskOutcome /
ensureFeatureAssertionLinked / startValidatorRun ran — 4 tests failed.

Add a no-op stub (matches mission-execution-loop.test.ts reference) with
an FNXC:MissionReconcile note. No-op is correct: supersession is not
exercised by these tests.
2026-07-07 08:24:35 -07:00
gsxdsm
860e42d1bf Merge branch 'main' into fix/push-after-merge-nff-retry-loop 2026-07-07 07:37:41 -07:00
fusion-merge-train
f70974b1af fix(engine): bounded retry loop for push-after-merge non-fast-forward rejections
A single retry can still lose the race on busy repos if origin moves again
in the pull-rebase/push window. Generalize the one-shot retry into a bounded
loop (3 attempts, 2s/5s/10s backoff), re-pulling+rebasing before each push
attempt and bailing early once the failure is no longer non-fast-forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 16:17:35 +02:00
fusion-merge-train
4a4819b3ee fix(engine): persist push-after-merge failures instead of dropping them
When pushAfterMerge fails (or throws) in direct-merge mode, the daemon
only wrote to the process-wide mergerLog and a transient MergeResult
field, then unconditionally marked the task done. There was no durable
record on the task or in the audit trail, so a diverged local main
could go unnoticed indefinitely — this is how our local main drifted
162 commits from origin before it was caught by hand.

Record the failure through the two channels merger.ts already has for
this: the dormant "push:origin" GitMutationType via audit.git(), and a
task log entry via store.logEntry(). Both calls are best-effort
(.catch(() => undefined)) so a logging failure can't abort the merge
flow itself — task completion behavior is unchanged, the failure is
just no longer invisible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 13:55:33 +02:00
gsxdsm
9e5c025113 FN-7608: block executors on pending approvals instead of allowing workarounds
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>
2026-07-06 19:03:07 -07:00
gsxdsm
203f879c8f FN-7611: respect workflow intake column on task creation
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>
2026-07-06 19:03:07 -07:00
gsxdsm
44442622c5 FN-7609: show gated action payload details on approval requests
Approval cards previously showed only a generic gating message with no visibility into the underlying command/arguments being approved, and repeated pending requests for the same action could pile up as duplicates.

- Add GatedActionApprovalDetails component to render the gated command/arguments payload on agent-gating approval cards in MailboxView
- Persist approvalDedupeKey in targetAction.context and a payload-bearing summary via buildAgentGatedActionSummary in permanent-agent-gating
- Wire agent-heartbeat, executor, and pi to pass through the richer gated-action context/summary
- Add changeset (patch) documenting the fix
- Update docs/dashboard-guide.md
- Add/extend tests: GatedActionApprovalDetails, MailboxView, permanent-agent-gating, pi-create-fn-agent

Files changed:
 .changeset/FN-7609-gated-action-approval-payload.md            |  7 ++
 docs/dashboard-guide.md                                        |  1 +
 packages/core/src/types.ts                                     |  8 +++
 .../app/components/GatedActionApprovalDetails.css              | 50 ++++++++++++++
 .../app/components/GatedActionApprovalDetails.tsx              | 72 +++++++++++++++++++
 packages/dashboard/app/components/MailboxView.tsx               | 12 ++++
 .../__tests__/GatedActionApprovalDetails.test.tsx               | 66 ++++++++++++++++++
 .../app/components/__tests__/MailboxView.test.tsx                | 41 +++++++++++
 .../src/__tests__/permanent-agent-gating.test.ts                 | 31 +++++++++
 .../src/__tests__/pi-create-fn-agent.test.ts                     | 80 ++++++++++++++++++++++
 packages/engine/src/agent-heartbeat.ts                           | 19 ++++-
 packages/engine/src/executor.ts                                  | 19 ++++-
 packages/engine/src/permanent-agent-gating.ts                    | 53 ++++++++++++++
 packages/engine/src/pi.ts                                        |  6 ++
 14 files changed, 461 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-7609

Fusion-Task-Lineage: 80a6bb5b-79f7-4b78-9204-402c2dea6171

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-06 19:03:06 -07:00
gsxdsm
60081fb1f4 FN-7610: route workspace-mode tasks around PR-merge auto-merge strategy
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>
2026-07-06 19:03:06 -07:00
gsxdsm
ed136b1e76 fix(engine): repair two stale test mocks broken by #1910 (#1919)
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 -->
2026-07-06 13:11:45 -07:00
gsxdsm
cc98852027 fix(engine): defer validator fail when the judged workspace predates the merged code (#1929)
## 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 -->
2026-07-06 13:11:21 -07:00
fusion-merge-train
58683f9610 fix(engine): defer validator fail when the judged workspace predates the merged code
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>
2026-07-06 08:23:07 +02:00
Phil Larson
3d60f67fab fix(missions): address generated fix review blockers 2026-07-05 22:14:22 -07:00
Phil Larson
3744fbcc2f fix(missions): supersede stale generated fix features 2026-07-05 21:50:28 -07:00
gsxdsm
6c9989c847 fix(engine): short-circuit zero-commits-ahead branch before AI-merge clean-room churn (#1920)
## 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 -->
2026-07-05 21:37:26 -07:00
gsxdsm
9b7921b7d8 fix(engine): defer validator fail to inconclusive while the linked task is unmerged (#1917)
## 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 -->
2026-07-05 21:37:04 -07:00
gsxdsm
51f354dec1 Merge branch 'main' into fix/stale-test-mocks 2026-07-05 21:36:07 -07:00
gsxdsm
f7d1346724 Merge branch 'main' into fix/validator-premerge-guard 2026-07-05 21:04:34 -07:00
gsxdsm
1ee2e3a27c Merge branch 'main' into fix/transient-auth-retry 2026-07-05 21:02:11 -07:00
gsxdsm
9bbb16c4a1 Merge branch 'main' into fix/empty-branch-merge-wedge 2026-07-05 21:01:51 -07:00
gsxdsm
78d570747f fix(engine): self-heal failed in-review cards whose PR merged on the remote (#1922)
## 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 -->
2026-07-05 20:59:01 -07:00
gsxdsm
ba9c9b5ca7 Merge branch 'main' into fix/self-heal-merged-pr-stale-base 2026-07-05 20:47:22 -07:00
gsxdsm
80a05e41fe Merge branch 'main' into fix/hermes-runtime-chat-state 2026-07-05 20:45:47 -07:00
gsxdsm
ed823c794c fix: preserve Claude OAuth scopes on token refresh so inference keeps working
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>
2026-07-05 18:47:40 -07:00
Phil Larson
8d6c92ac2e fix: preserve Hermes runtime chat session state 2026-07-05 17:50:13 -07:00
gsxdsm
09a1c9d843 FN-7596: regression-test the Coding (Ideas) manual-intake lifecycle end-to-end
Adds cross-layer regression coverage for the manual-intake parking lifecycle (create -> parked -> operator Start promotion -> poll-time todo-discovery), and clarifies the workflow-steps doc to describe the tested lifecycle.

- packages/core: covers store create -> moveTask promotion out of the parked intake column
- packages/engine: covers triage poll ordering/discovery of the still-unplanned bootstrap-stub card
- packages/dashboard: covers TaskCard's Start affordance for parked cards
- docs: documents the full regression-tested lifecycle for manual-intake column parking (FN-7596)

Files changed:
 docs/workflow-steps.md                             |   2 +-
 .../__tests__/store-create-intake-column.test.ts   |  26 ++++
 .../app/components/__tests__/TaskCard.test.tsx     | 153 +++++++++++++++++++++
 packages/engine/src/__tests__/triage.test.ts       | 119 +++++++++++++++-
 4 files changed, 298 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-7596

Fusion-Task-Lineage: 267c3d9a-6181-4ca5-b871-7009c0204372

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-05 16:33:21 -07:00
gsxdsm
f30d55fae7 FN-7593: move Before/After Transformation section to top of task definitions
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>
2026-07-05 13:44:50 -07:00
fusion-merge-train
15c2c831a8 fix(engine): self-heal failed in-review cards whose PR merged on the remote
A transient error at merge time can flag an in-review card `failed` even
when its PR actually squash-merged on the remote. `recoverAlreadyMergedReviewTasks`
only ran the already-merged evidence detector against the LOCAL base ref, so
when this process never fetched the merge, the owned commit was absent locally,
the detector returned null, and the card held its file-scope lease forever.

Fetch-then-prove: when a failed candidate has a recorded PR and the local base
yields no owned commit, best-effort `git fetch origin <base>` and re-run the
SAME evidence detector against `origin/<base>`. The owned-commit proof and every
foreign-ownership guard inside the detector remain the sole finalize gate, so
this only un-wedges a genuinely-merged task — it never phantom-finalizes on
unproven state. Gated on a recorded PR (no PR ⇒ nothing merged remotely ⇒ no
fetch); fail-closed on fetch error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:20 +02:00
gsxdsm
72b77bf621 fix(FN-7561): stop Plan Review replan loop and fix "can't find the plan" reviews
The Plan Review pre-merge gate could loop a task through triage↔plan-review
indefinitely (FN-7525 ran 13+ replans overnight with no operator visibility),
and its reviewer frequently produced "no PROMPT.md found / data lives in a DB"
non-verdicts that fed the loop.

Root cause of the non-verdicts: the reviewer runs readonly with cwd set to the
task worktree, but the spec lives at project-root .fusion/tasks/<id>/PROMPT.md —
outside the worktree — so telling it to "Read PROMPT.md" had it search the wrong
tree and give up. Four fixes:

1. Inject the PROMPT.md content (via readTaskArtifact, store-backed) directly
   into the Plan Review reviewer prompt so the verdict never depends on the
   agent locating the file.
2. Self-retry a malformed reviewer response once on the primary model when no
   fallback model is configured, so a single fumbled response gets a second
   chance instead of feeding the replan loop.
3. A malformed (advisory_failure, no parsed verdict) plan-review result can
   never trigger a triage replan — it is an infra failure, not a plan defect.
4. Cap the unbounded plan-review replan default at 15 attempts; past the cap it
   emits a loud halting log entry and leaves the task for a human instead of
   looping forever. Explicit numeric operator budgets are unchanged.

Tests: cap halts at 15 / still replans at 14 / malformed never replans. Existing
Plan Review replan and malformed-verdict-gate tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:31:49 -07:00
gsxdsm
42bbe58c03 FN-7579: add ask-user and exit-gate workflow nodes
Add workflow nodes for mid-flow user reach-out and early exit from a workflow run.

- Add `ask-user` IR node kind that reuses the await-input park/resume mechanism and surfaces the question in the task chat for brainstorming/clarification.
- Add `exit-gate` IR node kind that terminates the workflow early, with an optional condition.
- Wire both node kinds through the engine executor and workflow-node-handlers, including a new exit-gate-runner.
- Update the WorkflowNodeEditor palette, node summaries, and node help text for the two new node types.
- Extend workflow-flow-mapping to support the new node kinds.
- Keep `prompt`+`awaitInput` as a back-compat alias.
- Add core/engine/dashboard tests covering the new node kinds.
- Document the new nodes in docs/workflow-steps.md.
- Add changeset for the new minor feature.

Files changed:
 .changeset/fn-7579-ask-user-exit-gate-nodes.md     |   7 +
 docs/workflow-steps.md                             |  28 ++++
 packages/core/src/__tests__/workflow-ir.test.ts    | 120 ++++++++++++++
 packages/core/src/workflow-ir-types.ts             |  12 +-
 packages/core/src/workflow-ir.ts                   |  47 ++++++
 .../app/components/WorkflowNodeEditor.tsx          | 181 ++++++++++++++++++++-
 .../app/components/__tests__/node-summary.test.ts  |  43 +++++
 .../__tests__/workflow-flow-mapping.test.ts        |  49 ++++++
 .../app/components/nodes/WorkflowNodeTypes.tsx     |  14 +-
 .../dashboard/app/components/nodes/node-help.ts    |  24 +++
 .../dashboard/app/components/nodes/node-summary.ts |  28 ++++
 .../app/components/workflow-flow-mapping.ts        |   4 +
 .../workflow-graph-executor-handlers.test.ts       | 115 +++++++++++++
 .../src/__tests__/workflow-node-handlers.test.ts   |  66 ++++++++
 packages/engine/src/executor.ts                    |  23 ++-
 packages/engine/src/workflow-node-handlers.ts      |  18 +-
 .../src/workflow-node-runners/exit-gate-runner.ts  |  81 +++++++++
 17 files changed, 849 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7579
Fusion-Task-Lineage: 9a89ff49-200d-4a6c-b97c-15d219349ee5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-05 11:31:48 -07:00
gsxdsm
74358494a7 FN-7577: extend PR-based revert to workspace tasks under autoMerge:false
Extends FN-7554's single-repo PR revert path to workspace (multi-repo) tasks: when autoMerge is disabled, the revert route now opens one dedicated fusion/revert-<id> PR per sub-repo instead of refusing workspace tasks outright.

- Add prepareWorkspaceRevertPrBranches (packages/engine/src/task-revert.ts): classifies every sub-repo first and only prepares a per-sub-repo fusion/revert-<id> branch when all sub-repos are clean/already-reverted (all-or-nothing at branch-prep phase); never force-writes any sub-repo integration branch.
- Export the new helper from packages/engine/src/index.ts.
- Extend POST /api/tasks/:id/revert (register-task-workflow-routes.ts) to resolve owner/repo and check the GitHub rate limiter for every sub-repo before pushing/creating any PR, opening one PR per sub-repo and returning an additive { mode: "pr", clean: true, workspace: { repos: [...] } } result; degrades the whole task to needsHuman if GitHub is unconfigured or any sub-repo is rate-limited, rather than opening a partial subset of PRs.
- Leave existing { mode: "git" | "ai" | "pr" } shapes, the autoMerge:true workspace path, and FN-7554's single-repo PR path unchanged.
- Add engine real-git coverage (task-revert-workspace-pr.real-git.test.ts) and extend dashboard route tests (task-revert-route.test.ts) for the new workspace PR path.
- Add changeset (.changeset/fn-7577-workspace-pr-revert.md, minor) and update docs/task-management.md.

Files changed:
 .changeset/fn-7577-workspace-pr-revert.md          |   7 +
 docs/task-management.md                            |   3 +-
 .../src/__tests__/task-revert-route.test.ts        | 313 ++++++++++++++++-
 .../src/routes/register-task-workflow-routes.ts    | 181 +++++++++-
 .../task-revert-workspace-pr.real-git.test.ts      | 371 +++++++++++++++++++++
 packages/engine/src/index.ts                       |   4 +
 packages/engine/src/task-revert.ts                 | 295 ++++++++++++++++
 7 files changed, 1166 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-7577

Fusion-Task-Lineage: bedbfab7-5804-485f-9b40-64531edfc64a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-05 11:31:48 -07:00
gsxdsm
b173f76adb fix(FN-7577): stop planner overseer from "recovering" healthy in-progress tasks
decidePlannerRecovery fell through to inject_guidance for any non-failed
executor/workflow-gate signal, including the healthy `progressing` signal.
Under autonomous oversight this dispatched steering into the live agent of
every healthy task — flipping the card badge to "recovering", burning a
bounded-attempt slot, and consuming AI usage for no reason.

- Only problem signals (`stuck`/`blocked`, plus the existing `failed` path)
  now trigger autonomous steering; healthy (`progressing`/`complete`) and
  human-wait (`awaiting-human`) signals return `none`.
- PlannerRecoveryController.tick clears stale attempt/last-action records for
  a (taskId, stage) once its signal is healthy, so a recovered task drops
  from "recovering" back to "watching" and a later problem gets a fresh budget.
- PlannerOverseerMonitor dedupes the activity-feed heartbeat: an unchanged
  (stage, signal, reason) observation logs once per change, not every tick.

Invariant tests added across all signals for both fall-through stages.

Fusion-Task-Id: FN-7577

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:31:48 -07:00
gsxdsm
94e9d15e38 FN-7556: auto-select review-heavy workflow for AI-undo tasks
AI-undo tasks now default to a configurable, stricter review workflow instead of always inheriting the project default.

- Add project setting `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) to ProjectSettings type and DEFAULT_PROJECT_SETTINGS
- `POST /api/tasks/:id/revert` resolves and validates the configured workflow id (via `isBuiltinWorkflowId`/`getWorkflowDefinition`), falling back to inherit-with-warning on a blank/unknown value
- `createAiUndoTask` engine helper gains an optional `workflowId` param, forwarded verbatim to `createTask` only when non-blank, staying pure (no settings/store access itself)
- Add regression tests for the route resolution logic and the engine helper's workflow forwarding
- Update docs (`settings-reference.md`, `task-management.md`) and add changeset

Files changed:
 .changeset/fn-7556-ai-undo-workflow.md             |  7 +++
 docs/settings-reference.md                         |  1 +
 docs/task-management.md                            |  1 +
 packages/core/src/settings-schema.ts               |  4 ++
 packages/core/src/types.ts                         | 14 +++++
 .../settings-default-descriptions.test.tsx         |  2 +
 .../src/__tests__/task-revert-route.test.ts        | 52 ++++++++++++++++-
 .../src/routes/register-task-workflow-routes.ts    | 31 +++++++++-
 .../src/__tests__/task-revert-ai-undo.test.ts      | 67 ++++++++++++++++++++++
 packages/engine/src/task-revert.ts                 | 16 ++++++
 10 files changed, 192 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-7556
Fusion-Task-Lineage: dec5603c-10a8-4780-a1b8-8a836a0de4c1
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-05 11:31:47 -07:00
gsxdsm
2df6c356fc FN-7554: add PR-based revert path for autoMerge:false projects
Adds a PR-based revert path for done/archived tasks in autoMerge:false projects instead of refusing outright.

- New engine export `prepareRevertPrBranch` (packages/engine/src/task-revert.ts) prepares a dedicated `fusion/revert-<id>` branch off the base branch's HEAD and applies the revert commit(s) there, never mutating the base branch itself.
- `POST /api/tasks/:id/revert` route gains an additive `{ mode: "pr", clean: true, prUrl, prNumber, revertBranch, existingPr? }` result for clean single-repo reverts under autoMerge:false, reusing GitHubClient.createPr, findPrForBranch idempotency, and the manual:true PR handoff.
- Existing `{ mode: "git" | "ai", ... }` result shapes and the autoMerge:true path are unchanged.
- Workspace (multi-repo) tasks are explicitly refused for PR-based revert (out of scope; single PR cannot represent a multi-repo revert).
- Adds real-git integration tests for the new branch-prep/apply/commit flow and expands the dashboard route test coverage.
- Adds a changeset for @runfusion/fusion (minor) and a small task-management doc update.

Files changed:
 .changeset/fn-7554-pr-based-revert.md              |   7 +
 docs/task-management.md                            |   2 +-
 .../src/__tests__/task-revert-route.test.ts        | 196 +++++++++++++++++++-
 .../src/routes/register-task-workflow-routes.ts    | 182 ++++++++++++++++++
 .../src/__tests__/task-revert-pr.real-git.test.ts  | 206 +++++++++++++++++++++
 packages/engine/src/index.ts                       |   3 +
 packages/engine/src/task-revert.ts                 | 154 +++++++++++++++
 7 files changed, 747 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-7554

Fusion-Task-Lineage: 9b1bfd82-2428-4cf7-9b36-77afe3517a14

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-05 11:31:47 -07:00