diff --git a/docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md b/docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md new file mode 100644 index 0000000000..0ec4764659 --- /dev/null +++ b/docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md @@ -0,0 +1,503 @@ +--- +title: "refactor: Organize package internals and split god-files" +type: refactor +status: active +date: 2026-07-14 +deepened: 2026-07-14 +--- + +# refactor: Organize package internals and split god-files + +## Summary + +A multi-wave, behavior-preserving program to make `@fusion/core`, `@fusion/engine`, `@fusion/dashboard`, and `@runfusion/fusion` (CLI) easier to navigate: split oversized modules into domain-named folders, continue established extraction patterns (TaskStore facade, route registrars, merger satellites, App hooks-first), and graduate files under the 2,000-line line-count ratchet without re-ratcheting ceilings as a substitute for structure. + +--- + +## Problem Frame + +Individual packages already have clear *package* boundaries, but *inside* the large packages navigation is hard. Flat `src/` trees and multi-thousand-line god-files force every change through the same monofiles. **Measure live LOC, not only baseline ceilings** — the ratchet file is often stale high *or* under-grown. Current worst offenders: + +| Area | Examples (approx live / baseline ceiling) | +|------|-------------------------------------------| +| Core | `store.ts` ~2.5k thin facade (baseline still 17,371 — already extracted) · `types.ts` ~8k · `db.ts` ~278 stub (baseline 5,924 stale) · debt in `task-store/remaining-ops-1..10` | +| Engine | `executor.ts` ~19k · `merger.ts` ~12k · `self-healing.ts` ~12k · `agent-heartbeat.ts` ~5.5k | +| Dashboard | `app/api/legacy.ts` ~11.5k · large components 3–5k · residual `routes.ts` / `register-git-github.ts` | +| CLI | `extension.ts` ~5.5k · `dashboard-tui/app.tsx` ~4.6k | + +The repo already *wants* this outcome: `scripts/check-file-line-count.mjs` caps new files at 2,000 and ratchets grandfathered files downward; FNXC history on that script states wholesale god-file shrink is dedicated follow-up work. Prior work established templates (`docs/plans/2026-06-24-001-refactor-dashboard-app-tsx-module-breakup-plan.md` completed; `packages/core/src/task-store/`; `packages/dashboard/src/routes/register-*`; `merger-*.ts`; `auto-recovery-handlers/`). The remaining problem is incomplete application at scale—plus intermediate debt such as `task-store/remaining-ops-1..10` (and `*-ops-2` ordinal dumps) that recreated opacity under a new name. + +**Glossary:** A **unit** (U1–U9) is a roadmap work item. A **wave** is one landable PR/slice (a unit may contain multiple waves). A **slice** is an ordered peel inside a unit (e.g. U4 Slice A then B). R6/R13 “wave’s file set” means the landable PR’s files. + +This plan is **not** a product feature, public API redesign, or cross-package ownership redraw. Success is navigability + ratchet progress + green behavior contracts. + +--- + +## Requirements + +### Behavior and contracts + +- R1. All waves are behavior-preserving: no intentional functional, UX, lifecycle, or API-contract change. Compile-only fixes limited to import paths and re-exports; no access-modifier, async/sync, or control-flow “cleanups” while organizing. +- R2. Public package barrels stay stable: `@fusion/core`, `@fusion/engine`, dashboard `routes.ts` entry, `app/api.ts`, merger/self-healing/executor primary module paths used by tests. Temporary re-export shims are allowed; permanent dual homes are not. +- R3. Characterization oracles for each touched god-file stay green. Each unit names an **oracle family** (paths or suite prefixes); the implementer finalizes the exact file list for the symbols moved in that wave. File-scoped verification is required; for pure moves, also run the god-file’s highest-signal existing suites, not only tests that reverse-import empty new files. +- R4. FNXC requirement comments move with the owning behavior (dated, greppable). Facades must not keep the only copy of requirement text after extraction. + +### Structure and ratchet + +- R5. Every **new** file is ≤ 2,000 lines. Prefer domain-named modules and folders; **forbid** new `remaining-ops-N`, `*-ops-N` / `*-2` ordinal dumps, `*-misc.ts`, or similarly opaque files. Oversize domains split by subdomain name, not ordinal. +- R6. Every **touched** grandfathered file’s baseline ceiling ratchets **down** (or the entry is removed when under cap) after a peel. Graduation uses a reviewed baseline edit that **must not increase** any path’s ceiling outside the wave’s file set — **except** U1’s one-time scoped re-ratchet of organic growth (KTD10). +- R7. Folder organization follows existing conventions per package layer (see KTDs)—extend patterns already in-tree rather than inventing a new global layout. + +### Safety invariants + +- R8. Route registrar **mount order** is frozen unless a wave’s goal is explicitly a documented reordering with precedence characterization. Splits must not reorder registrars as a side-effect. +- R9. Dashboard lazy-view inventory (`lazy()` non-underscore consts + AGENTS “Lazy-Loaded Heavy Views” + `lazy-loaded-views-docs.test.ts`) is untouched by organize waves. Eager FOUC CSS imports stay at the documented site. +- R10. Static `@fusion/*` imports only; no dynamic engine import tricks; no React/CSS re-exports from server plugin barrels. +- R11. Cross-package direction preserved: core ↛ engine/dashboard; engine DI via existing `setCreateFnAgent` (and similar) only. +- R12. No changeset for pure internal refactors of private packages. `@runfusion/fusion` gets a changeset only if a wave changes published CLI surface (not expected). + +### Wave hygiene + +- R13. Before landing a hot-file wave, rebase/sync main and apply the **extract-vs-semantics protocol**: keep the structural call site; **port main’s semantics into the new module**; never `--ours`/`--theirs`; run the union of both sides’ targeted suites. +- R14. `pnpm check:line-count` is a hard wave exit criterion (even though it is opt-in for normal pretest). +- R15. Existing `remaining-ops-*` modules are only allowed as **migration sources** to domain-named targets; each wave that touches them must shrink or delete at least one, never add another. + +--- + +## Key Technical Decisions + +- KTD1. **Program shape = multi-wave roadmap, not one mega-PR.** Waves are file-disjoint where possible, landable independently, and ordered so core symbol moves complete before dependent engine/dashboard rebases. Prefer many small extractions over one un-rebaseable split. +- KTD2. **Reuse five extraction patterns already proven in-repo** (pick by surface; do not invent a sixth style): + 1. **Facade + `*Impl`** — `TaskStore` / `task-store/` (public class stays; bodies move). + 2. **Registrar + shared context** — `routes.ts` + `routes/register-*` + `ApiRoutesContext`. + 3. **Satellite modules + re-export** — `merger.ts` → `merger-*.ts` (deep test mocks keep parent path). + 4. **Hooks-first UI** — completed App.tsx playbook: hooks, utils, presentational components, transient shims. + 5. **Handler folder + dispatcher** — policy trees (`auto-recovery-handlers/`) when methods cluster by reconcile domain (self-healing). +- KTD3. **Domain-named modules only; ban extending `remaining-ops` and ordinal dump suffixes.** Intermediate numeric dump files are technical debt. New extracts must name the domain (`merge-queue`, `archive-lifecycle`, `workflow-definitions`, `executor/prompt`, `self-healing/workspace`, etc.). When a domain exceeds 2,000 lines, split by **subdomain** (`merge-queue-lease.ts`, `merge-queue-cleanup.ts`), never by ordinal (`*-ops-2`, `*-2`, `remaining-*`). Existing `remaining-ops-*` and `*-ops-2` / `*-2` files are **migration sources only**, not templates. Migrating *out of* them is in-scope for core waves. +- KTD4. **Import stability strategy:** package barrels and established deep entrypoints are permanent. Mechanical moves keep temporary re-exports on the old path until a follow-up re-point wave. Track shims in each landable PR description as `old path → new path → delete-when` (no separate standing ledger file). Drop shims only when consumers are re-pointed; do not leave dual homes indefinitely. +- KTD5. **Graduation policy is ratchet-down-only after U1.** Prefer hand-editing `scripts/line-count-baseline.json` for touched paths over full `--update`. If full `--update` is used, the PR must show no ceiling increases outside the wave’s file set. +- KTD6. **Facade vs dump metrics.** For `store.ts`, the facade is already ~2.5k thin wrappers — primary progress is **domain migration of `remaining-ops-*` / ordinal dumps**, not re-fighting a 17k monofile. Elsewhere, facade/parent LOC decline remains first-class: satellites under cap while the parent stays huge is partial progress only. Thin shells may stay slightly over 2,000 only when explicitly marked and on a path toward graduation. +- KTD7. **Characterization-first on engine hot files** (executor, self-healing, merger, heartbeat). Pin existing focused suites green before moves; pure free functions peel first (lowest risk); class method clusters second. +- KTD8. **Production-first; test mega-files co-located only when required.** Test splits use harness + sibling suites (FN-7035 style) when a production split forces co-located test moves, or when a wave’s navigation goal includes a specific suite. Bulk “split all 55 test offenders” is deferred follow-up. +- KTD9. **Cycle budget:** impl/domain modules may import pure helpers and types; facade may import impls; domain impls must not re-enter the same facade methods that would re-import the domain (except documented DI seams). Prefer passing narrow deps over `import { TaskStore } from "../store.js"` cycles. +- KTD10. **Baseline hygiene is wave zero (two operations).** (1) **Prune / tighten stale-high** entries where live LOC is far below ceiling (`db.ts`, `store.ts`, graduated files). (2) **One-time scoped re-ratchet** of paths that already grew past their ceiling (e.g. executor, self-healing, types, legacy, extension) to current live counts — FN-7046-style repair so `pnpm check:line-count` is truthful before peels. After U1, ceilings never rise again except by mistake (forbidden); peels only ratchet down. + +--- + +## High-Level Technical Design + +### Wave lifecycle (every unit) + +```mermaid +flowchart LR + A[God file on baseline] --> B[Domain extract ≤2000] + B --> C[Facade / re-export shims] + C --> D[Oracle suites + typecheck] + D --> E{Under 2000?} + E -->|yes| F[Ratchet-down baseline] + E -->|no| G[Next domain peel] + D --> H[Main churn] + H --> I[Port semantics into new home] + I --> D +``` + +### Pattern selection by package layer + +```mermaid +flowchart TB + subgraph Core + TS[types.ts] -->|domain barrels| TDir[types/*.ts] + ST[store.ts facade] -->|named *Impl| TStore[task-store/domain-*.ts] + end + subgraph Engine + EX[executor.ts] -->|pure first then lifecycle| EDir[executor/*.ts] + SH[self-healing.ts] -->|reconcile clusters| SHDir[self-healing/*.ts] + MG[merger.ts] -->|continue satellites| MSat[merger-*.ts] + HB[agent-heartbeat.ts] -->|prompts + helpers| HBSat[agent-heartbeat-*.ts] + end + subgraph Dashboard + LG[app/api/legacy.ts] -->|domain client API| Api[app/api/*.ts] + RT[routes.ts] -->|more registrars| Reg[routes/register-*.ts] + UI[Large views] -->|hooks-first| Hooks[app/hooks + components] + end + subgraph CLI + EXT[extension.ts] -->|tool families| Tools[extension/*.ts] + TUI[dashboard-tui/app.tsx] -->|views| TuiViews[dashboard-tui/views] + end +``` + +### Target folder sketches (directional — implementer may adjust names) + +```text +packages/core/src/ + types/ # domain type modules; types.ts becomes barrel + task-store/ # existing; replace remaining-ops-* with domain names over waves +packages/engine/src/ + executor/ # pure helpers, dispatch, worktree, step-session, completion + self-healing/ # startup, dependency, in-review, merge, workspace, surfacing +packages/dashboard/app/api/ + tasks.ts agents.ts settings.ts missions.ts ... # client fetch surface +packages/cli/src/ + extension/ # tool-family registrars + commands/dashboard-tui/views/ +``` + +Public import paths remain the parent module or package barrel until shim cutover. + +### Hot-file conflict protocol (R13) + +On rebase/merge conflict for an extracted block: + +1. Keep the **new structure** (call site / helper path). +2. Port **incoming semantics** into the helper’s body (not the old monofile copy). +3. Never resolve with `--ours` / `--theirs` alone. +4. Re-run the **union** of both branches’ oracle suites + package typecheck. +5. Fix FNXC/docs the move falsified. + +Reference: `docs/solutions/best-practices/merge-conflict-extraction-vs-semantics-and-parallel-bootstrap.md`. + +### Graduation baseline edit (R6 / KTD5) + +Preferred: hand-edit only keys for files this wave touched (lower ceiling or delete entry). +If `node scripts/check-file-line-count.mjs --update` is used: diff `scripts/line-count-baseline.json` and reject any ceiling **increase** for paths outside the wave. + +--- + +## Scope Boundaries + +**In scope** + +- Behavior-preserving organization inside `packages/core`, `packages/engine`, `packages/dashboard`, `packages/cli`. +- Domain folders, god-file splits, facade/shim stability, ratchet graduation, baseline hygiene. +- Migrating `task-store/remaining-ops-*` toward named domains. +- Co-located test harness/sibling splits when forced by production moves or when a wave explicitly targets a suite. + +**Out of scope (non-goals)** + +- Cross-package ownership redraws (e.g. moving engine logic into core or vice versa beyond existing DI). +- Public product features, UX redesign, settings schema changes. +- Plugin packages (`plugins/*`) except incidental import-path fixes if a consumer path breaks. +- Replacing the line-count cap policy or moving `check:line-count` into the merge gate (optional future). +- Wholesale split of all 55 oversized test files as a standalone campaign. +- Semantic “cleanups” bundled into organize waves (error-path merges, backendMode guard changes, soft-delete matrix edits). + +### Deferred to Follow-Up Work + +- Full test mega-file campaign (all oversized `*.test.ts(x)`). +- Permanent shim removal waves after consumer re-points (PR description `old → new → delete-when` history). +- Thin-shell graduation of residual facades that remain slightly over 2,000 after domain peels. +- Compounding a `docs/solutions/architecture-patterns/` entry for the organization playbook after first waves land. +- `dev-server-*` vs `devserver-*` consolidation (`docs/dev-server-module-boundary-audit.md`) — naming hygiene, not god-file size. +- Re-point pure-function unit tests off App/merger re-export shims from prior plans. +- **Mid-tier production peels (pain-ranked, not this program’s top-N):** `mission-store.ts`, `central-core.ts`, `agent-store.ts`, `agent-tools.ts`, `project-engine.ts`, `scheduler.ts`, `triage.ts`, `github.ts`, `mission-routes.ts`, `cli/commands/dashboard.ts`, and other grandfathered files outside U2–U9. They stay on the ratchet until a follow-up plan. + +--- + +## Phased Delivery + +| Phase | Units | Intent | +|-------|-------|--------| +| A — Foundations | U1 | Conventions, baseline truth, scoreboard | +| B — Core | U2, U3 | Types navigability + TaskStore domain peels | +| C — Engine | U4, U5, U6 | Executor / self-healing / merger+heartbeat | +| D — Surfaces | U7, U8, U9 | Dashboard API/routes · UI monofiles · CLI peels | + +Phases B–D can partially overlap only when file ownership is disjoint **and** no shared symbol is mid-move. Default: finish a core symbol wave before engine rebases that depend on it. U6 may land merger and heartbeat as **separate waves** with independent exits. + +--- + +## Implementation Units + +### U1. Foundations: conventions, baseline hygiene, wave checklist + +- **Goal:** Make the program’s rules and scoreboard accurate before large peels; give every later unit a shared exit checklist. +- **Requirements:** R5, R6, R10, R14, R15 (policy); R12; KTD10 +- **Dependencies:** None +- **Files:** + - `scripts/line-count-baseline.json` (modify — prune stale-high + one-time re-ratchet organic growth per KTD10) + - `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md` (this plan — already present) +- **Approach:** + - Measure live LOC for all baseline entries. + - **Prune/tighten stale-high** (`db.ts`, `store.ts` → actual ~2.5k, any graduated ≤2,000 removed). + - **One-time scoped re-ratchet** of paths already over ceiling (executor, self-healing, types, legacy, extension, …) to current live counts so the check is truthful. + - After U1, no further ceiling increases; peels only ratchet down. + - Encode the per-wave checklist (below) into implementer practice for U2+. +- **Patterns to follow:** FNXC history in `scripts/check-file-line-count.mjs`; FN-7046/7050 scoped baseline repairs. +- **Test scenarios:** + - Happy path: after baseline edit, `pnpm check:line-count` is green against the truthful scoreboard. + - Edge: JSON diff shows only intentional prune/tighten and documented re-ratchets; no silent ceiling creep on untouched paths beyond the U1 growth list. + - Error: removing an entry still >2,000 without a split fails the check — keep grandfathered until peeled. +- **Verification:** `pnpm check:line-count` green; baseline reflects live sizes. +- **Per-wave checklist (apply U2–U9):** + 1. Oracle family for the wave green (unit-specific). + 2. Package typecheck for touched package(s). + 3. Mock-path grep for moved modules (`vi.mock`, deep relative imports). + 4. For core graph moves: re-audit `packages/core/src/index.gate.ts` / engine-core gate consumers if barrel closure changes. + 5. Route mount order unchanged (dashboard). + 6. Lazy inventory untouched (dashboard UI). + 7. FNXC ownership map: comments live with behavior. + 8. New files ≤2,000; touched baseline ceilings ratchet **down** only (post-U1). + 9. Hot-file conflict protocol if rebased against main. + 10. PR description lists shims: `old path → new path → delete-when`. + +--- + +### U2. Core: split `types.ts` into domain barrels + +- **Goal:** Make domain types findable under `packages/core/src/types/` while `types.ts` remains the stable re-export surface (including the **dashboard Vite `@fusion/core` browser alias**). +- **Requirements:** R1, R2, R5, R6, R10, R11 +- **Dependencies:** U1 +- **Files:** + - `packages/core/src/types.ts` (modify → thin barrel; **must remain** the browser-safe alias target) + - `packages/core/src/types/*.ts` (new — domain clusters: e.g. task/board, agent/permission, merge/settings, workflow/work-item, messaging — names chosen by natural export clusters) + - `packages/core/src/index.ts` (modify only if re-export paths require it; prefer stable) + - `packages/core/src/index.gate.ts` (verify closure) + - Oracles: core typecheck; dashboard package typecheck/build (Vite alias surface); existing type-importing suites +- **Approach:** Peel by domain clusters already partially started (`planner-overseer-state`, capacity, gitlab-config, mcp-config re-exports). Keep every symbol the dashboard imports via the Vite alias as **type-only or pure/browser-safe** re-exports on `types.ts` — never pull Node-only stores into that surface. Prefer pure type/interface/const moves into domain modules. +- **Execution note:** Low risk — still run core + dashboard typecheck so the browser alias contract is proven. +- **Patterns to follow:** Existing partial peels re-exported from `types.ts` (see FNXC on `types.ts` for Vite alias); package barrel style in `index.ts`. +- **Test scenarios:** + - Happy path: every previously exported symbol remains importable from `@fusion/core` / `./types.js` with identical types. + - Edge: circular type imports between domains — resolve via shared `types/common` or one-way deps only; browser alias still resolves without Node-only deps. + - Integration: engine and dashboard packages typecheck against the barrel; dashboard client build does not pull store/engine into the browser bundle via the alias. +- **Verification:** Core + dashboard typecheck green; baseline ceiling for `types.ts` ratchets down; new type modules ≤2,000 each. + +--- + +### U3. Core: first domain peels out of `remaining-ops-*` (TaskStore facade) + +- **Goal:** Land **first domain peels** from opaque `remaining-ops-*` (and related ordinal dumps) into domain-named `task-store/` modules; eliminate ≥1 dump file and shrink the dump surface. The facade stays on `TaskStore` and already mostly delegates — further facade thinning is secondary. +- **Requirements:** R1–R4, R5–R7, R11, R13–R15 +- **Dependencies:** U1; prefer U2 landed if types move affect store signatures +- **Files:** + - `packages/core/src/store.ts` (modify only as import/wrapper thinness requires) + - `packages/core/src/task-store/*` (create domain modules; delete emptied `remaining-ops-N.ts` / ordinal dumps when fully migrated) + - `packages/core/src/task-store/index.ts` (update module map FNXC) + - Oracles: `packages/core/src/__tests__/store-settings.test.ts` plus soft-delete / move / merge-queue suites under `packages/core/src/__tests__/` matching the domain peeled + - `scripts/line-count-baseline.json` (ratchet touched paths) +- **Approach:** + - Publish a **first-wave symbol map** before coding: functions, current dump file, target module, co-moved callees (especially cross-dump imports). + - Prefer one cohesive domain first (e.g. merge-queue **or** workflow-definitions). + - Move `*Impl` functions; update facade imports; delete empty dump files. + - Do not add `remaining-ops-11` or `*-ops-3`; split by subdomain name if over 2,000. + - Remaining dumps continue in follow-up waves of this unit or a later plan — U3 is **not** “finish all ten dumps in one land.” +- **Execution note:** Characterization-first: green the relevant store suites **before** each peel; after peel re-run same suites. Rebase protocol if `store.ts` / dump files conflict with main. +- **Patterns to follow:** Existing `moves.ts`, `file-scope.ts`, `lifecycle*.ts` style; `task-store/index.ts` FNXC decomposition notes. +- **Test scenarios:** + - Happy path: suites for the peeled domain still pass (e.g. merge-queue lease/enqueue if that domain). + - Edge: soft-delete write blocks, dependency cycle rejection still pass when those symbols move. + - Error: TaskNotFound / soft-deleted write paths still throw the same error classes. + - Integration: no new store↔impl import cycle; gate bundle still loads if core entry changes. +- **Verification:** Oracles green; ≥1 `remaining-ops-*` (or ordinal dump) fully eliminated; dump LOC net-down; baseline ratchet-down; `pnpm check:line-count`. + +--- + +### U4. Engine: peel `executor.ts` pure helpers, then first lifecycle folders + +- **Goal:** Introduce `packages/engine/src/executor/` (or satellite `executor-*.ts` if flatter is clearer for first slice) for free-function clusters already at module top; then extract 1–2 lifecycle clusters from `TaskExecutor` without changing runtime behavior. +- **Requirements:** R1–R4, R5–R7, R10, R13, R14 +- **Dependencies:** U1; U3 if store symbols used by peels moved (usually independent) +- **Files:** + - `packages/engine/src/executor.ts` (modify — re-export + thinner body) + - `packages/engine/src/executor/*.ts` or `executor-*.ts` (new) + - Oracles: `packages/engine/src/__tests__/executor-*.test.ts` focused set for peels (prompt/refusal/requeue signature/worktree/task-done as applicable); helpers in `executor-test-helpers.ts` + - `scripts/line-count-baseline.json` +- **Approach:** + - **Slice A (required first):** pure exports — prompt/refusal/requeue signatures, browser probe, workflow feedback path helpers — move with re-exports from `executor.ts`. + - **Slice B:** one lifecycle cluster (e.g. worktree acquisition **or** step-session **or** completion handoff) only after Slice A green. + - Keep public symbols re-exported from `executor.ts` for deep `vi.mock("../executor.js")` stability. +- **Execution note:** Characterization-first. Do not change dissent patterns, refusal classification, or requeue constants “while here.” +- **Patterns to follow:** Merger satellite re-export pattern; workflow-native primitives split (policy vs side effects) in `docs/solutions/architecture-patterns/workflow-native-runtime-primitives.md`. +- **Test scenarios:** + - Happy path: existing executor suites for moved pure functions assert same outputs for same inputs. + - Edge: requeue loop signature progress detection unchanged; no-commit eligibility heuristics unchanged. + - Error: invalid assistant continuation and transient missing task.json classifiers unchanged. + - Integration: step-session / task-done paths still pass focused executor tests after lifecycle peel. +- **Verification:** Oracles green; `executor.ts` ceiling ratchets down; new modules ≤2,000; check:line-count. + +--- + +### U5. Engine: split `self-healing.ts` into domain handler modules + +- **Goal:** Organize `SelfHealingManager` reconcile/recover/surface methods into `packages/engine/src/self-healing/` (or `self-healing-*.ts`) by domain so AGENTS run-audit inventory maps to files. +- **Requirements:** R1–R4, R5–R7, R13, R14 +- **Dependencies:** U1; prefer after U4 if shared helpers overlap, else independent +- **Files:** + - `packages/engine/src/self-healing.ts` (facade / re-exports / manager orchestration) + - `packages/engine/src/self-healing/*.ts` (new — e.g. startup, dependency, in-review, merge-status, workspace, surfacing, temp-worktree) + - Oracles: per domain peel, name 1–3 focused files (`self-healing-*.test.ts`, `in-review-unmet-dependency-reconcile.test.ts`, workspace reconcile tests, meta-archive guards) **or** a specific describe block inside `self-healing.test.ts` — do not treat “whole 9k suite optional” as zero coverage. + - Optionally start a sibling suite only if extracting forces test navigability for a cluster + - `scripts/line-count-baseline.json` +- **Approach:** Mirror `auto-recovery-handlers/` (pattern 5): group methods by reconcile family; inject manager deps rather than free-floating globals; keep `SelfHealingManager` public class on the stable path or re-exported. Preserve existing cycle-break patterns (e.g. dynamic merger import, workspace-land predicates). +- **Execution note:** Characterization-first; hot-file rebase protocol. Do not merge distinct recovery outcomes into one branch. +- **Patterns to follow:** `auto-recovery-handlers/`; AGENTS run-audit event inventory as domain map. +- **Test scenarios:** + - Happy path: representative reconcile methods still emit expected run-audit event names/metadata shapes (ids/counts only). + - Edge: `autoMerge:false` / user-paused / triple-proof no-action paths still no-op. + - Error: unrecoverable parks remain parked; budgets not reset incorrectly. + - Integration: startup recovery vs steady-state sweep distinction preserved. +- **Verification:** Focused self-healing oracles green; line-count ratchet; manager file shrinks. + +--- + +### U6. Engine: continue merger satellites + heartbeat peel + +- **Goal:** Further reduce `merger.ts` and `agent-heartbeat.ts` via additional satellites (verification/autostash/commit ceremony leftovers; heartbeat prompts + recovery helpers). +- **Requirements:** R1–R4, R5–R7, R13, R14 +- **Dependencies:** U1; can parallel U4/U5 if file-disjoint +- **Files:** + - `packages/engine/src/merger.ts` + new/extended `merger-*.ts` + - `packages/engine/src/agent-heartbeat.ts` + `agent-heartbeat-prompts.ts` / helper satellites + - Oracles: `merger-merge-lifecycle`, `merger-verification`, `merger-conflict-resolution`, `merger-diff-scope` (and helpers); `heartbeat-monitor`, `heartbeat-executor`, `heartbeat-error-recovery`, `agent-heartbeat-*` + - `scripts/line-count-baseline.json` +- **Approach:** Same satellite + re-export pattern already used for merger AI/diff-volume/overlap/etc. Heartbeat: peel prompt constants and pure recovery helpers first. +- **Patterns to follow:** Existing `merger-*.ts` set; heartbeat pure helpers near file bottom already half-extracted. +- **Test scenarios:** + - Happy path: merge lifecycle + verification suites green. + - Edge: diff-volume gate and overlap guard behavior unchanged. + - Error: conflict classification and transient merge error classifier paths unchanged. + - Heartbeat: error-recovery budget metadata and park reasons unchanged. +- **Verification:** Oracles green; both baselines ratchet down; new files ≤2,000. + +--- + +### U7. Dashboard API: split `legacy.ts` client surface + continue route peels + +- **Goal:** Make client API and server route registration navigable: domain modules under `app/api/`; further split oversized registrars (`register-git-github.ts`, residual helpers in `routes.ts`) without changing mount order. +- **Requirements:** R1–R4, R5–R9, R13, R14 +- **Dependencies:** U1; independent of engine units if no shared symbol moves +- **Files:** + - `packages/dashboard/app/api/legacy.ts` → domain modules + **re-export shims on `legacy.ts`** + - `packages/dashboard/app/api.ts` (barrel stability — continues `export *` from legacy) + - `packages/dashboard/src/routes.ts`, `packages/dashboard/src/routes/register-*.ts`, `packages/dashboard/src/routes/README.md` (module map update) + - Oracles: `packages/dashboard/app/api/__tests__/legacy-*.test.ts`, `packages/dashboard/src/__tests__/routes-*.test.ts`, `routes/__tests__/register-*.test.ts` + - `scripts/line-count-baseline.json` +- **Approach:** + - Client: group exports by domain (tasks, agents, settings/memory, missions, git/github, remote, etc.). **Freeze both** `app/api.ts` and `app/api/legacy.ts` as stable import paths until a named re-point wave — many hooks deep-import `api/legacy` today. Domain modules are implementation only. + - Server: peel remaining residual handlers into registrars; split `register-git-github.ts` into git plumbing vs GitHub issue/PR if still oversized; **do not reorder mounts**. +- **Patterns to follow:** `packages/dashboard/src/routes/README.md`; `app/api.ts` re-export style. +- **Test scenarios:** + - Happy path: helpers still produce same paths/methods via `api.ts` **and** `api/legacy` imports; route registration tests pass. + - Edge: project-scoping helpers (`getScopedStore`) still applied on moved handlers. + - Error: `ApiRequestError` / duplicate-candidate error classes still thrown as today. + - Integration: mount-order sensitive routes still match the more-specific registrar first (spot-check overlapping patterns if any). +- **Verification:** Oracles green; ceilings ratchet; README registrar map updated; mount order unchanged; both client entrypaths still resolve. + +--- + +### U8. Dashboard UI monofile peels (App.tsx playbook) + +- **Goal:** Apply the completed App.tsx playbook to **1–2** highest-pain UI monofiles chosen at execution by live LOC + ownership (candidates: `AgentDetailView`, `TaskDetailModal`, `WorkflowNodeEditor`, `ChatView`, `MissionManager`). +- **Requirements:** R1–R5, R6, R7, R9, R14 +- **Dependencies:** U1; prefer U7 first if those components tightly couple to API module paths mid-move +- **Files:** + - Selected `packages/dashboard/app/components/*.tsx` + new hooks/subcomponents under `app/hooks/` / `app/components/` + - Component `__tests__/*` oracles for those surfaces + - `scripts/line-count-baseline.json` +- **Approach:** Hooks-first + presentational extract; preserve lazy inventory; transient re-export shims only if pure helpers are currently imported from the monofile. Do **not** include CLI work here (see U9). +- **Execution note:** Browser smoke after build when visual shell/modals move (worktree-safe; never port 4040). +- **Patterns to follow:** Completed App.tsx plan (`docs/plans/2026-06-24-001-refactor-dashboard-app-tsx-module-breakup-plan.md`). +- **Test scenarios:** + - Happy path: component behavior contracts for extracted surfaces remain green. + - Edge: mobile + desktop breakpoints covered by existing tests where present. + - Error: modal error/empty states unchanged. +- **Verification:** Oracles green; selected UI files under or toward 2,000; check:line-count; lazy inventory test untouched/green. + +--- + +### U9. CLI: extension tool-family peels + dashboard-tui views + +- **Goal:** First-wave CLI organization: peel `extension.ts` into tool-family modules and/or continue `dashboard-tui/app.tsx` into views — land as separate waves with independent exits. +- **Requirements:** R1–R5, R6, R7, R14 +- **Dependencies:** U1; independent of U8 +- **Files:** + - `packages/cli/src/extension.ts` → `packages/cli/src/extension/*.ts` with re-exports from `extension.ts` + - `packages/cli/src/commands/dashboard-tui/app.tsx` → views/components under `dashboard-tui/` + - Oracles: `packages/cli/src/__tests__/extension.test.ts`, `extension-task-tools.test.ts`, `dashboard-tui/__tests__/*` as matching the peel + - `scripts/line-count-baseline.json` +- **Approach:** Registrar composition like routes — `extension.ts` stays the public entry. TUI continues the existing controller/state folder split into views. Either target may land alone; both are not required for unit close. +- **Patterns to follow:** Dashboard route registrars; existing `dashboard-tui/controller.ts` + `state.ts`. +- **Test scenarios:** + - Happy path: tool registration still exposes the same tool names/schemas; TUI behavior contracts green for extracted views. + - Edge: extension host mock paths / `vi.mock` of extension entry still resolve via re-exports. + - Error: command failure messaging unchanged for peels that touch error surfaces. +- **Verification:** Oracles green for the landed wave(s); entry file(s) ratchet down; check:line-count. + +--- + +## Risk Analysis & Mitigation + +| Risk | Mitigation | +|------|------------| +| Silent semantic revert on rebase | Hot-file protocol (R13); never take a single merge side | +| `remaining-ops` opacity recreates itself | Ban new dumps (R15, KTD3); delete sources as domains land | +| Full `--update` re-grandfathers growth | Ratchet-down-only policy (KTD5) | +| Facade never shrinks | KTD6 metric; thin-wrapper collapse required | +| False green from too-narrow file-scoped tests | Oracle lists per unit; run god-file’s known suites | +| Circular imports explode | KTD9 cycle budget; prefer deps injection | +| Mount-order / lazy inventory breakage | R8, R9 explicit freezes | +| Merge gate misses non-blocking suite failures | Treat named oracles as required even if non-gate | +| Parallel agents on same monofile | File-disjoint waves; mass-migration fleet lesson | + +--- + +## System-Wide Impact + +- **Developers / agents:** Primary beneficiaries — shorter files, domain folders, clearer ownership. +- **CI / ratchet:** Baseline file changes in graduation PRs; opt-in check becomes required for these waves. +- **Runtime users:** No intentional product change; residual risk is regression from incomplete port on rebase. +- **Published CLI:** Unchanged surface expected; private package refactors only. +- **Downstream tests/mocks:** Deep relative `vi.mock` paths stay valid via re-exports until shim cutover. + +--- + +## Success Metrics + +- Navigability: top god-files either graduated (<2,000 and off baseline) or on a clear multi-domain folder map with continuous LOC decline. +- Ratchet: U1 makes the scoreboard truthful; post-U1 no wave increases any baseline ceiling outside its file set. +- Safety: zero intentional behavior changes; oracle families green per wave. +- Debt: net reduction in `remaining-ops-*` / ordinal dumps across U3 waves; PR-tracked shims shrink over time. + +--- + +## Open Questions + +### Deferred to implementation + +- Exact domain file names under `types/`, `executor/`, `self-healing/`, and which 1–2 UI monofiles U8 selects first (choose by live LOC + ownership at execution). +- Whether first executor layout is a folder (`executor/`) vs flat `executor-*.ts` satellites (both valid; folder preferred if ≥4 modules). +- Which first domain U3 peels (merge-queue vs workflow-definitions vs other) after the symbol map. +- How aggressively to split `self-healing.test.ts` when peels land (only if suite blocks navigation for a cluster). + +### Resolved defaults (from planning) + +- Multi-wave program across core → engine → dashboard/cli. +- Both folder organization and file splits. +- Production-first; tests co-located when needed. +- Mechanical moves only; characterization-first on engine hot files. + +--- + +## Documentation Plan + +- Update `packages/dashboard/src/routes/README.md` registrar map when registrars change (U7). +- Update `packages/core/src/task-store/index.ts` FNXC module map as domains replace `remaining-ops` (U3). +- After first successful waves, run `/ce-compound` to capture the organization playbook under `docs/solutions/architecture-patterns/` (currently a documented gap). +- Do not expand AGENTS package structure section unless a permanent convention (e.g. `executor/` folder) becomes the default—prefer solutions entry first. + +--- + +## Sources & Research + +- Repo patterns: TaskStore `task-store/`, routes registrars + README, merger satellites, App.tsx completed plan, auto-recovery-handlers, line-count ratchet script FNXC history. +- Institutional: extract-vs-semantics merge conflicts; workflow-native runtime primitives; characterization / surface matrix lessons; plugin registration drift / mock graph hazards; lazy-load static import constraints. +- Prior audits: `docs/codebase-improvement-audit.md`, `docs/gap-analysis.md` (LOC numbers stale; hotspot list still directionally valid). +- External research: **skipped** — strong local patterns; no unsettled library/architecture choice. + +--- + +## Assumptions + +- Confirmed user scope: multi-wave, pain-ranked packages, folder + split, production-first. +- Implementers will land units as multiple PRs/commits as needed; the plan does not prescribe git choreography. +- Engine-core gate membership may need awareness when moving symbols that gate tests import, but gate *policy* is unchanged. diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 795c7f909b..8263b17cb4 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -101,7 +101,7 @@ import { applyLegacyWorkflowStepOverridesImpl, applyTaskPatchImpl, archiveDbImpl import { clearNearDuplicateReferencesToFailSoftImpl, clearWorkflowRunStepInstancesImpl, computeMovedSettingsTargetWorkflowIdsImpl, ensureBranchGroupForSourceImpl, ensurePrEntityForSourceImpl, findRecentTasksByContentFingerprintImpl, getActiveMergingTaskImpl, getActivePrEntityBySourceImpl, getBranchGroupByBranchNameImpl, getBranchGroupBySourceImpl, getBranchGroupImpl, getBranchProgressByTaskImpl, getMutationsForRunImpl, getPrEntityByNumberImpl, getPrEntityImpl, getPrThreadStateImpl, getTasksByAssignedAgentImpl, getWorkflowPromptOverridesAsyncImpl, getWorkflowSettingValuesAsyncImpl, getWorkflowSettingValuesImpl, getWorkflowSettingsProjectIdImpl, getWorkflowWorkItemImpl, insertCompletionHandoffWorkflowWorkAuditImpl, listActivePrEntitiesImpl, listBranchGroupsImpl, listPrThreadStatesImpl, listTasksByBranchGroupImpl, listWorkflowSettingValuesForProjectImpl, loadWorkflowRunBranchesImpl, loadWorkflowRunStepInstancesImpl, mergeCustomFieldPatchImpl, normalizeMergeRequestStateImpl, normalizeWorkflowWorkItemKindImpl, normalizeWorkflowWorkItemStateImpl, parseWorkflowPromptOverrideJsonImpl, recordPrThreadOutcomeImpl, resetAllStepsToPendingImpl, resetPromptCheckboxesImpl, resolveWorkflowMoveActorImpl, resolveWorkflowSettingDeclarationsImpl, saveWorkflowRunStepInstanceImpl, transitionMergeRequestStateImpl, transitionWorkflowWorkItemSyncImpl, updateTaskImpl, updateWorkflowPromptOverridesImpl, upsertMergeRequestRecordImpl, workflowStateForMergeRequestStateImpl } from "./task-store/remaining-ops-6.js"; import { addPrInfoImpl, addSteeringCommentImpl, archiveAllDoneImpl, cleanupStaleMergeQueueRowsImpl, clearCompletionHandoffAcceptedMarkerImpl, clearDoneTransientFieldsImpl, clearStaleExecutionStartBranchReferencesImpl, computeWorkflowColumnsGraduationReportImpl, deleteTaskCommentImpl, deleteTaskDocumentImpl, emitUsageEventImpl, enqueueMergeQueueImpl, getAgentLogCountImpl, getAgentLogsImpl, getArtifactImpl, getArtifactsImpl, getAttachmentImpl, getCompletionHandoffAcceptedMarkerImpl, getTaskDocumentImpl, getTaskDocumentRevisionsImpl, getTaskDocumentsImpl, insertArtifactRowImpl, linkGithubIssueImpl, listWorkflowWorkItemsForTaskSyncImpl, moveToDoneImpl, parseDependenciesFromPromptImpl, parseFileScopeFromPromptImpl, parseStepsFromPromptImpl, peekMergeQueueHeadImpl, peekMergeQueueImpl, readPreArchiveColumnFromTaskFileImpl, recordPluginActivationImpl, recordRunAuditEventBackendImpl, removePrInfoByNumberImpl, resolvePrimaryPrInfoImpl, resolveUnarchiveTargetColumnImpl, rewriteLineageChildrenForRemovalImpl, runGitCommandImpl, stopWatchingImpl, syncAgentTaskLinkOnReassignmentImpl, updateArtifactImpl, updateGithubTrackingImpl, updatePrInfoByNumberImpl, updateTaskCommentImpl, upsertPrInfoByNumberImpl, writeArtifactDataImpl } from "./task-store/remaining-ops-7.js"; import { approveCliAutonomyImpl, approveWorkflowCliCommandImpl, cleanupOrphanedMaterializedStepsImpl, consumePluginGateVerdictsImpl, getAgentLogsByTimeRangeImpl, getDatabaseHealthImpl, getDistributedTaskIdAllocatorImpl, getExperimentSessionStoreImpl, getInReviewDurationEventsImpl, getMissionStoreImpl, getPluginStoreImpl, getSecretsStoreImpl, getSettingsSyncImpl, getTaskMergedTaskIdsImpl, getTaskWorkflowSelectionImpl, getVerificationCacheHitImpl, getWorkflowDefinitionImpl, healthCheckImpl, importLegacyAgentLogsOnceImpl, insertWorkflowDefinitionSyncImpl, isCliAutonomyApprovedImpl, isPluginInstalledImpl, isWorkflowCliCommandApprovedImpl, listWorkflowDefinitionsImpl, materializeExplicitWorkflowStepsImpl, materializeWorkflowStepsImpl, migrateActiveArchivedTasksToArchiveDbImpl, migrateLegacyArchiveEntriesToArchiveDbImpl, nextWorkflowDefinitionIdImpl, occupantsByColumnForWorkflowImpl, parseWorkflowLayoutImpl, pruneAgentLogFilesImpl, purgeTaskWorkflowSelectionRowsImpl, readAllWorkflowDefinitionsImpl, readRawProjectSettingsImpl, recordPluginGateVerdictImpl, recordVerificationCachePassImpl, removeMaterializedSelectionImpl, resolvePluginWorkflowStepImpl, resolveTaskWorkflowIrSyncImpl, revokeCliAutonomyImpl, selectTaskWorkflowAndReconcileImpl, writeTaskWorkflowSelectionImpl, getTaskWorkflowSelectionAsyncImpl, } from "./task-store/remaining-ops-8.js"; -import { getTaskCommitAssociationsByLineageIdImpl, replaceLegacyTaskCommitAssociationsImpl } from "./task-store/remaining-ops-9.js"; +import { getTaskCommitAssociationsByLineageIdImpl, replaceLegacyTaskCommitAssociationsImpl } from "./task-store/task-commit-associations.js"; import { addTaskCommentImpl, applyBuiltInPromptOverridesSyncImpl, areAllDependenciesDoneImpl, artifactStoredNameImpl, assertWorkflowIrTraitsValidImpl, clearActivityLogImpl, clearTaskWorkflowSelectionImpl, deleteTaskByIdImpl, getDefaultWorkflowIdImpl, getInsightStoreImpl, getMergeQueuedTaskIdsImpl, getMergeRequestRecordImpl, getMergeRequestRecordAsyncImpl, getResearchStoreImpl, getTaskIdFromDirImpl, getTodoStoreImpl, getWorkflowWorkItemByIdentityImpl, hasActiveTaskImpl, invalidateConfigCacheAfterMigrationImpl, isTaskIdConflictErrorImpl, listLegacyAutoMergeStampCandidatesImpl, readTaskRowFromDbImpl, recordBranchGroupMemberLandedImpl, refreshDatabaseHealthImpl, resolveEffectiveWorkflowIdSyncImpl, resolveTaskCustomFieldDefsSyncImpl, resolveWorkflowBypassGuardsImpl, serializeConfigForDiskImpl, setPluginWorkflowStepTemplatesImpl, shouldSkipWorkflowMovePoliciesImpl, suppressWatcherImpl, upsertTaskWithFtsRecoveryImpl } from "./task-store/remaining-ops-10.js"; import { getTaskSelectClauseImpl2, createTaskPersistSerializationContextImpl, getTaskPersistValuesImpl, getTaskPatchDescriptorsImpl, normalizeTaskFromDiskImpl, writeTaskJsonFileImpl, rowToPrEntityImpl, generatePrEntityIdImpl, readTaskForMoveImpl, rowToMergeQueueEntryImpl, rowToMergeRequestRecordImpl, rowToCompletionHandoffMarkerImpl, rowToWorkflowWorkItemImpl, rowToRunAuditEventImpl } from "./task-store/remaining-ops-3.js"; import { getTaskSelectClauseWithActivityLogLimitImpl, getChangedTaskColumnsImpl, getSoftDeletedWriteConflictImpl, readTaskJsonImpl, writeConfigImpl, _maybeAutoArchiveSameAgentDuplicateBackendImpl, updateBranchGroupImpl, updatePrEntityImpl, listTasksForGithubTrackingReconcileImpl, listTasksForGitlabTrackingReconcileImpl, renewCheckoutLeaseImpl, updateTaskAtomicImpl, getWorkflowPromptOverridesImpl, updateWorkflowSettingValuesImpl, cancelActiveWorkflowWorkItemsForTaskImpl, setCompletionHandoffAcceptedMarkerImpl, reconcileLegacyAutoMergeStampsImpl, recoverExpiredMergeQueueLeasesImpl, rewriteDependentsForRemovalImpl, cleanupBranchForTaskImpl, addAttachmentImpl, deleteAttachmentImpl, registerArtifactImpl, updatePrInfoImpl, unlinkGithubIssueImpl, cleanupArchivedTasksImpl, generatePromptFromArchiveEntryImpl, listWorkflowOccupantTaskIdsImpl, evacuateCustomColumnsToLegacyImpl, listApprovedCliAutonomyAdaptersImpl, closeImpl, getActivityLogImpl } from "./task-store/remaining-ops-2.js"; diff --git a/packages/core/src/task-store/remaining-ops-9.ts b/packages/core/src/task-store/task-commit-associations.ts similarity index 95% rename from packages/core/src/task-store/remaining-ops-9.ts rename to packages/core/src/task-store/task-commit-associations.ts index 116dc8e145..9f3fc95cd9 100644 --- a/packages/core/src/task-store/remaining-ops-9.ts +++ b/packages/core/src/task-store/task-commit-associations.ts @@ -1,10 +1,13 @@ /** - * remaining-ops-9 operations. + * Task commit-association persistence (domain module). * * FNXC:StoreModularization 2026-06-25-00:00: * Extracted from the monolithic packages/core/src/store.ts as a pure * behavior-preserving refactor. Each function receives the TaskStore * instance as its first parameter and performs byte-identical work. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Renamed from remaining-ops-9 to a domain name; no behavior change. */ import { TaskStore } from "../store.js"; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3d1d5336ff..aa5e0a223a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -32,1007 +32,239 @@ export { export type { GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput } from "./gitlab-config.js"; export { validateMcpServerDefinitionDetailed, validateMcpServerDefinitionsDetailed } from "./settings-validation.js"; -/** - * Valid thinking effort levels for AI agent sessions, controlling the cost/quality tradeoff of reasoning. - * Includes extra-high for maximum-effort requests on reasoning-capable models. - * - * FNXC:Settings-ThinkingLevel 2026-06-19-14:55: - * The central thinking-level enum must expose `xhigh` so UI settings and API validation can pass maximum reasoning requests through to CLI adapters. Runtime adapters map `xhigh` to `high` for non-Opus models and `max` for Opus models. - */ -export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; -export type ThinkingLevel = (typeof THINKING_LEVELS)[number]; - -/** - * The legacy default-workflow column set. Workflow-aware task movement resolves - * valid columns from each task's workflow definition (the default workflow's - * column IDs are byte-identical to these — KTD-1). New code should prefer the - * workflow-resolved path (`resolveAllowedColumns` / `workflowHasColumn` in - * `workflow-transitions.ts`) and trait predicates over string equality; this - * enum remains the canonical id set for the built-in default workflow. - */ -export const COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"] as const; -/** - * The closed legacy column union — still the correct type for default-workflow - * column ids. Movement entry points accept the wider {@link ColumnId}; runtime - * code validates ids against the task's resolved workflow. - */ -export type Column = (typeof COLUMNS)[number]; - -/** - * Column identifier accepted at task-movement entry points (KTD-1). - * Equals the legacy `Column` union for autocomplete purposes, but admits - * workflow-defined custom column ids; runtime paths validate the id against the - * task's resolved workflow. - */ -export type ColumnId = Column | (string & {}); - -export const DEFAULT_COLUMN: Column = "triage"; - -/** - * Tests membership against the closed legacy column enum. Note: under the - * workflowColumns flag, column validity is workflow-scoped — flag-aware code - * should use `workflowHasColumn(ir, columnId)` (`workflow-transitions.ts`); - * this remains correct for the flag-OFF path and default-workflow ids. - */ -export function isColumn(value: unknown): value is Column { - return typeof value === "string" && (COLUMNS as readonly string[]).includes(value); -} - -/** - * @deprecated (workflowColumns, U12) Coerces an arbitrary value to a legacy - * column, DISCARDING workflow-defined custom column ids — lossy under the - * flag. Resolve and validate against the task's workflow instead. Retained - * for the legacy flag-OFF path while the flag exists. - */ -export function normalizeColumn(value: unknown, fallback: Column = DEFAULT_COLUMN): Column { - return isColumn(value) ? value : fallback; -} - -/** Ordered task-priority levels for the core task domain contract. */ -export const TASK_PRIORITIES = ["low", "normal", "high", "urgent"] as const; -export type TaskPriority = (typeof TASK_PRIORITIES)[number]; - -/** - * Default task priority used for legacy rows/entries and create flows when - * callers omit the priority field. - */ -export const DEFAULT_TASK_PRIORITY: TaskPriority = "normal"; - -export const MERGE_REQUEST_STATES = [ - "queued", - "running", - "retrying", - "succeeded", - "exhausted", - "cancelled", - "manual-required", -] as const; - -export type MergeRequestState = (typeof MERGE_REQUEST_STATES)[number]; - -export const WORKFLOW_WORK_ITEM_KINDS = [ - "task", - "merge", - "retry", - "manual-hold", - "recovery", -] as const; - -export type WorkflowWorkItemKind = (typeof WORKFLOW_WORK_ITEM_KINDS)[number]; - -export const WORKFLOW_WORK_ITEM_STATES = [ - "runnable", - "running", - "held", - "retrying", - "manual-required", - "succeeded", - "failed", - "cancelled", - "exhausted", -] as const; - -export type WorkflowWorkItemState = (typeof WORKFLOW_WORK_ITEM_STATES)[number]; - -export interface WorkflowWorkItem { - id: string; - runId: string; - taskId: string; - nodeId: string; - kind: WorkflowWorkItemKind; - state: WorkflowWorkItemState; - attempt: number; - retryAfter: string | null; - leaseOwner: string | null; - leaseExpiresAt: string | null; - lastError: string | null; - blockedReason: string | null; - createdAt: string; - updatedAt: string; -} - -export interface WorkflowWorkItemUpsertInput { - id?: string; - runId: string; - taskId: string; - nodeId: string; - kind: WorkflowWorkItemKind; - state?: WorkflowWorkItemState; - attempt?: number; - retryAfter?: string | null; - leaseOwner?: string | null; - leaseExpiresAt?: string | null; - lastError?: string | null; - blockedReason?: string | null; - now?: string; -} - -export interface WorkflowWorkItemTransitionPatch { - attempt?: number; - retryAfter?: string | null; - leaseOwner?: string | null; - leaseExpiresAt?: string | null; - lastError?: string | null; - blockedReason?: string | null; - now?: string; -} - -export interface WorkflowWorkItemDueFilter { - now?: string; - limit?: number; - kinds?: WorkflowWorkItemKind[]; - states?: WorkflowWorkItemState[]; -} - -export interface MergeRequestWorkflowProjectionOptions { - runId?: string; - nodeId?: string; - now?: string; -} - -export interface MergeQueueEntry { - taskId: string; - enqueuedAt: string; - priority: TaskPriority; - leasedBy: string | null; - leasedAt: string | null; - leaseExpiresAt: string | null; - attemptCount: number; - lastError: string | null; -} - -export interface MergeRequestRecord { - taskId: string; - state: MergeRequestState; - createdAt: string; - updatedAt: string; - attemptCount: number; - lastError: string | null; -} - -export interface CompletionHandoffMarker { - taskId: string; - acceptedAt: string; - source: string; -} - -export interface MergeQueueEnqueueOptions { - priority?: TaskPriority; - now?: string; -} - -export interface MergeQueueAcquireOptions { - leaseDurationMs: number; - now?: string; - /** If provided, the lease attempt targets this specific task first. - * The task must be unexpired/available; otherwise falls back to normal queue-head selection. */ - targetTaskId?: string; -} - -export type MergeQueueReleaseOutcome = - | { kind: "success" } - | { kind: "failure"; error: string }; - -export interface HandoffEvidence { - /** Reason text recorded on the run-audit event (for example "fn_task_done"). */ - reason: string; - /** Optional run id captured for forensics. */ - runId?: string; - /** Optional agent id captured for forensics. */ - agentId?: string; -} - -export interface HandoffToReviewOptions { - ownerAgentId: string | null; - evidence: HandoffEvidence; - moveOptions?: { - preserveResumeState?: boolean; - preserveProgress?: boolean; - preserveWorktree?: boolean; - preserveStatus?: boolean; - moveSource?: "user" | "engine"; - skipMergeBlocker?: boolean; - }; - /** Inject a clock for tests. */ - now?: string; -} - -/** - * Dashboard high-fan-out blocker threshold. A blocker is considered high impact - * when at least this many active todo tasks are waiting on it. - */ -export const HIGH_FANOUT_BLOCKER_TODO_THRESHOLD = 5; - -/** - * Default age gate (ms) before a high fan-out blocker is escalated in dashboards. - */ -export const STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS = 2 * 60 * 60 * 1000; - -/** - * Execution mode for task implementation. - * Controls how the executor agent approaches the task: - * - "standard": Full execution with complete review workflow (default) - * - "fast": Expedited execution with minimal overhead for simple tasks - */ -export const EXECUTION_MODES = ["standard", "fast"] as const; -export type ExecutionMode = (typeof EXECUTION_MODES)[number]; - -/** Default execution mode for new tasks */ -export const DEFAULT_EXECUTION_MODE: ExecutionMode = "standard"; /* - * FNXC:PlannerOversight 2026-07-04-00:00: - * Per-task override of the workflow-native `plannerOversightLevel` setting - * (declared in `BUILTIN_OVERSIGHT_SETTINGS`, packages/core/src/builtin-workflow-settings.ts). - * When a task sets this field, it wins over the workflow's effective oversight - * value; unset (NULL in storage) means "inherit the workflow default". Values, - * order, and default here must stay in sync with `BUILTIN_OVERSIGHT_SETTINGS`. - */ -export const PLANNER_OVERSIGHT_LEVELS = ["off", "observe", "steer", "autonomous"] as const; -export type PlannerOversightLevel = (typeof PLANNER_OVERSIGHT_LEVELS)[number]; -export const DEFAULT_PLANNER_OVERSIGHT_LEVEL: PlannerOversightLevel = "autonomous"; - -/** Controls whether triage should require completion documentation artifacts in task specs. */ -export const COMPLETION_DOCUMENTATION_MODES = ["off", "changeset", "changelog"] as const; -export type CompletionDocumentationMode = (typeof COMPLETION_DOCUMENTATION_MODES)[number]; - -/** Theme mode for light/dark/system preference */ -export const THEME_MODES = ["dark", "light", "system"] as const; -export type ThemeMode = (typeof THEME_MODES)[number]; - -/** Color theme options for the dashboard */ -export const COLOR_THEMES = [ - "default", - "ocean", - "forest", - "sunset", - "zen", - "berry", - "high-contrast", - "industrial", - "monochrome", - "slate", - "ash", - // FNXC:DashboardTheming 2026-06-19-15:36: "air" is the source-of-truth id for the minimal, borderless, paper-like dashboard theme; keep bootstrap validators and theme options in sync with this union. - "air", - "graphite", - "silver", - "solarized", - "factory", - "factory-mono", - "ayu", - "one-dark", - "nord", - "dracula", - "gruvbox", - "tokyo-night", - "catppuccin-mocha", - "github-dark", - "everforest", - "rose-pine", - "kanagawa", - "night-owl", - "palenight", - "monokai-pro", - "slime", - "brutalist", - "neon-city", - "parchment", - "terminal", - "glass", - // FNXC:DashboardTheming 2026-07-01-00:00: Glass Silver is the silver/gray frosted sibling of Glass; keep this id in lockstep with dashboard/desktop validators and selector metadata so persisted explicit choices survive startup. - "glass-silver", - "horizon", - "vitesse", - "outrun", - "snazzy", - "porple", - "espresso", - "mars", - "poimandres", - "ember", - "rust", - "copper", - "foundry", - "carbon", - "sandstone", - "lagoon", - "frost", - "lavender", - "neon-bloom", - "sepia", - "shadcn", - // FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default for unset installs; keep it adjacent to the shadcn base so dashboard options and bootstrap validators preserve published theme order while explicit legacy ids remain valid. - "shadcn-ember", - // FNXC:DashboardTheming 2026-06-20-18:20: FN-6816 adds the user-customizable shadcn variant; keep this union in lockstep with dashboard theme options, swatches, theme-data base blocks, and the shadcn custom color token list. - "shadcn-custom", - // FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list. - // FNXC:DashboardTheming 2026-06-20-00:00: FN-6813 renames the grayscale-base mono theme to shadcn-mono-red and adds the remaining mono accent variants; keep Shadcn Gray adjacent to Shadcn Black so the color-family order stays stable with FN-6814. - // FNXC:DashboardTheming 2026-06-21-00:00: FN-6815 adds shadcn-gray-blue as the slate-neutral blue-gray sibling; keep it adjacent to Shadcn Gray so the published union mirrors dashboard option order. - "shadcn-blue", - "shadcn-green", - "shadcn-red", - "shadcn-purple", - "shadcn-pink", - "shadcn-orange", - "shadcn-yellow", - "shadcn-mono-red", - "shadcn-mono-blue", - "shadcn-mono-green", - "shadcn-mono-purple", - "shadcn-mono-pink", - "shadcn-mono-orange", - "shadcn-mono-yellow", - "shadcn-black", - "shadcn-gray", - "shadcn-gray-blue", -] as const; -export type ColorTheme = (typeof COLOR_THEMES)[number]; - -/** UI locales supported across the dashboard and terminal UI. `en` is the - * source-of-truth language and the fallback for all others. Adding a locale - * here (plus translated catalogs) is the only code change a new language - * needs — see `@fusion/i18n`. zh-CN and zh-TW are independent catalogs and - * are never auto-converted between scripts. */ -export const SUPPORTED_LOCALES = ["en", "zh-CN", "zh-TW", "fr", "es", "ko"] as const; -export type Locale = (typeof SUPPORTED_LOCALES)[number]; -/** Source-of-truth language and the fallback for all locales. */ -export const DEFAULT_LOCALE: Locale = "en"; - -/** Narrow an arbitrary value to a supported `Locale`. */ -export function isLocale(value: unknown): value is Locale { - return ( - typeof value === "string" && - (SUPPORTED_LOCALES as readonly string[]).includes(value) - ); -} - -export type PrStatus = "open" | "closed" | "merged" | "draft"; -export type MergeStrategy = "direct" | "pull-request"; -export type MergeIntegrationWorktreeMode = - | "reuse-task-worktree" - | "cwd-integration-branch" // explicit opt-in; surfaces a warning at startup. See FN-5348. - | "cwd-main"; // legacy alias for cwd-integration-branch; deprecated. Normalized at read time. - -let warnedLegacyCwdMain = false; - -export function __resetLegacyCwdMainWarningForTests(): void { - warnedLegacyCwdMain = false; -} - -export function normalizeMergeIntegrationWorktreeMode( - value: unknown, -): MergeIntegrationWorktreeMode { - if (value === "reuse-task-worktree" || value === "cwd-integration-branch") { - return value; - } - - if (value === "cwd-main") { - if (!warnedLegacyCwdMain) { - warnedLegacyCwdMain = true; - console.warn("[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch"); - } - return "cwd-integration-branch"; - } - - return "reuse-task-worktree"; -} - -export const DIRECT_MERGE_COMMIT_STRATEGIES = ["auto", "always-squash", "always-rebase"] as const; -export type DirectMergeCommitStrategy = (typeof DIRECT_MERGE_COMMIT_STRATEGIES)[number]; - -export const MERGE_ADVANCE_AUTO_SYNC_MODES = ["off", "ff-only", "stash-and-ff"] as const; -export type MergeAdvanceAutoSyncMode = (typeof MERGE_ADVANCE_AUTO_SYNC_MODES)[number]; -export function normalizeMergeAdvanceAutoSyncMode(value: unknown): MergeAdvanceAutoSyncMode { - return value === "off" || value === "ff-only" || value === "stash-and-ff" ? value : "stash-and-ff"; -} -/** How merge conflicts are resolved when the AI agent can't (or shouldn't) decide. - * - * Both `smart-*` strategies share the same cascade: pre-merge fetch + - * fast-forward of local main from origin (graceful degrade on failure), - * then AI, then auto-resolve lock/generated/trivial files. They differ only - * in the final per-file fallback when conflicts remain: - * - * - "smart-prefer-main" (default): fall back to `-X ours` so main's state - * wins. Best when concurrent tasks could regress just-merged sibling work. - * - "smart-prefer-branch": fall back to `-X theirs` so the task branch wins. - * Best when one agent at a time is dominant and you trust their output. - * - "ai-only": run AI on every attempt; never silently prefer one side. - * - "abort": run AI once; if conflict remains, fail the merge so a human - * can resolve it. - * - * Legacy values `"smart"` and `"prefer-main"` are accepted for backwards - * compatibility and normalized via {@link normalizeMergeConflictStrategy}. - * `"smart"` maps to `"smart-prefer-branch"` (its historical fallback) and - * `"prefer-main"` maps to `"smart-prefer-main"`. */ -export type MergeConflictStrategy = - | "smart-prefer-main" - | "smart-prefer-branch" - | "ai-only" - | "abort" - /** @deprecated use "smart-prefer-branch" */ - | "smart" - /** @deprecated use "smart-prefer-main" */ - | "prefer-main"; - -/** Canonical (post-migration) values that the merger actually dispatches on. */ -export type CanonicalMergeConflictStrategy = Exclude< - MergeConflictStrategy, - "smart" | "prefer-main" ->; - -/** Translate legacy `mergeConflictStrategy` values into their canonical form. - * Pass-through for already-canonical values; defaults to "smart-prefer-main" - * when the input is undefined. */ -export function normalizeMergeConflictStrategy( - value: MergeConflictStrategy | undefined, -): CanonicalMergeConflictStrategy { - switch (value) { - case "smart": - return "smart-prefer-branch"; - case "prefer-main": - return "smart-prefer-main"; - case undefined: - return "smart-prefer-main"; - default: - return value; - } -} - -export const MERGE_STRATEGY_OVERLAP_BEHAVIORS = [ - "flip-to-prefer-branch", - "warn-only", - "ignore", -] as const; - -export type MergeStrategyOverlapBehavior = (typeof MERGE_STRATEGY_OVERLAP_BEHAVIORS)[number]; - -export function normalizeMergeStrategyOverlapBehavior( - value: unknown, -): MergeStrategyOverlapBehavior { - return typeof value === "string" - && (MERGE_STRATEGY_OVERLAP_BEHAVIORS as readonly string[]).includes(value) - ? value as MergeStrategyOverlapBehavior - : "flip-to-prefer-branch"; -} - -export const POST_MERGE_AUDIT_MODES = ["block", "warn", "off"] as const; - -/** Controls how the merger reacts to a dirty post-merge audit (FN-4333). */ -export type PostMergeAuditMode = (typeof POST_MERGE_AUDIT_MODES)[number]; - -export function normalizePostMergeAuditMode(value: unknown): PostMergeAuditMode { - return typeof value === "string" - && (POST_MERGE_AUDIT_MODES as readonly string[]).includes(value) - ? (value as PostMergeAuditMode) - : "block"; -} - -export const MERGE_AUDIT_AUTO_RECOVERY_MODES = ["deterministic-only", "programmatic", "ai-assisted", "off"] as const; - -/** Controls how aggressively the merger tries to auto-recover from audit blocks (FN-4315). */ -export type MergeAuditAutoRecoveryMode = (typeof MERGE_AUDIT_AUTO_RECOVERY_MODES)[number]; - -export function normalizeMergeAuditAutoRecovery(value: unknown): MergeAuditAutoRecoveryMode { - return typeof value === "string" - && (MERGE_AUDIT_AUTO_RECOVERY_MODES as readonly string[]).includes(value) - ? (value as MergeAuditAutoRecoveryMode) - : "ai-assisted"; -} - -export const MERGER_MODES = ["ai", "deterministic"] as const; - -/** - * Merge execution path (FN-5633). - * - "ai" (default): the standalone AI merge path — a clean-room worktree where - * an AI agent merges the task branch and an AI reviewer audits it (with - * corrective retries) before a fast-forward landing. Bypasses the legacy - * scaffolding entirely. - * - "deterministic": **DEPRECATED (master-plan U0, 2026-06-21) and INERT.** Once - * routed to the legacy `aiMergeTask` pipeline; now ignored — every merge uses - * the unified "ai" path (`runAiMerge`). The value is retained (not removed) to - * avoid a breaking `@runfusion/fusion` type change, and the engine logs a - * one-time deprecation warning when it observes a resolved "deterministic". - * - * FNXC:MergerUnification 2026-06-21-19:05: `merger.mode` is published surface, so - * the type and the `MergerSettings.mode` field stay; only the "deterministic" - * VALUE is deprecated/inert. Removing the type is a separate breaking change. - */ -export type MergerMode = (typeof MERGER_MODES)[number]; - -export function normalizeMergerMode(value: unknown): MergerMode { - return typeof value === "string" && (MERGER_MODES as readonly string[]).includes(value) - ? (value as MergerMode) - : "ai"; -} - -/** Settings for the AI merge path (FN-5633). */ -export interface MergerSettings { - /** - * Which merge path to use. Default: "ai". - * @deprecated master-plan U0 (2026-06-21): the value is inert — every merge now - * uses the unified AI merge path (`runAiMerge`). Field retained as published - * surface; "deterministic" only triggers a one-time deprecation warning. - */ - mode?: MergerMode; - /** How many AI corrective rounds before landing the best result (advisory) or - * hard-failing (blocking). Default: 3. The reviewer uses the project's - * validator/reviewer model lane — there is no merge-specific model setting. */ - maxReviewPasses?: number; - /** Dangerous compatibility escape hatch for the AI merge landing path. - * When true (default for resolved project settings), Fusion restores the legacy - * stash → fast-forward → restore behavior when the checked-out integration - * worktree is dirty. Set false to explicitly opt out and fail closed before - * unrelated local edits can be reintroduced after landing. */ - allowDirtyLocalCheckoutSync?: boolean; -} - -export const AUTO_RECOVERY_MODES = ["off", "deterministic-only", "programmatic", "ai-assisted"] as const; - -export type AutoRecoveryMode = (typeof AUTO_RECOVERY_MODES)[number]; - -export type AutoRecoveryFailureClass = - | "file-scope-invariant" - | "post-squash-audit-blocker" - | "branch-cross-contamination" - | "branch-conflict-tripwire" - | "branch-conflict-recovery-exhausted" - | "branch-conflict-unrecoverable" - | "message-delivery-failure"; - -export interface AutoRecoverySettings { - mode: AutoRecoveryMode; - perClass?: Partial>; - maxRetries?: number; -} - -export function normalizeAutoRecovery(value: unknown): AutoRecoverySettings { - const fallback: AutoRecoverySettings = { mode: "deterministic-only", maxRetries: 3 }; - if (!value || typeof value !== "object") return fallback; - - const candidate = value as { - mode?: unknown; - perClass?: unknown; - maxRetries?: unknown; - }; - const mode = typeof candidate.mode === "string" && (AUTO_RECOVERY_MODES as readonly string[]).includes(candidate.mode) - ? candidate.mode as AutoRecoveryMode - : fallback.mode; - const perClass = typeof candidate.perClass === "object" && candidate.perClass - ? Object.fromEntries( - Object.entries(candidate.perClass as Record) - .filter(([k, v]) => ( - [ - "file-scope-invariant", - "post-squash-audit-blocker", - "branch-cross-contamination", - "branch-conflict-tripwire", - "branch-conflict-recovery-exhausted", - "branch-conflict-unrecoverable", - "message-delivery-failure", - ].includes(k) - && typeof v === "string" - && (AUTO_RECOVERY_MODES as readonly string[]).includes(v) - )), - ) as Partial> - : undefined; - const maxRetries = typeof candidate.maxRetries === "number" && Number.isFinite(candidate.maxRetries) - ? Math.max(0, Math.floor(candidate.maxRetries)) - : fallback.maxRetries; - - return { mode, perClass, maxRetries }; -} -/** Policy for handling task execution when the selected node is unavailable/unhealthy. */ -export type UnavailableNodePolicy = "block" | "fallback-local"; - -export type OwningNodeHandoffPolicy = "block" | "reassign-to-local" | "reassign-any-healthy"; - -export interface ModelPreset { - id: string; - name: string; - executorProvider?: string; - executorModelId?: string; - validatorProvider?: string; - validatorModelId?: string; -} - -/** A reusable workflow step definition that can run after task implementation. */ -/** Execution mode for a workflow step. */ -export type WorkflowStepMode = "prompt" | "script"; -export type WorkflowStepToolMode = "readonly" | "coding"; -export type WorkflowStepGateMode = "gate" | "advisory"; - -/** Lifecycle phase for workflow step execution. */ -export type WorkflowStepPhase = "pre-merge" | "post-merge"; - -export interface WorkflowStep { - /** Unique identifier (e.g., "WS-001") */ - id: string; - /** Built-in template source ID when this step was materialized from a template. */ - templateId?: string; - /** Display name (e.g., "Documentation Review") */ - name: string; - /** Short description for UI display */ - description: string; - /** Execution mode — "prompt" runs an AI agent, "script" runs a named project script */ - mode: WorkflowStepMode; - /** Lifecycle phase — "pre-merge" runs before merge (default), "post-merge" runs after merge success */ - phase?: WorkflowStepPhase; - /** Gate behavior — gate blocks merge/auto-revive on failure, advisory records non-blocking findings. */ - gateMode: WorkflowStepGateMode; - /** Full agent prompt to execute when this step runs (used when mode is "prompt") */ - prompt: string; - /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ - toolMode?: WorkflowStepToolMode; - /** Name of a skill to load into this step's session (e.g. - * "compound-engineering:ce-work"). When set, the step session loads the named - * skill (discovery + selection) and the engine injects the Fusion workflow-step - * conventions preamble. Only meaningful for skill-executor graph nodes. */ - skillName?: string; - /** - * Browser capability requested by prompt-mode steps. When true, the executor - * loads the agent-browser navigation skill when available, preflights the - * `agent-browser` CLI, and records browser-verification activity in the agent - * log. Ignored for script-mode steps. - */ - requiresBrowser?: boolean; - /** Name of a script from project settings `scripts` map to execute (required when mode is "script") */ - scriptName?: string; - /** Whether this step is available for selection on new tasks */ - enabled: boolean; - /** When true, this step is automatically pre-selected when creating new tasks. - * Users can still deselect it — this only controls the initial default state. */ - defaultOn?: boolean; - /** AI model provider override for the workflow step agent (e.g., "anthropic"). - * Must be set together with `modelId`. When both model fields are undefined, - * the executor uses global settings defaults. Only used when mode is "prompt". */ - modelProvider?: string; - /** AI model ID override for the workflow step agent (e.g., "claude-sonnet-4-5"). - * Must be set together with `modelProvider`. When both model fields are undefined, - * the executor uses global settings defaults. Only used when mode is "prompt". */ - modelId?: string; - /** - * FNXC:Settings-ThinkingLevel 2026-07-10-00:00: - * Workflow IR nodes may pin reasoning effort independently from the model pair so authors can inherit the model while overriding thinking level. Runtime precedence is node/step `thinkingLevel` > task `thinkingLevel` > settings `defaultThinkingLevel`. - */ - thinkingLevel?: ThinkingLevel; - /** (workflow-editor-consolidation U1/U2, KTD-1/KTD-3) when this legacy step has - * been migrated into a fragment WorkflowDefinition, the fragment's id is stamped - * here so the lazy step migration is idempotent (already-stamped rows are - * skipped). Stored in the `migrated_fragment_id` column. */ - migratedFragmentId?: string; - /** ISO-8601 timestamp of creation */ - createdAt: string; - /** ISO-8601 timestamp of last update */ - updatedAt: string; -} - -/** Input for creating a new workflow step. */ -/** Event types that can trigger ntfy notifications */ -export type NtfyNotificationEvent = - | "in-review" - | "merged" - | "failed" - | "awaiting-approval" - | "awaiting-user-review" - | "planning-awaiting-input" - | "cli-agent-awaiting-input" - | "gridlock" - | "board-stall-unrecovered" - | "db-corruption-detected" - | "fallback-used" - | "memory-dreams-processed" - | "token-budget" - | "message:agent-to-user" - | "message:agent-to-agent" - | "message:room" - | "oauth-token-expired" - | "task-created" - | "workflow-notify"; - -/** Known notification event types. Providers may support additional custom events. */ -export const NOTIFICATION_EVENTS = [ - "in-review", - "merged", - "failed", - "awaiting-approval", - "awaiting-user-review", - "planning-awaiting-input", - /* - * FNXC:ToolPermissionNotifications 2026-06-27-00:00: - * CLI tool-permission requests are a distinct user-facing notification event from plan approval. Operators must be able to enable external alerts when a terminal-backed agent waits for human input. - */ - "cli-agent-awaiting-input", - "gridlock", - "board-stall-unrecovered", - "db-corruption-detected", - "fallback-used", - "memory-dreams-processed", - "token-budget", - "message:agent-to-user", - "message:agent-to-agent", - "message:room", - "oauth-token-expired", - "task-created", - "workflow-notify", -] as const; - -/** Notification event type. Known events plus provider-specific custom events. */ -export type NotificationEvent = (typeof NOTIFICATION_EVENTS)[number] | (string & {}); - -/** Standard payload shape shared across notification providers. */ -export interface NotificationPayload { - taskId?: string; - taskTitle?: string; - taskDescription?: string; - event: NotificationEvent; - timestamp?: string; - metadata?: Record; -} - -/** Declarative notification provider configuration persisted in settings. */ -export interface NotificationProviderConfig { - id: string; - name: string; - enabled: boolean; - config: Record; -} - -export interface CustomProvider { - id: string; - name: string; - apiType: "openai-compatible" | "anthropic-compatible" | "google-generative-ai" | "openai-responses"; - baseUrl: string; - apiKey?: string; - /** - * OpenAI-compatible opt-in for providers that explicitly support the `developer` role. - * Omitted/false forces legacy `system` role emission to avoid provider 400s. - */ - supportsDeveloperRole?: boolean; - /** - * FNXC:ProviderAuth 2026-07-08-00:00: - * FN-7689: opt-in for custom `openai-compatible`/`openai-responses` gateways that proxy an - * Anthropic-format backend (e.g. `usai/claude_4_6_sonnet`). When true, registered - * `openai-completions` models get pi-ai's `compat.cacheControlFormat = "anthropic"`, which makes - * pi-ai emit Anthropic-style `cache_control` breakpoints on the system prompt, last - * conversation message, and last tool. Without this, pi-ai's `detectCompat` only auto-enables - * caching for OpenRouter `anthropic/*` models, so a generic custom gateway re-bills the entire - * context prefix uncached every turn (measured cachedTokens=0/cacheWriteTokens=0 across 243 - * runs, ~327.5:1 input:output ratio). Default off — never force cache_control on gateways that - * did not opt in, since non-Anthropic-compatible backends (Together, Fireworks, etc.) can 400 on - * unexpected `cache_control` fields. Inert for `anthropic-compatible` (already auto-caches) and - * `google-generative-ai` (no cache_control concept). - */ - anthropicPromptCaching?: boolean; - models?: { id: string; name: string }[]; -} - -export interface WorkflowStepInput { - /** Built-in template source ID when creating a concrete step from a template. */ - templateId?: string; - name: string; - description: string; - /** Execution mode — defaults to "prompt" if not specified */ - mode?: WorkflowStepMode; - /** Lifecycle phase — defaults to "pre-merge" if not specified */ - phase?: WorkflowStepPhase; - /** Gate behavior — defaults by mode (prompt: advisory, script: gate) when omitted. */ - gateMode?: WorkflowStepGateMode; - /** Agent prompt (used when mode is "prompt"). Optional — can be AI-generated later via refinement. */ - prompt?: string; - /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ - toolMode?: WorkflowStepToolMode; - /** Name of a skill to load into this step's session (e.g. - * "compound-engineering:ce-work"). See `WorkflowStep.skillName`. */ - skillName?: string; - /** Script name from project settings (required when mode is "script"). - * Must reference a named script in `settings.scripts` — no raw commands. */ - scriptName?: string; - /** Defaults to true if not specified */ - enabled?: boolean; - /** When true, this step is automatically pre-selected when creating new tasks. - * Users can still deselect — this only controls the initial default state. */ - defaultOn?: boolean; - /** AI model provider override. Must be set together with modelId. Only used when mode is "prompt". */ - modelProvider?: string; - /** AI model ID override. Must be set together with modelProvider. Only used when mode is "prompt". */ - modelId?: string; - /** Optional per-node reasoning-effort override; inherits from task/settings when omitted. */ - thinkingLevel?: ThinkingLevel; - /** (workflow-editor-consolidation U2, KTD-3) fragment id stamped when this step - * was migrated into a fragment WorkflowDefinition. Set by the migration only. */ - migratedFragmentId?: string; -} - -/** Result of a workflow step execution on a task. */ -export interface WorkflowStepResult { - /** ID of the workflow step that ran (e.g., "WS-001") */ - workflowStepId: string; - /** Name of the workflow step at execution time */ - workflowStepName: string; - /** Lifecycle phase at execution time */ - phase?: WorkflowStepPhase; - /** Runtime source for distinguishing graph-authored node progress from optional-toggle checks. */ - source?: "optional-group" | "node"; - /** Execution status */ - status: "passed" | "failed" | "advisory_failure" | "skipped" | "pending"; - /** Output from the workflow step agent (findings, errors, etc.) */ - output?: string; - /** - * Machine-readable verdict from prompt-mode structured output. - * Absent for script-mode steps and legacy prose-only prompt outputs. - */ - verdict?: "APPROVE" | "APPROVE_WITH_NOTES" | "REVISE"; - /** - * Optional notes from prompt-mode structured output. - * Absent for script-mode steps and legacy prose-only prompt outputs. - */ - notes?: string; - /** ISO-8601 timestamp when the step started */ - startedAt?: string; - /** ISO-8601 timestamp when the step completed */ - completedAt?: string; - /* - * FNXC:ReviewLaneBypass 2026-07-09-00:00: - * A privileged operator can bypass a `status:"failed"` pre-merge review step - * (leading real-world cause: the Runfusion/Fusion#1946 `(no feedback captured)` - * no-verdict dispatch defect) so a card stranded solely by that failure can - * advance to merge (FN-7720). The bypass REWRITES this result's `status` to a - * terminal, non-blocking value (`"skipped"`) and stamps the fields below as an - * explicit audit trail — it never fabricates a reviewer `verdict`. Only the - * `getTaskMergeBlocker` "task has failed pre-merge workflow steps" reason is - * cleared; every other merge-blocker condition (paused, incomplete steps, - * blocking task status, still-`pending` pre-merge steps) is untouched. - */ - /** Operator identity that performed the bypass, if this result was bypassed. */ - bypassedBy?: string; - /** ISO-8601 timestamp when the bypass was applied. */ - bypassedAt?: string; - /** Mandatory operator-supplied justification for the bypass. */ - bypassReason?: string; - /** The `status` this result carried immediately before the bypass rewrote it (always `"failed"` for the supported bypass path). */ - bypassedFromStatus?: WorkflowStepResult["status"]; - /** The `verdict` (if any) this result carried immediately before the bypass, preserved for audit only — never promoted to `verdict`. */ - bypassedFromVerdict?: WorkflowStepResult["verdict"]; - /* - * FNXC:WorkflowStepResults 2026-07-09-00:10: - * FN-7727: self-healing recovery re-runs a failed pre-merge review node - * (`code-review`, `code-review-remediation`, `plan-review`, - * `browser-verification`) in place, and the recorder upsert previously - * REPLACED the prior `status:"failed"` entry — erasing its captured - * `output`/`notes`/`verdict`/timestamps forever (the diagnostic trail - * FN-7642 worked to capture, and the history FN-7720's bypass affordance - * needs to show). `priorAttempts` preserves a BOUNDED, single-level history - * of prior terminal-failure (`failed`/`advisory_failure`) attempts on the - * surviving entry — snapshots never carry their own nested `priorAttempts`, - * so history cannot grow unbounded. This field is READ-ONLY history: it - * never participates in merge-blocking (`getTaskMergeBlocker`), self-healing - * recovery selection (`latestFailedPreMergeStep`), or progress/timing - * computation — only the current (this) entry's fields do. Written by the - * shared `upsertWorkflowStepResult` helper (`workflow-step-results.ts`). - */ - /** Bounded, single-level history of prior terminal-failure attempts this entry replaced. Read-only; never affects merge-blocking or recovery selection. */ - priorAttempts?: WorkflowStepResult[]; -} - -/** - * Lifecycle status of one persisted step instance (step-inversion U4, KTD-6). - * - `pending` — expanded but not yet started. - * - `in-progress` — actively executing inside its foreach sub-walk. - * - `awaiting-integration` — work complete on a parallel-mode branch, waiting - * for the ordered integration stage (KTD-11; unused at concurrency 1). - * - `completed` — terminal success (integrated in parallel mode). - * - `failed` — terminal failure. - */ -export type WorkflowRunStepInstanceStatus = - | "pending" - | "in-progress" - | "awaiting-integration" - | "completed" - | "failed"; - -/** - * Persisted run-state for one expanded step instance inside a foreach region - * (step-inversion U4, KTD-6). One row per `(taskId, runId, foreachNodeId, - * stepIndex)`; mirrors the `workflow_run_branches` posture. Resume reconstructs - * the instance set from `pinnedStepCount` + per-instance `currentNodeId` / - * `reworkCount`. `baselineSha` / `checkpointId` are the RETHINK reset anchors - * (previously in-memory, lost on restart). `branchName` / `integratedAt` and the - * `awaiting-integration` status serve parallel mode (KTD-11); null/unused at - * concurrency 1. This is the core row shape; the engine-side instance model is - * separate and engine-owned. - */ -export interface WorkflowRunStepInstance { - taskId: string; - runId: string; - /** Node id of the foreach region that expanded this instance. */ - foreachNodeId: string; - /** Zero-based index of the step this instance runs. */ - stepIndex: number; - /** Step count pinned at expansion; resume fails on mismatch with live steps[]. */ - pinnedStepCount: number; - /** Current sub-walk node id for the in-flight instance; null when not started. */ - currentNodeId?: string | null; - status: WorkflowRunStepInstanceStatus; - /** Git sha the RETHINK reset rewinds to; null when no baseline captured. */ - baselineSha?: string | null; - /** Session checkpoint to rewind to on RETHINK; null when none captured. */ - checkpointId?: string | null; - /** Number of rework cycles consumed against the rework budget. */ - reworkCount: number; - /** Per-instance branch name in worktree-isolation mode (KTD-11); null otherwise. */ - branchName?: string | null; - /** ISO-8601 timestamp the instance branch was integrated (KTD-11); null otherwise. */ - integratedAt?: string | null; - /** ISO-8601 timestamp of the last write to this row. */ - updatedAt: string; -} - -/* -FNXC:WorkflowStepTemplate 2026-06-25-00:00: -U6 deleted the built-in step-template catalog array (the former value export). The -`WorkflowStepTemplate` SHAPE is KEPT because plugin-contributed step templates still use -it (they feed the -workflow-editor optional-group palette via `getPluginWorkflowStepTemplates`). It is no -longer backed by any built-in catalog: the former built-in `browser-verification` / -`code-review` literals now live inlined in their optional-group node builders -(`builtin-browser-verification-group.ts` / `builtin-code-review-group.ts`). +FNXC:CodeOrganization 2026-07-15-00:00: +Domain peels live under types/*.ts. Import locally so residual interfaces in this +barrel can reference them, then re-export so the Vite @fusion/core alias and +package consumers keep stable import paths. */ -/** A workflow step template shape used by plugin-contributed steps (palette entries). */ -export interface WorkflowStepTemplate { - /** Unique template identifier (e.g., "documentation-review") */ - id: string; - /** Display name (e.g., "Documentation Review") */ - name: string; - /** Short description for UI */ - description: string; - /** Full agent prompt template */ - prompt: string; - /** Execution mode for plugin-contributed templates; defaults to prompt. */ - mode?: WorkflowStepMode; - /** Task lifecycle phase for plugin-contributed templates; defaults to pre-merge. */ - phase?: "pre-merge" | "post-merge"; - /** Script name for script-mode plugin templates. */ - scriptName?: string; - /** Tool set available when the template runs as a prompt-mode step. */ - toolMode?: WorkflowStepToolMode; - /** Failure behavior for materialized steps from this template. */ - gateMode?: WorkflowStepGateMode; - /** Whether this template should be auto-selected for new tasks. */ - defaultOn?: boolean; - /** AI model provider override for prompt-mode templates. */ - modelProvider?: string; - /** AI model ID override for prompt-mode templates. */ - modelId?: string; - /** Optional per-node reasoning-effort override for prompt-mode templates. */ - thinkingLevel?: ThinkingLevel; - /** Grouping category (e.g., "Quality", "Security") */ - category: string; - /** Optional icon identifier for UI (e.g., "file-text", "shield") */ - icon?: string; - /** Optional default enabled state for plugin-provided templates. */ - enabled?: boolean; -} +import { + THINKING_LEVELS, + COLUMNS, + DEFAULT_COLUMN, + isColumn, + normalizeColumn, + TASK_PRIORITIES, + DEFAULT_TASK_PRIORITY, +} from "./types/board.js"; +import type { ThinkingLevel, Column, ColumnId, TaskPriority } from "./types/board.js"; +export { + THINKING_LEVELS, + COLUMNS, + DEFAULT_COLUMN, + isColumn, + normalizeColumn, + TASK_PRIORITIES, + DEFAULT_TASK_PRIORITY, +}; +export type { ThinkingLevel, Column, ColumnId, TaskPriority }; + +import { + MERGE_REQUEST_STATES, + WORKFLOW_WORK_ITEM_KINDS, + WORKFLOW_WORK_ITEM_STATES, +} from "./types/merge-queue.js"; +import type { + MergeRequestState, + WorkflowWorkItemKind, + WorkflowWorkItemState, + WorkflowWorkItem, + WorkflowWorkItemUpsertInput, + WorkflowWorkItemTransitionPatch, + WorkflowWorkItemDueFilter, + MergeRequestWorkflowProjectionOptions, + MergeQueueEntry, + MergeRequestRecord, + CompletionHandoffMarker, + MergeQueueEnqueueOptions, + MergeQueueAcquireOptions, + MergeQueueReleaseOutcome, + HandoffEvidence, + HandoffToReviewOptions, +} from "./types/merge-queue.js"; +export { + MERGE_REQUEST_STATES, + WORKFLOW_WORK_ITEM_KINDS, + WORKFLOW_WORK_ITEM_STATES, +}; +export type { + MergeRequestState, + WorkflowWorkItemKind, + WorkflowWorkItemState, + WorkflowWorkItem, + WorkflowWorkItemUpsertInput, + WorkflowWorkItemTransitionPatch, + WorkflowWorkItemDueFilter, + MergeRequestWorkflowProjectionOptions, + MergeQueueEntry, + MergeRequestRecord, + CompletionHandoffMarker, + MergeQueueEnqueueOptions, + MergeQueueAcquireOptions, + MergeQueueReleaseOutcome, + HandoffEvidence, + HandoffToReviewOptions, +}; + +import { + HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, + STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, + EXECUTION_MODES, + DEFAULT_EXECUTION_MODE, + PLANNER_OVERSIGHT_LEVELS, + DEFAULT_PLANNER_OVERSIGHT_LEVEL, + COMPLETION_DOCUMENTATION_MODES, + THEME_MODES, + COLOR_THEMES, + SUPPORTED_LOCALES, + DEFAULT_LOCALE, + isLocale, +} from "./types/execution-and-ui.js"; +import type { + ExecutionMode, + PlannerOversightLevel, + CompletionDocumentationMode, + ThemeMode, + ColorTheme, + Locale, +} from "./types/execution-and-ui.js"; +export { + HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, + STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, + EXECUTION_MODES, + DEFAULT_EXECUTION_MODE, + PLANNER_OVERSIGHT_LEVELS, + DEFAULT_PLANNER_OVERSIGHT_LEVEL, + COMPLETION_DOCUMENTATION_MODES, + THEME_MODES, + COLOR_THEMES, + SUPPORTED_LOCALES, + DEFAULT_LOCALE, + isLocale, +}; +export type { + ExecutionMode, + PlannerOversightLevel, + CompletionDocumentationMode, + ThemeMode, + ColorTheme, + Locale, +}; + +import { + __resetLegacyCwdMainWarningForTests, + normalizeMergeIntegrationWorktreeMode, + DIRECT_MERGE_COMMIT_STRATEGIES, + MERGE_ADVANCE_AUTO_SYNC_MODES, + normalizeMergeAdvanceAutoSyncMode, + normalizeMergeConflictStrategy, + MERGE_STRATEGY_OVERLAP_BEHAVIORS, + normalizeMergeStrategyOverlapBehavior, + POST_MERGE_AUDIT_MODES, + normalizePostMergeAuditMode, + MERGE_AUDIT_AUTO_RECOVERY_MODES, + normalizeMergeAuditAutoRecovery, + MERGER_MODES, + normalizeMergerMode, + AUTO_RECOVERY_MODES, + normalizeAutoRecovery, +} from "./types/merge-policy.js"; +import type { + PrStatus, + MergeStrategy, + MergeIntegrationWorktreeMode, + DirectMergeCommitStrategy, + MergeAdvanceAutoSyncMode, + MergeConflictStrategy, + CanonicalMergeConflictStrategy, + MergeStrategyOverlapBehavior, + PostMergeAuditMode, + MergeAuditAutoRecoveryMode, + MergerMode, + MergerSettings, + AutoRecoveryMode, + AutoRecoveryFailureClass, + AutoRecoverySettings, + UnavailableNodePolicy, + OwningNodeHandoffPolicy, +} from "./types/merge-policy.js"; +export { + __resetLegacyCwdMainWarningForTests, + normalizeMergeIntegrationWorktreeMode, + DIRECT_MERGE_COMMIT_STRATEGIES, + MERGE_ADVANCE_AUTO_SYNC_MODES, + normalizeMergeAdvanceAutoSyncMode, + normalizeMergeConflictStrategy, + MERGE_STRATEGY_OVERLAP_BEHAVIORS, + normalizeMergeStrategyOverlapBehavior, + POST_MERGE_AUDIT_MODES, + normalizePostMergeAuditMode, + MERGE_AUDIT_AUTO_RECOVERY_MODES, + normalizeMergeAuditAutoRecovery, + MERGER_MODES, + normalizeMergerMode, + AUTO_RECOVERY_MODES, + normalizeAutoRecovery, +}; +export type { + PrStatus, + MergeStrategy, + MergeIntegrationWorktreeMode, + DirectMergeCommitStrategy, + MergeAdvanceAutoSyncMode, + MergeConflictStrategy, + CanonicalMergeConflictStrategy, + MergeStrategyOverlapBehavior, + PostMergeAuditMode, + MergeAuditAutoRecoveryMode, + MergerMode, + MergerSettings, + AutoRecoveryMode, + AutoRecoveryFailureClass, + AutoRecoverySettings, + UnavailableNodePolicy, + OwningNodeHandoffPolicy, +}; + +import { NOTIFICATION_EVENTS } from "./types/workflow-steps.js"; +import type { + ModelPreset, + WorkflowStepMode, + WorkflowStepToolMode, + WorkflowStepGateMode, + WorkflowStepPhase, + WorkflowStep, + NtfyNotificationEvent, + NotificationEvent, + NotificationPayload, + NotificationProviderConfig, + CustomProvider, + WorkflowStepInput, + WorkflowStepResult, + WorkflowRunStepInstanceStatus, + WorkflowRunStepInstance, + WorkflowStepTemplate, +} from "./types/workflow-steps.js"; +export { NOTIFICATION_EVENTS }; +export type { + ModelPreset, + WorkflowStepMode, + WorkflowStepToolMode, + WorkflowStepGateMode, + WorkflowStepPhase, + WorkflowStep, + NtfyNotificationEvent, + NotificationEvent, + NotificationPayload, + NotificationProviderConfig, + CustomProvider, + WorkflowStepInput, + WorkflowStepResult, + WorkflowRunStepInstanceStatus, + WorkflowRunStepInstance, + WorkflowStepTemplate, +}; export type PrConflictState = "clean" | "conflicting" | "behind" | "blocked" | "unknown"; diff --git a/packages/core/src/types/board.ts b/packages/core/src/types/board.ts new file mode 100644 index 0000000000..828614f29c --- /dev/null +++ b/packages/core/src/types/board.ts @@ -0,0 +1,73 @@ +/** + * Board column, priority, and thinking-level domain types for the Fusion core contract. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Extracted from types.ts barrel so domain types are navigable while types.ts remains the + * browser-safe @fusion/core Vite alias re-export surface. + */ + +/** + * Valid thinking effort levels for AI agent sessions, controlling the cost/quality tradeoff of reasoning. + * Includes extra-high for maximum-effort requests on reasoning-capable models. + * + * FNXC:Settings-ThinkingLevel 2026-06-19-14:55: + * The central thinking-level enum must expose `xhigh` so UI settings and API validation can pass maximum reasoning requests through to CLI adapters. Runtime adapters map `xhigh` to `high` for non-Opus models and `max` for Opus models. + */ +export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; +export type ThinkingLevel = (typeof THINKING_LEVELS)[number]; + +/** + * The legacy default-workflow column set. Workflow-aware task movement resolves + * valid columns from each task's workflow definition (the default workflow's + * column IDs are byte-identical to these — KTD-1). New code should prefer the + * workflow-resolved path (`resolveAllowedColumns` / `workflowHasColumn` in + * `workflow-transitions.ts`) and trait predicates over string equality; this + * enum remains the canonical id set for the built-in default workflow. + */ +export const COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"] as const; +/** + * The closed legacy column union — still the correct type for default-workflow + * column ids. Movement entry points accept the wider {@link ColumnId}; runtime + * code validates ids against the task's resolved workflow. + */ +export type Column = (typeof COLUMNS)[number]; + +/** + * Column identifier accepted at task-movement entry points (KTD-1). + * Equals the legacy `Column` union for autocomplete purposes, but admits + * workflow-defined custom column ids; runtime paths validate the id against the + * task's resolved workflow. + */ +export type ColumnId = Column | (string & {}); + +export const DEFAULT_COLUMN: Column = "triage"; + +/** + * Tests membership against the closed legacy column enum. Note: under the + * workflowColumns flag, column validity is workflow-scoped — flag-aware code + * should use `workflowHasColumn(ir, columnId)` (`workflow-transitions.ts`); + * this remains correct for the flag-OFF path and default-workflow ids. + */ +export function isColumn(value: unknown): value is Column { + return typeof value === "string" && (COLUMNS as readonly string[]).includes(value); +} + +/** + * @deprecated (workflowColumns, U12) Coerces an arbitrary value to a legacy + * column, DISCARDING workflow-defined custom column ids — lossy under the + * flag. Resolve and validate against the task's workflow instead. Retained + * for the legacy flag-OFF path while the flag exists. + */ +export function normalizeColumn(value: unknown, fallback: Column = DEFAULT_COLUMN): Column { + return isColumn(value) ? value : fallback; +} + +/** Ordered task-priority levels for the core task domain contract. */ +export const TASK_PRIORITIES = ["low", "normal", "high", "urgent"] as const; +export type TaskPriority = (typeof TASK_PRIORITIES)[number]; + +/** + * Default task priority used for legacy rows/entries and create flows when + * callers omit the priority field. + */ +export const DEFAULT_TASK_PRIORITY: TaskPriority = "normal"; diff --git a/packages/core/src/types/execution-and-ui.ts b/packages/core/src/types/execution-and-ui.ts new file mode 100644 index 0000000000..48078225ef --- /dev/null +++ b/packages/core/src/types/execution-and-ui.ts @@ -0,0 +1,156 @@ +/** + * Execution modes, planner oversight, theme, and locale domain types. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Extracted from types.ts; re-exported from the browser-safe types barrel. + */ + +/** + * Dashboard high-fan-out blocker threshold. A blocker is considered high impact + * when at least this many active todo tasks are waiting on it. + */ +export const HIGH_FANOUT_BLOCKER_TODO_THRESHOLD = 5; + +/** + * Default age gate (ms) before a high fan-out blocker is escalated in dashboards. + */ +export const STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS = 2 * 60 * 60 * 1000; + +/** + * Execution mode for task implementation. + * Controls how the executor agent approaches the task: + * - "standard": Full execution with complete review workflow (default) + * - "fast": Expedited execution with minimal overhead for simple tasks + */ +export const EXECUTION_MODES = ["standard", "fast"] as const; +export type ExecutionMode = (typeof EXECUTION_MODES)[number]; + +/** Default execution mode for new tasks */ +export const DEFAULT_EXECUTION_MODE: ExecutionMode = "standard"; + +/* + * FNXC:PlannerOversight 2026-07-04-00:00: + * Per-task override of the workflow-native `plannerOversightLevel` setting + * (declared in `BUILTIN_OVERSIGHT_SETTINGS`, packages/core/src/builtin-workflow-settings.ts). + * When a task sets this field, it wins over the workflow's effective oversight + * value; unset (NULL in storage) means "inherit the workflow default". Values, + * order, and default here must stay in sync with `BUILTIN_OVERSIGHT_SETTINGS`. + */ +export const PLANNER_OVERSIGHT_LEVELS = ["off", "observe", "steer", "autonomous"] as const; +export type PlannerOversightLevel = (typeof PLANNER_OVERSIGHT_LEVELS)[number]; +export const DEFAULT_PLANNER_OVERSIGHT_LEVEL: PlannerOversightLevel = "autonomous"; + +/** Controls whether triage should require completion documentation artifacts in task specs. */ +export const COMPLETION_DOCUMENTATION_MODES = ["off", "changeset", "changelog"] as const; +export type CompletionDocumentationMode = (typeof COMPLETION_DOCUMENTATION_MODES)[number]; + +/** Theme mode for light/dark/system preference */ +export const THEME_MODES = ["dark", "light", "system"] as const; +export type ThemeMode = (typeof THEME_MODES)[number]; + +/** Color theme options for the dashboard */ +export const COLOR_THEMES = [ + "default", + "ocean", + "forest", + "sunset", + "zen", + "berry", + "high-contrast", + "industrial", + "monochrome", + "slate", + "ash", + // FNXC:DashboardTheming 2026-06-19-15:36: "air" is the source-of-truth id for the minimal, borderless, paper-like dashboard theme; keep bootstrap validators and theme options in sync with this union. + "air", + "graphite", + "silver", + "solarized", + "factory", + "factory-mono", + "ayu", + "one-dark", + "nord", + "dracula", + "gruvbox", + "tokyo-night", + "catppuccin-mocha", + "github-dark", + "everforest", + "rose-pine", + "kanagawa", + "night-owl", + "palenight", + "monokai-pro", + "slime", + "brutalist", + "neon-city", + "parchment", + "terminal", + "glass", + // FNXC:DashboardTheming 2026-07-01-00:00: Glass Silver is the silver/gray frosted sibling of Glass; keep this id in lockstep with dashboard/desktop validators and selector metadata so persisted explicit choices survive startup. + "glass-silver", + "horizon", + "vitesse", + "outrun", + "snazzy", + "porple", + "espresso", + "mars", + "poimandres", + "ember", + "rust", + "copper", + "foundry", + "carbon", + "sandstone", + "lagoon", + "frost", + "lavender", + "neon-bloom", + "sepia", + "shadcn", + // FNXC:DashboardTheming 2026-06-30-00:00: Shadcn Ember is the default for unset installs; keep it adjacent to the shadcn base so dashboard options and bootstrap validators preserve published theme order while explicit legacy ids remain valid. + "shadcn-ember", + // FNXC:DashboardTheming 2026-06-20-18:20: FN-6816 adds the user-customizable shadcn variant; keep this union in lockstep with dashboard theme options, swatches, theme-data base blocks, and the shadcn custom color token list. + "shadcn-custom", + // FNXC:DashboardTheming 2026-06-19-16:07: FN-6756 extends the published color-theme union with shadcn-family accent variants; keep dashboard theme options, bootstrap validation, swatches, and theme-data token blocks in lockstep with this ordered list. + // FNXC:DashboardTheming 2026-06-20-00:00: FN-6813 renames the grayscale-base mono theme to shadcn-mono-red and adds the remaining mono accent variants; keep Shadcn Gray adjacent to Shadcn Black so the color-family order stays stable with FN-6814. + // FNXC:DashboardTheming 2026-06-21-00:00: FN-6815 adds shadcn-gray-blue as the slate-neutral blue-gray sibling; keep it adjacent to Shadcn Gray so the published union mirrors dashboard option order. + "shadcn-blue", + "shadcn-green", + "shadcn-red", + "shadcn-purple", + "shadcn-pink", + "shadcn-orange", + "shadcn-yellow", + "shadcn-mono-red", + "shadcn-mono-blue", + "shadcn-mono-green", + "shadcn-mono-purple", + "shadcn-mono-pink", + "shadcn-mono-orange", + "shadcn-mono-yellow", + "shadcn-black", + "shadcn-gray", + "shadcn-gray-blue", +] as const; +export type ColorTheme = (typeof COLOR_THEMES)[number]; + +/** UI locales supported across the dashboard and terminal UI. `en` is the + * source-of-truth language and the fallback for all others. Adding a locale + * here (plus translated catalogs) is the only code change a new language + * needs — see `@fusion/i18n`. zh-CN and zh-TW are independent catalogs and + * are never auto-converted between scripts. */ +export const SUPPORTED_LOCALES = ["en", "zh-CN", "zh-TW", "fr", "es", "ko"] as const; +export type Locale = (typeof SUPPORTED_LOCALES)[number]; +/** Source-of-truth language and the fallback for all locales. */ +export const DEFAULT_LOCALE: Locale = "en"; + +/** Narrow an arbitrary value to a supported `Locale`. */ +export function isLocale(value: unknown): value is Locale { + return ( + typeof value === "string" && + (SUPPORTED_LOCALES as readonly string[]).includes(value) + ); +} diff --git a/packages/core/src/types/merge-policy.ts b/packages/core/src/types/merge-policy.ts new file mode 100644 index 0000000000..77bc5880eb --- /dev/null +++ b/packages/core/src/types/merge-policy.ts @@ -0,0 +1,246 @@ +/** + * Merger strategy, conflict, audit, and auto-recovery policy types + normalizers. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Extracted from types.ts; re-exported from the browser-safe types barrel. + */ + +export type PrStatus = "open" | "closed" | "merged" | "draft"; +export type MergeStrategy = "direct" | "pull-request"; +export type MergeIntegrationWorktreeMode = + | "reuse-task-worktree" + | "cwd-integration-branch" // explicit opt-in; surfaces a warning at startup. See FN-5348. + | "cwd-main"; // legacy alias for cwd-integration-branch; deprecated. Normalized at read time. + +let warnedLegacyCwdMain = false; + +export function __resetLegacyCwdMainWarningForTests(): void { + warnedLegacyCwdMain = false; +} + +export function normalizeMergeIntegrationWorktreeMode( + value: unknown, +): MergeIntegrationWorktreeMode { + if (value === "reuse-task-worktree" || value === "cwd-integration-branch") { + return value; + } + + if (value === "cwd-main") { + if (!warnedLegacyCwdMain) { + warnedLegacyCwdMain = true; + console.warn("[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch"); + } + return "cwd-integration-branch"; + } + + return "reuse-task-worktree"; +} + +export const DIRECT_MERGE_COMMIT_STRATEGIES = ["auto", "always-squash", "always-rebase"] as const; +export type DirectMergeCommitStrategy = (typeof DIRECT_MERGE_COMMIT_STRATEGIES)[number]; + +export const MERGE_ADVANCE_AUTO_SYNC_MODES = ["off", "ff-only", "stash-and-ff"] as const; +export type MergeAdvanceAutoSyncMode = (typeof MERGE_ADVANCE_AUTO_SYNC_MODES)[number]; +export function normalizeMergeAdvanceAutoSyncMode(value: unknown): MergeAdvanceAutoSyncMode { + return value === "off" || value === "ff-only" || value === "stash-and-ff" ? value : "stash-and-ff"; +} +/** How merge conflicts are resolved when the AI agent can't (or shouldn't) decide. + * + * Both `smart-*` strategies share the same cascade: pre-merge fetch + + * fast-forward of local main from origin (graceful degrade on failure), + * then AI, then auto-resolve lock/generated/trivial files. They differ only + * in the final per-file fallback when conflicts remain: + * + * - "smart-prefer-main" (default): fall back to `-X ours` so main's state + * wins. Best when concurrent tasks could regress just-merged sibling work. + * - "smart-prefer-branch": fall back to `-X theirs` so the task branch wins. + * Best when one agent at a time is dominant and you trust their output. + * - "ai-only": run AI on every attempt; never silently prefer one side. + * - "abort": run AI once; if conflict remains, fail the merge so a human + * can resolve it. + * + * Legacy values `"smart"` and `"prefer-main"` are accepted for backwards + * compatibility and normalized via {@link normalizeMergeConflictStrategy}. + * `"smart"` maps to `"smart-prefer-branch"` (its historical fallback) and + * `"prefer-main"` maps to `"smart-prefer-main"`. */ +export type MergeConflictStrategy = + | "smart-prefer-main" + | "smart-prefer-branch" + | "ai-only" + | "abort" + /** @deprecated use "smart-prefer-branch" */ + | "smart" + /** @deprecated use "smart-prefer-main" */ + | "prefer-main"; + +/** Canonical (post-migration) values that the merger actually dispatches on. */ +export type CanonicalMergeConflictStrategy = Exclude< + MergeConflictStrategy, + "smart" | "prefer-main" +>; + +/** Translate legacy `mergeConflictStrategy` values into their canonical form. + * Pass-through for already-canonical values; defaults to "smart-prefer-main" + * when the input is undefined. */ +export function normalizeMergeConflictStrategy( + value: MergeConflictStrategy | undefined, +): CanonicalMergeConflictStrategy { + switch (value) { + case "smart": + return "smart-prefer-branch"; + case "prefer-main": + return "smart-prefer-main"; + case undefined: + return "smart-prefer-main"; + default: + return value; + } +} + +export const MERGE_STRATEGY_OVERLAP_BEHAVIORS = [ + "flip-to-prefer-branch", + "warn-only", + "ignore", +] as const; + +export type MergeStrategyOverlapBehavior = (typeof MERGE_STRATEGY_OVERLAP_BEHAVIORS)[number]; + +export function normalizeMergeStrategyOverlapBehavior( + value: unknown, +): MergeStrategyOverlapBehavior { + return typeof value === "string" + && (MERGE_STRATEGY_OVERLAP_BEHAVIORS as readonly string[]).includes(value) + ? value as MergeStrategyOverlapBehavior + : "flip-to-prefer-branch"; +} + +export const POST_MERGE_AUDIT_MODES = ["block", "warn", "off"] as const; + +/** Controls how the merger reacts to a dirty post-merge audit (FN-4333). */ +export type PostMergeAuditMode = (typeof POST_MERGE_AUDIT_MODES)[number]; + +export function normalizePostMergeAuditMode(value: unknown): PostMergeAuditMode { + return typeof value === "string" + && (POST_MERGE_AUDIT_MODES as readonly string[]).includes(value) + ? (value as PostMergeAuditMode) + : "block"; +} + +export const MERGE_AUDIT_AUTO_RECOVERY_MODES = ["deterministic-only", "programmatic", "ai-assisted", "off"] as const; + +/** Controls how aggressively the merger tries to auto-recover from audit blocks (FN-4315). */ +export type MergeAuditAutoRecoveryMode = (typeof MERGE_AUDIT_AUTO_RECOVERY_MODES)[number]; + +export function normalizeMergeAuditAutoRecovery(value: unknown): MergeAuditAutoRecoveryMode { + return typeof value === "string" + && (MERGE_AUDIT_AUTO_RECOVERY_MODES as readonly string[]).includes(value) + ? (value as MergeAuditAutoRecoveryMode) + : "ai-assisted"; +} + +export const MERGER_MODES = ["ai", "deterministic"] as const; + +/** + * Merge execution path (FN-5633). + * - "ai" (default): the standalone AI merge path — a clean-room worktree where + * an AI agent merges the task branch and an AI reviewer audits it (with + * corrective retries) before a fast-forward landing. Bypasses the legacy + * scaffolding entirely. + * - "deterministic": **DEPRECATED (master-plan U0, 2026-06-21) and INERT.** Once + * routed to the legacy `aiMergeTask` pipeline; now ignored — every merge uses + * the unified "ai" path (`runAiMerge`). The value is retained (not removed) to + * avoid a breaking `@runfusion/fusion` type change, and the engine logs a + * one-time deprecation warning when it observes a resolved "deterministic". + * + * FNXC:MergerUnification 2026-06-21-19:05: `merger.mode` is published surface, so + * the type and the `MergerSettings.mode` field stay; only the "deterministic" + * VALUE is deprecated/inert. Removing the type is a separate breaking change. + */ +export type MergerMode = (typeof MERGER_MODES)[number]; + +export function normalizeMergerMode(value: unknown): MergerMode { + return typeof value === "string" && (MERGER_MODES as readonly string[]).includes(value) + ? (value as MergerMode) + : "ai"; +} + +/** Settings for the AI merge path (FN-5633). */ +export interface MergerSettings { + /** + * Which merge path to use. Default: "ai". + * @deprecated master-plan U0 (2026-06-21): the value is inert — every merge now + * uses the unified AI merge path (`runAiMerge`). Field retained as published + * surface; "deterministic" only triggers a one-time deprecation warning. + */ + mode?: MergerMode; + /** How many AI corrective rounds before landing the best result (advisory) or + * hard-failing (blocking). Default: 3. The reviewer uses the project's + * validator/reviewer model lane — there is no merge-specific model setting. */ + maxReviewPasses?: number; + /** Dangerous compatibility escape hatch for the AI merge landing path. + * When true (default for resolved project settings), Fusion restores the legacy + * stash → fast-forward → restore behavior when the checked-out integration + * worktree is dirty. Set false to explicitly opt out and fail closed before + * unrelated local edits can be reintroduced after landing. */ + allowDirtyLocalCheckoutSync?: boolean; +} + +export const AUTO_RECOVERY_MODES = ["off", "deterministic-only", "programmatic", "ai-assisted"] as const; + +export type AutoRecoveryMode = (typeof AUTO_RECOVERY_MODES)[number]; + +export type AutoRecoveryFailureClass = + | "file-scope-invariant" + | "post-squash-audit-blocker" + | "branch-cross-contamination" + | "branch-conflict-tripwire" + | "branch-conflict-recovery-exhausted" + | "branch-conflict-unrecoverable" + | "message-delivery-failure"; + +export interface AutoRecoverySettings { + mode: AutoRecoveryMode; + perClass?: Partial>; + maxRetries?: number; +} + +export function normalizeAutoRecovery(value: unknown): AutoRecoverySettings { + const fallback: AutoRecoverySettings = { mode: "deterministic-only", maxRetries: 3 }; + if (!value || typeof value !== "object") return fallback; + + const candidate = value as { + mode?: unknown; + perClass?: unknown; + maxRetries?: unknown; + }; + const mode = typeof candidate.mode === "string" && (AUTO_RECOVERY_MODES as readonly string[]).includes(candidate.mode) + ? candidate.mode as AutoRecoveryMode + : fallback.mode; + const perClass = typeof candidate.perClass === "object" && candidate.perClass + ? Object.fromEntries( + Object.entries(candidate.perClass as Record) + .filter(([k, v]) => ( + [ + "file-scope-invariant", + "post-squash-audit-blocker", + "branch-cross-contamination", + "branch-conflict-tripwire", + "branch-conflict-recovery-exhausted", + "branch-conflict-unrecoverable", + "message-delivery-failure", + ].includes(k) + && typeof v === "string" + && (AUTO_RECOVERY_MODES as readonly string[]).includes(v) + )), + ) as Partial> + : undefined; + const maxRetries = typeof candidate.maxRetries === "number" && Number.isFinite(candidate.maxRetries) + ? Math.max(0, Math.floor(candidate.maxRetries)) + : fallback.maxRetries; + + return { mode, perClass, maxRetries }; +} +/** Policy for handling task execution when the selected node is unavailable/unhealthy. */ +export type UnavailableNodePolicy = "block" | "fallback-local"; + +export type OwningNodeHandoffPolicy = "block" | "reassign-to-local" | "reassign-any-healthy"; diff --git a/packages/core/src/types/merge-queue.ts b/packages/core/src/types/merge-queue.ts new file mode 100644 index 0000000000..c3b63943e0 --- /dev/null +++ b/packages/core/src/types/merge-queue.ts @@ -0,0 +1,167 @@ +/** + * Merge-queue, merge-request, and workflow work-item domain types. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Extracted from types.ts; re-exported from the browser-safe types barrel. + */ + +import type { TaskPriority } from "./board.js"; + +export const MERGE_REQUEST_STATES = [ + "queued", + "running", + "retrying", + "succeeded", + "exhausted", + "cancelled", + "manual-required", +] as const; + +export type MergeRequestState = (typeof MERGE_REQUEST_STATES)[number]; + +export const WORKFLOW_WORK_ITEM_KINDS = [ + "task", + "merge", + "retry", + "manual-hold", + "recovery", +] as const; + +export type WorkflowWorkItemKind = (typeof WORKFLOW_WORK_ITEM_KINDS)[number]; + +export const WORKFLOW_WORK_ITEM_STATES = [ + "runnable", + "running", + "held", + "retrying", + "manual-required", + "succeeded", + "failed", + "cancelled", + "exhausted", +] as const; + +export type WorkflowWorkItemState = (typeof WORKFLOW_WORK_ITEM_STATES)[number]; + +export interface WorkflowWorkItem { + id: string; + runId: string; + taskId: string; + nodeId: string; + kind: WorkflowWorkItemKind; + state: WorkflowWorkItemState; + attempt: number; + retryAfter: string | null; + leaseOwner: string | null; + leaseExpiresAt: string | null; + lastError: string | null; + blockedReason: string | null; + createdAt: string; + updatedAt: string; +} + +export interface WorkflowWorkItemUpsertInput { + id?: string; + runId: string; + taskId: string; + nodeId: string; + kind: WorkflowWorkItemKind; + state?: WorkflowWorkItemState; + attempt?: number; + retryAfter?: string | null; + leaseOwner?: string | null; + leaseExpiresAt?: string | null; + lastError?: string | null; + blockedReason?: string | null; + now?: string; +} + +export interface WorkflowWorkItemTransitionPatch { + attempt?: number; + retryAfter?: string | null; + leaseOwner?: string | null; + leaseExpiresAt?: string | null; + lastError?: string | null; + blockedReason?: string | null; + now?: string; +} + +export interface WorkflowWorkItemDueFilter { + now?: string; + limit?: number; + kinds?: WorkflowWorkItemKind[]; + states?: WorkflowWorkItemState[]; +} + +export interface MergeRequestWorkflowProjectionOptions { + runId?: string; + nodeId?: string; + now?: string; +} + +export interface MergeQueueEntry { + taskId: string; + enqueuedAt: string; + priority: TaskPriority; + leasedBy: string | null; + leasedAt: string | null; + leaseExpiresAt: string | null; + attemptCount: number; + lastError: string | null; +} + +export interface MergeRequestRecord { + taskId: string; + state: MergeRequestState; + createdAt: string; + updatedAt: string; + attemptCount: number; + lastError: string | null; +} + +export interface CompletionHandoffMarker { + taskId: string; + acceptedAt: string; + source: string; +} + +export interface MergeQueueEnqueueOptions { + priority?: TaskPriority; + now?: string; +} + +export interface MergeQueueAcquireOptions { + leaseDurationMs: number; + now?: string; + /** If provided, the lease attempt targets this specific task first. + * The task must be unexpired/available; otherwise falls back to normal queue-head selection. */ + targetTaskId?: string; +} + +export type MergeQueueReleaseOutcome = + | { kind: "success" } + | { kind: "failure"; error: string }; + +export interface HandoffEvidence { + /** Reason text recorded on the run-audit event (for example "fn_task_done"). */ + reason: string; + /** Optional run id captured for forensics. */ + runId?: string; + /** Optional agent id captured for forensics. */ + agentId?: string; +} + +export interface HandoffToReviewOptions { + ownerAgentId: string | null; + evidence: HandoffEvidence; + moveOptions?: { + preserveResumeState?: boolean; + preserveProgress?: boolean; + preserveWorktree?: boolean; + preserveStatus?: boolean; + moveSource?: "user" | "engine"; + skipMergeBlocker?: boolean; + }; + /** Inject a clock for tests. */ + now?: string; +} diff --git a/packages/core/src/types/workflow-steps.ts b/packages/core/src/types/workflow-steps.ts new file mode 100644 index 0000000000..1854f727b2 --- /dev/null +++ b/packages/core/src/types/workflow-steps.ts @@ -0,0 +1,395 @@ +/** + * Workflow step templates, run instances, notifications, and model-preset types. + * + * FNXC:CodeOrganization 2026-07-15-00:00: + * Extracted from types.ts; re-exported from the browser-safe types barrel. + */ + +import type { ThinkingLevel } from "./board.js"; + +export interface ModelPreset { + id: string; + name: string; + executorProvider?: string; + executorModelId?: string; + validatorProvider?: string; + validatorModelId?: string; +} + +/** A reusable workflow step definition that can run after task implementation. */ +/** Execution mode for a workflow step. */ +export type WorkflowStepMode = "prompt" | "script"; +export type WorkflowStepToolMode = "readonly" | "coding"; +export type WorkflowStepGateMode = "gate" | "advisory"; + +/** Lifecycle phase for workflow step execution. */ +export type WorkflowStepPhase = "pre-merge" | "post-merge"; + +export interface WorkflowStep { + /** Unique identifier (e.g., "WS-001") */ + id: string; + /** Built-in template source ID when this step was materialized from a template. */ + templateId?: string; + /** Display name (e.g., "Documentation Review") */ + name: string; + /** Short description for UI display */ + description: string; + /** Execution mode — "prompt" runs an AI agent, "script" runs a named project script */ + mode: WorkflowStepMode; + /** Lifecycle phase — "pre-merge" runs before merge (default), "post-merge" runs after merge success */ + phase?: WorkflowStepPhase; + /** Gate behavior — gate blocks merge/auto-revive on failure, advisory records non-blocking findings. */ + gateMode: WorkflowStepGateMode; + /** Full agent prompt to execute when this step runs (used when mode is "prompt") */ + prompt: string; + /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ + toolMode?: WorkflowStepToolMode; + /** Name of a skill to load into this step's session (e.g. + * "compound-engineering:ce-work"). When set, the step session loads the named + * skill (discovery + selection) and the engine injects the Fusion workflow-step + * conventions preamble. Only meaningful for skill-executor graph nodes. */ + skillName?: string; + /** + * Browser capability requested by prompt-mode steps. When true, the executor + * loads the agent-browser navigation skill when available, preflights the + * `agent-browser` CLI, and records browser-verification activity in the agent + * log. Ignored for script-mode steps. + */ + requiresBrowser?: boolean; + /** Name of a script from project settings `scripts` map to execute (required when mode is "script") */ + scriptName?: string; + /** Whether this step is available for selection on new tasks */ + enabled: boolean; + /** When true, this step is automatically pre-selected when creating new tasks. + * Users can still deselect it — this only controls the initial default state. */ + defaultOn?: boolean; + /** AI model provider override for the workflow step agent (e.g., "anthropic"). + * Must be set together with `modelId`. When both model fields are undefined, + * the executor uses global settings defaults. Only used when mode is "prompt". */ + modelProvider?: string; + /** AI model ID override for the workflow step agent (e.g., "claude-sonnet-4-5"). + * Must be set together with `modelProvider`. When both model fields are undefined, + * the executor uses global settings defaults. Only used when mode is "prompt". */ + modelId?: string; + /** + * FNXC:Settings-ThinkingLevel 2026-07-10-00:00: + * Workflow IR nodes may pin reasoning effort independently from the model pair so authors can inherit the model while overriding thinking level. Runtime precedence is node/step `thinkingLevel` > task `thinkingLevel` > settings `defaultThinkingLevel`. + */ + thinkingLevel?: ThinkingLevel; + /** (workflow-editor-consolidation U1/U2, KTD-1/KTD-3) when this legacy step has + * been migrated into a fragment WorkflowDefinition, the fragment's id is stamped + * here so the lazy step migration is idempotent (already-stamped rows are + * skipped). Stored in the `migrated_fragment_id` column. */ + migratedFragmentId?: string; + /** ISO-8601 timestamp of creation */ + createdAt: string; + /** ISO-8601 timestamp of last update */ + updatedAt: string; +} + +/** Input for creating a new workflow step. */ +/** Event types that can trigger ntfy notifications */ +export type NtfyNotificationEvent = + | "in-review" + | "merged" + | "failed" + | "awaiting-approval" + | "awaiting-user-review" + | "planning-awaiting-input" + | "cli-agent-awaiting-input" + | "gridlock" + | "board-stall-unrecovered" + | "db-corruption-detected" + | "fallback-used" + | "memory-dreams-processed" + | "token-budget" + | "message:agent-to-user" + | "message:agent-to-agent" + | "message:room" + | "oauth-token-expired" + | "task-created" + | "workflow-notify"; + +/** Known notification event types. Providers may support additional custom events. */ +export const NOTIFICATION_EVENTS = [ + "in-review", + "merged", + "failed", + "awaiting-approval", + "awaiting-user-review", + "planning-awaiting-input", + /* + * FNXC:ToolPermissionNotifications 2026-06-27-00:00: + * CLI tool-permission requests are a distinct user-facing notification event from plan approval. Operators must be able to enable external alerts when a terminal-backed agent waits for human input. + */ + "cli-agent-awaiting-input", + "gridlock", + "board-stall-unrecovered", + "db-corruption-detected", + "fallback-used", + "memory-dreams-processed", + "token-budget", + "message:agent-to-user", + "message:agent-to-agent", + "message:room", + "oauth-token-expired", + "task-created", + "workflow-notify", +] as const; + +/** Notification event type. Known events plus provider-specific custom events. */ +export type NotificationEvent = (typeof NOTIFICATION_EVENTS)[number] | (string & {}); + +/** Standard payload shape shared across notification providers. */ +export interface NotificationPayload { + taskId?: string; + taskTitle?: string; + taskDescription?: string; + event: NotificationEvent; + timestamp?: string; + metadata?: Record; +} + +/** Declarative notification provider configuration persisted in settings. */ +export interface NotificationProviderConfig { + id: string; + name: string; + enabled: boolean; + config: Record; +} + +export interface CustomProvider { + id: string; + name: string; + apiType: "openai-compatible" | "anthropic-compatible" | "google-generative-ai" | "openai-responses"; + baseUrl: string; + apiKey?: string; + /** + * OpenAI-compatible opt-in for providers that explicitly support the `developer` role. + * Omitted/false forces legacy `system` role emission to avoid provider 400s. + */ + supportsDeveloperRole?: boolean; + /** + * FNXC:ProviderAuth 2026-07-08-00:00: + * FN-7689: opt-in for custom `openai-compatible`/`openai-responses` gateways that proxy an + * Anthropic-format backend (e.g. `usai/claude_4_6_sonnet`). When true, registered + * `openai-completions` models get pi-ai's `compat.cacheControlFormat = "anthropic"`, which makes + * pi-ai emit Anthropic-style `cache_control` breakpoints on the system prompt, last + * conversation message, and last tool. Without this, pi-ai's `detectCompat` only auto-enables + * caching for OpenRouter `anthropic/*` models, so a generic custom gateway re-bills the entire + * context prefix uncached every turn (measured cachedTokens=0/cacheWriteTokens=0 across 243 + * runs, ~327.5:1 input:output ratio). Default off — never force cache_control on gateways that + * did not opt in, since non-Anthropic-compatible backends (Together, Fireworks, etc.) can 400 on + * unexpected `cache_control` fields. Inert for `anthropic-compatible` (already auto-caches) and + * `google-generative-ai` (no cache_control concept). + */ + anthropicPromptCaching?: boolean; + models?: { id: string; name: string }[]; +} + +export interface WorkflowStepInput { + /** Built-in template source ID when creating a concrete step from a template. */ + templateId?: string; + name: string; + description: string; + /** Execution mode — defaults to "prompt" if not specified */ + mode?: WorkflowStepMode; + /** Lifecycle phase — defaults to "pre-merge" if not specified */ + phase?: WorkflowStepPhase; + /** Gate behavior — defaults by mode (prompt: advisory, script: gate) when omitted. */ + gateMode?: WorkflowStepGateMode; + /** Agent prompt (used when mode is "prompt"). Optional — can be AI-generated later via refinement. */ + prompt?: string; + /** Tool set available to prompt-mode workflow agents. Defaults to readonly. */ + toolMode?: WorkflowStepToolMode; + /** Name of a skill to load into this step's session (e.g. + * "compound-engineering:ce-work"). See `WorkflowStep.skillName`. */ + skillName?: string; + /** Script name from project settings (required when mode is "script"). + * Must reference a named script in `settings.scripts` — no raw commands. */ + scriptName?: string; + /** Defaults to true if not specified */ + enabled?: boolean; + /** When true, this step is automatically pre-selected when creating new tasks. + * Users can still deselect — this only controls the initial default state. */ + defaultOn?: boolean; + /** AI model provider override. Must be set together with modelId. Only used when mode is "prompt". */ + modelProvider?: string; + /** AI model ID override. Must be set together with modelProvider. Only used when mode is "prompt". */ + modelId?: string; + /** Optional per-node reasoning-effort override; inherits from task/settings when omitted. */ + thinkingLevel?: ThinkingLevel; + /** (workflow-editor-consolidation U2, KTD-3) fragment id stamped when this step + * was migrated into a fragment WorkflowDefinition. Set by the migration only. */ + migratedFragmentId?: string; +} + +/** Result of a workflow step execution on a task. */ +export interface WorkflowStepResult { + /** ID of the workflow step that ran (e.g., "WS-001") */ + workflowStepId: string; + /** Name of the workflow step at execution time */ + workflowStepName: string; + /** Lifecycle phase at execution time */ + phase?: WorkflowStepPhase; + /** Runtime source for distinguishing graph-authored node progress from optional-toggle checks. */ + source?: "optional-group" | "node"; + /** Execution status */ + status: "passed" | "failed" | "advisory_failure" | "skipped" | "pending"; + /** Output from the workflow step agent (findings, errors, etc.) */ + output?: string; + /** + * Machine-readable verdict from prompt-mode structured output. + * Absent for script-mode steps and legacy prose-only prompt outputs. + */ + verdict?: "APPROVE" | "APPROVE_WITH_NOTES" | "REVISE"; + /** + * Optional notes from prompt-mode structured output. + * Absent for script-mode steps and legacy prose-only prompt outputs. + */ + notes?: string; + /** ISO-8601 timestamp when the step started */ + startedAt?: string; + /** ISO-8601 timestamp when the step completed */ + completedAt?: string; + /* + * FNXC:ReviewLaneBypass 2026-07-09-00:00: + * A privileged operator can bypass a `status:"failed"` pre-merge review step + * (leading real-world cause: the Runfusion/Fusion#1946 `(no feedback captured)` + * no-verdict dispatch defect) so a card stranded solely by that failure can + * advance to merge (FN-7720). The bypass REWRITES this result's `status` to a + * terminal, non-blocking value (`"skipped"`) and stamps the fields below as an + * explicit audit trail — it never fabricates a reviewer `verdict`. Only the + * `getTaskMergeBlocker` "task has failed pre-merge workflow steps" reason is + * cleared; every other merge-blocker condition (paused, incomplete steps, + * blocking task status, still-`pending` pre-merge steps) is untouched. + */ + /** Operator identity that performed the bypass, if this result was bypassed. */ + bypassedBy?: string; + /** ISO-8601 timestamp when the bypass was applied. */ + bypassedAt?: string; + /** Mandatory operator-supplied justification for the bypass. */ + bypassReason?: string; + /** The `status` this result carried immediately before the bypass rewrote it (always `"failed"` for the supported bypass path). */ + bypassedFromStatus?: WorkflowStepResult["status"]; + /** The `verdict` (if any) this result carried immediately before the bypass, preserved for audit only — never promoted to `verdict`. */ + bypassedFromVerdict?: WorkflowStepResult["verdict"]; + /* + * FNXC:WorkflowStepResults 2026-07-09-00:10: + * FN-7727: self-healing recovery re-runs a failed pre-merge review node + * (`code-review`, `code-review-remediation`, `plan-review`, + * `browser-verification`) in place, and the recorder upsert previously + * REPLACED the prior `status:"failed"` entry — erasing its captured + * `output`/`notes`/`verdict`/timestamps forever (the diagnostic trail + * FN-7642 worked to capture, and the history FN-7720's bypass affordance + * needs to show). `priorAttempts` preserves a BOUNDED, single-level history + * of prior terminal-failure (`failed`/`advisory_failure`) attempts on the + * surviving entry — snapshots never carry their own nested `priorAttempts`, + * so history cannot grow unbounded. This field is READ-ONLY history: it + * never participates in merge-blocking (`getTaskMergeBlocker`), self-healing + * recovery selection (`latestFailedPreMergeStep`), or progress/timing + * computation — only the current (this) entry's fields do. Written by the + * shared `upsertWorkflowStepResult` helper (`workflow-step-results.ts`). + */ + /** Bounded, single-level history of prior terminal-failure attempts this entry replaced. Read-only; never affects merge-blocking or recovery selection. */ + priorAttempts?: WorkflowStepResult[]; +} + +/** + * Lifecycle status of one persisted step instance (step-inversion U4, KTD-6). + * - `pending` — expanded but not yet started. + * - `in-progress` — actively executing inside its foreach sub-walk. + * - `awaiting-integration` — work complete on a parallel-mode branch, waiting + * for the ordered integration stage (KTD-11; unused at concurrency 1). + * - `completed` — terminal success (integrated in parallel mode). + * - `failed` — terminal failure. + */ +export type WorkflowRunStepInstanceStatus = + | "pending" + | "in-progress" + | "awaiting-integration" + | "completed" + | "failed"; + +/** + * Persisted run-state for one expanded step instance inside a foreach region + * (step-inversion U4, KTD-6). One row per `(taskId, runId, foreachNodeId, + * stepIndex)`; mirrors the `workflow_run_branches` posture. Resume reconstructs + * the instance set from `pinnedStepCount` + per-instance `currentNodeId` / + * `reworkCount`. `baselineSha` / `checkpointId` are the RETHINK reset anchors + * (previously in-memory, lost on restart). `branchName` / `integratedAt` and the + * `awaiting-integration` status serve parallel mode (KTD-11); null/unused at + * concurrency 1. This is the core row shape; the engine-side instance model is + * separate and engine-owned. + */ +export interface WorkflowRunStepInstance { + taskId: string; + runId: string; + /** Node id of the foreach region that expanded this instance. */ + foreachNodeId: string; + /** Zero-based index of the step this instance runs. */ + stepIndex: number; + /** Step count pinned at expansion; resume fails on mismatch with live steps[]. */ + pinnedStepCount: number; + /** Current sub-walk node id for the in-flight instance; null when not started. */ + currentNodeId?: string | null; + status: WorkflowRunStepInstanceStatus; + /** Git sha the RETHINK reset rewinds to; null when no baseline captured. */ + baselineSha?: string | null; + /** Session checkpoint to rewind to on RETHINK; null when none captured. */ + checkpointId?: string | null; + /** Number of rework cycles consumed against the rework budget. */ + reworkCount: number; + /** Per-instance branch name in worktree-isolation mode (KTD-11); null otherwise. */ + branchName?: string | null; + /** ISO-8601 timestamp the instance branch was integrated (KTD-11); null otherwise. */ + integratedAt?: string | null; + /** ISO-8601 timestamp of the last write to this row. */ + updatedAt: string; +} + +/* +FNXC:WorkflowStepTemplate 2026-06-25-00:00: +U6 deleted the built-in step-template catalog array (the former value export). The +`WorkflowStepTemplate` SHAPE is KEPT because plugin-contributed step templates still use +it (they feed the +workflow-editor optional-group palette via `getPluginWorkflowStepTemplates`). It is no +longer backed by any built-in catalog: the former built-in `browser-verification` / +`code-review` literals now live inlined in their optional-group node builders +(`builtin-browser-verification-group.ts` / `builtin-code-review-group.ts`). +*/ +/** A workflow step template shape used by plugin-contributed steps (palette entries). */ +export interface WorkflowStepTemplate { + /** Unique template identifier (e.g., "documentation-review") */ + id: string; + /** Display name (e.g., "Documentation Review") */ + name: string; + /** Short description for UI */ + description: string; + /** Full agent prompt template */ + prompt: string; + /** Execution mode for plugin-contributed templates; defaults to prompt. */ + mode?: WorkflowStepMode; + /** Task lifecycle phase for plugin-contributed templates; defaults to pre-merge. */ + phase?: "pre-merge" | "post-merge"; + /** Script name for script-mode plugin templates. */ + scriptName?: string; + /** Tool set available when the template runs as a prompt-mode step. */ + toolMode?: WorkflowStepToolMode; + /** Failure behavior for materialized steps from this template. */ + gateMode?: WorkflowStepGateMode; + /** Whether this template should be auto-selected for new tasks. */ + defaultOn?: boolean; + /** AI model provider override for prompt-mode templates. */ + modelProvider?: string; + /** AI model ID override for prompt-mode templates. */ + modelId?: string; + /** Optional per-node reasoning-effort override for prompt-mode templates. */ + thinkingLevel?: ThinkingLevel; + /** Grouping category (e.g., "Quality", "Security") */ + category: string; + /** Optional icon identifier for UI (e.g., "file-text", "shield") */ + icon?: string; + /** Optional default enabled state for plugin-provided templates. */ + enabled?: boolean; +} diff --git a/packages/engine/src/__tests__/executor-missing-task-json-transient.test.ts b/packages/engine/src/__tests__/executor-missing-task-json-transient.test.ts index 64edad7855..b369faebc6 100644 --- a/packages/engine/src/__tests__/executor-missing-task-json-transient.test.ts +++ b/packages/engine/src/__tests__/executor-missing-task-json-transient.test.ts @@ -13,6 +13,22 @@ describe("isTransientMissingTaskJsonError", () => { expect(isTransientMissingTaskJsonError(err, task)).toBe(true); }); + it("matches Windows worktree paths with backslash separators", () => { + const windowsTask = { + id: "FN-5624", + worktree: "C:\\worktrees\\fn-5624", + }; + const err = "ENOENT: no such file or directory, open 'C:\\worktrees\\fn-5624\\.fusion\\tasks\\FN-5624\\task.json'"; + + expect(isTransientMissingTaskJsonError(err, windowsTask)).toBe(true); + }); + + it("does not treat a sibling worktree with the same prefix as contained", () => { + const err = "ENOENT: no such file or directory, open '/tmp/worktrees/fn-5624-copy/.fusion/tasks/FN-5624/task.json'"; + + expect(isTransientMissingTaskJsonError(err, task)).toBe(false); + }); + it("does not match task.json parse failures", () => { const err = "Failed to parse task.json at /tmp/worktrees/fn-5624/.fusion/tasks/FN-5624/task.json: Unexpected token"; expect(isTransientMissingTaskJsonError(err, task)).toBe(false); diff --git a/packages/engine/src/agent-heartbeat-prompts.ts b/packages/engine/src/agent-heartbeat-prompts.ts new file mode 100644 index 0000000000..f5ebb72083 --- /dev/null +++ b/packages/engine/src/agent-heartbeat-prompts.ts @@ -0,0 +1,517 @@ +/** + * FNXC:CodeOrganization 2026-07-15-00:00: + * Heartbeat system prompts and procedures peeled from agent-heartbeat.ts. + */ +import { + FUSION_RUNTIME_SELF_AWARENESS, + TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION, +} from "@fusion/core"; + +export const HEARTBEAT_CRITICAL_RULES = `## Critical Rules + +- ONE concrete coordination action per tick, then call fn_heartbeat_done (or an explicit no-op with reason). +- Do NOT implement task body work (code, tests, commits, multi-step coding) in a heartbeat — that is the executor path. +- Do NOT call fn_task_pause for failures or blockers; pause is only for explicit user manual control. +- Checkout/claim conflict: do NOT retry. Treat as terminal for this tick; pick other work or exit. +- Blocked-task dedup: if the same blocker is already logged and Wake Delta shows no new context, do not re-chase or re-comment — no-op with reason. +- Before fn_task_create, scan open tasks; do not create duplicates of work already covered. +- Prefer create/delegate to another agent over asking a human when an agent can do the work. +- Escalate via reports-to / chain of command when stuck after a concrete chase attempt. +- Progress notes: short status line + done / remaining / next owner + task ids (FN-####). +- Your assigned tasks list (when present) is coordination inventory, not an implement-from-heartbeat queue.`; + +export const HEARTBEAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a heartbeat agent running in a short execution window. + +## Your Role + +This is an ambient heartbeat. Task implementation work (coding, running tests, making commits) runs in a separate +execution path handled by the executor. Do NOT do task body work or implementation in this heartbeat. + +Your purpose is to keep momentum through coordination: surface blockers, respond to messages, manage memory, +delegate, and route work to the right place. Think in single-pass interventions, not coding sessions. + +${HEARTBEAT_CRITICAL_RULES} + +Your job: +1. Check your assigned task context — review its state, blockedBy field, and any new comments. +2. Do ONE useful coordination action. +3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations, and fn_task_document_write for durable artifacts. Before calling fn_task_create, scan existing open tasks (the board context provided to you, or fn_task_list when in doubt) — if an open task already covers this work, log against it or update it instead of creating a duplicate. +4. Use fn_list_agents + fn_delegate_task when work should be assigned to a specific capable agent now. +5. Use fn_get_agent_config and fn_update_agent_config to tune direct reports before delegating recurring work. +6. Call fn_heartbeat_done when finished with an optional summary of what was accomplished. + +**If your bound task is blocked** (blockedBy is set in the task context): +- Surface the blocker concretely with fn_task_log. +- Chase the dependency: comment on the blocking task, send a message to the responsible agent, or ping an owner. +- Look for unblocking work you can spawn or delegate right now. +- Do NOT call fn_task_pause to handle a failed or blocked task. Pausing is reserved for explicit user requests for manual control. +- If the task needs recovery, create/delegate focused follow-up work with fn_task_create or fn_delegate_task, log the needed operator action, or let the task surface as failed. +- Pivot to other relevant coordination work if the blocker cannot be immediately resolved. + +**If your bound task is not blocked:** +- Surface progress, status, or coordination needs with fn_task_log or fn_task_document_write. +- Create follow-up tasks for discovered risks or gaps. +- Respond to new steering comments or user messages. + +Examples of ONE useful coordination action: +- DO: log a concrete blocker with next steps and message the agent responsible for unblocking. +- DO: create a focused follow-up task when a missing dependency is discovered. +- DO: delegate a well-scoped task to an appropriate idle specialist agent. +- DO: save a short investigation note with fn_task_document_write when the analysis is reusable. +- DON'T: attempt full implementation, run tests, commit code, or do multi-step coding work. +- DON'T: create vague tasks like "investigate stuff" without actionable scope. + +Keep work lightweight — this is a single-pass coordination check, not an implementation run. +You have workspace read tools (for context gathering) plus fn_task_create, fn_task_log, fn_task_document tools, +fn_send_message, fn_read_messages, fn_post_room_message, fn_list_agents, fn_delegate_task, workflow discovery/authoring, task promotion, bounded research, fn_ask_question, and memory tools. + +**Task Documents:** Save important findings with fn_task_document_write(key="...", content="..."). +Documents persist across sessions and are visible in the dashboard's Documents tab. + +## Triage and Routing Decisions + +Use this decision rule: +- **Log only (fn_task_log):** when the information is contextual, transient, or tied to this task's current state. +- **Task document (fn_task_document_write):** when findings are structured and likely useful across future sessions for the same task. +- **Create task (fn_task_create):** when someone must do new executable work. +- **Delegate task (fn_delegate_task):** when that new work should go to a specific agent based on role/availability. +- **Manage report config (fn_get_agent_config / fn_update_agent_config):** when direct reports need heartbeat, instruction, or personality tuning. + +Prefer fn_task_create when assignment is unclear and scheduler routing is fine. +Prefer fn_delegate_task when immediate ownership by a specific agent materially reduces latency or risk. + +## Common Patterns + +- **Blocked task:** log the concrete blocker once, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed; do not pause it unless the user explicitly requested manual control. If you already logged the same blocker and nothing new arrived, no-op with reason. +- **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause; do not use fn_task_pause as failure handling. +- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick. +- **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log. +- **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation. +- **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies. + +## Memory Boundaries + +You may receive an Agent Memory section and a Project Memory section. +- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/. +- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval. +- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace. +- Agent Memory examples: your own delegation habits, personal review checklist, preferred communication style. +- Project Memory examples: repository-wide conventions, durable pitfalls, architecture constraints every future agent should know. + +## Processing Messages + +When you are woken by an incoming message (source includes "wake-on-message"), you should: +1. Use fn_read_messages to check your inbox for unread messages. +2. For each message, classify it: informational, question, request, or escalation. +3. Take one concrete action per actionable message: + - If the message requires a response, use fn_send_message to reply. + - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output. + - If the message is informational, acknowledge it by logging with fn_task_log. + - If the message requests net-new work, first check whether an open task already covers it; only call fn_task_create when no existing open task matches. + - If ownership is clear and an agent is available, delegate using fn_delegate_task. +4. If a Pending Room Messages section is present, review it too: + - Use fn_post_room_message only when the room content is relevant to your role, soul, or identity. + - If a Room Ambiguity Notices section is present, follow it exactly: echo resolved referents before acting, and under clarification notices do not create tasks. + - If a Room Coordination Notices section is present, follow its claim/defer branch exactly: under "claim" post a one-line claim before calling fn_task_create; under "defer-suggested" do NOT call fn_task_create and instead acknowledge the prior claim via fn_post_room_message. + - Reference room message IDs when replying so humans can trace context. +5. After processing messages, continue with your normal heartbeat duties. + +Example flow: +- Read unread messages → identify "needs action" item → reply with intent (reply_to_message_id) → create/delegate task if execution is needed → log key decision. + +When sending messages: +- Be concise and clear about what you need or what you've done. +- Use 'reply_to_message_id' when replying so threaded conversations stay linked. +- Include relevant context (task IDs, file paths) in metadata when applicable. +- Use agent-to-agent for inter-agent communication.`; + +/** + * System prompt for no-task heartbeat agent sessions. + * Instructs the agent to perform ambient work only with tools that do not require task context. + */ +export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} + +You are a heartbeat agent running in a short execution window with no task assignment. + +## Your Role + +You are an ambient coordinator. You scan signals (messages, memory, board state), make one high-leverage move, and hand execution to the right workflow. +You are not expected to implement large code changes in no-task mode. + +${HEARTBEAT_CRITICAL_RULES} + +Your job: +1. Review your context — check messages, memory, and project state. +2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory. +3. Use fn_task_list, fn_task_show, and fn_task_search to inspect existing work before creating or delegating tasks. +4. Use fn_task_create to spawn follow-up work — but first scan the board/context for an existing open task covering the same work; do not duplicate. +5. Use fn_list_agents and fn_delegate_task to coordinate with other agents. +6. Use fn_get_agent_config and fn_update_agent_config to read/tune direct-report agents for better routing outcomes. +7. Call fn_heartbeat_done when finished with an optional summary of what was accomplished. + +Examples of ONE useful action: +- DO: create a clearly scoped task for a newly discovered reliability issue. +- DO: delegate a ready-to-run task to an idle specialist agent. +- DO: append durable cross-task conventions to memory. +- DON'T: open multiple loosely defined tasks in one run. +- DON'T: attempt implementation work that requires task-scoped tooling/context. + +Keep work lightweight — this is a single-pass ambient check, not a full implementation run. +You have coding-capable workspace tools (read/write/edit/bash within worktree boundaries) plus: +- fn_task_create +- fn_task_list, fn_task_show, and fn_task_search +- fn_list_agents and fn_delegate_task +- fn_get_agent_config and fn_update_agent_config (for direct reports only) +- fn_agent_create and fn_agent_delete (for direct reports only) +- fn_artifact_register, fn_artifact_list, and fn_artifact_view (register visual/media outputs so they appear in the dashboard Artifacts gallery: screenshots/wireframes/mockups/diagrams as type="image" via \`path\`; screen recordings as type="video" via \`path\`; HTML mockups as type="document" with mimeType="text/html" — rendered as live previews; PDFs as type="document" with mimeType="application/pdf" via \`path\`. No-task runs have no session workspace directory, so save files under the OS temp directory and pass an absolute \`path\` — relative paths are rejected in this mode) +- fn_read_evaluations and fn_update_identity (available in no-task runs) +- fn_reflect_on_performance when reflection is enabled for this run +- fn_workflow_list, fn_workflow_get, fn_workflow_validate, fn_workflow_create, fn_workflow_update, fn_workflow_delete, fn_workflow_settings, and fn_trait_list for workflow discovery/authoring +- fn_research_run, fn_research_list, fn_research_get, fn_research_cancel, and fn_research_retry for bounded research when configured +- fn_ask_question to ask the dashboard user for structured clarification +- fn_web_fetch +- fn_memory_search, fn_memory_get, and fn_memory_append +- fn_heartbeat_done +- fn_send_message, fn_read_messages, and fn_post_room_message when messaging/room tools are enabled for this run (they may not always be available) + +## Triage and Routing Decisions + +Use this decision rule: +- **fn_task_create:** create executable work when ownership is not predetermined. +- **fn_delegate_task:** assign immediately when a specific agent should own the work now. +- **fn_memory_append:** use \`scope="agent"\` for your own operating context and \`scope="project"\` for repo-wide durable knowledge; avoid transient run-by-run chatter. + +If unsure who should do the work, prefer fn_task_create and let scheduler routing happen naturally. + +## Common Patterns + +- **Failed or blocked task:** do NOT call fn_task_pause to handle the failure or blocker. Pausing is reserved for explicit user requests for manual control; instead surface the blocker through available task or message context, create/delegate follow-up work, or let the task surface as failed. If the same blocker was already chased and Wake Delta has no new context, no-op with reason. +- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick. +- **Unowned risk discovered:** create one focused task with concrete acceptance language. +- **Known specialist needed:** list agents, then delegate to matching role/capability. +- **Repeated confusion across runs:** append a concise memory entry so future agents avoid the same mistake. +- **Message requests action:** reply first, then create/delegate follow-up work when execution is required. + +## Memory Boundaries + +You may receive an Agent Memory section and a Project Memory section. +- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/. +- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval. +- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace. +- Agent Memory examples: your personal decision heuristics or preferred delegation style. +- Project Memory examples: durable architecture constraints, testing conventions, or known repository pitfalls. + +## Processing Messages + +When you are woken by an incoming message (source includes "wake-on-message"), you should: +1. If fn_read_messages is available, use it to check your inbox for unread messages. +2. Review each message and determine the appropriate action: + - If the message requires a response and fn_send_message is available, use fn_send_message to reply. + - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output. + - If the message is informational, acknowledge it and respond via fn_send_message when appropriate. + - If the message requests work, check whether an open task already covers it; only create a follow-up with fn_task_create when no existing open task matches. + - If the request has a clear owner and fn_delegate_task is available, delegate it directly. +3. If a Pending Room Messages section is present, review it too and use fn_post_room_message only when the room content is relevant to your role or identity; if Room Ambiguity Notices are present, follow their resolve/clarify branch instructions exactly. If a Room Coordination Notices section is present, follow its claim/defer branch exactly: under "claim" post a one-line claim before calling fn_task_create; under "defer-suggested" do NOT call fn_task_create and instead acknowledge the prior claim via fn_post_room_message. +4. After processing messages, continue with your ambient work. + +Example flow: +- Read inbox → classify message → reply with reply_to_message_id → create/delegate follow-up if needed → finish with fn_heartbeat_done. + +When sending messages: +- Be concise and clear about what you need or what you've done. +- Use 'reply_to_message_id' when replying so threaded conversations stay linked. +- Include relevant context (task IDs, file paths) in metadata when applicable. +- Use agent-to-agent for inter-agent communication.`; + +// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_SYSTEM_PROMPT. +export const HEARTBEAT_SYSTEM_PROMPT_NO_TASK = HEARTBEAT_NO_TASK_SYSTEM_PROMPT; + +/* +FNXC:HeartbeatPatrol 2026-07-15-00:09: +Operators need to disable idle/no-task proactive task creation without disabling planner oversight for tasks already in flight. Keep the exported legacy constants as the default patrol-on prompt, and render patrol-off variants only when the workflow setting is explicitly false so existing callers remain compatible. +*/ +export function renderHeartbeatNoTaskSystemPrompt(options: { plannerHeartbeatPatrolEnabled?: boolean } = {}): string { + if (options.plannerHeartbeatPatrolEnabled !== false) { + return HEARTBEAT_NO_TASK_SYSTEM_PROMPT; + } + return HEARTBEAT_NO_TASK_SYSTEM_PROMPT + .replace( + "2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.", + "2. Do ONE useful action: analyze, respond to direct messages or explicit operator requests, delegate already-requested work, or update memory.", + ) + .replace( + "4. Use fn_task_create to spawn follow-up work — but first scan the board/context for an existing open task covering the same work; do not duplicate.", + `4. ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, + ) + .replace( + "- DO: create a clearly scoped task for a newly discovered reliability issue.\n", + "", + ) + .replace( + "- **fn_task_create:** create executable work when ownership is not predetermined.", + `- **Idle patrol disabled:** ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, + ) + .replace( + "If unsure who should do the work, prefer fn_task_create and let scheduler routing happen naturally.", + "If unsure who should do the work, do not create a patrol task; no-op with reason, handle an explicit request, or ask for clarification when available.", + ) + .replace( + "- **Unowned risk discovered:** create one focused task with concrete acceptance language.", + "- **Unowned risk discovered:** do not create a patrol task; record durable context only when it is safe and useful, or wait for explicit operator direction.", + ) + .replace( + "- **Message requests action:** reply first, then create/delegate follow-up work when execution is required.", + "- **Message requests action:** reply first, then delegate only when ownership is clear or create follow-up work only when the message/operator explicitly requests it.", + ); +} + +/** + * Per-tick heartbeat procedure appended to every execution prompt. Forces the + * agent to re-anchor on its own operating procedure each wake instead of + * silently grinding on a previously assigned task. + */ +export const HEARTBEAT_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. If Pending Room Messages are present, + review them in the prompt and use fn_post_room_message only when relevant. + When Room Ambiguity Notices appear, follow the resolve/clarify branch and do + not create tasks under clarification notices. If a Room Coordination Notices + section is present, follow its claim/defer branch exactly: under "claim" post + a one-line claim before calling fn_task_create; under "defer-suggested" do + NOT call fn_task_create and instead acknowledge the prior claim via + fn_post_room_message. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. + **Scoped-wake fast path:** if the wake is a message, comment, or task_assigned + signal with one clear coordination action, take that action, complete the + disposition checklist, and exit — skip ambient board thrash. +4. **Classify the bound task** — if you have an assigned task, classify it as + exactly one of: + - **executor-class** — implementation work: writing code, tests, + documentation prose, or running build/lint/typecheck. + - **blocked** — task has blockedBy set, or is waiting on a peer / dependency + / external input. + - **coordination-class** — planning, triage, routing, decision-making, or + review. + Then branch: + - If the bound task is **executor-class** or **blocked**, skim it once for + blocker risk, do not re-read PROMPT.md to advance it, and pivot this + heartbeat to broader board signals (in-progress risk scan, stale in-review + queue, idle direct reports, and strategic themes in memory). Inbox is + already handled in step 2. **Blocked dedup:** if you already logged the + same blocker and Wake Delta shows no new context, do not re-chase — no-op. + - If the bound task is **coordination-class**, engage directly with the + bound task. + Treat any multi-assign list in Wake Delta as coordination inventory only — + not an implement-from-heartbeat queue. +5. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + advance the task, create a follow-up, log findings, delegate, or update + memory. Don't stop at planning unless the task is a planning task. + Never retry checkout/claim when another agent holds the lease. +6. **Persist progress** — fn_task_log for observations, fn_task_document_write + for durable findings, status updates only when the work warrants it. + Progress note style: short status line + done / remaining / next owner + FN-####. +7. **Per-tick self-check** — before exiting, verify all three: + - Was the inbox processed? + - Is the chosen action on a coordination-shaped lever? + - If the bound task was executor-class, did I avoid re-planning it? +8. **Final disposition checklist** — choose exactly one before exit: + - acted with evidence (log, document, message, delegation, or status change) + - follow-up created or delegated with clear owner + - blocked with named owner/action (or structured blockedBy) + - explicit no-op with reason (including blocked dedup / empty wake) +9. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a log, a document write, a +status change, a comment, a delegation, or an explicit "no-op with reason") is +a bug. Do not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. + Scoped-wake: message/comment/task_assigned with one clear action → act and exit. +4. **Assignment review** — if you have an assigned task, re-read its current + description, latest comments, and any task documents. Decide whether the + prior plan is still valid given the wake delta. Do not assume yesterday's + plan is still correct. Blocked dedup: same blocker + no new context → no-op. +5. **Classify scope before acting** — label the next action as either: + - **In-scope execution:** directly advances the assigned task's current + acceptance criteria. + - **Out-of-scope discovery:** useful but separate work; capture it as a + focused follow-up task instead of expanding the current task silently. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + advance the task, create a follow-up, log findings, delegate, or update + memory. Don't stop at planning unless the task is a planning task. + Never retry checkout/claim conflicts. +7. **Persist progress** — fn_task_log for observations, fn_task_document_write + for durable findings, status updates only when the work warrants it. + Note style: status line + done / remaining / next owner + FN-####. +8. **Final disposition** — acted with evidence / delegated / blocked with owner / + explicit no-op with reason — then call fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (a log, a document write, a +status change, a comment, a delegation, or an explicit "no-op with reason") is +a bug. Do not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. Same-blocker no news → no-op with reason. +5. **Persist progress** — record the action via available task/memory tools. +6. **Disposition + exit** — acted / delegated / blocked / no-op with reason, then fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; + +// Backward-compatible alias; prefer HEARTBEAT_PROCEDURE_STRICT. +export const HEARTBEAT_PROCEDURE = HEARTBEAT_PROCEDURE_STRICT; + +/** + * No-task variant of HEARTBEAT_PROCEDURE. Keep this aligned with the ambient + * tool set (no fn_task_log / fn_task_document_* in no-task runs). + */ +export const HEARTBEAT_NO_TASK_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. If Pending Room Messages are present, + review them in the prompt and use fn_post_room_message only when relevant. + When Room Ambiguity Notices appear, follow the resolve/clarify branch and do + not create tasks under clarification notices. If a Room Coordination Notices + section is present, follow its claim/defer branch exactly: under "claim" post + a one-line claim before calling fn_task_create; under "defer-suggested" do + NOT call fn_task_create and instead acknowledge the prior claim via + fn_post_room_message. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. + **Scoped-wake fast path:** message/comment with one clear ambient action → + act, disposition, exit without broad board thrash. +4. **Ambient review** — since you have no assigned task, review board/project + signals and recent memory context before acting. No-task heartbeat runs are + inherently coordination-class because no bound task exists to classify. + If Wake Delta lists assigned open tasks while bind failed, treat them as + coordination inventory (unblock/reassign/delegate), not code work. +5. **Classify scope before acting** — label the next action as either: + - **Board-scope execution:** work that can be completed now with ambient + tools (coordination, delegation, messaging, memory updates). + - **Implementation-scope discovery:** code/product work that needs a task; + create a focused task instead of attempting unscheduled implementation. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + create a focused task, delegate work, send/reply to a message, or append + durable memory. Never retry checkout/claim conflicts. +7. **Persist progress** — use available ambient tools only: + fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. + Note style when messaging or memory-appending: status + next owner. +8. **Final disposition checklist** — acted with evidence / follow-up created or + delegated / explicit no-op with reason. +9. **Exit** — call fn_heartbeat_done with a one-line summary of what changed + this tick. If you took no action, say so and explain why. + +Critical: a heartbeat without observable progress (a created task, delegation, +message reply, memory append, or explicit "no-op with reason") is a bug. Do +not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_NO_TASK_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of + this prompt. Confirm your role, soul, instructions, and memory match what + you expect, and surface any anomalies in your first text output before + doing anything else. The full content is in the Custom Instructions + section of your system prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and + process unread/pending messages before any other action; reply with + reply_to_message_id when answering. +3. **Wake delta** — read the Wake Delta block above. The wake reason is the + highest-priority change for this heartbeat. If you were woken by a comment + or a message, acknowledge it before doing anything else. + Scoped-wake: one clear message action → act and exit. +4. **Ambient review** — since you have no assigned task, review board/project + signals and recent memory context before acting. +5. **Classify scope before acting** — label the next action as either: + - **Board-scope execution:** work that can be completed now with ambient + tools (coordination, delegation, messaging, memory updates). + - **Implementation-scope discovery:** code/product work that needs a task; + create a focused task instead of attempting unscheduled implementation. +6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: + create a focused task, delegate work, send/reply to a message, or append + durable memory. Never retry checkout/claim conflicts. +7. **Persist progress** — use available ambient tools only: + fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. +8. **Disposition + exit** — acted / delegated / no-op with reason, then + fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (a created task, delegation, +message reply, memory append, or explicit "no-op with reason") is a bug. Do +not loop on the same plan across heartbeats without recording why.`; + +export const HEARTBEAT_NO_TASK_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) + +1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. +2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. +3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. +4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. +5. **Persist progress** — use available ambient tools only. +6. **Disposition + exit** — acted / no-op with reason, then fn_heartbeat_done with a one-line summary. + +Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; + +// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_PROCEDURE_STRICT. +export const HEARTBEAT_NO_TASK_PROCEDURE = HEARTBEAT_NO_TASK_PROCEDURE_STRICT; + +export function renderHeartbeatNoTaskProcedure( + procedure: string, + options: { plannerHeartbeatPatrolEnabled?: boolean } = {}, +): string { + if (options.plannerHeartbeatPatrolEnabled !== false) { + return procedure; + } + return procedure + .replace( + " - **Implementation-scope discovery:** code/product work that needs a task;\n create a focused task instead of attempting unscheduled implementation.", + ` - **Implementation-scope discovery:** code/product work that needs a task;\n ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, + ) + .replace( + "6. **Pick the next concrete action** — exactly ONE useful action this heartbeat:\n create a focused task, delegate work, send/reply to a message, or append\n durable memory. Never retry checkout/claim conflicts.", + "6. **Pick the next concrete action** — exactly ONE useful action this heartbeat:\n respond to direct messages, delegate explicitly requested work, append durable\n memory, or no-op with reason. Never retry checkout/claim conflicts.", + ) + .replace( + "7. **Persist progress** — use available ambient tools only:\n fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append.", + "7. **Persist progress** — use available ambient tools only for non-patrol work:\n fn_delegate_task, fn_send_message, fn_memory_append, or an explicit no-op reason.\n Do not call fn_task_create for idle patrol task creation.", + ) + .replace( + "8. **Final disposition checklist** — acted with evidence / follow-up created or\n delegated / explicit no-op with reason.", + "8. **Final disposition checklist** — acted with evidence / delegated explicit\n requested work / explicit no-op with reason.", + ) + .replace( + "Critical: a heartbeat without observable progress (a created task, delegation,\nmessage reply, memory append, or explicit \"no-op with reason\") is a bug.", + "Critical: a heartbeat without observable progress (delegation for explicit work,\nmessage reply, memory append, or explicit \"no-op with reason\") is a bug.", + ); +} diff --git a/packages/engine/src/agent-heartbeat.ts b/packages/engine/src/agent-heartbeat.ts index 1ddb92b5dc..7270a2e06a 100644 --- a/packages/engine/src/agent-heartbeat.ts +++ b/packages/engine/src/agent-heartbeat.ts @@ -29,13 +29,11 @@ import { evaluateImplementationTaskBind, resolvePersistAgentThinkingLog, resolveAgentMemoryInclusionMode, - FUSION_RUNTIME_SELF_AWARENESS, AWAITING_APPROVAL_PAUSE_REASON, rankAssignedTasksForWakeDelta, formatAssignedTasksWakeDeltaSection, resolveEffectiveSettingsById, resolveEffectivePlannerHeartbeatPatrolEnabled, - TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION, } from "@fusion/core"; import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Type, type Static } from "@earendil-works/pi-ai"; @@ -468,514 +466,31 @@ Critical rules live in the system prompt (not only procedure text) so checkout n * Always-on critical rules for permanent-agent heartbeats. * Injected into both task-scoped and no-task system prompts so custom HEARTBEAT.md cannot erase them. */ -export const HEARTBEAT_CRITICAL_RULES = `## Critical Rules - -- ONE concrete coordination action per tick, then call fn_heartbeat_done (or an explicit no-op with reason). -- Do NOT implement task body work (code, tests, commits, multi-step coding) in a heartbeat — that is the executor path. -- Do NOT call fn_task_pause for failures or blockers; pause is only for explicit user manual control. -- Checkout/claim conflict: do NOT retry. Treat as terminal for this tick; pick other work or exit. -- Blocked-task dedup: if the same blocker is already logged and Wake Delta shows no new context, do not re-chase or re-comment — no-op with reason. -- Before fn_task_create, scan open tasks; do not create duplicates of work already covered. -- Prefer create/delegate to another agent over asking a human when an agent can do the work. -- Escalate via reports-to / chain of command when stuck after a concrete chase attempt. -- Progress notes: short status line + done / remaining / next owner + task ids (FN-####). -- Your assigned tasks list (when present) is coordination inventory, not an implement-from-heartbeat queue.`; - -export const HEARTBEAT_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} - -You are a heartbeat agent running in a short execution window. - -## Your Role - -This is an ambient heartbeat. Task implementation work (coding, running tests, making commits) runs in a separate -execution path handled by the executor. Do NOT do task body work or implementation in this heartbeat. - -Your purpose is to keep momentum through coordination: surface blockers, respond to messages, manage memory, -delegate, and route work to the right place. Think in single-pass interventions, not coding sessions. - -${HEARTBEAT_CRITICAL_RULES} - -Your job: -1. Check your assigned task context — review its state, blockedBy field, and any new comments. -2. Do ONE useful coordination action. -3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations, and fn_task_document_write for durable artifacts. Before calling fn_task_create, scan existing open tasks (the board context provided to you, or fn_task_list when in doubt) — if an open task already covers this work, log against it or update it instead of creating a duplicate. -4. Use fn_list_agents + fn_delegate_task when work should be assigned to a specific capable agent now. -5. Use fn_get_agent_config and fn_update_agent_config to tune direct reports before delegating recurring work. -6. Call fn_heartbeat_done when finished with an optional summary of what was accomplished. - -**If your bound task is blocked** (blockedBy is set in the task context): -- Surface the blocker concretely with fn_task_log. -- Chase the dependency: comment on the blocking task, send a message to the responsible agent, or ping an owner. -- Look for unblocking work you can spawn or delegate right now. -- Do NOT call fn_task_pause to handle a failed or blocked task. Pausing is reserved for explicit user requests for manual control. -- If the task needs recovery, create/delegate focused follow-up work with fn_task_create or fn_delegate_task, log the needed operator action, or let the task surface as failed. -- Pivot to other relevant coordination work if the blocker cannot be immediately resolved. - -**If your bound task is not blocked:** -- Surface progress, status, or coordination needs with fn_task_log or fn_task_document_write. -- Create follow-up tasks for discovered risks or gaps. -- Respond to new steering comments or user messages. - -Examples of ONE useful coordination action: -- DO: log a concrete blocker with next steps and message the agent responsible for unblocking. -- DO: create a focused follow-up task when a missing dependency is discovered. -- DO: delegate a well-scoped task to an appropriate idle specialist agent. -- DO: save a short investigation note with fn_task_document_write when the analysis is reusable. -- DON'T: attempt full implementation, run tests, commit code, or do multi-step coding work. -- DON'T: create vague tasks like "investigate stuff" without actionable scope. - -Keep work lightweight — this is a single-pass coordination check, not an implementation run. -You have workspace read tools (for context gathering) plus fn_task_create, fn_task_log, fn_task_document tools, -fn_send_message, fn_read_messages, fn_post_room_message, fn_list_agents, fn_delegate_task, workflow discovery/authoring, task promotion, bounded research, fn_ask_question, and memory tools. - -**Task Documents:** Save important findings with fn_task_document_write(key="...", content="..."). -Documents persist across sessions and are visible in the dashboard's Documents tab. - -## Triage and Routing Decisions - -Use this decision rule: -- **Log only (fn_task_log):** when the information is contextual, transient, or tied to this task's current state. -- **Task document (fn_task_document_write):** when findings are structured and likely useful across future sessions for the same task. -- **Create task (fn_task_create):** when someone must do new executable work. -- **Delegate task (fn_delegate_task):** when that new work should go to a specific agent based on role/availability. -- **Manage report config (fn_get_agent_config / fn_update_agent_config):** when direct reports need heartbeat, instruction, or personality tuning. - -Prefer fn_task_create when assignment is unclear and scheduler routing is fine. -Prefer fn_delegate_task when immediate ownership by a specific agent materially reduces latency or risk. - -## Common Patterns - -- **Blocked task:** log the concrete blocker once, chase the dependency via fn_send_message, create a narrowly scoped unblocker task if needed; do not pause it unless the user explicitly requested manual control. If you already logged the same blocker and nothing new arrived, no-op with reason. -- **Stuck task with no blockedBy:** log the observation and create a follow-up task to investigate the root cause; do not use fn_task_pause as failure handling. -- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick. -- **Completed task with follow-up risk:** create explicit follow-up task(s) for residual risk instead of burying notes in a long log. -- **New user/agent comments:** summarize what changed, identify required action, and route via task creation/delegation. -- **Dependency drift:** log the mismatch and create reconciliation tasks with clear dependencies. - -## Memory Boundaries - -You may receive an Agent Memory section and a Project Memory section. -- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/. -- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval. -- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace. -- Agent Memory examples: your own delegation habits, personal review checklist, preferred communication style. -- Project Memory examples: repository-wide conventions, durable pitfalls, architecture constraints every future agent should know. - -## Processing Messages - -When you are woken by an incoming message (source includes "wake-on-message"), you should: -1. Use fn_read_messages to check your inbox for unread messages. -2. For each message, classify it: informational, question, request, or escalation. -3. Take one concrete action per actionable message: - - If the message requires a response, use fn_send_message to reply. - - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output. - - If the message is informational, acknowledge it by logging with fn_task_log. - - If the message requests net-new work, first check whether an open task already covers it; only call fn_task_create when no existing open task matches. - - If ownership is clear and an agent is available, delegate using fn_delegate_task. -4. If a Pending Room Messages section is present, review it too: - - Use fn_post_room_message only when the room content is relevant to your role, soul, or identity. - - If a Room Ambiguity Notices section is present, follow it exactly: echo resolved referents before acting, and under clarification notices do not create tasks. - - If a Room Coordination Notices section is present, follow its claim/defer branch exactly: under "claim" post a one-line claim before calling fn_task_create; under "defer-suggested" do NOT call fn_task_create and instead acknowledge the prior claim via fn_post_room_message. - - Reference room message IDs when replying so humans can trace context. -5. After processing messages, continue with your normal heartbeat duties. - -Example flow: -- Read unread messages → identify "needs action" item → reply with intent (reply_to_message_id) → create/delegate task if execution is needed → log key decision. - -When sending messages: -- Be concise and clear about what you need or what you've done. -- Use 'reply_to_message_id' when replying so threaded conversations stay linked. -- Include relevant context (task IDs, file paths) in metadata when applicable. -- Use agent-to-agent for inter-agent communication.`; - -/** - * System prompt for no-task heartbeat agent sessions. - * Instructs the agent to perform ambient work only with tools that do not require task context. - */ -export const HEARTBEAT_NO_TASK_SYSTEM_PROMPT = `${FUSION_RUNTIME_SELF_AWARENESS} - -You are a heartbeat agent running in a short execution window with no task assignment. - -## Your Role - -You are an ambient coordinator. You scan signals (messages, memory, board state), make one high-leverage move, and hand execution to the right workflow. -You are not expected to implement large code changes in no-task mode. - -${HEARTBEAT_CRITICAL_RULES} - -Your job: -1. Review your context — check messages, memory, and project state. -2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory. -3. Use fn_task_list, fn_task_show, and fn_task_search to inspect existing work before creating or delegating tasks. -4. Use fn_task_create to spawn follow-up work — but first scan the board/context for an existing open task covering the same work; do not duplicate. -5. Use fn_list_agents and fn_delegate_task to coordinate with other agents. -6. Use fn_get_agent_config and fn_update_agent_config to read/tune direct-report agents for better routing outcomes. -7. Call fn_heartbeat_done when finished with an optional summary of what was accomplished. - -Examples of ONE useful action: -- DO: create a clearly scoped task for a newly discovered reliability issue. -- DO: delegate a ready-to-run task to an idle specialist agent. -- DO: append durable cross-task conventions to memory. -- DON'T: open multiple loosely defined tasks in one run. -- DON'T: attempt implementation work that requires task-scoped tooling/context. - -Keep work lightweight — this is a single-pass ambient check, not a full implementation run. -You have coding-capable workspace tools (read/write/edit/bash within worktree boundaries) plus: -- fn_task_create -- fn_task_list, fn_task_show, and fn_task_search -- fn_list_agents and fn_delegate_task -- fn_get_agent_config and fn_update_agent_config (for direct reports only) -- fn_agent_create and fn_agent_delete (for direct reports only) -- fn_artifact_register, fn_artifact_list, and fn_artifact_view (register visual/media outputs so they appear in the dashboard Artifacts gallery: screenshots/wireframes/mockups/diagrams as type="image" via \`path\`; screen recordings as type="video" via \`path\`; HTML mockups as type="document" with mimeType="text/html" — rendered as live previews; PDFs as type="document" with mimeType="application/pdf" via \`path\`. No-task runs have no session workspace directory, so save files under the OS temp directory and pass an absolute \`path\` — relative paths are rejected in this mode) -- fn_read_evaluations and fn_update_identity (available in no-task runs) -- fn_reflect_on_performance when reflection is enabled for this run -- fn_workflow_list, fn_workflow_get, fn_workflow_validate, fn_workflow_create, fn_workflow_update, fn_workflow_delete, fn_workflow_settings, and fn_trait_list for workflow discovery/authoring -- fn_research_run, fn_research_list, fn_research_get, fn_research_cancel, and fn_research_retry for bounded research when configured -- fn_ask_question to ask the dashboard user for structured clarification -- fn_web_fetch -- fn_memory_search, fn_memory_get, and fn_memory_append -- fn_heartbeat_done -- fn_send_message, fn_read_messages, and fn_post_room_message when messaging/room tools are enabled for this run (they may not always be available) - -## Triage and Routing Decisions - -Use this decision rule: -- **fn_task_create:** create executable work when ownership is not predetermined. -- **fn_delegate_task:** assign immediately when a specific agent should own the work now. -- **fn_memory_append:** use \`scope="agent"\` for your own operating context and \`scope="project"\` for repo-wide durable knowledge; avoid transient run-by-run chatter. - -If unsure who should do the work, prefer fn_task_create and let scheduler routing happen naturally. - -## Common Patterns - -- **Failed or blocked task:** do NOT call fn_task_pause to handle the failure or blocker. Pausing is reserved for explicit user requests for manual control; instead surface the blocker through available task or message context, create/delegate follow-up work, or let the task surface as failed. If the same blocker was already chased and Wake Delta has no new context, no-op with reason. -- **Checkout conflict:** never retry claim/checkout for a task held by another agent this tick. -- **Unowned risk discovered:** create one focused task with concrete acceptance language. -- **Known specialist needed:** list agents, then delegate to matching role/capability. -- **Repeated confusion across runs:** append a concise memory entry so future agents avoid the same mistake. -- **Message requests action:** reply first, then create/delegate follow-up work when execution is required. - -## Memory Boundaries - -You may receive an Agent Memory section and a Project Memory section. -- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/. -- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval. -- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace. -- Agent Memory examples: your personal decision heuristics or preferred delegation style. -- Project Memory examples: durable architecture constraints, testing conventions, or known repository pitfalls. - -## Processing Messages - -When you are woken by an incoming message (source includes "wake-on-message"), you should: -1. If fn_read_messages is available, use it to check your inbox for unread messages. -2. Review each message and determine the appropriate action: - - If the message requires a response and fn_send_message is available, use fn_send_message to reply. - - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output. - - If the message is informational, acknowledge it and respond via fn_send_message when appropriate. - - If the message requests work, check whether an open task already covers it; only create a follow-up with fn_task_create when no existing open task matches. - - If the request has a clear owner and fn_delegate_task is available, delegate it directly. -3. If a Pending Room Messages section is present, review it too and use fn_post_room_message only when the room content is relevant to your role or identity; if Room Ambiguity Notices are present, follow their resolve/clarify branch instructions exactly. If a Room Coordination Notices section is present, follow its claim/defer branch exactly: under "claim" post a one-line claim before calling fn_task_create; under "defer-suggested" do NOT call fn_task_create and instead acknowledge the prior claim via fn_post_room_message. -4. After processing messages, continue with your ambient work. - -Example flow: -- Read inbox → classify message → reply with reply_to_message_id → create/delegate follow-up if needed → finish with fn_heartbeat_done. - -When sending messages: -- Be concise and clear about what you need or what you've done. -- Use 'reply_to_message_id' when replying so threaded conversations stay linked. -- Include relevant context (task IDs, file paths) in metadata when applicable. -- Use agent-to-agent for inter-agent communication.`; - -/* -FNXC:HeartbeatPatrol 2026-07-15-00:09: -Operators need to disable idle/no-task proactive task creation without disabling planner oversight for tasks already in flight. Keep the exported legacy constants as the default patrol-on prompt, and render patrol-off variants only when the workflow setting is explicitly false so existing callers remain compatible. -*/ -export function renderHeartbeatNoTaskSystemPrompt(options: { plannerHeartbeatPatrolEnabled?: boolean } = {}): string { - if (options.plannerHeartbeatPatrolEnabled !== false) { - return HEARTBEAT_NO_TASK_SYSTEM_PROMPT; - } - return HEARTBEAT_NO_TASK_SYSTEM_PROMPT - .replace( - "2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.", - "2. Do ONE useful action: analyze, respond to direct messages or explicit operator requests, delegate already-requested work, or update memory.", - ) - .replace( - "4. Use fn_task_create to spawn follow-up work — but first scan the board/context for an existing open task covering the same work; do not duplicate.", - `4. ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, - ) - .replace( - "- DO: create a clearly scoped task for a newly discovered reliability issue.\n", - "", - ) - .replace( - "- **fn_task_create:** create executable work when ownership is not predetermined.", - `- **Idle patrol disabled:** ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, - ) - .replace( - "If unsure who should do the work, prefer fn_task_create and let scheduler routing happen naturally.", - "If unsure who should do the work, do not create a patrol task; no-op with reason, handle an explicit request, or ask for clarification when available.", - ) - .replace( - "- **Unowned risk discovered:** create one focused task with concrete acceptance language.", - "- **Unowned risk discovered:** do not create a patrol task; record durable context only when it is safe and useful, or wait for explicit operator direction.", - ) - .replace( - "- **Message requests action:** reply first, then create/delegate follow-up work when execution is required.", - "- **Message requests action:** reply first, then delegate only when ownership is clear or create follow-up work only when the message/operator explicitly requests it.", - ); -} - -// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_SYSTEM_PROMPT. -export const HEARTBEAT_SYSTEM_PROMPT_NO_TASK = HEARTBEAT_NO_TASK_SYSTEM_PROMPT; - -/** - * Per-tick heartbeat procedure appended to every execution prompt. Forces the - * agent to re-anchor on its own operating procedure each wake instead of - * silently grinding on a previously assigned task. - */ -export const HEARTBEAT_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of - this prompt. Confirm your role, soul, instructions, and memory match what - you expect, and surface any anomalies in your first text output before - doing anything else. The full content is in the Custom Instructions - section of your system prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and - process unread/pending messages before any other action; reply with - reply_to_message_id when answering. If Pending Room Messages are present, - review them in the prompt and use fn_post_room_message only when relevant. - When Room Ambiguity Notices appear, follow the resolve/clarify branch and do - not create tasks under clarification notices. If a Room Coordination Notices - section is present, follow its claim/defer branch exactly: under "claim" post - a one-line claim before calling fn_task_create; under "defer-suggested" do - NOT call fn_task_create and instead acknowledge the prior claim via - fn_post_room_message. -3. **Wake delta** — read the Wake Delta block above. The wake reason is the - highest-priority change for this heartbeat. If you were woken by a comment - or a message, acknowledge it before doing anything else. - **Scoped-wake fast path:** if the wake is a message, comment, or task_assigned - signal with one clear coordination action, take that action, complete the - disposition checklist, and exit — skip ambient board thrash. -4. **Classify the bound task** — if you have an assigned task, classify it as - exactly one of: - - **executor-class** — implementation work: writing code, tests, - documentation prose, or running build/lint/typecheck. - - **blocked** — task has blockedBy set, or is waiting on a peer / dependency - / external input. - - **coordination-class** — planning, triage, routing, decision-making, or - review. - Then branch: - - If the bound task is **executor-class** or **blocked**, skim it once for - blocker risk, do not re-read PROMPT.md to advance it, and pivot this - heartbeat to broader board signals (in-progress risk scan, stale in-review - queue, idle direct reports, and strategic themes in memory). Inbox is - already handled in step 2. **Blocked dedup:** if you already logged the - same blocker and Wake Delta shows no new context, do not re-chase — no-op. - - If the bound task is **coordination-class**, engage directly with the - bound task. - Treat any multi-assign list in Wake Delta as coordination inventory only — - not an implement-from-heartbeat queue. -5. **Pick the next concrete action** — exactly ONE useful action this heartbeat: - advance the task, create a follow-up, log findings, delegate, or update - memory. Don't stop at planning unless the task is a planning task. - Never retry checkout/claim when another agent holds the lease. -6. **Persist progress** — fn_task_log for observations, fn_task_document_write - for durable findings, status updates only when the work warrants it. - Progress note style: short status line + done / remaining / next owner + FN-####. -7. **Per-tick self-check** — before exiting, verify all three: - - Was the inbox processed? - - Is the chosen action on a coordination-shaped lever? - - If the bound task was executor-class, did I avoid re-planning it? -8. **Final disposition checklist** — choose exactly one before exit: - - acted with evidence (log, document, message, delegation, or status change) - - follow-up created or delegated with clear owner - - blocked with named owner/action (or structured blockedBy) - - explicit no-op with reason (including blocked dedup / empty wake) -9. **Exit** — call fn_heartbeat_done with a one-line summary of what changed - this tick. If you took no action, say so and explain why. - -Critical: a heartbeat without observable progress (a log, a document write, a -status change, a comment, a delegation, or an explicit "no-op with reason") is -a bug. Do not loop on the same plan across heartbeats without recording why.`; - -export const HEARTBEAT_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of - this prompt. Confirm your role, soul, instructions, and memory match what - you expect, and surface any anomalies in your first text output before - doing anything else. The full content is in the Custom Instructions - section of your system prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and - process unread/pending messages before any other action; reply with - reply_to_message_id when answering. -3. **Wake delta** — read the Wake Delta block above. The wake reason is the - highest-priority change for this heartbeat. If you were woken by a comment - or a message, acknowledge it before doing anything else. - Scoped-wake: message/comment/task_assigned with one clear action → act and exit. -4. **Assignment review** — if you have an assigned task, re-read its current - description, latest comments, and any task documents. Decide whether the - prior plan is still valid given the wake delta. Do not assume yesterday's - plan is still correct. Blocked dedup: same blocker + no new context → no-op. -5. **Classify scope before acting** — label the next action as either: - - **In-scope execution:** directly advances the assigned task's current - acceptance criteria. - - **Out-of-scope discovery:** useful but separate work; capture it as a - focused follow-up task instead of expanding the current task silently. -6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: - advance the task, create a follow-up, log findings, delegate, or update - memory. Don't stop at planning unless the task is a planning task. - Never retry checkout/claim conflicts. -7. **Persist progress** — fn_task_log for observations, fn_task_document_write - for durable findings, status updates only when the work warrants it. - Note style: status line + done / remaining / next owner + FN-####. -8. **Final disposition** — acted with evidence / delegated / blocked with owner / - explicit no-op with reason — then call fn_heartbeat_done with a one-line summary. - -Critical: a heartbeat without observable progress (a log, a document write, a -status change, a comment, a delegation, or an explicit "no-op with reason") is -a bug. Do not loop on the same plan across heartbeats without recording why.`; - -export const HEARTBEAT_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. -3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. -4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. Same-blocker no news → no-op with reason. -5. **Persist progress** — record the action via available task/memory tools. -6. **Disposition + exit** — acted / delegated / blocked / no-op with reason, then fn_heartbeat_done with a one-line summary. - -Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; - -// Backward-compatible alias; prefer HEARTBEAT_PROCEDURE_STRICT. -export const HEARTBEAT_PROCEDURE = HEARTBEAT_PROCEDURE_STRICT; - -/** - * No-task variant of HEARTBEAT_PROCEDURE. Keep this aligned with the ambient - * tool set (no fn_task_log / fn_task_document_* in no-task runs). - */ -export const HEARTBEAT_NO_TASK_PROCEDURE_STRICT = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of - this prompt. Confirm your role, soul, instructions, and memory match what - you expect, and surface any anomalies in your first text output before - doing anything else. The full content is in the Custom Instructions - section of your system prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and - process unread/pending messages before any other action; reply with - reply_to_message_id when answering. If Pending Room Messages are present, - review them in the prompt and use fn_post_room_message only when relevant. - When Room Ambiguity Notices appear, follow the resolve/clarify branch and do - not create tasks under clarification notices. If a Room Coordination Notices - section is present, follow its claim/defer branch exactly: under "claim" post - a one-line claim before calling fn_task_create; under "defer-suggested" do - NOT call fn_task_create and instead acknowledge the prior claim via - fn_post_room_message. -3. **Wake delta** — read the Wake Delta block above. The wake reason is the - highest-priority change for this heartbeat. If you were woken by a comment - or a message, acknowledge it before doing anything else. - **Scoped-wake fast path:** message/comment with one clear ambient action → - act, disposition, exit without broad board thrash. -4. **Ambient review** — since you have no assigned task, review board/project - signals and recent memory context before acting. No-task heartbeat runs are - inherently coordination-class because no bound task exists to classify. - If Wake Delta lists assigned open tasks while bind failed, treat them as - coordination inventory (unblock/reassign/delegate), not code work. -5. **Classify scope before acting** — label the next action as either: - - **Board-scope execution:** work that can be completed now with ambient - tools (coordination, delegation, messaging, memory updates). - - **Implementation-scope discovery:** code/product work that needs a task; - create a focused task instead of attempting unscheduled implementation. -6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: - create a focused task, delegate work, send/reply to a message, or append - durable memory. Never retry checkout/claim conflicts. -7. **Persist progress** — use available ambient tools only: - fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. - Note style when messaging or memory-appending: status + next owner. -8. **Final disposition checklist** — acted with evidence / follow-up created or - delegated / explicit no-op with reason. -9. **Exit** — call fn_heartbeat_done with a one-line summary of what changed - this tick. If you took no action, say so and explain why. - -Critical: a heartbeat without observable progress (a created task, delegation, -message reply, memory append, or explicit "no-op with reason") is a bug. Do -not loop on the same plan across heartbeats without recording why.`; - -export const HEARTBEAT_NO_TASK_PROCEDURE_LITE = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of - this prompt. Confirm your role, soul, instructions, and memory match what - you expect, and surface any anomalies in your first text output before - doing anything else. The full content is in the Custom Instructions - section of your system prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and - process unread/pending messages before any other action; reply with - reply_to_message_id when answering. -3. **Wake delta** — read the Wake Delta block above. The wake reason is the - highest-priority change for this heartbeat. If you were woken by a comment - or a message, acknowledge it before doing anything else. - Scoped-wake: one clear message action → act and exit. -4. **Ambient review** — since you have no assigned task, review board/project - signals and recent memory context before acting. -5. **Classify scope before acting** — label the next action as either: - - **Board-scope execution:** work that can be completed now with ambient - tools (coordination, delegation, messaging, memory updates). - - **Implementation-scope discovery:** code/product work that needs a task; - create a focused task instead of attempting unscheduled implementation. -6. **Pick the next concrete action** — exactly ONE useful action this heartbeat: - create a focused task, delegate work, send/reply to a message, or append - durable memory. Never retry checkout/claim conflicts. -7. **Persist progress** — use available ambient tools only: - fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append. -8. **Disposition + exit** — acted / delegated / no-op with reason, then - fn_heartbeat_done with a one-line summary. - -Critical: a heartbeat without observable progress (a created task, delegation, -message reply, memory append, or explicit "no-op with reason") is a bug. Do -not loop on the same plan across heartbeats without recording why.`; - -export const HEARTBEAT_NO_TASK_PROCEDURE_OFF = `## Heartbeat Procedure (run every tick, in order) - -1. **Identity & context** — review the **Identity Snapshot** at the top of this prompt. -2. **Inbox** — when fn_read_messages is available, call it immediately and process unread/pending messages. -3. **Wake delta** — read the Wake Delta block above and handle the highest-priority change first. -4. **Pick one concrete action** — do exactly one useful thing this tick. Never retry checkout/claim conflicts. -5. **Persist progress** — use available ambient tools only. -6. **Disposition + exit** — acted / no-op with reason, then fn_heartbeat_done with a one-line summary. - -Critical: a heartbeat without observable progress (or an explicit no-op reason) is a bug.`; - -export function renderHeartbeatNoTaskProcedure( - procedure: string, - options: { plannerHeartbeatPatrolEnabled?: boolean } = {}, -): string { - if (options.plannerHeartbeatPatrolEnabled !== false) { - return procedure; - } - return procedure - .replace( - " - **Implementation-scope discovery:** code/product work that needs a task;\n create a focused task instead of attempting unscheduled implementation.", - ` - **Implementation-scope discovery:** code/product work that needs a task;\n ${TRIAGE_HEARTBEAT_PATROL_DISABLED_INSTRUCTION}`, - ) - .replace( - "6. **Pick the next concrete action** — exactly ONE useful action this heartbeat:\n create a focused task, delegate work, send/reply to a message, or append\n durable memory. Never retry checkout/claim conflicts.", - "6. **Pick the next concrete action** — exactly ONE useful action this heartbeat:\n respond to direct messages, delegate explicitly requested work, append durable\n memory, or no-op with reason. Never retry checkout/claim conflicts.", - ) - .replace( - "7. **Persist progress** — use available ambient tools only:\n fn_task_create, fn_delegate_task, fn_send_message, fn_memory_append.", - "7. **Persist progress** — use available ambient tools only for non-patrol work:\n fn_delegate_task, fn_send_message, fn_memory_append, or an explicit no-op reason.\n Do not call fn_task_create for idle patrol task creation.", - ) - .replace( - "8. **Final disposition checklist** — acted with evidence / follow-up created or\n delegated / explicit no-op with reason.", - "8. **Final disposition checklist** — acted with evidence / delegated explicit\n requested work / explicit no-op with reason.", - ) - .replace( - "Critical: a heartbeat without observable progress (a created task, delegation,\nmessage reply, memory append, or explicit \"no-op with reason\") is a bug.", - "Critical: a heartbeat without observable progress (delegation for explicit work,\nmessage reply, memory append, or explicit \"no-op with reason\") is a bug.", - ); -} - -// Backward-compatible alias; prefer HEARTBEAT_NO_TASK_PROCEDURE_STRICT. -export const HEARTBEAT_NO_TASK_PROCEDURE = HEARTBEAT_NO_TASK_PROCEDURE_STRICT; +export { + HEARTBEAT_CRITICAL_RULES, + HEARTBEAT_SYSTEM_PROMPT, + HEARTBEAT_NO_TASK_SYSTEM_PROMPT, + HEARTBEAT_SYSTEM_PROMPT_NO_TASK, + HEARTBEAT_PROCEDURE_STRICT, + HEARTBEAT_PROCEDURE_LITE, + HEARTBEAT_PROCEDURE_OFF, + HEARTBEAT_PROCEDURE, + HEARTBEAT_NO_TASK_PROCEDURE_STRICT, + HEARTBEAT_NO_TASK_PROCEDURE_LITE, + HEARTBEAT_NO_TASK_PROCEDURE_OFF, + HEARTBEAT_NO_TASK_PROCEDURE, +} from "./agent-heartbeat-prompts.js"; +import { + HEARTBEAT_SYSTEM_PROMPT, + HEARTBEAT_PROCEDURE_STRICT, + HEARTBEAT_PROCEDURE_LITE, + HEARTBEAT_PROCEDURE_OFF, + HEARTBEAT_NO_TASK_PROCEDURE_STRICT, + HEARTBEAT_NO_TASK_PROCEDURE_LITE, + HEARTBEAT_NO_TASK_PROCEDURE_OFF, + renderHeartbeatNoTaskProcedure, + renderHeartbeatNoTaskSystemPrompt, +} from "./agent-heartbeat-prompts.js"; /** Parameter schema for the fn_heartbeat_done tool */ const heartbeatDoneParams = Type.Object({ diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 54ff4d9173..3d28b72bba 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -13,7 +13,7 @@ import { existsSync, lstatSync, realpathSync } from "node:fs"; import { readFile, rm, writeFile } from "node:fs/promises"; import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, AsyncMissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent, AgentMemoryInclusionMode, ProjectSettings, MergeResult, WorkflowIrNode, WorkflowIrNodeKind, WorkflowStepResult as CoreWorkflowStepResult, ThinkingLevel } from "@fusion/core"; import { getUnmetSchedulingDependencies } from "./scheduler.js"; -import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON, THINKING_LEVELS, AgentStore } from "@fusion/core"; +import { RetryStormError, serializeRetryStormError, isExperimentalFeatureEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON, THINKING_LEVELS, AgentStore } from "@fusion/core"; import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js"; import { mergeEffectiveSettings } from "./effective-settings.js"; import { moveTaskToReplanColumn, resolveReplanTargetColumn } from "./replan-target.js"; @@ -94,7 +94,6 @@ import { resolveValidatorFallbackThinkingLevel, } from "./agent-session-helpers.js"; import { buildSessionSkillContext } from "./session-skill-context.js"; -import type { SkillSelectionContext } from "./skill-resolver.js"; import { assertMcpResolutionSucceeded, resolveMcpServersForStore } from "./mcp-resolution.js"; import { reviewStep, proseSignalsClearApproval, extractJsonObjectCandidates, ReviewerProviderError, type ReviewVerdict, type ReviewResult } from "./reviewer.js"; import { buildUserCommentsPromptSection, selectUserCommentsForAgentContext } from "./agent-user-comments.js"; @@ -277,94 +276,25 @@ export { taskLogParams, } from "./agent-tools.js"; -export const AGENT_BROWSER_NAVIGATION_SKILL_ID = "agent-browser-navigation"; - -export interface AgentBrowserAvailabilityProbeResult { - available: boolean; - version?: string; - reason?: string; -} - -type AgentBrowserExec = ( - command: string, - options: { encoding: BufferEncoding; timeout: number; maxBuffer: number; env?: NodeJS.ProcessEnv; cwd?: string }, -) => Promise<{ stdout: string; stderr: string }>; - -function isAgentBrowserNotFoundError(error: unknown): boolean { - const err = error as { code?: unknown; stderr?: unknown; message?: unknown } | null; - const code = typeof err?.code === "string" || typeof err?.code === "number" ? String(err.code) : undefined; - if (code === "ENOENT" || code === "127") return true; - const combined = `${typeof err?.stderr === "string" ? err.stderr : ""}\n${typeof err?.message === "string" ? err.message : ""}`.toLowerCase(); - return combined.includes("agent-browser") && (combined.includes("not found") || combined.includes("command not found")); -} - -function isAgentBrowserProbeTimeout(error: unknown): boolean { - const err = error as { code?: unknown; killed?: unknown; signal?: unknown; message?: unknown } | null; - return err?.code === "ETIMEDOUT" - || err?.killed === true - || err?.signal === "SIGTERM" - || (typeof err?.message === "string" && err.message.toLowerCase().includes("timed out")); -} - -/** - * Probe the agent-browser CLI without making browser verification fatal. - * - * FNXC:WorkflowBrowserVerification 2026-06-27-13:20: - * Browser Verification needs an actionable signal when `agent-browser` is absent or hung. Keep this async, bounded, and injectable so the executor logs availability without blocking or requiring the plugin at import time. - */ -export async function probeAgentBrowserAvailability( - execImpl: AgentBrowserExec = execAsync as AgentBrowserExec, - opts?: { timeoutMs?: number; maxBuffer?: number; env?: NodeJS.ProcessEnv; cwd?: string }, -): Promise { - try { - const { stdout, stderr } = await execImpl("agent-browser --version", { - encoding: "utf-8", - timeout: Math.min(Math.max(opts?.timeoutMs ?? 5_000, 1_000), 10_000), - maxBuffer: opts?.maxBuffer ?? 64 * 1024, - ...(opts?.env ? { env: opts.env } : {}), - ...(opts?.cwd ? { cwd: opts.cwd } : {}), - }); - const version = (stdout.trim() || stderr.trim() || "unknown").split("\n")[0]?.trim() || "unknown"; - return { available: true, version }; - } catch (error) { - if (isAgentBrowserNotFoundError(error)) { - return { available: false, reason: "not installed" }; - } - if (isAgentBrowserProbeTimeout(error)) { - return { available: false, reason: "probe timed out" }; - } - const reason = error instanceof Error ? error.message : String(error); - return { available: false, reason }; - } -} - -/** Merge the agent-browser navigation skill into a workflow-step session. */ -export function augmentSessionSkillsForBrowserStep( - skillSelection: SkillSelectionContext | undefined, - projectRootDir: string, -): SkillSelectionContext { - const existing = skillSelection?.requestedSkillNames ?? []; - return { - projectRootDir: skillSelection?.projectRootDir ?? projectRootDir, - sessionPurpose: skillSelection?.sessionPurpose ?? "executor", - requestedSkillNames: [...new Set([...existing, AGENT_BROWSER_NAVIGATION_SKILL_ID])], - }; -} +export { + AGENT_BROWSER_NAVIGATION_SKILL_ID, + probeAgentBrowserAvailability, + augmentSessionSkillsForBrowserStep, + formatAgentBrowserAvailabilityLog, +} from "./executor/browser-probe.js"; +export type { AgentBrowserAvailabilityProbeResult } from "./executor/browser-probe.js"; +import { + probeAgentBrowserAvailability, + augmentSessionSkillsForBrowserStep, + formatAgentBrowserAvailabilityLog, +} from "./executor/browser-probe.js"; +import type { AgentBrowserExec } from "./executor/browser-probe.js"; function mergeAdditionalSkillPaths(...pathGroups: Array): string[] | undefined { const merged = Array.from(new Set(pathGroups.flatMap((paths) => paths ?? []))); return merged.length > 0 ? merged : undefined; } -export function formatAgentBrowserAvailabilityLog(result: AgentBrowserAvailabilityProbeResult): string { - if (result.available) { - return `[browser-verification] agent-browser available — version ${result.version ?? "unknown"}`; - } - if (result.reason === "probe timed out") { - return "[browser-verification] agent-browser availability probe timed out — the step relies on the agent-browser CLI; continuing so the step can fast-bail or report its own failure."; - } - return "[browser-verification] agent-browser not found on PATH — the step relies on the agent-browser CLI; install the agent-browser plugin/binary. Continuing; the step may fast-bail or fail."; -} const yieldEventLoop = (): Promise => new Promise((resolve) => setImmediateCb(resolve)); function getPromptSection(prompt: string, heading: string): string { @@ -503,15 +433,21 @@ const MAX_WORKFLOW_STEP_RETRIES = 3; const MAX_TASK_DONE_SESSION_RETRIES = 3; /** Maximum todo requeues after exhausting in-session fn_task_done retries. */ const MAX_TASK_DONE_REQUEUE_RETRIES = 3; -/** Maximum no-progress execute-node self-requeues before terminalizing the loop. */ -export const MAX_EXECUTE_REQUEUE_LOOP_CYCLES = 6; -/** Low-water mark for surfacing a visible warning before loop terminalization. */ -export const EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD = 3; -/** - * Maximum bounded retries for the narrow resume-after-restart graph transient. - * Budget exhaustion falls through to terminal status:"failed" so FN-5704's - * self-healing anti-loop exemption remains intact for genuine graph failures. - */ +export { + MAX_EXECUTE_REQUEUE_LOOP_CYCLES, + EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD, + buildExecuteRequeueLoopSignature, + isTransientMissingTaskJsonError, +} from "./executor/requeue-loop.js"; +import { + MAX_EXECUTE_REQUEUE_LOOP_CYCLES, + EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD, + buildExecuteRequeueLoopHighWaterSignature, + isInvalidAssistantContinuationErrorMessage, + isTransientMissingTaskJsonError, + TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN, +} from "./executor/requeue-loop.js"; + const MAX_TRANSIENT_GRAPH_RESUME_RETRIES = 2; const TRANSIENT_GRAPH_RESUME_RETRY_BACKOFF_MS = process.env.VITEST || process.env.NODE_ENV === "test" ? 0 : 1_000; /** @@ -548,88 +484,6 @@ const LOOP_COMPACTION_TIMEOUT_MS = 60_000; const TASK_DONE_REFUSAL_SUFFIX = "Either finish the work and resubmit, or do not call fn_task_done — exit the session and the engine will requeue."; -function countExecuteRequeueTerminalSteps(live: TaskDetail): number { - return live.steps?.filter((step) => step.status === "done" || step.status === "skipped").length ?? 0; -} - -function parseExecuteRequeueLoopProgressSignature(signature: string | null | undefined): { terminalStepCount: number; totalSteps: number } | null { - if (!signature) return null; - try { - const parsed = JSON.parse(signature) as { terminalStepCount?: unknown; totalSteps?: unknown }; - if (typeof parsed.terminalStepCount !== "number" || typeof parsed.totalSteps !== "number") return null; - return { - terminalStepCount: parsed.terminalStepCount, - totalSteps: parsed.totalSteps, - }; - } catch { - return null; - } -} - -export function buildExecuteRequeueLoopSignature(live: TaskDetail): string { - /* - FNXC:WorkflowLifecycle 2026-07-13-07:42: - FN-7941: human reports #2043/#2045/#2046/#2047 showed that FN-7863's raw currentStep/status signature could drift on every execute self-requeue while no step reached a terminal state, resetting the loop counter to 1 forever. Anchor the bounded streak to monotonic terminal-step progress instead: pending/in-progress/currentStep oscillation still counts toward exhaustion, while real done/skipped progress resets the streak and FN-7926 still diverts completed-blocked work before this guard can fail it. - */ - return JSON.stringify({ - terminalStepCount: countExecuteRequeueTerminalSteps(live), - totalSteps: live.steps?.length ?? 0, - }); -} - -function buildExecuteRequeueLoopHighWaterSignature(live: TaskDetail, previousSignature: string | null | undefined): { signature: string; madeForwardProgress: boolean } { - // FNXC:WorkflowLifecycle 2026-07-13-08:20: derive current terminal-step - // progress by parsing buildExecuteRequeueLoopSignature's own output rather - // than duplicating countExecuteRequeueTerminalSteps/totalSteps inline, so - // the two functions cannot silently drift out of sync. - const current = parseExecuteRequeueLoopProgressSignature(buildExecuteRequeueLoopSignature(live)); - const currentTerminalStepCount = current?.terminalStepCount ?? countExecuteRequeueTerminalSteps(live); - const totalSteps = current?.totalSteps ?? (live.steps?.length ?? 0); - const previous = parseExecuteRequeueLoopProgressSignature(previousSignature); - const previousTerminalStepCount = previous?.terminalStepCount ?? currentTerminalStepCount; - const madeForwardProgress = previous != null && currentTerminalStepCount > previousTerminalStepCount; - return { - madeForwardProgress, - signature: JSON.stringify({ - terminalStepCount: Math.max(previousTerminalStepCount, currentTerminalStepCount), - totalSteps, - }), - }; -} - -const INVALID_ASSISTANT_CONTINUATION_PATTERN = /cannot continue from message role:\s*assistant/i; - -function isInvalidAssistantContinuationErrorMessage(errorMessage: string): boolean { - return INVALID_ASSISTANT_CONTINUATION_PATTERN.test(errorMessage); -} - -const TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+\/\.fusion\/tasks\/([^/]+)\/task\.json)'/; - -export function isTransientMissingTaskJsonError(error: unknown, task: Pick): boolean { - if (error instanceof TaskDeletedError) { - return false; - } - const message = typeof error === "string" - ? error - : error instanceof Error - ? error.message - : ""; - const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message); - if (!match) { - return false; - } - const [, filePath, taskIdFromPath] = match; - if (taskIdFromPath !== task.id) { - return false; - } - if (typeof task.worktree !== "string" || task.worktree.length === 0) { - return false; - } - const normalizedWorktree = resolvePath(task.worktree); - const normalizedTaskJsonPath = resolvePath(filePath); - return normalizedTaskJsonPath.startsWith(`${normalizedWorktree}/`); -} - export const DISSENT_PATTERNS: RegExp[] = [ /\btask (is|was)(?: not|n['’]?t) complete\b/i, /\b(?:i (?:could|can)(?:not|n['’]?t)|unable to|failed to) (?:complete|finish|implement)\b/i, @@ -19202,44 +19056,12 @@ function formatCommentForInjection(comment: import("@fusion/core").SteeringComme return `📣 **New feedback** — ${timestamp} (${comment.author}):\n\n${comment.text}\n\nPlease adjust your approach based on this feedback.`; } -/** - * Result of a pseudo-pause detection check. - */ -export interface PseudoPauseResult { - /** Detection method: "regex" if a regex pattern matched, "structural" for structural - * heuristics, or "none" if no pseudo-pause was detected. */ - kind: "regex" | "structural" | "none"; - /** The matched text or pattern description when kind is not "none". */ - matched?: string; -} - function hasNonTerminalWorkflowSteps(task: Pick): boolean { return task.steps.length > 0 && task.steps.some((step) => step.status !== "done" && step.status !== "skipped"); } -/* -FNXC:ReviewLeniency 2026-07-02-01:00: -Retrying a task must clear PRIOR FAILURE states so the retry starts clean — including on optional gate nodes like code-review / browser-verification. Results are upserted by node id, so a re-running node overwrites its own stale entry, but a send-back-for-fix leaves the failed entry in place until (and unless) that node re-runs; meanwhile self-healing's failed-pre-merge scan and the dashboard both see a stale failure, and a node that is skipped/relaxed on the retry never clears it. Drop every terminal failure result (`failed`/`advisory_failure`) on retry while keeping `passed`/`skipped`/`pending` evidence (so a previously-passed Plan Review is not re-run). Returns the same array reference when nothing changed so callers can skip a no-op write. - -FNXC:WorkflowStepResults 2026-07-09-00:30: -FN-7727 explicit decision: an explicit user/agent RETRY remains a clean-slate — -it MAY drop the current `failed`/`advisory_failure` entry entirely (along with -any `priorAttempts` history it carried), since retry is deliberately -discarding prior failure state, not preserving it. This is DIFFERENT from the -self-healing recovery re-run path (`recoverFailedPreMergeWorkflowStep` / -`recoverReviewTasksWithFailedPreMergeSteps`), which does NOT call this -function — that path re-runs the SAME node in place and its result goes -through `upsertWorkflowStepResult`, which is where prior-attempt history is -preserved. This filter must not throw on entries carrying `priorAttempts` -(it only reads `status`, so `priorAttempts` is inert here regardless). -*/ -export function clearTerminalWorkflowStepFailures( - results: CoreWorkflowStepResult[] | undefined, -): CoreWorkflowStepResult[] { - const current = results ?? []; - const kept = current.filter((result) => result.status !== "failed" && result.status !== "advisory_failure"); - return kept.length === current.length ? current : kept; -} +export { clearTerminalWorkflowStepFailures } from "./executor/workflow-step-failures.js"; +import { clearTerminalWorkflowStepFailures } from "./executor/workflow-step-failures.js"; function workflowStepResultPassed(task: Pick | undefined, workflowStepId: string): boolean { const results = task?.workflowStepResults ?? []; @@ -19326,80 +19148,12 @@ function preservePreExecutionWorkflowStepResults(task: Pick 200) { - if (trimmed.endsWith("?")) { - const lastLine = trimmed.split("\n").at(-1) ?? trimmed; - return { kind: "structural", matched: lastLine.trim() }; - } - const nextStepsPattern = /(?:^|\n)#+\s*(?:notes?|next steps?|summary|what'?s? next)\s*:?\s*$/i; - if (nextStepsPattern.test(trimmed)) { - const lastLine = trimmed.split("\n").at(-1) ?? trimmed; - return { kind: "structural", matched: lastLine.trim() }; - } - // Also catch plain "Next steps:" or "### Next steps" at the very end - if (/next steps?\s*:?\s*$/i.test(trimmed)) { - const lastLine = trimmed.split("\n").at(-1) ?? trimmed; - return { kind: "structural", matched: lastLine.trim() }; - } - } - - return { kind: "none" }; -} - -/** - * Detect if a steering comment contains a review handoff request. - * Matches common handoff phrases that agents can use to request - * human review of their work. - */ -export function detectReviewHandoffIntent(commentText: string): boolean { - const text = commentText.toLowerCase(); - const handoffPhrases = [ - "send it back to me", - "hand off to user", - "needs human review", - "assign to user", - "return to user", - "user review needed", - "requesting user review", - ]; - - return handoffPhrases.some((phrase) => text.includes(phrase)); -} +export { + detectPseudoPause, + detectReviewHandoffIntent, +} from "./executor/pseudo-pause.js"; +export type { PseudoPauseResult } from "./executor/pseudo-pause.js"; +import { + detectPseudoPause, + detectReviewHandoffIntent, +} from "./executor/pseudo-pause.js"; diff --git a/packages/engine/src/executor/browser-probe.ts b/packages/engine/src/executor/browser-probe.ts new file mode 100644 index 0000000000..eb7c63832e --- /dev/null +++ b/packages/engine/src/executor/browser-probe.ts @@ -0,0 +1,94 @@ +/** + * FNXC:CodeOrganization 2026-07-15-00:00: + * Agent-browser availability probe helpers peeled from executor.ts so the + * monofile shrinks without changing browser-verification behavior. + */ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import type { SkillSelectionContext } from "../skill-resolver.js"; + +const execAsync = promisify(exec); + +export const AGENT_BROWSER_NAVIGATION_SKILL_ID = "agent-browser-navigation"; + +export interface AgentBrowserAvailabilityProbeResult { + available: boolean; + version?: string; + reason?: string; +} + +export type AgentBrowserExec = ( + command: string, + options: { encoding: BufferEncoding; timeout: number; maxBuffer: number; env?: NodeJS.ProcessEnv; cwd?: string }, +) => Promise<{ stdout: string; stderr: string }>; + +function isAgentBrowserNotFoundError(error: unknown): boolean { + const err = error as { code?: unknown; stderr?: unknown; message?: unknown } | null; + const code = typeof err?.code === "string" || typeof err?.code === "number" ? String(err.code) : undefined; + if (code === "ENOENT" || code === "127") return true; + const combined = `${typeof err?.stderr === "string" ? err.stderr : ""}\n${typeof err?.message === "string" ? err.message : ""}`.toLowerCase(); + return combined.includes("agent-browser") && (combined.includes("not found") || combined.includes("command not found")); +} + +function isAgentBrowserProbeTimeout(error: unknown): boolean { + const err = error as { code?: unknown; killed?: unknown; signal?: unknown; message?: unknown } | null; + return err?.code === "ETIMEDOUT" + || err?.killed === true + || err?.signal === "SIGTERM" + || (typeof err?.message === "string" && err.message.toLowerCase().includes("timed out")); +} + +/** + * Probe the agent-browser CLI without making browser verification fatal. + * + * FNXC:WorkflowBrowserVerification 2026-06-27-13:20: + * Browser Verification needs an actionable signal when `agent-browser` is absent or hung. Keep this async, bounded, and injectable so the executor logs availability without blocking or requiring the plugin at import time. + */ +export async function probeAgentBrowserAvailability( + execImpl: AgentBrowserExec = execAsync as AgentBrowserExec, + opts?: { timeoutMs?: number; maxBuffer?: number; env?: NodeJS.ProcessEnv; cwd?: string }, +): Promise { + try { + const { stdout, stderr } = await execImpl("agent-browser --version", { + encoding: "utf-8", + timeout: Math.min(Math.max(opts?.timeoutMs ?? 5_000, 1_000), 10_000), + maxBuffer: opts?.maxBuffer ?? 64 * 1024, + ...(opts?.env ? { env: opts.env } : {}), + ...(opts?.cwd ? { cwd: opts.cwd } : {}), + }); + const version = (stdout.trim() || stderr.trim() || "unknown").split("\n")[0]?.trim() || "unknown"; + return { available: true, version }; + } catch (error) { + if (isAgentBrowserNotFoundError(error)) { + return { available: false, reason: "not installed" }; + } + if (isAgentBrowserProbeTimeout(error)) { + return { available: false, reason: "probe timed out" }; + } + const reason = error instanceof Error ? error.message : String(error); + return { available: false, reason }; + } +} + +/** Merge the agent-browser navigation skill into a workflow-step session. */ +export function augmentSessionSkillsForBrowserStep( + skillSelection: SkillSelectionContext | undefined, + projectRootDir: string, +): SkillSelectionContext { + const existing = skillSelection?.requestedSkillNames ?? []; + return { + projectRootDir: skillSelection?.projectRootDir ?? projectRootDir, + sessionPurpose: skillSelection?.sessionPurpose ?? "executor", + requestedSkillNames: [...new Set([...existing, AGENT_BROWSER_NAVIGATION_SKILL_ID])], + }; +} + +export function formatAgentBrowserAvailabilityLog(result: AgentBrowserAvailabilityProbeResult): string { + if (result.available) { + return `[browser-verification] agent-browser available — version ${result.version ?? "unknown"}`; + } + if (result.reason === "probe timed out") { + return "[browser-verification] agent-browser availability probe timed out — the step relies on the agent-browser CLI; continuing so the step can fast-bail or report its own failure."; + } + return "[browser-verification] agent-browser not found on PATH — the step relies on the agent-browser CLI; install the agent-browser plugin/binary. Continuing; the step may fast-bail or fail."; +} diff --git a/packages/engine/src/executor/pseudo-pause.ts b/packages/engine/src/executor/pseudo-pause.ts new file mode 100644 index 0000000000..7ca6be70d3 --- /dev/null +++ b/packages/engine/src/executor/pseudo-pause.ts @@ -0,0 +1,93 @@ +/** + * FNXC:CodeOrganization 2026-07-15-00:00: + * Pseudo-pause and review-handoff detectors peeled from executor.ts. + */ + +/** + * Result of a pseudo-pause detection check. + */ +export interface PseudoPauseResult { + /** Detection method: "regex" if a regex pattern matched, "structural" for structural + * heuristics, or "none" if no pseudo-pause was detected. */ + kind: "regex" | "structural" | "none"; + /** The matched text or pattern description when kind is not "none". */ + matched?: string; +} + +/** + * Detect whether the last assistant text output looks like a "pseudo-pause" — + * where the agent ended a turn by asking for permission or summarizing progress + * instead of calling a tool. + * + * Returns a {@link PseudoPauseResult} describing the detection kind and the + * matched text/pattern. Returns `{ kind: "none" }` when no pseudo-pause is found. + * + * @param lastText - The last assistant text output from the session. + */ +export function detectPseudoPause(lastText: string): PseudoPauseResult { + if (!lastText || lastText.trim().length === 0) { + return { kind: "none" }; + } + + const regexPatterns: RegExp[] = [ + /\bif you (?:want|wish|need|like|prefer|'?d like)\b/i, + /\bshould I (?:continue|proceed|go ahead|move on|start|begin)\b/i, + /\blet me know\b/i, + /\b(?:want|would you like) me to (?:continue|proceed|finish|complete|do)\b/i, + /\bready to (?:proceed|continue|move on|begin)\b/i, + /\bshall I\b/i, + /\b(?:awaiting|waiting for) (?:your )?(?:approval|confirmation|go-ahead|response)\b/i, + ]; + + for (const pattern of regexPatterns) { + const match = pattern.exec(lastText); + if (match) { + // Capture surrounding context (up to 120 chars around the match) + const start = Math.max(0, match.index - 40); + const end = Math.min(lastText.length, match.index + match[0].length + 80); + const snippet = lastText.slice(start, end).replace(/\n+/g, " ").trim(); + return { kind: "regex", matched: snippet }; + } + } + + // Structural fallback: long output that ends with a question or a markdown "next steps" heading + const trimmed = lastText.trimEnd(); + if (trimmed.length > 200) { + if (trimmed.endsWith("?")) { + const lastLine = trimmed.split("\n").at(-1) ?? trimmed; + return { kind: "structural", matched: lastLine.trim() }; + } + const nextStepsPattern = /(?:^|\n)#+\s*(?:notes?|next steps?|summary|what'?s? next)\s*:?\s*$/i; + if (nextStepsPattern.test(trimmed)) { + const lastLine = trimmed.split("\n").at(-1) ?? trimmed; + return { kind: "structural", matched: lastLine.trim() }; + } + // Also catch plain "Next steps:" or "### Next steps" at the very end + if (/next steps?\s*:?\s*$/i.test(trimmed)) { + const lastLine = trimmed.split("\n").at(-1) ?? trimmed; + return { kind: "structural", matched: lastLine.trim() }; + } + } + + return { kind: "none" }; +} + +/** + * Detect if a steering comment contains a review handoff request. + * Matches common handoff phrases that agents can use to request + * human review of their work. + */ +export function detectReviewHandoffIntent(commentText: string): boolean { + const text = commentText.toLowerCase(); + const handoffPhrases = [ + "send it back to me", + "hand off to user", + "needs human review", + "assign to user", + "return to user", + "user review needed", + "requesting user review", + ]; + + return handoffPhrases.some((phrase) => text.includes(phrase)); +} diff --git a/packages/engine/src/executor/requeue-loop.ts b/packages/engine/src/executor/requeue-loop.ts new file mode 100644 index 0000000000..a6e39a3f50 --- /dev/null +++ b/packages/engine/src/executor/requeue-loop.ts @@ -0,0 +1,105 @@ +/** + * FNXC:CodeOrganization 2026-07-15-00:00: + * Execute-node self-requeue loop signature helpers peeled from executor.ts. + */ +import type { TaskDetail, Task } from "@fusion/core"; +import { TaskDeletedError } from "@fusion/core"; +import { isAbsolute, relative, resolve as resolvePath } from "node:path"; + +/** Maximum no-progress execute-node self-requeues before terminalizing the loop. */ +export const MAX_EXECUTE_REQUEUE_LOOP_CYCLES = 6; +/** Low-water mark for surfacing a visible warning before loop terminalization. */ +export const EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD = 3; + +function countExecuteRequeueTerminalSteps(live: TaskDetail): number { + return live.steps?.filter((step) => step.status === "done" || step.status === "skipped").length ?? 0; +} + +function parseExecuteRequeueLoopProgressSignature(signature: string | null | undefined): { terminalStepCount: number; totalSteps: number } | null { + if (!signature) return null; + try { + const parsed = JSON.parse(signature) as { terminalStepCount?: unknown; totalSteps?: unknown }; + if (typeof parsed.terminalStepCount !== "number" || typeof parsed.totalSteps !== "number") return null; + return { + terminalStepCount: parsed.terminalStepCount, + totalSteps: parsed.totalSteps, + }; + } catch { + return null; + } +} + +export function buildExecuteRequeueLoopSignature(live: TaskDetail): string { + /* + FNXC:WorkflowLifecycle 2026-07-13-07:42: + FN-7941: human reports #2043/#2045/#2046/#2047 showed that FN-7863's raw currentStep/status signature could drift on every execute self-requeue while no step reached a terminal state, resetting the loop counter to 1 forever. Anchor the bounded streak to monotonic terminal-step progress instead: pending/in-progress/currentStep oscillation still counts toward exhaustion, while real done/skipped progress resets the streak and FN-7926 still diverts completed-blocked work before this guard can fail it. + */ + return JSON.stringify({ + terminalStepCount: countExecuteRequeueTerminalSteps(live), + totalSteps: live.steps?.length ?? 0, + }); +} + +export function buildExecuteRequeueLoopHighWaterSignature(live: TaskDetail, previousSignature: string | null | undefined): { signature: string; madeForwardProgress: boolean } { + // FNXC:WorkflowLifecycle 2026-07-13-08:20: derive current terminal-step + // progress by parsing buildExecuteRequeueLoopSignature's own output rather + // than duplicating countExecuteRequeueTerminalSteps/totalSteps inline, so + // the two functions cannot silently drift out of sync. + const current = parseExecuteRequeueLoopProgressSignature(buildExecuteRequeueLoopSignature(live)); + const currentTerminalStepCount = current?.terminalStepCount ?? countExecuteRequeueTerminalSteps(live); + const totalSteps = current?.totalSteps ?? (live.steps?.length ?? 0); + const previous = parseExecuteRequeueLoopProgressSignature(previousSignature); + const previousTerminalStepCount = previous?.terminalStepCount ?? currentTerminalStepCount; + const madeForwardProgress = previous != null && currentTerminalStepCount > previousTerminalStepCount; + return { + madeForwardProgress, + signature: JSON.stringify({ + terminalStepCount: Math.max(previousTerminalStepCount, currentTerminalStepCount), + totalSteps, + }), + }; +} + +const INVALID_ASSISTANT_CONTINUATION_PATTERN = /cannot continue from message role:\s*assistant/i; + +export function isInvalidAssistantContinuationErrorMessage(errorMessage: string): boolean { + return INVALID_ASSISTANT_CONTINUATION_PATTERN.test(errorMessage); +} + +export const TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+[\\/]\.fusion[\\/]tasks[\\/]([^\\/]+)[\\/]task\.json)'/; + +function normalizeErrorPath(path: string): string { + return resolvePath(path.replace(/\\/g, "/")); +} + +function isContainedByWorktree(filePath: string, worktree: string): boolean { + /* + FNXC:ExecutorRecovery 2026-07-15-13:20: + Transient task.json recovery must recognize Node ENOENT paths on both Windows and POSIX, but only when the missing file belongs to the task's exact worktree. Normalize separator-only error-message differences before path resolution, then use relative-path containment so sibling prefixes such as `fn-1-copy` cannot be mistaken for `fn-1`. + */ + const relativePath = relative(normalizeErrorPath(worktree), normalizeErrorPath(filePath)); + return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath); +} + +export function isTransientMissingTaskJsonError(error: unknown, task: Pick): boolean { + if (error instanceof TaskDeletedError) { + return false; + } + const message = typeof error === "string" + ? error + : error instanceof Error + ? error.message + : ""; + const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message); + if (!match) { + return false; + } + const [, filePath, taskIdFromPath] = match; + if (taskIdFromPath !== task.id) { + return false; + } + if (typeof task.worktree !== "string" || task.worktree.length === 0) { + return false; + } + return isContainedByWorktree(filePath, task.worktree); +} diff --git a/packages/engine/src/executor/workflow-step-failures.ts b/packages/engine/src/executor/workflow-step-failures.ts new file mode 100644 index 0000000000..9fc285c7f1 --- /dev/null +++ b/packages/engine/src/executor/workflow-step-failures.ts @@ -0,0 +1,29 @@ +/** + * FNXC:CodeOrganization 2026-07-15-00:00: + * Workflow step failure filter helpers peeled from executor.ts. + */ +import type { WorkflowStepResult as CoreWorkflowStepResult } from "@fusion/core"; + +/* +FNXC:ReviewLeniency 2026-07-02-01:00: +Retrying a task must clear PRIOR FAILURE states so the retry starts clean — including on optional gate nodes like code-review / browser-verification. Results are upserted by node id, so a re-running node overwrites its own stale entry, but a send-back-for-fix leaves the failed entry in place until (and unless) that node re-runs; meanwhile self-healing's failed-pre-merge scan and the dashboard both see a stale failure, and a node that is skipped/relaxed on the retry never clears it. Drop every terminal failure result (`failed`/`advisory_failure`) on retry while keeping `passed`/`skipped`/`pending` evidence (so a previously-passed Plan Review is not re-run). Returns the same array reference when nothing changed so callers can skip a no-op write. + +FNXC:WorkflowStepResults 2026-07-09-00:30: +FN-7727 explicit decision: an explicit user/agent RETRY remains a clean-slate — +it MAY drop the current `failed`/`advisory_failure` entry entirely (along with +any `priorAttempts` history it carried), since retry is deliberately +discarding prior failure state, not preserving it. This is DIFFERENT from the +self-healing recovery re-run path (`recoverFailedPreMergeWorkflowStep` / +`recoverReviewTasksWithFailedPreMergeSteps`), which does NOT call this +function — that path re-runs the SAME node in place and its result goes +through `upsertWorkflowStepResult`, which is where prior-attempt history is +preserved. This filter must not throw on entries carrying `priorAttempts` +(it only reads `status`, so `priorAttempts` is inert here regardless). +*/ +export function clearTerminalWorkflowStepFailures( + results: CoreWorkflowStepResult[] | undefined, +): CoreWorkflowStepResult[] { + const current = results ?? []; + const kept = current.filter((result) => result.status !== "failed" && result.status !== "advisory_failure"); + return kept.length === current.length ? current : kept; +} diff --git a/scripts/line-count-baseline.json b/scripts/line-count-baseline.json index b58e323196..4cdc1d57d5 100644 --- a/scripts/line-count-baseline.json +++ b/scripts/line-count-baseline.json @@ -1,101 +1,104 @@ { - "packages/cli/src/__tests__/extension.test.ts": 4186, - "packages/cli/src/bin.ts": 2070, - "packages/cli/src/commands/__tests__/dashboard.test.ts": 3376, - "packages/cli/src/commands/__tests__/serve.test.ts": 2100, - "packages/cli/src/commands/__tests__/task.test.ts": 3439, - "packages/cli/src/commands/dashboard-tui/app.tsx": 4681, - "packages/cli/src/commands/dashboard.ts": 3000, - "packages/cli/src/extension.ts": 4704, - "packages/core/src/__tests__/agent-store.test.ts": 3003, - "packages/core/src/__tests__/central-core.test.ts": 3263, - "packages/core/src/__tests__/db.test.ts": 3606, - "packages/core/src/__tests__/mission-store.test.ts": 4525, - "packages/core/src/__tests__/plugin-loader.test.ts": 2783, - "packages/core/src/__tests__/store-settings.test.ts": 2249, - "packages/core/src/agent-store.ts": 2946, - "packages/core/src/central-core.ts": 3854, - "packages/core/src/db.ts": 5924, - "packages/core/src/mission-store.ts": 4390, - "packages/core/src/store.ts": 17371, - "packages/core/src/types.ts": 7522, - "packages/dashboard/app/api/legacy.ts": 10865, - "packages/dashboard/app/components/AgentDetailView.tsx": 5400, - "packages/dashboard/app/components/AgentsView.tsx": 2147, - "packages/dashboard/app/components/ChatView.tsx": 4075, - "packages/dashboard/app/components/GitManagerModal.tsx": 3387, - "packages/dashboard/app/components/ListView.tsx": 2464, - "packages/dashboard/app/components/MissionManager.tsx": 5042, - "packages/dashboard/app/components/ModelOnboardingModal.tsx": 3212, - "packages/dashboard/app/components/PlanningModeModal.tsx": 3531, - "packages/dashboard/app/components/QuickEntryBox.tsx": 2288, - "packages/dashboard/app/components/SettingsModal.tsx": 3505, - "packages/dashboard/app/components/TaskCard.tsx": 2544, - "packages/dashboard/app/components/TaskDetailModal.tsx": 4636, - "packages/dashboard/app/components/TerminalModal.tsx": 2313, - "packages/dashboard/app/components/WorkflowNodeEditor.tsx": 4868, - "packages/dashboard/app/components/__tests__/AgentsView.test.tsx": 2817, - "packages/dashboard/app/components/__tests__/App.test.tsx": 4437, - "packages/dashboard/app/components/__tests__/ChatView.test.tsx": 5822, - "packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx": 3530, - "packages/dashboard/app/components/__tests__/ListView.test.tsx": 4349, - "packages/dashboard/app/components/__tests__/MailboxView.test.tsx": 2202, - "packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx": 4679, - "packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx": 3002, - "packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx": 4850, - "packages/dashboard/app/components/__tests__/TaskCard.test.tsx": 5121, - "packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx": 2558, - "packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx": 2917, - "packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx": 2476, - "packages/dashboard/app/components/__tests__/TerminalModal.test.tsx": 5707, - "packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx": 2905, - "packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx": 3724, - "packages/dashboard/app/hooks/__tests__/useChat.test.ts": 4097, - "packages/dashboard/app/hooks/__tests__/useTasks.test.ts": 2582, - "packages/dashboard/src/__tests__/chat-manager.test.ts": 2636, - "packages/dashboard/src/__tests__/file-service.test.ts": 2123, + "packages/cli/src/__tests__/extension.test.ts": 4558, + "packages/cli/src/bin.ts": 2307, + "packages/cli/src/commands/__tests__/dashboard.test.ts": 3709, + "packages/cli/src/commands/__tests__/serve.test.ts": 2250, + "packages/cli/src/commands/__tests__/task.test.ts": 3585, + "packages/cli/src/commands/dashboard-tui/app.tsx": 4674, + "packages/cli/src/commands/dashboard.ts": 3645, + "packages/cli/src/commands/task.ts": 2373, + "packages/cli/src/extension.ts": 5575, + "packages/core/src/agent-store.ts": 3475, + "packages/core/src/async-mission-store-queries.ts": 2067, + "packages/core/src/central-core.ts": 4474, + "packages/core/src/index.gate.ts": 2157, + "packages/core/src/index.ts": 2388, + "packages/core/src/mission-store.ts": 4364, + "packages/core/src/postgres/sqlite-migrator.ts": 2068, + "packages/core/src/store.ts": 2631, + "packages/core/src/types.ts": 7336, + "packages/dashboard/app/api/legacy.ts": 11627, + "packages/dashboard/app/components/AgentDetailView.tsx": 5552, + "packages/dashboard/app/components/AgentsView.tsx": 2254, + "packages/dashboard/app/components/ChatView.tsx": 3850, + "packages/dashboard/app/components/GitManagerModal.tsx": 3560, + "packages/dashboard/app/components/ListView.tsx": 3087, + "packages/dashboard/app/components/MissionManager.tsx": 5010, + "packages/dashboard/app/components/ModelOnboardingModal.tsx": 3590, + "packages/dashboard/app/components/PlanningModeModal.tsx": 3783, + "packages/dashboard/app/components/QuickEntryBox.tsx": 2518, + "packages/dashboard/app/components/SettingsModal.tsx": 4572, + "packages/dashboard/app/components/TaskCard.tsx": 3856, + "packages/dashboard/app/components/TaskDetailModal.tsx": 6114, + "packages/dashboard/app/components/TerminalModal.tsx": 3322, + "packages/dashboard/app/components/WorkflowNodeEditor.tsx": 5683, + "packages/dashboard/app/components/__tests__/AgentsView.test.tsx": 2845, + "packages/dashboard/app/components/__tests__/App.test.tsx": 4567, + "packages/dashboard/app/components/__tests__/Board.test.tsx": 2107, + "packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx": 2395, + "packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx": 2622, + "packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx": 3788, + "packages/dashboard/app/components/__tests__/ListView.test.tsx": 5198, + "packages/dashboard/app/components/__tests__/MailboxView.test.tsx": 2377, + "packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx": 5140, + "packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx": 2079, + "packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx": 4040, + "packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx": 5337, + "packages/dashboard/app/components/__tests__/TaskCard.test.tsx": 7115, + "packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx": 2891, + "packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx": 3340, + "packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx": 2812, + "packages/dashboard/app/components/__tests__/TerminalModal.test.tsx": 9176, + "packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx": 2931, + "packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx": 4353, + "packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx": 2062, + "packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts": 2458, + "packages/dashboard/app/hooks/__tests__/useChat.test.ts": 5355, + "packages/dashboard/app/hooks/__tests__/useTasks.test.ts": 3712, + "packages/dashboard/src/__tests__/chat-manager.test.ts": 3695, + "packages/dashboard/src/__tests__/file-service.test.ts": 2310, "packages/dashboard/src/__tests__/github.test.ts": 2319, - "packages/dashboard/src/__tests__/plugin-routes.test.ts": 2069, - "packages/dashboard/src/__tests__/routes-agents.test.ts": 4921, - "packages/dashboard/src/__tests__/routes-auth.test.ts": 3980, - "packages/dashboard/src/__tests__/routes-automation.test.ts": 2393, - "packages/dashboard/src/__tests__/routes-github.test.ts": 2855, - "packages/dashboard/src/__tests__/routes-nodes-sync.test.ts": 2479, - "packages/dashboard/src/__tests__/routes-planning.test.ts": 4468, - "packages/dashboard/src/__tests__/routes-settings.test.ts": 3259, - "packages/dashboard/src/__tests__/routes-tasks-ops.test.ts": 4247, - "packages/dashboard/src/__tests__/routes-tasks.test.ts": 2696, - "packages/dashboard/src/__tests__/server.test.ts": 3177, - "packages/dashboard/src/__tests__/usage.test.ts": 4328, - "packages/dashboard/src/chat.ts": 2197, + "packages/dashboard/src/__tests__/routes-auth.test.ts": 5510, + "packages/dashboard/src/__tests__/routes-automation.test.ts": 3068, + "packages/dashboard/src/__tests__/routes-github.test.ts": 3169, + "packages/dashboard/src/__tests__/routes-planning.test.ts": 5119, + "packages/dashboard/src/__tests__/routes-tasks.test.ts": 2949, + "packages/dashboard/src/__tests__/usage.test.ts": 5261, + "packages/dashboard/src/chat.ts": 2861, "packages/dashboard/src/github.ts": 4587, - "packages/dashboard/src/mission-routes.ts": 3968, - "packages/dashboard/src/planning.ts": 2866, - "packages/dashboard/src/routes.ts": 5321, - "packages/dashboard/src/routes/register-git-github.ts": 5879, + "packages/dashboard/src/mission-routes.ts": 3895, + "packages/dashboard/src/planning.ts": 3364, + "packages/dashboard/src/routes.ts": 5600, + "packages/dashboard/src/routes/register-git-github.ts": 5984, "packages/dashboard/src/routes/register-settings-memory-routes.ts": 2382, - "packages/dashboard/src/routes/register-task-workflow-routes.ts": 3902, - "packages/dashboard/src/server.ts": 2467, - "packages/engine/src/__tests__/executor-prompt.test.ts": 2573, - "packages/engine/src/__tests__/executor-step-session.test.ts": 3779, - "packages/engine/src/__tests__/executor-worktree.test.ts": 2536, - "packages/engine/src/__tests__/heartbeat-executor.test.ts": 4162, - "packages/engine/src/__tests__/merger-merge-lifecycle.test.ts": 3408, + "packages/dashboard/src/routes/register-task-workflow-routes.ts": 5366, + "packages/dashboard/src/server.ts": 2860, + "packages/dashboard/src/usage.ts": 2398, + "packages/engine/src/__tests__/cron-runner.test.ts": 2326, + "packages/engine/src/__tests__/executor-prompt.test.ts": 2685, + "packages/engine/src/__tests__/executor-worktree.test.ts": 2723, + "packages/engine/src/__tests__/heartbeat-executor.test.ts": 4295, + "packages/engine/src/__tests__/heartbeat-scheduler.test.ts": 3018, + "packages/engine/src/__tests__/merger-merge-lifecycle.test.ts": 3693, "packages/engine/src/__tests__/merger-verification.test.ts": 3163, - "packages/engine/src/__tests__/mission-execution-loop.test.ts": 2463, - "packages/engine/src/__tests__/pi-create-fn-agent.test.ts": 2300, - "packages/engine/src/__tests__/project-engine.test.ts": 3238, - "packages/engine/src/__tests__/self-healing.test.ts": 9739, - "packages/engine/src/__tests__/step-session-executor.test.ts": 2911, - "packages/engine/src/__tests__/triage.test.ts": 4673, - "packages/engine/src/agent-heartbeat.ts": 4660, - "packages/engine/src/agent-tools.ts": 3986, - "packages/engine/src/executor.ts": 16750, - "packages/engine/src/merger.ts": 12886, - "packages/engine/src/pi.ts": 2507, - "packages/engine/src/project-engine.ts": 4030, - "packages/engine/src/scheduler.ts": 3186, - "packages/engine/src/self-healing.ts": 11091, - "packages/engine/src/triage.ts": 2787, - "plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx": 2583 + "packages/engine/src/__tests__/mission-execution-loop.test.ts": 2904, + "packages/engine/src/__tests__/pi-create-fn-agent.test.ts": 3067, + "packages/engine/src/__tests__/project-engine.test.ts": 3482, + "packages/engine/src/__tests__/restart.integration.test.ts": 2013, + "packages/engine/src/__tests__/self-healing.test.ts": 11148, + "packages/engine/src/__tests__/step-session-executor.test.ts": 3217, + "packages/engine/src/__tests__/triage.test.ts": 6605, + "packages/engine/src/agent-heartbeat.ts": 5084, + "packages/engine/src/agent-tools.ts": 4793, + "packages/engine/src/executor.ts": 19107, + "packages/engine/src/merger-ai.ts": 2148, + "packages/engine/src/merger.ts": 12487, + "packages/engine/src/pi.ts": 2799, + "packages/engine/src/project-engine.ts": 5051, + "packages/engine/src/scheduler.ts": 3370, + "packages/engine/src/self-healing.ts": 12183, + "packages/engine/src/triage.ts": 3133, + "packages/pi-claude-cli/src/__tests__/provider.test.ts": 2028, + "plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx": 2583, + "scripts/__tests__/test-changed.test.mjs": 2116 }