- Await async spawned child session disposal
- Use own-key iteration for structured tool result previews
- Add FNXC requirement comments for new regression assertions
Resolves the actionable CodeRabbit threads on the workspace-mode foundation:
- project-resolver: defer saveWorkspaceConfig until after the user confirms init
and store.init() succeeds (no partial .fusion/ on a declined/non-interactive run).
- git-repository: validate each candidate with a real `git rev-parse` work-tree
probe before counting it (no false-positive repos from stray .git markers);
loadWorkspaceConfig now rejects absolute paths, `..` escapes, and non-string
entries so a corrupt/malicious config can't resolve outside the workspace root.
- executor: gate workspace mode on repos.length > 0 at all three sites so an
empty { repos: [] } can't bypass the git-repo guard or enable an empty workspace.
- worktree-acquisition: thread the configured-command runner through the workspace
acquire path (sub-repos run their init setup); validate repoRelPath as an in-root
relative path before joining; liveness-check a remembered worktree before
reporting it ready (pruned paths fall through to re-acquire); clear the singular
task.worktree/branch after persisting per-repo state (per-repo state lives only
in workspaceWorktrees).
- agent-tools: forward runContext into acquireWorkspaceRepoWorktree for log attribution.
The executor-workspace test's mock-the-subject pattern is left for the
session-scoping follow-up that rewrites it with a real two-repo fixture (FN-5048).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
acquireWorkspaceRepoWorktree now hardens each sub-repo worktree at acquisition:
(1) installs the identity guard with the executor's settings args
(commitMsgHookEnabled/taskPrefix/taskAttributionTrailerName) for single-repo
parity — it was installing no guard before; (2) captures a per-repo
baseCommitSha local-first against the repo's resolved integration branch via
resolveIntegrationBranch(repoAbsPath, {...settings, integrationBranch: undefined})
— stripping the shared override so each sub-repo falls through to its own
origin/HEAD, not a project-wide branch; (3) persists baseCommitSha into the
workspaceWorktrees[repo] entry (Task type extended); (4) registers same-sub-repo
exclusivity on the sub-repo path via activeSessionRegistry under a distinct
"workspace-repo-acquire" kind (released in finally), so two concurrent workspace
tasks contending for the same sub-repo are serialized (throws
WorkspaceRepoAcquireBusyError). Idempotent re-acquire short-circuits.
resolveCapturedBaseCommitSha gains an optional trailing integrationBranch param
defaulting to "main", so existing single-repo callers + base-commit-capture
real-git tests stay green. New audit events worktree:workspace-repo-acquire-busy
/-failed. 6 new real-fixture tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reject failure-condition edges inside optional-group templates (the single-pass
walk surfaces template failures as the group's outcome, so an internal failure
edge was silently dead) — Greptile P2.
- flowToIr: a container/group node (foreach/loop/optional-group) is v2-only — its
presence now forces v2 serialization (an inserted optional-group on a plain
workflow no longer serializes as invalid v1) — CodeRabbit.
- Disabled optional-group bypass routes a plain success with no distinguishing
value, so an outcome:* edge can't preempt success routing (inertness) — CodeRabbit.
- Downgrade heuristic: presence of a legacy optionalSteps key (incl. []) keeps v2.
- Resolver docblock corrected (config-less groups resolve to a fallback entry).
- Strengthen tests: assert both inserted groups + v2 round-trip; failure-edge
rejection case.
- Changeset: bump to major (removed exported WorkflowOptionalStep type).
- Plan: record U7a as delivered in this cohort; only the workflow-step seam
infra removal remains deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional steps are now graph-native optional-group nodes, so the dead
declaration model is removed: the WorkflowOptionalStep type + WorkflowIrV2
.optionalSteps field + validateOptionalSteps (core), and the editor's
declaration AUTHORING surface — WorkflowOptionalStepsPanel, optionalStepsOf,
and the flowToIr/serializeGraph optionalSteps threading (dashboard). A legacy
persisted optionalSteps key is tolerated (ignored) at parse. The per-task
TOGGLE surfaces (dropdown, inline card, modal, Workflow tab) are unchanged —
they consume ResolvedWorkflowOptionalStep, which stays. The workflow-step seam
infrastructure removal remains a separate documented follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review found that enabling a built-in optional-group whose node id collides
with a WORKFLOW_STEP_TEMPLATES id (browser-verification) was silently bypassed:
resolveEnabledWorkflowSteps materialized a WorkflowStep row and stored its id
instead of the group node id, so the executor's enabledWorkflowSteps.includes(
node.id) check never matched. Pass the task's workflow optional-group ids through
untouched on both create-with-enable and update/toggle paths (new
resolveAllOptionalGroupIds + optionalGroupIdSet). Add colliding-id regression
tests. Also (review P3) record the group's own outcome on the disabled-bypass
path so a disabled group is fully context-inert, matching the enabled path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies ce-code-review (autofix) feedback — 5 reviewers, P1s corroborated.
F1 [P1, ×4 reviewers] Guard the merge chokepoint, not just the 4 doors. The
per-caller `getTask().catch(()=>null); if(t) assert` pattern failed open on a
transient read, and runAiMerge re-read the task unguarded — so a workspace
task could reach git work against the non-git root. Added a named
WorkspaceTaskMergeError and call assertNotWorkspaceTaskMerge inside runAiMerge
(the sole merge path) and the deprecated aiMergeTask body; door guards remain
as fast-fail defense-in-depth.
F2 [P1] The dispatch catch treated the guard throw as a merge failure and set
mergeRetries=MAX, permanently blocking manual retry. It now recognizes
WorkspaceTaskMergeError and parks without burning retries.
F3 [P2] Deprecation-warning test asserted toBeLessThanOrEqual(1) — vacuously
true on zero emissions. Now resets the per-project flag and asserts the
warning fires exactly once and not again on a second deterministic merge.
F6 [P2] The once-per-process warning flag suppressed the notice for all other
projects in a multi-project host; now keyed per project (Set by cwd).
F5/F7/F8 [P3] @deprecated propagated to the aiMergeTask barrel re-export; CLI
runTaskMerge guard moved inside the formatted try/catch; FNXC placeholder
timestamps corrected; test .at(-1) -> length index.
Documented as residual (deferred to master-plan U8, not bugs in U0's window):
self-healing auto-finalize + store.mergeTask are additional merge-completing
paths not hardened here — workspace tasks are not end-to-end runnable until
master-plan Phase A, and U8 makes self-healing workspace-aware.
Gate green: typecheck (29 projects), lint, build, test:gate (649+58),
affected tests (206+4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make runAiMerge (the FN-5633 clean-room AI merge path, already the default)
the sole merge path; soft-deprecate the legacy aiMergeTask pipeline. Phase 0
of the workspace-mode master plan — a standalone merge-consolidation refactor
that lands first so downstream workspace work targets one merge function.
U1 — collapse the dispatch: project-engine.ts now calls runAiMerge
unconditionally; the two direct callers that bypassed the dispatch
(dashboard.ts onMergeImpl in --no-engine mode, task.ts runTaskMerge /
`fn task merge`) now route to runAiMerge too. Export runAiMerge from
@fusion/engine.
U2 — soft-deprecate: aiMergeTask is @deprecated (body retained for a later
deletion pass; shared helpers runAiMerge imports, e.g.
captureSingleCommitLandedMetadata, left intact). merger.mode "deterministic"
is annotated deprecated and made inert (type + field kept — published
@runfusion/fusion surface); the dispatch logs a one-time deprecation warning
and routes to runAiMerge. Changeset added (minor).
U3 — R7 workspace merge-boundary guard: shared @fusion/core predicate
assertNotWorkspaceTaskMerge(task) rejects tasks with populated
workspaceWorktrees at all four merge entry points (dispatch, store.mergeTask,
onMergeImpl, runTaskMerge) with an error naming master-plan U6. Covers the
window until per-repo merge support lands; U6 removes it.
U4 — deterministic-mode blast-radius audit: no production project, CI config,
or seeded setting pins merger.mode "deterministic"; only four engine tests
used it to drive the dispatch to aiMergeTask as a mockable seam — migrated to
mock runAiMerge instead. Other module-level aiMergeTask mocks were dead under
the default "ai" mode or test aiMergeTask directly (body retained), so they
are unaffected.
Also removes an unused acquireWorkspaceRepoWorktree import inherited from the
foundation branch (executor.ts) that was failing lint; master-plan U1 re-adds
it with its per-repo usage.
Merge gate green: lint, typecheck (29 projects), build, test:gate
(649 + 58), plus the migrated (114) and new predicate (4) tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the pre-merge `workflow-step` seam node + `optionalSteps` declaration in
the coding and stepwise-coding built-ins with a `browser-verification`
optional-group (default OFF) whose template runs the browser-verification step.
Enabled tasks run it once pre-merge; disabled tasks bypass it (two-task
divergence engine test; stepwise confirmed run-once post-foreach). Coding is now
interpreter-deferred like stepwise (the group is a branching node). Parity
oracles updated deliberately. Legacy seam-anchor/runWorkflowSteps left for U7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-point resolveWorkflowOptionalSteps to scan v2 optional-group nodes instead
of the legacy ir.optionalSteps declaration, and seed a new task's
enabledWorkflowSteps from each group's defaultOn (materializeDefault/Explicit
WorkflowSteps). Preserves the ResolvedWorkflowOptionalStep shape so the create/
edit toggle surfaces keep working (templateId now carries the group node id).
Legacy optionalSteps type/field left in place for U7. Exports the new config
type + resolveDefaultOnOptionalGroupIds helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce the `optional-group` node kind and `WorkflowOptionalGroupConfig`
(defaultOn + single-pass template subgraph), validated by mirroring the loop
template rules: non-empty template, single entry/exit, no nested groups, no
rework edges (single-pass guarantee), no template/top-level id collisions,
boolean defaultOn. Foundation for graph-native optional steps; additive and
inert until producers/consumers land in later units. Includes the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow registering a non-git parent directory that contains multiple git
repositories as a single Fusion project. The agent acquires per-repo
worktrees on demand via a new `fn_acquire_repo_worktree` tool as it
discovers it needs to work in each sub-repo.
This commit lays the foundation:
- detectWorkspaceRepos / loadWorkspaceConfig / saveWorkspaceConfig in
@fusion/core (config persisted to .fusion/workspace.json)
- Task.workspaceWorktrees data model + store plumbing (per-repo
worktree/branch map, distinct from the singular task.worktree)
- acquireWorkspaceRepoWorktree wraps acquireTaskWorktree per sub-repo,
clearing the singular worktree/branch fields so each sub-repo gets a
fresh worktree instead of resuming a sibling repo's worktree
- fn_acquire_repo_worktree agent tool + workspace prompt injection
- executor git-repository validation bypassed when a workspace config
is present
- CLI `fn init` detects a non-git dir containing sub-repos and writes a
workspace config
Known gap (intentionally left for design discussion, see PR): the
executor's main worktree-acquisition path still assumes a single git
root and is not yet workspace-aware. End-to-end execution (skipping the
root acquisition, per-repo merge, per-repo session scoping) is a
follow-on once the execution model is agreed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add explicit OpenAI Codex pricing so token analytics can estimate Codex run costs.
- Add openai-codex pricing entries for GPT-5 Codex variants and codex-mini-latest.
- Bump the pricing verification date and document why Codex keys must use the openai-codex provider.
- Cover Codex pricing and lookup behavior in model-pricing tests.
- Add a changeset for the published Fusion package.
Files changed:
.changeset/fn-6869-codex-pricing.md | 7 ++++
packages/core/src/__tests__/model-pricing.test.ts | 37 ++++++++++++++++++-
packages/core/src/model-pricing.ts | 44 ++++++++++++++++++++++-
3 files changed, 86 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-6869
Fusion-Task-Lineage: 4bf119db-0401-4b18-b389-4a762e665cfc
stuckKillCount is a lifetime counter — incremented by self-healing on each stuck-kill
and reset ONLY by a manual retry — so a long task that genuinely advances between
intermittent stalls could be terminalized by accumulation toward maxStuckKills (default 6),
even though it is making progress.
Reset the streak in TaskStore.updateStep when a step reaches a terminal forward status
(done/skipped), the single chokepoint every step-completion surface routes through (legacy
fn_task_update, graph markStepDone, fn_task_done). It deliberately does NOT rescue a task
wedged re-running the same failing step — no step completes between those kills, so it still
terminalizes as designed; it only bounds the budget to consecutive stalls. Complements the
verification fan-out cap (PR #1708) that keeps verification fast in the first place.
New regression suite asserts reset on done/skipped/graph-source-done and NO reset on
in-progress advance or ignored out-of-order transitions. Merge gate + full @fusion/core
suite (6242 tests) + engine self-healing/stuck-detector (442 tests) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the executor agent guidance and the fn_run_verification tool guidance so
verification stays scoped to changed files instead of running the full workspace
test suite, which (for a foundational-package edit) reverse-expands across the
whole workspace and stalls the task.
- agent-prompts.ts: remove the "during final integration" blanket permission to
run workspace-wide suites; name the forbidden full-run commands explicitly.
- run-verification-tool.ts: strengthen BOUNDED_VERIFICATION_GUIDANCE to forbid
`pnpm test:full` / `pnpm verify:workspace` / whole-package runs as verification.
Engine + core typecheck pass; run-verification-command and executor-review-verdicts
prompt-assertion suites stay green (93 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-test `createTaskStoreTestHarness` recreated a TaskStore from scratch
every test — mkdtemp ×2 + new store + recursive rm with retry backoff — which
dominated wall-clock across these high-test-count files. Switch the files that
tolerate a shared store to `createSharedTaskStoreTestHarness`, which builds the
store once and resets state between tests via full table truncation + FTS
rebuild + filesystem reset, preserving isolation.
Measured on store-create.test.ts (53 tests): 4.44s → 1.78s wall
(test execution 3.59s → 0.93s, ~3.9x). Full @fusion/core suite stays green
(346 files, 6236 tests) and typecheck passes.
19 files that test disk persistence / migration / workflow-definition state are
incompatible with a shared in-memory store and were intentionally left on the
per-test harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>