refactor: package code organization (god-file peels, wave 1) (#2139)
## Summary
First wave of package-internal code organization: split oversized
modules into domain-named files/folders while preserving public import
paths via re-exports, and refresh the line-count ratchet scoreboard.
- **Plan:**
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`
(multi-wave program; this PR lands U1–U4 + first U3/U6 slices)
- **Core types:** peel `types.ts` into
`types/{board,merge-queue,execution-and-ui,merge-policy,workflow-steps}.ts`
with browser-safe Vite alias preserved
- **Core TaskStore:** rename `remaining-ops-9` →
`task-commit-associations` (domain-named, not ordinal dump)
- **Engine executor:** peel pure helpers into
`executor/{browser-probe,requeue-loop,pseudo-pause,workflow-step-failures}.ts`
- **Engine heartbeat:** peel system prompts/procedures into
`agent-heartbeat-prompts.ts`
- **Ratchet:** one-time baseline truth-up + ratchet-down for touched
files
### Deferred to follow-up PRs (plan U5, U7–U9 + remaining waves)
- Self-healing folder split
- Further remaining-ops domain peels
- Dashboard `legacy.ts` / routes / UI monofiles
- CLI extension + TUI peels
## Test plan
- [x] `pnpm --filter @fusion/core exec tsc --noEmit`
- [x] `pnpm --filter @fusion/engine exec tsc --noEmit`
- [x] Focused vitest: `detect-pseudo-pause`,
`executor-browser-verification`, `clear-terminal-workflow-step-failures`
- [x] `node scripts/check-file-line-count.mjs` clean against updated
baseline
- [ ] CI merge gate (lint/typecheck/build/gate)
- [ ] Browser smoke: N/A for this PR (no dashboard UI route changes)
## Residual Review Findings
None. Review autofix applied dual-home wiring for
`clearTerminalWorkflowStepFailures` only.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added configurable heartbeat procedures for task and no-task scenarios
(including patrol-aware rendering).
* Improved agent-browser availability verification with clearer
availability/status reporting.
* Added detection for pseudo-pauses and review-handoff requests.
* Expanded core configuration/contract options for
execution/UI/localization, merges, merge queues, and workflow steps.
* **Bug Fixes**
* Improved handling of transient execute-requeue and workflow-step
retry/cleanup behavior, including better Windows path support.
* Preserved existing public interfaces during internal restructuring.
* **Documentation**
* Added a multi-phase roadmap for future package reorganization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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.
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
File diff suppressed because it is too large
Load Diff
73
packages/core/src/types/board.ts
Normal file
73
packages/core/src/types/board.ts
Normal file
@@ -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";
|
||||
156
packages/core/src/types/execution-and-ui.ts
Normal file
156
packages/core/src/types/execution-and-ui.ts
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
246
packages/core/src/types/merge-policy.ts
Normal file
246
packages/core/src/types/merge-policy.ts
Normal file
@@ -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<Record<AutoRecoveryFailureClass, AutoRecoveryMode>>;
|
||||
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<string, unknown>)
|
||||
.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<Record<AutoRecoveryFailureClass, AutoRecoveryMode>>
|
||||
: 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";
|
||||
167
packages/core/src/types/merge-queue.ts
Normal file
167
packages/core/src/types/merge-queue.ts
Normal file
@@ -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;
|
||||
}
|
||||
395
packages/core/src/types/workflow-steps.ts
Normal file
395
packages/core/src/types/workflow-steps.ts
Normal file
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/** Declarative notification provider configuration persisted in settings. */
|
||||
export interface NotificationProviderConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
517
packages/engine/src/agent-heartbeat-prompts.ts
Normal file
517
packages/engine/src/agent-heartbeat-prompts.ts
Normal file
@@ -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.",
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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<AgentBrowserAvailabilityProbeResult> {
|
||||
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>): 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<void> => 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<Task, "id" | "worktree">): 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<TaskDetail, "steps">): 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<Task, "workflowStepResults"> | undefined, workflowStepId: string): boolean {
|
||||
const results = task?.workflowStepResults ?? [];
|
||||
@@ -19326,80 +19148,12 @@ function preservePreExecutionWorkflowStepResults(task: Pick<Task, "workflowStepR
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
export {
|
||||
detectPseudoPause,
|
||||
detectReviewHandoffIntent,
|
||||
} from "./executor/pseudo-pause.js";
|
||||
export type { PseudoPauseResult } from "./executor/pseudo-pause.js";
|
||||
import {
|
||||
detectPseudoPause,
|
||||
detectReviewHandoffIntent,
|
||||
} from "./executor/pseudo-pause.js";
|
||||
|
||||
94
packages/engine/src/executor/browser-probe.ts
Normal file
94
packages/engine/src/executor/browser-probe.ts
Normal file
@@ -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<AgentBrowserAvailabilityProbeResult> {
|
||||
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.";
|
||||
}
|
||||
93
packages/engine/src/executor/pseudo-pause.ts
Normal file
93
packages/engine/src/executor/pseudo-pause.ts
Normal file
@@ -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));
|
||||
}
|
||||
105
packages/engine/src/executor/requeue-loop.ts
Normal file
105
packages/engine/src/executor/requeue-loop.ts
Normal file
@@ -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<Task, "id" | "worktree">): 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);
|
||||
}
|
||||
29
packages/engine/src/executor/workflow-step-failures.ts
Normal file
29
packages/engine/src/executor/workflow-step-failures.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user