feat(FN-2605): merge fusion/fn-2605 (auto-resolved)

- test(FN-2605): complete Step 4 — align tests with planning labels
- docs(FN-2605): complete Step 3 — update demo and script terminology
- docs(FN-2605): complete Step 2 — update docs terminology
- docs(FN-2605): complete Step 1 — update README terminology
This commit is contained in:
Fusion
2026-04-26 12:34:02 -07:00
committed by gsxdsm
parent fa60ada0fc
commit c85ffa9198
22 changed files with 153 additions and 148 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Rename status values: specifying→planning, needs-respecify→needs-replan. Display label "Triage"→"Planning". Includes DB migration for existing records.

View File

@@ -23,7 +23,7 @@
<br /> <br />
<a href="https://runfusion.ai"> <a href="https://runfusion.ai">
<img src="https://runfusion.ai/fusion-dashboard.png" alt="Fusion dashboard: Triage, Todo, In Progress, In Review, Done kanban columns with active task cards" width="900" /> <img src="https://runfusion.ai/fusion-dashboard.png" alt="Fusion dashboard: Planning, Todo, In Progress, In Review, Done kanban columns with active task cards" width="900" />
</a> </a>
</div> </div>
@@ -32,7 +32,7 @@
## Your entire dev environment. On a single pane of glass. ## Your entire dev environment. On a single pane of glass.
Describe a task in plain language. A triage agent reads your project, understands context, and writes a full `PROMPT.md` spec — steps, file scope, acceptance criteria. Then Fusion plans, reviews, executes, and reviews again, in an isolated git worktree, with a human approval gate wherever you want one. Describe a task in plain language. A planning agent reads your project, understands context, and writes a full `PROMPT.md` plan — steps, file scope, acceptance criteria. Then Fusion plans, reviews, executes, and reviews again, in an isolated git worktree, with a human approval gate wherever you want one.
One board. Controlled from anywhere. Laptop, Mac mini, Linux server, cloud VM, phone — all connected. One board. Controlled from anywhere. Laptop, Mac mini, Linux server, cloud VM, phone — all connected.
@@ -43,7 +43,7 @@ One board. Controlled from anywhere. Laptop, Mac mini, Linux server, cloud VM, p
## The flow ## The flow
``` ```
① Describe ② Triage ③ The board ④ Isolated worktree ① Describe ② Planning ③ The board ④ Isolated worktree
───────────── ───────────── ───────────── ───────────────────── ───────────── ───────────── ───────────── ─────────────────────
"Add dark mode → Agent writes → Plan → Review → → fusion/FN-123 branch "Add dark mode → Agent writes → Plan → Review → → fusion/FN-123 branch
toggle to PROMPT.md Execute → Review concurrent, zero toggle to PROMPT.md Execute → Review concurrent, zero
@@ -65,7 +65,7 @@ Every task shows its plan, its reviews, its diffs, and its file changes in real
| | | | | |
|---|---| |---|---|
| 🧠 **AI specification** | Describe a task in plain language. Triage agents turn it into a `PROMPT.md` spec with steps, file scope, and acceptance criteria. | | 🧠 **AI planning** | Describe a task in plain language. Planning agents turn it into a `PROMPT.md` plan with steps, file scope, and acceptance criteria. |
| 🔁 **Workflow gates** | Plan → Review → Execute → Review on every step. Pre-merge gates block bad code; post-merge gates run informational checks. | | 🔁 **Workflow gates** | Plan → Review → Execute → Review on every step. Pre-merge gates block bad code; post-merge gates run informational checks. |
| 🌳 **Worktree isolation** | Each task runs in its own branch and worktree (`fusion/{task-id}`). Parallel tasks. Zero conflicts. | | 🌳 **Worktree isolation** | Each task runs in its own branch and worktree (`fusion/{task-id}`). Parallel tasks. Zero conflicts. |
| ⚡ **Smart merge** | Passing every gate? Fusion squash-merges and moves on. Opt into manual approval anywhere. | | ⚡ **Smart merge** | Passing every gate? Fusion squash-merges and moves on. Opt into manual approval anywhere. |
@@ -83,7 +83,7 @@ Every task shows its plan, its reviews, its diffs, and its file changes in real
```mermaid ```mermaid
graph TD graph TD
H((You)) -->|rough idea| T["Triage<br/><i>auto-specification</i>"] H((You)) -->|rough idea| T["Planning<br/><i>auto-planning</i>"]
T --> TD["Todo<br/><i>scheduled for execution</i>"] T --> TD["Todo<br/><i>scheduled for execution</i>"]
TD --> IP["In Progress<br/><i>for each step:<br/>plan, review, execute, review</i>"] TD --> IP["In Progress<br/><i>for each step:<br/>plan, review, execute, review</i>"]
@@ -115,7 +115,7 @@ graph TD
style D fill:#1a1a1a,stroke:#8b949e,color:#8b949e style D fill:#1a1a1a,stroke:#8b949e,color:#8b949e
``` ```
Tasks with dependencies are processed sequentially. Independent tasks run in parallel. Optionally require manual approval before tasks move from Triage to Todo (`requirePlanApproval` setting). Tasks with dependencies are processed sequentially. Independent tasks run in parallel. Optionally require manual approval before tasks move from Planning to Todo (`requirePlanApproval` setting).
--- ---
@@ -306,7 +306,7 @@ For Capacitor + PWA workflow, see [MOBILE.md](./MOBILE.md).
## Core features ## Core features
- **AI Specification** — Triage agent generates detailed `PROMPT.md` with steps, file scope, and acceptance criteria - **AI Planning** — Planning agent generates detailed `PROMPT.md` with steps, file scope, and acceptance criteria
- **Step-by-step Execution** — Plan → Review → Execute → Review cycle for each task step - **Step-by-step Execution** — Plan → Review → Execute → Review cycle for each task step
- **Git Worktree Isolation** — Each task runs in its own worktree (`fusion/{task-id}` branch) - **Git Worktree Isolation** — Each task runs in its own worktree (`fusion/{task-id}` branch)
- **Workflow Steps** — Configurable quality gates (pre-merge: blocks merge; post-merge: informational) - **Workflow Steps** — Configurable quality gates (pre-merge: blocks merge; post-merge: informational)
@@ -331,7 +331,7 @@ Fusion uses a dual-scope model hierarchy with five independent lanes. Global set
| Lane | Purpose | Global Baseline Keys | Project Override Keys | | Lane | Purpose | Global Baseline Keys | Project Override Keys |
|------|---------|---------------------|----------------------| |------|---------|---------------------|----------------------|
| Executor | Task execution agent | `executionGlobalProvider` + `executionGlobalModelId` | `executionProvider` + `executionModelId` | | Executor | Task execution agent | `executionGlobalProvider` + `executionGlobalModelId` | `executionProvider` + `executionModelId` |
| Planning/Triage | Task specification agent | `planningGlobalProvider` + `planningGlobalModelId` | `planningProvider` + `planningModelId` | | Planning | Task planning agent | `planningGlobalProvider` + `planningGlobalModelId` | `planningProvider` + `planningModelId` |
| Validator | Plan/code reviewer | `validatorGlobalProvider` + `validatorGlobalModelId` | `validatorProvider` + `validatorModelId` | | Validator | Plan/code reviewer | `validatorGlobalProvider` + `validatorGlobalModelId` | `validatorProvider` + `validatorModelId` |
| Title Summarization | Auto-title generation | `titleSummarizerGlobalProvider` + `titleSummarizerGlobalModelId` | `titleSummarizerProvider` + `titleSummarizerModelId` | | Title Summarization | Auto-title generation | `titleSummarizerGlobalProvider` + `titleSummarizerGlobalModelId` | `titleSummarizerProvider` + `titleSummarizerModelId` |
| Workflow Step Refinement | AI prompt refinement | (uses `defaultProvider`/`defaultModelId`) | (uses `modelProvider`/`modelId` on WorkflowStep) | | Workflow Step Refinement | AI prompt refinement | (uses `defaultProvider`/`defaultModelId`) | (uses `modelProvider`/`modelId` on WorkflowStep) |
@@ -403,7 +403,7 @@ Routines are AI agent tasks triggered by cron schedules, webhooks, or manual exe
## CLI quick examples ## CLI quick examples
```bash ```bash
fn task create "Fix the login bug" # Quick entry → triage fn task create "Fix the login bug" # Quick entry → planning
fn task plan "Build auth system" # AI-guided planning fn task plan "Build auth system" # AI-guided planning
fn task import owner/repo --labels bug # Import GitHub issues fn task import owner/repo --labels bug # Import GitHub issues
fn task show FN-001 # View task details fn task show FN-001 # View task details
@@ -431,7 +431,7 @@ fn skills install firebase/agent-skills # Install agent skills
|---------|-------------| |---------|-------------|
| `@fusion/core` | Domain model — tasks, board columns, SQLite store | | `@fusion/core` | Domain model — tasks, board columns, SQLite store |
| `@fusion/dashboard` | Web UI — Express server + kanban board with SSE | | `@fusion/dashboard` | Web UI — Express server + kanban board with SSE |
| `@fusion/engine` | AI engine — triage, execution, scheduling, workflow steps | | `@fusion/engine` | AI engine — planning, execution, scheduling, workflow steps |
| `@runfusion/fusion` | CLI + pi extension — published to npm | | `@runfusion/fusion` | CLI + pi extension — published to npm |
--- ---

View File

@@ -6,8 +6,8 @@
* - Done: shipped features * - Done: shipped features
* - In Review: finished work waiting for merge * - In Review: finished work waiting for merge
* - In Progress: agents actively executing (with steps partially done) * - In Progress: agents actively executing (with steps partially done)
* - Todo: specified and queued * - Todo: planned and queued
* - Triage: raw ideas just landing * - Planning: raw ideas just landing
*/ */
import { TaskStore } from "../packages/core/src/index.js"; import { TaskStore } from "../packages/core/src/index.js";
import { writeFile } from "node:fs/promises"; import { writeFile } from "node:fs/promises";
@@ -66,7 +66,7 @@ async function main() {
doneIds.push(task.id); doneIds.push(task.id);
await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel }); await store.updateTask(task.id, { size: t.size, reviewLevel: t.reviewLevel });
// Move through the pipeline: triage → todo → in-progress → in-review → done // Move through the pipeline: planning → todo → in-progress → in-review → done
await store.moveTask(task.id, "todo"); await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress"); await store.moveTask(task.id, "in-progress");
@@ -221,9 +221,9 @@ async function main() {
await writePrompt(store, task.id, t.title, t.desc, steps, t.deps); await writePrompt(store, task.id, t.title, t.desc, steps, t.deps);
} }
// ── Triage ──────────────────────────────────────────────────────── // ── Planning ──────────────────────────────────────────────────────
const triage = [ const planning = [
{ {
desc: "Users are reporting slow page loads on the dashboard when they have more than 200 tasks. Probably need virtual scrolling or pagination in the UI.", desc: "Users are reporting slow page loads on the dashboard when they have more than 200 tasks. Probably need virtual scrolling or pagination in the UI.",
}, },
@@ -241,7 +241,7 @@ async function main() {
}, },
]; ];
for (const t of triage) { for (const t of planning) {
await store.createTask({ description: t.desc }); await store.createTask({ description: t.desc });
} }
@@ -252,7 +252,7 @@ async function main() {
} }
console.log(`\nSeeded ${tasks.length} tasks:`); console.log(`\nSeeded ${tasks.length} tasks:`);
console.log(` Triage: ${byColumn["triage"] || 0}`); console.log(` Planning: ${byColumn["triage"] || 0}`);
console.log(` Todo: ${byColumn["todo"] || 0}`); console.log(` Todo: ${byColumn["todo"] || 0}`);
console.log(` In Progress: ${byColumn["in-progress"] || 0}`); console.log(` In Progress: ${byColumn["in-progress"] || 0}`);
console.log(` In Review: ${byColumn["in-review"] || 0}`); console.log(` In Review: ${byColumn["in-review"] || 0}`);
@@ -341,7 +341,7 @@ ${stepsSection}
async function addLogs(store: TaskStore, id: string, targetColumn: string) { async function addLogs(store: TaskStore, id: string, targetColumn: string) {
const actions: Record<string, string[][]> = { const actions: Record<string, string[][]> = {
done: [ done: [
["Triage complete — spec written", "approved"], ["Planning complete — plan written", "approved"],
["Scheduled for execution", "worktree created"], ["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"], ["Step 0 started", "in-progress"],
["Review: step 0", "approved"], ["Review: step 0", "approved"],
@@ -355,7 +355,7 @@ async function addLogs(store: TaskStore, id: string, targetColumn: string) {
["Auto-merged into main"], ["Auto-merged into main"],
], ],
"in-review": [ "in-review": [
["Triage complete — spec written", "approved"], ["Planning complete — plan written", "approved"],
["Scheduled for execution", "worktree created"], ["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"], ["Step 0 started", "in-progress"],
["Review: step 0", "approved"], ["Review: step 0", "approved"],
@@ -368,7 +368,7 @@ async function addLogs(store: TaskStore, id: string, targetColumn: string) {
["All steps complete — moved to review"], ["All steps complete — moved to review"],
], ],
"in-progress": [ "in-progress": [
["Triage complete — spec written", "approved"], ["Planning complete — plan written", "approved"],
["Scheduled for execution", "worktree created"], ["Scheduled for execution", "worktree created"],
["Step 0 started", "in-progress"], ["Step 0 started", "in-progress"],
["Review: step 0", "approved"], ["Review: step 0", "approved"],

View File

@@ -1,6 +1,6 @@
/** /**
* Simulates live board activity — tasks progressing, new ideas landing, * Simulates live board activity — tasks progressing, new ideas landing,
* triage completing, reviews finishing. Run alongside `kb dashboard`. * planning completing, reviews finishing. Run alongside `kb dashboard`.
* *
* Usage: `npx tsx demo/simulate.ts [dir]` * Usage: `npx tsx demo/simulate.ts [dir]`
* *
@@ -44,7 +44,7 @@ async function main() {
while (true) { while (true) {
const tasks = await store.listTasks(); const tasks = await store.listTasks();
const triage = tasks.filter((t) => t.column === "triage" && !t.paused); const planning = tasks.filter((t) => t.column === "triage" && !t.paused);
const inProgress = tasks.filter((t) => t.column === "in-progress" && !t.paused); const inProgress = tasks.filter((t) => t.column === "in-progress" && !t.paused);
const inReview = tasks.filter((t) => t.column === "in-review" && !t.paused); const inReview = tasks.filter((t) => t.column === "in-review" && !t.paused);
const todo = tasks.filter((t) => t.column === "todo" && !t.paused); const todo = tasks.filter((t) => t.column === "todo" && !t.paused);
@@ -53,23 +53,23 @@ async function main() {
const roll = Math.random(); const roll = Math.random();
if (roll < 0.2 && ideaIndex < NEW_TASK_IDEAS.length) { if (roll < 0.2 && ideaIndex < NEW_TASK_IDEAS.length) {
// New task lands in triage // New task lands in planning
const desc = NEW_TASK_IDEAS[ideaIndex++]; const desc = NEW_TASK_IDEAS[ideaIndex++];
const task = await store.createTask({ description: desc }); const task = await store.createTask({ description: desc });
console.log(` + New task: ${task.id} — "${desc.slice(0, 50)}..."`); console.log(` + New task: ${task.id} — "${desc.slice(0, 50)}..."`);
await sleep(2000 + Math.random() * 3000); await sleep(2000 + Math.random() * 3000);
} else if (roll < 0.4 && triage.length > 0) { } else if (roll < 0.4 && planning.length > 0) {
// Triage completes — task gets spec'd and moves to todo // Planning completes — task gets planned and moves to todo
const task = pick(triage); const task = pick(planning);
const title = task.description.slice(0, 60).replace(/\.$/, ""); const title = task.description.slice(0, 60).replace(/\.$/, "");
await store.updateTask(task.id, { await store.updateTask(task.id, {
title, title,
size: pick(["S", "M", "L"] as const), size: pick(["S", "M", "L"] as const),
reviewLevel: pick([0, 1, 1, 2, 2, 3]), reviewLevel: pick([0, 1, 1, 2, 2, 3]),
}); });
await store.logEntry(task.id, "Triage complete — spec written", "approved"); await store.logEntry(task.id, "Planning complete — plan written", "approved");
await store.moveTask(task.id, "todo"); await store.moveTask(task.id, "todo");
console.log(`Triaged: ${task.id} → todo`); console.log(`Planned: ${task.id} → todo`);
await sleep(3000 + Math.random() * 4000); await sleep(3000 + Math.random() * 4000);
} else if (roll < 0.6 && todo.length > 0 && inProgress.length < 3) { } else if (roll < 0.6 && todo.length > 0 && inProgress.length < 3) {
// Scheduler picks up a todo task // Scheduler picks up a todo task

View File

@@ -2,7 +2,7 @@
[← Back to repository root](../README.md) [← Back to repository root](../README.md)
Fusion is an AI-orchestrated task board that turns ideas into reviewed, merged code using a structured workflow: **triage → todo → in-progress → in-review → done**. Fusion is an AI-orchestrated task board that turns ideas into reviewed, merged code using a structured workflow: **planning → todo → in-progress → in-review → done**.
![Fusion Dashboard Overview](screenshots/dashboard-overview.png) ![Fusion Dashboard Overview](screenshots/dashboard-overview.png)

View File

@@ -151,10 +151,10 @@ Recommended priority: first add ownership/inbox primitives (checkout + inbox-lit
### 12) Approval workflows ### 12) Approval workflows
**Status:** PARTIAL **Status:** PARTIAL
**Description:** Approval entities linked to issues, reviewed first on wake. **Description:** Approval entities linked to issues, reviewed first on wake.
**Current State:** Triaged specs can require manual approval (`awaiting-approval` + approve/reject routes). **Current State:** Planned tasks can require manual approval (`awaiting-approval` + approve/reject routes).
**Gap:** No standalone approval objects with linked issue resolution metadata. **Gap:** No standalone approval objects with linked issue resolution metadata.
**Recommendation:** Add optional approval domain only if needed beyond existing triage-plan approval. **Recommendation:** Add optional approval domain only if needed beyond existing planning approval.
**Files Affected:** `packages/engine/src/triage.ts`, `packages/dashboard/src/routes.ts`, `packages/core/src/types.ts` **Files Affected:** `packages/engine/src/planning.ts`, `packages/dashboard/src/routes.ts`, `packages/core/src/types.ts`
### 13) Self-assignment constraints ### 13) Self-assignment constraints
**Status:** MISSING **Status:** MISSING
@@ -167,7 +167,7 @@ Recommended priority: first add ownership/inbox primitives (checkout + inbox-lit
### 14) Status value parity ### 14) Status value parity
**Status:** PARTIAL **Status:** PARTIAL
**Description:** Paperclip issue statuses differ from Fusions board columns/status fields. **Description:** Paperclip issue statuses differ from Fusions board columns/status fields.
**Current State:** Fusion has columns (`triage`, `todo`, `in-progress`, `in-review`, `done`, `archived`) and ad-hoc task status strings. **Current State:** Fusion has columns (`planning`, `todo`, `in-progress`, `in-review`, `done`, `archived`) and ad-hoc task status strings.
**Gap:** No direct `blocked/cancelled/backlog` canonical lifecycle. **Gap:** No direct `blocked/cancelled/backlog` canonical lifecycle.
**Recommendation:** Decide whether to add a normalized execution status enum alongside board columns. **Recommendation:** Decide whether to add a normalized execution status enum alongside board columns.
**Files Affected:** `packages/core/src/types.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/scheduler.ts` **Files Affected:** `packages/core/src/types.ts`, `packages/core/src/store.ts`, `packages/dashboard/src/routes.ts`, `packages/engine/src/scheduler.ts`

View File

@@ -2,7 +2,7 @@
[← Docs index](./README.md) [← Docs index](./README.md)
Fusion uses multiple agent roles for triage, execution, review, and merge workflows. Fusion uses multiple agent roles for planning, execution, review, and merge workflows.
## Agent Field Parity Matrix ## Agent Field Parity Matrix
@@ -64,7 +64,7 @@ The `taskId` field is suppressed in API responses when the linked task is in a t
- `GET /api/agents/stats``assignedTaskCount` excludes agents with terminal linked tasks - `GET /api/agents/stats``assignedTaskCount` excludes agents with terminal linked tasks
**Non-terminal task statuses (taskId is preserved):** **Non-terminal task statuses (taskId is preserved):**
- `triage` - `planning`
- `todo` - `todo`
- `in-progress` - `in-progress`
- `in-review` - `in-review`
@@ -126,12 +126,12 @@ Agent deletion is available from both the detail header lifecycle controls and t
Fusion includes built-in templates for role prompts: Fusion includes built-in templates for role prompts:
- `default-executor` - `default-executor`
- `default-triage` - `default-planning`
- `default-reviewer` - `default-reviewer`
- `default-merger` - `default-merger`
- `senior-engineer` - `senior-engineer`
- `strict-reviewer` - `strict-reviewer`
- `concise-triage` - `concise-planning`
These can be assigned per role using `agentPrompts.roleAssignments`. These can be assigned per role using `agentPrompts.roleAssignments`.
@@ -235,7 +235,7 @@ agent-presets/
├── designer/soul.md ├── designer/soul.md
├── marketing-manager/soul.md ├── marketing-manager/soul.md
├── technical-writer/soul.md ├── technical-writer/soul.md
├── triage/soul.md ├── planning/soul.md
└── reviewer/soul.md └── reviewer/soul.md
``` ```
@@ -302,11 +302,11 @@ The **Prompts** section in the Settings modal provides a user-friendly interface
| `executor-guardrails` | executor | Behavioral guardrails and constraints | | `executor-guardrails` | executor | Behavioral guardrails and constraints |
| `executor-spawning` | executor | Instructions for spawning child agents | | `executor-spawning` | executor | Instructions for spawning child agents |
| `executor-completion` | executor | Completion criteria and signaling | | `executor-completion` | executor | Completion criteria and signaling |
| `triage-welcome` | triage | Introductory section for the triage/specification agent | | `triage-welcome` | planning | Introductory section for the planning agent |
| `triage-context` | triage | Context-gathering instructions | | `triage-context` | planning | Context-gathering instructions |
| `reviewer-verdict` | reviewer | Verdict criteria and format | | `reviewer-verdict` | reviewer | Verdict criteria and format |
| `merger-conflicts` | merger | Merge conflict resolution instructions | | `merger-conflicts` | merger | Merge conflict resolution instructions |
| `agent-generation-system` | — | System prompt for AI-assisted agent specification generation | | `agent-generation-system` | — | System prompt for AI-assisted agent plan generation |
| `workflow-step-refine` | — | System prompt for refining workflow step descriptions | | `workflow-step-refine` | — | System prompt for refining workflow step descriptions |
### How It Works ### How It Works
@@ -433,7 +433,7 @@ Heartbeat runs from the Agents panel run on a **separate control-plane lane** th
| Component | Path | Concurrency | | Component | Path | Concurrency |
|-----------|------|------------| |-----------|------|------------|
| TriageProcessor | Task lane | Semaphore-gated | | PlanningProcessor | Task lane | Semaphore-gated |
| TaskExecutor | Task lane | Semaphore-gated | | TaskExecutor | Task lane | Semaphore-gated |
| Scheduler | Task lane | Semaphore-gated | | Scheduler | Task lane | Semaphore-gated |
| onMerge | Task lane | Semaphore-gated | | onMerge | Task lane | Semaphore-gated |

View File

@@ -8,7 +8,7 @@ This document describes the actual architecture of Fusion as implemented in this
## 1) Overview ## 1) Overview
Fusion is an AI-orchestrated task board. It takes tasks through a structured lifecycle (`triage → todo → in-progress → in-review → done → archived`) and automates triage, execution, review, merge, and operational recovery. Fusion is an AI-orchestrated task board. It takes tasks through a structured lifecycle (`planning → todo → in-progress → in-review → done → archived`) and automates planning, execution, review, merge, and operational recovery.
At a high level, Fusion is split into: At a high level, Fusion is split into:
- **Core domain + persistence** (`@fusion/core`) - **Core domain + persistence** (`@fusion/core`)
@@ -37,7 +37,7 @@ At a high level, Fusion is split into:
│ │ │ │
┌────────▼───────────────────────▼───────┐ ┌────────▼───────────────────────▼───────┐
│ Engine Runtime │ │ Engine Runtime │
│ Scheduler / Triage / Executor / Merger │ │ Scheduler / Planning / Executor / Merger │
│ Heartbeat / Self-healing / Autopilot │ │ Heartbeat / Self-healing / Autopilot │
└────────┬───────────────────────┬────────┘ └────────┬───────────────────────┬────────┘
│ │ │ │
@@ -61,7 +61,7 @@ At a high level, Fusion is split into:
| Package | Published | Role | Key files | | Package | Published | Role | Key files |
|---|---|---|---| |---|---|---|---|
| `@fusion/core` | Private | Domain model, stores, SQLite adapters, settings, shared types | `packages/core/src/types.ts`, `store.ts`, `db.ts`, `central-core.ts`, `agent-store.ts` | | `@fusion/core` | Private | Domain model, stores, SQLite adapters, settings, shared types | `packages/core/src/types.ts`, `store.ts`, `db.ts`, `central-core.ts`, `agent-store.ts` |
| `@fusion/engine` | Private | AI orchestration runtime (triage, scheduler, executor, merger, recovery) | `packages/engine/src/triage.ts`, `scheduler.ts`, `executor.ts`, `merger.ts`, `project-runtime.ts` | | `@fusion/engine` | Private | AI orchestration runtime (planning, scheduler, executor, merger, recovery) | planning processor, `scheduler.ts`, `executor.ts`, `merger.ts`, `project-runtime.ts` |
| `@fusion/dashboard` | Private | Express API server + React app | `packages/dashboard/src/server.ts`, `routes.ts`, `sse.ts`, `websocket.ts`, `packages/dashboard/app/App.tsx` | | `@fusion/dashboard` | Private | Express API server + React app | `packages/dashboard/src/server.ts`, `routes.ts`, `sse.ts`, `websocket.ts`, `packages/dashboard/app/App.tsx` |
| `@runfusion/fusion` | **Published** | CLI binary (`fn`) + Pi extension | `packages/cli/src/bin.ts`, `commands/*`, `project-resolver.ts`, `extension.ts` | | `@runfusion/fusion` | **Published** | CLI binary (`fn`) + Pi extension | `packages/cli/src/bin.ts`, `commands/*`, `project-resolver.ts`, `extension.ts` |
| `@fusion/desktop` | Private | Electron shell around Fusion dashboard/client | `packages/desktop/src/main.ts`, `ipc.ts`, `preload.ts`, `scripts/build.ts` | | `@fusion/desktop` | Private | Electron shell around Fusion dashboard/client | `packages/desktop/src/main.ts`, `ipc.ts`, `preload.ts`, `scripts/build.ts` |
@@ -284,7 +284,7 @@ The QMD backend (`qmd`) delegates read/write I/O to the file backend and schedul
**Dashboard API:** **Dashboard API:**
- `GET /api/memory/backend` — Returns current backend status and capabilities - `GET /api/memory/backend` — Returns current backend status and capabilities
See [Memory Plugin Contract](./memory-plugin-contract.md) for the full specification. See [Memory Plugin Contract](./memory-plugin-contract.md) for the full plan.
--- ---
@@ -293,7 +293,7 @@ See [Memory Plugin Contract](./memory-plugin-contract.md) for the full specifica
`@fusion/engine` executes the autonomous workflow. `@fusion/engine` executes the autonomous workflow.
### Agent roles ### Agent roles
- **Triage**: `TriageProcessor` (`triage.ts`) generates task specs (`PROMPT.md`) and selects eligible triage tasks by priority first, then FIFO (`createdAt` ascending) within each priority tier. - **Planning**: the planning processor generates task plans (`PROMPT.md`) and selects eligible planning tasks by priority first, then FIFO (`createdAt` ascending) within each priority tier.
- **Executor**: `TaskExecutor` (`executor.ts`) implements tasks in worktrees - **Executor**: `TaskExecutor` (`executor.ts`) implements tasks in worktrees
- **Reviewer**: `reviewStep()` (`reviewer.ts`) performs plan/code reviews - **Reviewer**: `reviewStep()` (`reviewer.ts`) performs plan/code reviews
- **Merger**: `aiMergeTask()` (`merger.ts`) merges approved work - **Merger**: `aiMergeTask()` (`merger.ts`) merges approved work
@@ -596,14 +596,14 @@ Some data remains intentionally filesystem-based:
## 9) Task Lifecycle ## 9) Task Lifecycle
Lifecycle constants are defined in `packages/core/src/types.ts`: Lifecycle constants are defined in `packages/core/src/types.ts`:
- Columns: `triage`, `todo`, `in-progress`, `in-review`, `done`, `archived` - Columns: `planning`, `todo`, `in-progress`, `in-review`, `done`, `archived`
- Transition rules via `VALID_TRANSITIONS` - Transition rules via `VALID_TRANSITIONS`
### Lifecycle flow ### Lifecycle flow
```text ```text
triage planning
│ (TriageProcessor writes PROMPT.md) │ (Planning processor writes PROMPT.md)
todo todo
│ (Scheduler selects task, dependencies satisfied) │ (Scheduler selects task, dependencies satisfied)
@@ -620,7 +620,7 @@ done
``` ```
### Execution detail ### Execution detail
- **Triage phase**: `TriageProcessor` generates executable spec - **Planning phase**: the planning processor generates an executable plan
- **Execution phase**: `TaskExecutor` performs implementation, tool calls, tests/build commands - **Execution phase**: `TaskExecutor` performs implementation, tool calls, tests/build commands
- **Review phase**: optional `reviewStep()` workflow depending on prompt review level (bypassed in fast mode) - **Review phase**: optional `reviewStep()` workflow depending on prompt review level (bypassed in fast mode)
- **Merge phase**: `aiMergeTask()` handles merge strategy and post-merge workflow steps - **Merge phase**: `aiMergeTask()` handles merge strategy and post-merge workflow steps
@@ -641,7 +641,7 @@ Task steps use statuses: `pending`, `in-progress`, `done`, `skipped`.
Fusion has two complementary agent models: Fusion has two complementary agent models:
1. **Task pipeline agents** (triage/executor/reviewer/merger) managed by engine runtime 1. **Task pipeline agents** (planning/executor/reviewer/merger) managed by engine runtime
2. **Persistent registered agents** managed by `AgentStore` 2. **Persistent registered agents** managed by `AgentStore`
### Persistent agent storage ### Persistent agent storage

View File

@@ -71,7 +71,7 @@ fn dashboard --dev
| `--no-auth` | Disable bearer-token auth. Not recommended when binding to `0.0.0.0`. | | `--no-auth` | Disable bearer-token auth. Not recommended when binding to `0.0.0.0`. |
| `--paused` | Start with the engine paused (automation disabled). | | `--paused` | Start with the engine paused (automation disabled). |
| `--interactive` | Interactive port selection. | | `--interactive` | Interactive port selection. |
| `--dev` | Start dashboard only (no AI engine, no triage/scheduler). | | `--dev` | Start dashboard only (no AI engine, no planning/scheduler). |
### Interactive Terminal UI (TTY Mode) ### Interactive Terminal UI (TTY Mode)

View File

@@ -31,7 +31,7 @@ pnpm build
|---|---| |---|---|
| `@fusion/core` | Shared domain types, stores, persistence, and core utilities | | `@fusion/core` | Shared domain types, stores, persistence, and core utilities |
| `@fusion/dashboard` | Express API + React UI (including dashboard TUI in CLI) | | `@fusion/dashboard` | Express API + React UI (including dashboard TUI in CLI) |
| `@fusion/engine` | Scheduling, triage, execution, merge orchestration | | `@fusion/engine` | Scheduling, planning, execution, merge orchestration |
| `@fusion/desktop` | Electron shell around Fusion dashboard/client | | `@fusion/desktop` | Electron shell around Fusion dashboard/client |
| `@fusion/mobile` | Capacitor + PWA mobile packaging | | `@fusion/mobile` | Capacitor + PWA mobile packaging |
| `@fusion/plugin-sdk` | Plugin SDK for building Fusion extensions | | `@fusion/plugin-sdk` | Plugin SDK for building Fusion extensions |

View File

@@ -37,7 +37,7 @@ Method used:
| dashboard | 8 | `mission-routes.ts`, `script-store.ts`, `subtask-breakdown.ts`, `terminal.ts`, `plugins/{network,splash-screen,status-bar}.ts`, `test-request.ts` | | dashboard | 8 | `mission-routes.ts`, `script-store.ts`, `subtask-breakdown.ts`, `terminal.ts`, `plugins/{network,splash-screen,status-bar}.ts`, `test-request.ts` |
| cli | 4 | `commands/settings-export.ts`, `commands/settings-import.ts`, `companies-sh-parser.ts`, `runtime/native-patch.ts` | | cli | 4 | `commands/settings-export.ts`, `commands/settings-import.ts`, `companies-sh-parser.ts`, `runtime/native-patch.ts` |
### Finding 2.2 — Risk triage for key backend files ### Finding 2.2 — Risk review for key backend files
- **High** - **High**
- `packages/dashboard/src/subtask-breakdown.ts` — AI/session orchestration and persistence interactions; only route-level behavior is exercised. - `packages/dashboard/src/subtask-breakdown.ts` — AI/session orchestration and persistence interactions; only route-level behavior is exercised.

View File

@@ -191,7 +191,7 @@ You can create tasks from the board or CLI.
1. Type a short request in the quick entry input. 1. Type a short request in the quick entry input.
2. Press Enter. 2. Press Enter.
3. Task appears in **Triage** and the triage agent generates `PROMPT.md`. 3. Task appears in **Planning** and the planning agent generates `PROMPT.md`.
### Option B: Plan Mode (Board) ### Option B: Plan Mode (Board)
@@ -231,8 +231,8 @@ fn task plan "Implement role-based access control"
Fusion uses six columns: Fusion uses six columns:
1. **Triage** — raw idea; AI writes spec 1. **Planning** — raw idea; AI writes plan
2. **Todo**specified and queued 2. **Todo**planned and queued
3. **In Progress** — executor implements in a dedicated worktree 3. **In Progress** — executor implements in a dedicated worktree
4. **In Review** — implementation complete, awaiting merge/finalization 4. **In Review** — implementation complete, awaiting merge/finalization
5. **Done** — merged and complete 5. **Done** — merged and complete

View File

@@ -55,7 +55,7 @@ Fusion currently has two related but distinct memory systems:
| `readProjectMemoryWithBackend()` | Backend-aware read helper | | `readProjectMemoryWithBackend()` | Backend-aware read helper |
| `searchProjectMemory()`, `getProjectMemory()` | Backend-aware search/get wrappers | | `searchProjectMemory()`, `getProjectMemory()` | Backend-aware search/get wrappers |
| `resolveMemoryInstructionContext()` | Backend-aware instruction context | | `resolveMemoryInstructionContext()` | Backend-aware instruction context |
| `buildTriageMemoryInstructions()` | Triage prompt memory instructions | | Planning instruction builder | Planning prompt memory instructions |
| `buildExecutionMemoryInstructions()` | Executor prompt memory instructions | | `buildExecutionMemoryInstructions()` | Executor prompt memory instructions |
| `buildReviewerMemoryInstructions()` | Reviewer prompt memory instructions | | `buildReviewerMemoryInstructions()` | Reviewer prompt memory instructions |
| `readProjectMemory()` | Direct canonical long-term file read | | `readProjectMemory()` | Direct canonical long-term file read |
@@ -319,10 +319,10 @@ Contract-critical behavior is covered by:
- `packages/core/src/project-memory.test.ts` - `packages/core/src/project-memory.test.ts`
- canonical long-term path bootstrap/read behavior - canonical long-term path bootstrap/read behavior
- `resolveMemoryInstructionContext()` branching - `resolveMemoryInstructionContext()` branching
- triage/execution/reviewer instruction generation per backend - planning/execution/reviewer instruction generation per backend
- `packages/core/src/store.test.ts` - `packages/core/src/store.test.ts`
- memory bootstrap behavior when memory is enabled/disabled and toggled - memory bootstrap behavior when memory is enabled/disabled and toggled
- `packages/engine/src/triage.test.ts` and `packages/engine/src/executor.test.ts` - Engine planning and executor tests
- memory instruction injection behavior by settings/backend - memory instruction injection behavior by settings/backend
--- ---

View File

@@ -53,7 +53,7 @@ The dashboard supports mission planning workflows where you can:
When missions are created through the interview planning workflow, Fusion automatically generates contract assertions for each feature: When missions are created through the interview planning workflow, Fusion automatically generates contract assertions for each feature:
- **Assertion text source priority**: `acceptanceCriteria``feature.description` → fallback text (`"Verify implementation of: {feature.title}"`) - **Assertion text source priority**: `acceptanceCriteria``feature.description` → fallback text (`"Verify implementation of: {feature.title}"`)
- **Assertions are linked to features**: Each auto-generated assertion is automatically linked to its feature, enabling mission validation rollup and enriched triage context - **Assertions are linked to features**: Each auto-generated assertion is automatically linked to its feature, enabling mission validation rollup and enriched planning context
- **Verification fields**: Milestone and slice verification criteria from the interview are stored in dedicated `verification` fields rather than concatenated into descriptions - **Verification fields**: Milestone and slice verification criteria from the interview are stored in dedicated `verification` fields rather than concatenated into descriptions
- **Partial plans handled**: Auto-generation is robust to partial plans (missing slices/features or empty criteria) without throwing errors - **Partial plans handled**: Auto-generation is robust to partial plans (missing slices/features or empty criteria) without throwing errors
@@ -87,13 +87,13 @@ Typical flow:
## `autopilotEnabled` vs `autoAdvance` ## `autopilotEnabled` vs `autoAdvance`
- **`autopilotEnabled`**: primary control for autopilot behavior — enables background monitoring, orchestration, and automatic slice activation when a slice completes. Also triggers auto-triage (converting features to tasks) when a slice is activated. - **`autopilotEnabled`**: primary control for autopilot behavior — enables background monitoring, orchestration, and automatic slice activation when a slice completes. Also triggers auto-planning (converting features to tasks) when a slice is activated.
- **`autoAdvance`**: legacy fallback for backward compatibility with existing mission data. Kept for compatibility — new missions should use `autopilotEnabled`. - **`autoAdvance`**: legacy fallback for backward compatibility with existing mission data. Kept for compatibility — new missions should use `autopilotEnabled`.
**Auto-triage behavior:** **Auto-planning behavior:**
- `autopilotEnabled=true` → features in activated slices are automatically triaged (converted to tasks) - `autopilotEnabled=true` → features in activated slices are automatically planned (converted to tasks)
- `autopilotEnabled=false`, `autoAdvance=true` → features are triaged (legacy compat) - `autopilotEnabled=false`, `autoAdvance=true` → features are planned (legacy compat)
- `autopilotEnabled=false`, `autoAdvance=false` → manual slice activation only - `autopilotEnabled=false`, `autoAdvance=false` → manual slice activation only
**Slice progression (on slice completion):** **Slice progression (on slice completion):**
@@ -144,7 +144,7 @@ interface MissionContractAssertion {
id: string; // e.g., "CA-A3B7CD-E9F2" id: string; // e.g., "CA-A3B7CD-E9F2"
milestoneId: string; // Parent milestone milestoneId: string; // Parent milestone
title: string; // Human-readable title title: string; // Human-readable title
assertion: string; // Behavioral specification assertion: string; // Behavioral plan
status: AssertionStatus; // pending | passed | failed | blocked status: AssertionStatus; // pending | passed | failed | blocked
orderIndex: number; // Sort order within milestone orderIndex: number; // Sort order within milestone
featureIds: string[]; // Linked features (many-to-many) featureIds: string[]; // Linked features (many-to-many)
@@ -246,7 +246,7 @@ interface MissionFixFeatureLineage {
} }
``` ```
The fix feature is **auto-triaged** (converted to tasks) for immediate execution. Each fix increments `implementationAttemptCount`. The fix feature is **auto-planned** (converted to tasks) for immediate execution. Each fix increments `implementationAttemptCount`.
**Default retry budget:** 3 (`DEFAULT_IMPLEMENTATION_RETRY_BUDGET`). When `implementationAttemptCount >= maxRetryBudget`, the feature transitions to `blocked`. **Default retry budget:** 3 (`DEFAULT_IMPLEMENTATION_RETRY_BUDGET`). When `implementationAttemptCount >= maxRetryBudget`, the feature transitions to `blocked`.
@@ -291,7 +291,7 @@ These are independent tracking mechanisms — autopilot monitors mission progres
- `inProgressCount`, `passedCount`, `failedCount`, `blockedCount` - `inProgressCount`, `passedCount`, `failedCount`, `blockedCount`
**MissionEvent audit types:** **MissionEvent audit types:**
- `slice_activated`, `feature_triaged`, `feature_completed` - `slice_activated`, `feature_planned`, `feature_completed`
- `validation:started`, `validation:passed`, `validation:failed`, `validation:blocked` - `validation:started`, `validation:passed`, `validation:failed`, `validation:blocked`
- `fix_feature:created`, `feature:blocked` - `fix_feature:created`, `feature:blocked`
@@ -320,7 +320,7 @@ interface MissionAssertionFailureRecord {
| Symptom | Diagnosis | Resolution | | Symptom | Diagnosis | Resolution |
|---------|-----------|------------| |---------|-----------|------------|
| Feature stuck in "validating" | `activeValidations` set may be stale; engine restart needed | Check logs for validator errors; restart engine to trigger `recoverActiveMissions()` | | Feature stuck in "validating" | `activeValidations` set may be stale; engine restart needed | Check logs for validator errors; restart engine to trigger `recoverActiveMissions()` |
| Fix feature not auto-triaging | `triageFeature()` may have errored; check logs | Manual triage via `fn mission triage-feature <id>`; investigate `triageFeature()` errors | | Fix feature not auto-planning | `planFeature()` may have errored; check logs | Manual planning via `fn mission plan-feature <id>`; investigate `planFeature()` errors |
| Budget exhaustion loop | `implementationAttemptCount >= maxRetryBudget` (default: 3) | Increase `maxRetryBudget` in mission settings or fix root cause | | Budget exhaustion loop | `implementationAttemptCount >= maxRetryBudget` (default: 3) | Increase `maxRetryBudget` in mission settings or fix root cause |
| Blocked mission not advancing | `MilestoneValidationRollup.state` shows `blocked` | Identify blocked assertions; operator must resolve root cause | | Blocked mission not advancing | `MilestoneValidationRollup.state` shows `blocked` | Identify blocked assertions; operator must resolve root cause |
| Validation agent errors | AI session creation failed or `VALIDATION_TIMEOUT_MS` (10 min) exceeded | Check model configuration and logs; verify AI provider auth | | Validation agent errors | AI session creation failed or `VALIDATION_TIMEOUT_MS` (10 min) exceeded | Check model configuration and logs; verify AI provider auth |

View File

@@ -51,8 +51,8 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`.
| `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. | | `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. |
| `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. | | `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. |
| `executionGlobalModelId` | `string` | `undefined` | Global baseline model ID for task execution. | | `executionGlobalModelId` | `string` | `undefined` | Global baseline model ID for task execution. |
| `planningGlobalProvider` | `string` | `undefined` | Global baseline provider for planning/triage. Project `planningProvider` overrides this. | | `planningGlobalProvider` | `string` | `undefined` | Global baseline provider for planning. Project `planningProvider` overrides this. |
| `planningGlobalModelId` | `string` | `undefined` | Global baseline model ID for planning/triage. | | `planningGlobalModelId` | `string` | `undefined` | Global baseline model ID for planning. |
| `validatorGlobalProvider` | `string` | `undefined` | Global baseline provider for validator/reviewer runs. Project `validatorProvider` overrides this. | | `validatorGlobalProvider` | `string` | `undefined` | Global baseline provider for validator/reviewer runs. Project `validatorProvider` overrides this. |
| `validatorGlobalModelId` | `string` | `undefined` | Global baseline model ID for validator/reviewer runs. | | `validatorGlobalModelId` | `string` | `undefined` | Global baseline model ID for validator/reviewer runs. |
| `titleSummarizerGlobalProvider` | `string` | `undefined` | Global baseline provider for title summarization. Project `titleSummarizerProvider` overrides this. | | `titleSummarizerGlobalProvider` | `string` | `undefined` | Global baseline provider for title summarization. Project `titleSummarizerProvider` overrides this. |
@@ -78,8 +78,8 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `globalPause` | `boolean` | `false` | Hard stop: terminate active engine sessions and pause scheduling immediately. | | `globalPause` | `boolean` | `false` | Hard stop: terminate active engine sessions and pause scheduling immediately. |
| `globalPauseReason` | `string` | `undefined` | Optional reason for `globalPause` (`"rate-limit"` for automatic pauses, `"manual"` for user-triggered pauses). Cleared on unpause. | | `globalPauseReason` | `string` | `undefined` | Optional reason for `globalPause` (`"rate-limit"` for automatic pauses, `"manual"` for user-triggered pauses). Cleared on unpause. |
| `enginePaused` | `boolean` | `false` | Soft pause: stop dispatching new work while letting active sessions finish. | | `enginePaused` | `boolean` | `false` | Soft pause: stop dispatching new work while letting active sessions finish. |
| `maxConcurrent` | `number` | `2` | Max concurrent task-lane AI agents (triage, executor, merge). | | `maxConcurrent` | `number` | `2` | Max concurrent task-lane AI agents (planning, executor, merge). |
| `maxTriageConcurrent` | `number` | `2` | Max concurrent triage/specification agents. | | `maxTriageConcurrent` | `number` | `2` | Max concurrent planning agents. |
| `globalMaxConcurrent` | `number` | `4` | System-wide max concurrent agents across all projects. | | `globalMaxConcurrent` | `number` | `4` | System-wide max concurrent agents across all projects. |
| `maxWorktrees` | `number` | `4` | Max git worktrees. | | `maxWorktrees` | `number` | `4` | Max git worktrees. |
| `pollIntervalMs` | `number` | `15000` | Scheduler poll interval (ms). | | `pollIntervalMs` | `number` | `15000` | Scheduler poll interval (ms). |
@@ -100,10 +100,10 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `commitAuthorEnabled` | `boolean` | `true` | Apply explicit `--author` attribution on Fusion commits. | | `commitAuthorEnabled` | `boolean` | `true` | Apply explicit `--author` attribution on Fusion commits. |
| `commitAuthorName` | `string` | `"Fusion"` | Commit author name when `commitAuthorEnabled` is true. | | `commitAuthorName` | `string` | `"Fusion"` | Commit author name when `commitAuthorEnabled` is true. |
| `commitAuthorEmail` | `string` | `"noreply@runfusion.ai"` | Commit author email when `commitAuthorEnabled` is true. | | `commitAuthorEmail` | `string` | `"noreply@runfusion.ai"` | Commit author email when `commitAuthorEnabled` is true. |
| `planningProvider` | `string` | `undefined` | Provider for planning/triage agents. | | `planningProvider` | `string` | `undefined` | Provider for planning agents. |
| `planningModelId` | `string` | `undefined` | Model ID for planning/triage agents. | | `planningModelId` | `string` | `undefined` | Model ID for planning agents. |
| `planningFallbackProvider` | `string` | `undefined` | Fallback provider for planning/triage. | | `planningFallbackProvider` | `string` | `undefined` | Fallback provider for planning. |
| `planningFallbackModelId` | `string` | `undefined` | Fallback model ID for planning/triage. | | `planningFallbackModelId` | `string` | `undefined` | Fallback model ID for planning. |
| `defaultProviderOverride` | `string` | `undefined` | Project-level override for global default provider baseline. | | `defaultProviderOverride` | `string` | `undefined` | Project-level override for global default provider baseline. |
| `defaultModelIdOverride` | `string` | `undefined` | Project-level override for global default model baseline. | | `defaultModelIdOverride` | `string` | `undefined` | Project-level override for global default model baseline. |
| `executionProvider` | `string` | `undefined` | Provider for task execution agents. | | `executionProvider` | `string` | `undefined` | Provider for task execution agents. |
@@ -121,8 +121,8 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. | | `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
| `verificationFixRetries` | `number` | `3` | Auto-fix retry attempts when verification fails during merge. | | `verificationFixRetries` | `number` | `3` | Auto-fix retry attempts when verification fails during merge. |
| `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). | | `buildTimeoutMs` | `number` | `300000` | Build timeout in milliseconds (5 minutes). |
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before triage → todo. | | `requirePlanApproval` | `boolean` | `false` | Require manual approval before planning → todo. |
| `specStalenessEnabled` | `boolean` | `false` | Enforce automatic re-triage for stale specs. | | `specStalenessEnabled` | `boolean` | `false` | Enforce automatic re-planning for stale plans. |
| `specStalenessMaxAgeMs` | `number` | `21600000` | Spec staleness threshold in ms (6 hours). | | `specStalenessMaxAgeMs` | `number` | `21600000` | Spec staleness threshold in ms (6 hours). |
| `taskStuckTimeoutMs` | `number` | `undefined` | Inactivity timeout for stuck-task recovery. | | `taskStuckTimeoutMs` | `number` | `undefined` | Inactivity timeout for stuck-task recovery. |
| `aiSessionTtlMs` | `number` | `604800000` | TTL in ms for persisted planning/subtask/mission sessions (7 days). | | `aiSessionTtlMs` | `number` | `604800000` | TTL in ms for persisted planning/subtask/mission sessions (7 days). |
@@ -268,7 +268,7 @@ Short-lived token bounds are enforced server-side:
Fusion uses a dual-scope model settings system with five lanes. Global settings provide baseline defaults, and project settings provide per-project overrides. Fusion uses a dual-scope model settings system with five lanes. Global settings provide baseline defaults, and project settings provide per-project overrides.
### Triage/specification model ### Planning model
1. Per-task `planningModelProvider` + `planningModelId` 1. Per-task `planningModelProvider` + `planningModelId`
2. Project `planningProvider` + `planningModelId` 2. Project `planningProvider` + `planningModelId`
@@ -442,11 +442,11 @@ Fusion supports fine-grained customization of AI agent prompts through the `prom
| `executor-guardrails` | executor | Behavioral guardrails and constraints | | `executor-guardrails` | executor | Behavioral guardrails and constraints |
| `executor-spawning` | executor | Instructions for spawning child agents | | `executor-spawning` | executor | Instructions for spawning child agents |
| `executor-completion` | executor | Completion criteria and signaling | | `executor-completion` | executor | Completion criteria and signaling |
| `triage-welcome` | triage | Introductory section for the triage/specification agent | | `triage-welcome` | planning | Introductory section for the planning agent |
| `triage-context` | triage | Context-gathering instructions | | `triage-context` | planning | Context-gathering instructions |
| `reviewer-verdict` | reviewer | Verdict criteria and format | | `reviewer-verdict` | reviewer | Verdict criteria and format |
| `merger-conflicts` | merger | Merge conflict resolution instructions | | `merger-conflicts` | merger | Merge conflict resolution instructions |
| `agent-generation-system` | — | System prompt for AI-assisted agent specification generation | | `agent-generation-system` | — | System prompt for AI-assisted agent plan generation |
| `workflow-step-refine` | — | System prompt for refining workflow step descriptions into detailed agent prompts | | `workflow-step-refine` | — | System prompt for refining workflow step descriptions into detailed agent prompts |
### How It Works ### How It Works
@@ -485,7 +485,7 @@ To clear all overrides, set `promptOverrides` to `null`:
"promptOverrides": { "promptOverrides": {
"executor-welcome": "Custom executor welcome message for this project...", "executor-welcome": "Custom executor welcome message for this project...",
"executor-guardrails": "## Custom Guardrails\n- Project-specific rules...", "executor-guardrails": "## Custom Guardrails\n- Project-specific rules...",
"triage-welcome": "Custom triage introduction..." "triage-welcome": "Custom planning introduction..."
} }
} }
} }

View File

@@ -79,8 +79,8 @@ API endpoints reviewed:
| `modelOnboardingComplete` | Global | `GET/PUT /api/settings/global` | Onboarding completion flag | | `modelOnboardingComplete` | Global | `GET/PUT /api/settings/global` | Onboarding completion flag |
| `executionGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for task execution | | `executionGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for task execution |
| `executionGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for task execution | | `executionGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for task execution |
| `planningGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for planning/triage | | `planningGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for planning |
| `planningGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for planning/triage | | `planningGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for planning |
| `validatorGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for validator/reviewer | | `validatorGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for validator/reviewer |
| `validatorGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for validator/reviewer | | `validatorGlobalModelId` | Global | `GET/PUT /api/settings/global` | Global baseline AI model ID for validator/reviewer |
| `titleSummarizerGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for title summarization | | `titleSummarizerGlobalProvider` | Global | `GET/PUT /api/settings/global` | Global baseline AI provider for title summarization |

View File

@@ -12,7 +12,7 @@ Use the inline input on board/list view:
- Type description - Type description
- Press Enter - Press Enter
- Task is created in `triage` - Task is created in `planning`
### 2) Plan Mode (AI interview) ### 2) Plan Mode (AI interview)
@@ -55,7 +55,7 @@ fn task create "Bug" --attach screenshot.png --depends FN-002
Fusion task columns: Fusion task columns:
1. **triage** — idea intake; AI writes a full specification 1. **planning** — idea intake; AI writes a full plan
2. **todo** — ready for scheduling 2. **todo** — ready for scheduling
3. **in-progress** — executor active in isolated worktree 3. **in-progress** — executor active in isolated worktree
4. **in-review** — implementation complete; awaiting finalization 4. **in-review** — implementation complete; awaiting finalization
@@ -139,9 +139,9 @@ The task detail modal exposes multiple tabs:
- **Comments** — collaboration thread + steering controls - **Comments** — collaboration thread + steering controls
- **Model** — per-task model overrides and thinking level - **Model** — per-task model overrides and thinking level
## `PROMPT.md` Specification Structure ## `PROMPT.md` Plan Structure
After triage, each task gets a structured `PROMPT.md` with sections like: After planning, each task gets a structured `PROMPT.md` with sections like:
- Mission - Mission
- Dependencies - Dependencies
@@ -163,7 +163,7 @@ Steering comments can be injected mid-run into active executor sessions.
## Refinement Tasks ## Refinement Tasks
`fn task refine <id>` creates a new triage task that depends on the original done/in-review task. `fn task refine <id>` creates a new planning task that depends on the original done/in-review task.
Example: Example:
@@ -175,7 +175,7 @@ Behavior:
- New title format: `Refinement: <source label>` - New title format: `Refinement: <source label>`
- New task depends on source task - New task depends on source task
- Created in `triage` - Created in `planning`
## Archive and Restore ## Archive and Restore
@@ -252,8 +252,8 @@ Review levels control the rigor of the review process for a task:
| Level | Name | Description | | Level | Name | Description |
|-------|------|-------------| |-------|------|-------------|
| 0 | None | No review | | 0 | None | No review |
| 1 | Plan Only | Review only the specification/plan | | 1 | Plan Only | Review only the plan |
| 2 | Plan and Code | Review both the specification and implementation | | 2 | Plan and Code | Review both the plan and implementation |
| 3 | Full | Full review with all checks | | 3 | Full | Full review with all checks |
Review level can be set during task creation (in the New Task dialog under More options) or when editing a task (in the task detail modal). Review level can be set during task creation (in the New Task dialog under More options) or when editing a task (in the task detail modal).

View File

@@ -205,7 +205,7 @@ This audit acknowledges and does not duplicate the following existing backlog it
### 3.3 Color Contrast in Status Badges ### 3.3 Color Contrast in Status Badges
- **Component:** `styles.css` (34 color themes) - **Component:** `styles.css` (34 color themes)
- **Current behavior:** Status badge colors (triage, todo, in-progress, etc.) may have insufficient contrast with background colors in certain themes, especially light themes. - **Current behavior:** Status badge colors (planning, todo, in-progress, etc.) may have insufficient contrast with background colors in certain themes, especially light themes.
- **Recommended fix:** Audit all status badge colors across all 34 themes for WCAG AA compliance (4.5:1 for text). Use darker variants of status colors in light themes. Test with accessibility tools. - **Recommended fix:** Audit all status badge colors across all 34 themes for WCAG AA compliance (4.5:1 for text). Use darker variants of status colors in light themes. Test with accessibility tools.
- **Impact:** Users with visual impairments may struggle to distinguish status badges. - **Impact:** Users with visual impairments may struggle to distinguish status badges.
- **Effort estimate:** M - **Effort estimate:** M

View File

@@ -51,7 +51,7 @@ The **Frontend UX Design** template verifies visual polish and consistency with
## Model Overrides for Prompt Steps ## Model Overrides for Prompt Steps
A prompt-mode workflow step can specify its own model with: A prompt-mode workflow step can set its own model with:
- `modelProvider` - `modelProvider`
- `modelId` - `modelId`

View File

@@ -427,7 +427,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir), makeCtx(tmpDir),
); );
expect(result.content[0].text).toContain("Triage (2)"); expect(result.content[0].text).toContain("Planning (2)");
expect(result.content[0].text).toContain("FN-001"); expect(result.content[0].text).toContain("FN-001");
expect(result.content[0].text).toContain("FN-002"); expect(result.content[0].text).toContain("FN-002");
expect(result.details.count).toBe(2); expect(result.details.count).toBe(2);
@@ -451,7 +451,7 @@ describe("fn pi extension", () => {
undefined, undefined,
makeCtx(tmpDir), makeCtx(tmpDir),
); );
expect(triageResult.content[0].text).toContain("Triage (1)"); expect(triageResult.content[0].text).toContain("Planning (1)");
expect(triageResult.content[0].text).toContain("FN-001"); expect(triageResult.content[0].text).toContain("FN-001");
const todoResult = await listTool.execute( const todoResult = await listTool.execute(
@@ -485,7 +485,7 @@ describe("fn pi extension", () => {
makeCtx(tmpDir), makeCtx(tmpDir),
); );
expect(result.content[0].text).toContain("Triage (5)"); expect(result.content[0].text).toContain("Planning (5)");
expect(result.content[0].text).toContain("FN-001"); expect(result.content[0].text).toContain("FN-001");
expect(result.content[0].text).toContain("FN-002"); expect(result.content[0].text).toContain("FN-002");
expect(result.content[0].text).not.toContain("FN-003"); expect(result.content[0].text).not.toContain("FN-003");
@@ -515,7 +515,7 @@ describe("fn pi extension", () => {
expect(result.content[0].text).toContain("FN-001"); expect(result.content[0].text).toContain("FN-001");
expect(result.content[0].text).toContain("Implement caching layer"); expect(result.content[0].text).toContain("Implement caching layer");
expect(result.content[0].text).toContain("Triage"); expect(result.content[0].text).toContain("Planning");
expect(result.details.task).toBeDefined(); expect(result.details.task).toBeDefined();
expect(result.details.task.id).toBe("FN-001"); expect(result.details.task.id).toBe("FN-001");
}); });

View File

@@ -418,7 +418,7 @@ describe("ListView", () => {
// Check that all column badges are rendered in the table // Check that all column badges are rendered in the table
// Use getAllByText and check length since column names appear in both drop zones and badges // Use getAllByText and check length since column names appear in both drop zones and badges
expect(screen.getAllByText("Triage").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Planning").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Todo").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Todo").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Progress").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("In Progress").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Review").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("In Review").length).toBeGreaterThanOrEqual(1);
@@ -426,7 +426,7 @@ describe("ListView", () => {
// Check that badges have the correct styling by querying within the table // Check that badges have the correct styling by querying within the table
const table = document.querySelector(".list-table"); const table = document.querySelector(".list-table");
expect(table?.textContent).toContain("Triage"); expect(table?.textContent).toContain("Planning");
expect(table?.textContent).toContain("Todo"); expect(table?.textContent).toContain("Todo");
expect(table?.textContent).toContain("In Progress"); expect(table?.textContent).toContain("In Progress");
expect(table?.textContent).toContain("In Review"); expect(table?.textContent).toContain("In Review");
@@ -589,7 +589,7 @@ describe("ListView", () => {
it("renders drop zones for each column", () => { it("renders drop zones for each column", () => {
renderListView(); renderListView();
expect(screen.getByText("Triage")).toBeDefined(); expect(screen.getByText("Planning")).toBeDefined();
expect(screen.getByText("Todo")).toBeDefined(); expect(screen.getByText("Todo")).toBeDefined();
expect(screen.getByText("In Progress")).toBeDefined(); expect(screen.getByText("In Progress")).toBeDefined();
expect(screen.getByText("In Review")).toBeDefined(); expect(screen.getByText("In Review")).toBeDefined();
@@ -719,7 +719,7 @@ describe("ListView", () => {
renderListView({ tasks }); renderListView({ tasks });
// Check that section headers are rendered with column names // Check that section headers are rendered with column names
expect(screen.getAllByText("Triage").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Planning").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Todo").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Todo").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Progress").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("In Progress").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("In Review").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("In Review").length).toBeGreaterThanOrEqual(1);
@@ -740,7 +740,7 @@ describe("ListView", () => {
expect(sectionHeaders.length).toBe(6); // One for each column expect(sectionHeaders.length).toBe(6); // One for each column
// Check that triage section shows count of 2 // Check that triage section shows count of 2
const triageHeader = sectionHeaders.find(h => h.textContent?.includes("Triage")); const triageHeader = sectionHeaders.find(h => h.textContent?.includes("Planning"));
expect(triageHeader?.textContent).toContain("2"); expect(triageHeader?.textContent).toContain("2");
// Check that todo section shows count of 1 // Check that todo section shows count of 1
@@ -796,7 +796,7 @@ describe("ListView", () => {
// Only triage section should be visible (todo section should be hidden) // Only triage section should be visible (todo section should be hidden)
const sectionHeaders = screen.getAllByRole("row").filter(r => r.className.includes("list-section-header")); const sectionHeaders = screen.getAllByRole("row").filter(r => r.className.includes("list-section-header"));
expect(sectionHeaders.length).toBe(1); expect(sectionHeaders.length).toBe(1);
expect(sectionHeaders[0].textContent).toContain("Triage"); expect(sectionHeaders[0].textContent).toContain("Planning");
// Verify the filtered task is visible // Verify the filtered task is visible
expect(screen.getByText("FN-001")).toBeDefined(); expect(screen.getByText("FN-001")).toBeDefined();
@@ -818,7 +818,7 @@ describe("ListView", () => {
// Get only data rows within the triage section // Get only data rows within the triage section
const allRows = screen.getAllByRole("row"); const allRows = screen.getAllByRole("row");
const triageSectionStart = allRows.findIndex(r => r.className.includes("list-section-header") && r.textContent?.includes("Triage")); const triageSectionStart = allRows.findIndex(r => r.className.includes("list-section-header") && r.textContent?.includes("Planning"));
// The next 3 rows after the section header should be the sorted tasks // The next 3 rows after the section header should be the sorted tasks
const dataRows = allRows.slice(triageSectionStart + 1, triageSectionStart + 4).filter(r => r.getAttribute("data-id")); const dataRows = allRows.slice(triageSectionStart + 1, triageSectionStart + 4).filter(r => r.getAttribute("data-id"));
@@ -836,7 +836,7 @@ describe("ListView Column Filtering", () => {
it("filters tasks by column when drop zone is clicked", () => { it("filters tasks by column when drop zone is clicked", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
createMockTask({ id: "FN-003", column: "in-progress", title: "In Progress Task" }), createMockTask({ id: "FN-003", column: "in-progress", title: "In Progress Task" }),
]; ];
@@ -855,12 +855,12 @@ describe("ListView Column Filtering", () => {
// Only triage section header should be visible // Only triage section header should be visible
const sectionHeaders = screen.getAllByRole("row").filter(r => r.className.includes("list-section-header")); const sectionHeaders = screen.getAllByRole("row").filter(r => r.className.includes("list-section-header"));
expect(sectionHeaders.length).toBe(1); expect(sectionHeaders.length).toBe(1);
expect(sectionHeaders[0].textContent).toContain("Triage"); expect(sectionHeaders[0].textContent).toContain("Planning");
}); });
it("clears column filter when same drop zone is clicked again", () => { it("clears column filter when same drop zone is clicked again", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
]; ];
@@ -888,7 +888,7 @@ describe("ListView Column Filtering", () => {
it("switches column filter when different drop zone is clicked", () => { it("switches column filter when different drop zone is clicked", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
]; ];
@@ -918,7 +918,7 @@ describe("ListView Column Filtering", () => {
it("clears column filter when clear button is clicked", () => { it("clears column filter when clear button is clicked", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
]; ];
@@ -942,8 +942,8 @@ describe("ListView Column Filtering", () => {
it("shows correct filtered stats when column filter is active", () => { it("shows correct filtered stats when column filter is active", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "triage", title: "Triage Task 2" }), createMockTask({ id: "FN-002", column: "triage", title: "Planning Task 2" }),
createMockTask({ id: "FN-003", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-003", column: "todo", title: "Todo Task" }),
]; ];
@@ -954,13 +954,13 @@ describe("ListView Column Filtering", () => {
fireEvent.click(triageZone); fireEvent.click(triageZone);
// Stats should show filtered count with column name // Stats should show filtered count with column name
expect(screen.getByText("2 of 3 tasks in Triage")).toBeDefined(); expect(screen.getByText("2 of 3 tasks in Planning")).toBeDefined();
}); });
it("applies text filter within column filter", () => { it("applies text filter within column filter", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Alpha Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Alpha Planning Task" }),
createMockTask({ id: "FN-002", column: "triage", title: "Beta Triage Task" }), createMockTask({ id: "FN-002", column: "triage", title: "Beta Planning Task" }),
createMockTask({ id: "FN-003", column: "todo", title: "Alpha Todo Task" }), createMockTask({ id: "FN-003", column: "todo", title: "Alpha Todo Task" }),
]; ];
@@ -976,12 +976,12 @@ describe("ListView Column Filtering", () => {
expect(screen.queryByText("FN-003")).toBeNull(); expect(screen.queryByText("FN-003")).toBeNull();
// Stats should reflect combined filtering // Stats should reflect combined filtering
expect(screen.getByText("1 of 3 tasks in Triage")).toBeDefined(); expect(screen.getByText("1 of 3 tasks in Planning")).toBeDefined();
}); });
it("applies active class to selected column drop zone", () => { it("applies active class to selected column drop zone", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
@@ -1180,7 +1180,7 @@ describe("ListView Column Visibility", () => {
expect(screen.getByText("pending")).toBeDefined(); expect(screen.getByText("pending")).toBeDefined();
// Check for column badge specifically using the class // Check for column badge specifically using the class
const columnBadge = document.querySelector(".list-column-badge"); const columnBadge = document.querySelector(".list-column-badge");
expect(columnBadge?.textContent).toContain("Triage"); expect(columnBadge?.textContent).toContain("Planning");
}); });
}); });
@@ -1371,9 +1371,9 @@ describe("ListView Hide Done Tasks", () => {
); );
expect(archivedSection).toBeUndefined(); expect(archivedSection).toBeUndefined();
// Triage section should still be visible // Planning section should still be visible
const triageSection = screen.getAllByRole("row").find(r => const triageSection = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
expect(triageSection).toBeDefined(); expect(triageSection).toBeDefined();
}); });
@@ -1438,7 +1438,7 @@ describe("ListView Hide Done Tasks", () => {
it("shows done section when selectedColumn is done even with hide done active", () => { it("shows done section when selectedColumn is done even with hide done active", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "done", title: "Done Task" }), createMockTask({ id: "FN-001", column: "done", title: "Done Task" }),
createMockTask({ id: "FN-002", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-002", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
@@ -1462,7 +1462,7 @@ describe("ListView Hide Done Tasks", () => {
it("shows archived section when selectedColumn is archived even with hide done active", () => { it("shows archived section when selectedColumn is archived even with hide done active", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "archived", title: "Archived Task" }), createMockTask({ id: "FN-001", column: "archived", title: "Archived Task" }),
createMockTask({ id: "FN-002", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-002", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
@@ -1652,8 +1652,8 @@ describe("ListView Collapsible Sections", () => {
it("clicking section header toggles collapse and hides task rows", () => { it("clicking section header toggles collapse and hides task rows", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task 1" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task 1" }),
createMockTask({ id: "FN-002", column: "triage", title: "Triage Task 2" }), createMockTask({ id: "FN-002", column: "triage", title: "Planning Task 2" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
@@ -1664,7 +1664,7 @@ describe("ListView Collapsible Sections", () => {
// Find and click the triage section header // Find and click the triage section header
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
expect(triageHeader).toBeDefined(); expect(triageHeader).toBeDefined();
fireEvent.click(triageHeader!); fireEvent.click(triageHeader!);
@@ -1683,14 +1683,14 @@ describe("ListView Collapsible Sections", () => {
it("clicking again expands section and shows task rows", () => { it("clicking again expands section and shows task rows", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
// Find the triage section header // Find the triage section header
let triageHeader = screen.getAllByRole("row").find(r => let triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
// Click to collapse // Click to collapse
@@ -1707,7 +1707,7 @@ describe("ListView Collapsible Sections", () => {
// Re-query for the header to get fresh DOM reference after re-render // Re-query for the header to get fresh DOM reference after re-render
triageHeader = screen.getAllByRole("row").find(r => triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
// Section header should not have collapsed class // Section header should not have collapsed class
@@ -1719,14 +1719,14 @@ describe("ListView Collapsible Sections", () => {
it("collapse state persists to localStorage", () => { it("collapse state persists to localStorage", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
// Click to collapse // Click to collapse
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
fireEvent.click(triageHeader!); fireEvent.click(triageHeader!);
@@ -1742,28 +1742,28 @@ describe("ListView Collapsible Sections", () => {
localStorage.setItem(scopedStorageKey("kb-dashboard-list-collapsed"), JSON.stringify(["triage"])); localStorage.setItem(scopedStorageKey("kb-dashboard-list-collapsed"), JSON.stringify(["triage"]));
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
// Triage task should be hidden initially (collapsed from localStorage) // Planning task should be hidden initially (collapsed from localStorage)
expect(screen.queryByText("FN-001")).toBeNull(); expect(screen.queryByText("FN-001")).toBeNull();
// Todo task should be visible // Todo task should be visible
expect(screen.getByText("FN-002")).toBeDefined(); expect(screen.getByText("FN-002")).toBeDefined();
// Triage section header should have collapsed class // Planning section header should have collapsed class
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
expect(triageHeader?.className).toContain("list-section-header--collapsed"); expect(triageHeader?.className).toContain("list-section-header--collapsed");
}); });
it("multiple sections can be collapsed independently", () => { it("multiple sections can be collapsed independently", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }), createMockTask({ id: "FN-002", column: "todo", title: "Todo Task" }),
createMockTask({ id: "FN-003", column: "in-progress", title: "In Progress Task" }), createMockTask({ id: "FN-003", column: "in-progress", title: "In Progress Task" }),
]; ];
@@ -1774,7 +1774,7 @@ describe("ListView Collapsible Sections", () => {
const allHeaders = screen.getAllByRole("row").filter(r => const allHeaders = screen.getAllByRole("row").filter(r =>
r.className.includes("list-section-header") r.className.includes("list-section-header")
); );
const triageHeader = allHeaders.find(h => h.textContent?.includes("Triage")); const triageHeader = allHeaders.find(h => h.textContent?.includes("Planning"));
const todoHeader = allHeaders.find(h => h.textContent?.includes("Todo")); const todoHeader = allHeaders.find(h => h.textContent?.includes("Todo"));
// Collapse triage section // Collapse triage section
@@ -1783,7 +1783,7 @@ describe("ListView Collapsible Sections", () => {
// Collapse todo section // Collapse todo section
fireEvent.click(todoHeader!); fireEvent.click(todoHeader!);
// Triage and todo tasks should be hidden // Planning and todo tasks should be hidden
expect(screen.queryByText("FN-001")).toBeNull(); expect(screen.queryByText("FN-001")).toBeNull();
expect(screen.queryByText("FN-002")).toBeNull(); expect(screen.queryByText("FN-002")).toBeNull();
@@ -1813,7 +1813,7 @@ describe("ListView Collapsible Sections", () => {
// Collapse triage section // Collapse triage section
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
fireEvent.click(triageHeader!); fireEvent.click(triageHeader!);
@@ -1841,7 +1841,7 @@ describe("ListView Collapsible Sections", () => {
// Collapse triage section // Collapse triage section
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
fireEvent.click(triageHeader!); fireEvent.click(triageHeader!);
@@ -1855,14 +1855,14 @@ describe("ListView Collapsible Sections", () => {
it("section header has aria-expanded attribute for accessibility", () => { it("section header has aria-expanded attribute for accessibility", () => {
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
// Find triage section header // Find triage section header
const triageHeader = screen.getAllByRole("row").find(r => const triageHeader = screen.getAllByRole("row").find(r =>
r.className.includes("list-section-header") && r.textContent?.includes("Triage") r.className.includes("list-section-header") && r.textContent?.includes("Planning")
); );
// Should have aria-expanded="true" when expanded // Should have aria-expanded="true" when expanded
@@ -1878,7 +1878,7 @@ describe("ListView Collapsible Sections", () => {
it("collapsed section hides No tasks placeholder", () => { it("collapsed section hides No tasks placeholder", () => {
// Create tasks in one column, leave another column empty // Create tasks in one column, leave another column empty
const tasks = [ const tasks = [
createMockTask({ id: "FN-001", column: "triage", title: "Triage Task" }), createMockTask({ id: "FN-001", column: "triage", title: "Planning Task" }),
]; ];
renderListView({ tasks }); renderListView({ tasks });
@@ -2376,7 +2376,7 @@ describe("ListView - Bulk Selection", () => {
tasks: [createMockTask({ id: "FN-001", title: "Collapsible task" })], tasks: [createMockTask({ id: "FN-001", title: "Collapsible task" })],
}); });
const sectionHeader = screen.getByRole("button", { name: /Triage/i }); const sectionHeader = screen.getByRole("button", { name: /Planning/i });
expect(container.querySelector('.list-card[data-id="FN-001"]')).toBeInTheDocument(); expect(container.querySelector('.list-card[data-id="FN-001"]')).toBeInTheDocument();
fireEvent.click(sectionHeader); fireEvent.click(sectionHeader);

View File

@@ -17,7 +17,7 @@
// agent is expected to review each flagged item itself (diff the matching // agent is expected to review each flagged item itself (diff the matching
// main commit against HEAD, confirm its net contribution survived) and // main commit against HEAD, confirm its net contribution survived) and
// restore anything silently dropped. No human handoff required; the audit // restore anything silently dropped. No human handoff required; the audit
// is only "clean" once the agent has triaged every item or reapplied any // is only "clean" once the agent has reviewed every item or reapplied any
// losses as a follow-up commit on the same branch. // losses as a follow-up commit on the same branch.
// //
// Example: // Example:
@@ -105,5 +105,5 @@ if (overlaps.length === 0) {
} }
const issues = dupes.length + overlaps.length; const issues = dupes.length + overlaps.length;
console.log(`Audit complete. ${issues} item(s) for the calling agent to triage.`); console.log(`Audit complete. ${issues} item(s) for the calling agent to review.`);
process.exit(issues === 0 ? 0 : 1); process.exit(issues === 0 ? 0 : 1);