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

- feat(FN-2604): complete Step 8 — add changeset and delivery docs
- test(FN-2604): complete Step 7 — align tests and regenerate extension skill docs
- test(FN-2604): complete Step 6 — update dashboard tests for planning statuses
- feat(FN-2604): complete Step 5 — update CLI planning terminology
- feat(FN-2604): complete Step 4 — update workflow route status strings
- feat(FN-2604): complete Step 3 — update settings planning terminology
- feat(FN-2604): complete Step 2 — rename replanning actions in column and spec editor
- feat(FN-2604): complete Step 1 — update dashboard component status strings
This commit is contained in:
Fusion
2026-04-26 14:06:48 -07:00
committed by gsxdsm
parent 651ae34307
commit 03a48ae9bb
14 changed files with 97 additions and 92 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Update dashboard and CLI status strings: specifying→planning, needs-respecify→needs-replan. Update user-facing text from "triage/specify" terminology to "planning/replan" terminology.

View File

@@ -10,7 +10,7 @@ All tools are registered via the pi extension. They are available in any pi agen
### fn_task_create ### fn_task_create
Create a new task on the Fusion task board. The task enters the triage column where the AI triage agent will specify it into a full prompt with steps, file scope, and acceptance criteria. Create a new task on the Fusion task board. The task enters the planning column where the AI planning agent will plan it into a full prompt with steps, file scope, and acceptance criteria.
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|----------|-------------| |-----------|------|----------|-------------|
@@ -82,7 +82,7 @@ Retry a failed task — clears the error state and moves it back to the todo col
### fn_task_duplicate ### fn_task_duplicate
Duplicate an existing task, creating a fresh copy in triage. Copies the title and description but resets all execution state. The AI triage agent will re-specify the new task. Duplicate an existing task, creating a fresh copy in planning. Copies the title and description but resets all execution state. The AI planning agent will replan the new task.
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|----------|-------------| |-----------|------|----------|-------------|
@@ -90,7 +90,7 @@ Duplicate an existing task, creating a fresh copy in triage. Copies the title an
### fn_task_refine ### fn_task_refine
Request a refinement of a completed or in-review task. Creates a new follow-up task in triage that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. Request a refinement of a completed or in-review task. Creates a new follow-up task in planning that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes.
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|----------|-------------| |-----------|------|----------|-------------|
@@ -133,7 +133,7 @@ Create a task via AI-guided planning mode — interactive conversation to refine
### fn_task_import_github ### fn_task_import_github
Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the triage column. Each task includes the issue title and body with a link to the source issue. Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the planning column. Each task includes the issue title and body with a link to the source issue.
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|----------|-------------| |-----------|------|----------|-------------|
@@ -143,7 +143,7 @@ Import GitHub issues as Fusion tasks. Fetches open issues from a repository and
### fn_task_import_github_issue ### fn_task_import_github_issue
Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the triage column with the issue title and body. Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the planning column with the issue title and body.
| Parameter | Type | Required | Description | | Parameter | Type | Required | Description |
|-----------|------|----------|-------------| |-----------|------|----------|-------------|

View File

@@ -12,7 +12,7 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
<!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) --> <!-- BEGIN: fusion-capabilities-tool-table (auto-generated by scripts/sync-fusion-skill-tools.mjs — do not edit by hand) -->
| Tool | Purpose | | Tool | Purpose |
|------|---------| |------|---------|
| `fn_task_create` | Create a new task on the Fusion task board. The task enters the triage column where the AI triage agent will specify it into a full prompt with steps, file scope, and acceptance criteria. | | `fn_task_create` | Create a new task on the Fusion task board. The task enters the planning column where the AI planning agent will plan it into a full prompt with steps, file scope, and acceptance criteria. |
| `fn_task_update` | Update fields on an existing task. Supports modifying the title, description, dependencies, and assigned agent after task creation. | | `fn_task_update` | Update fields on an existing task. Supports modifying the title, description, dependencies, and assigned agent after task creation. |
| `fn_task_list` | List all tasks on the Fusion board, grouped by column. | | `fn_task_list` | List all tasks on the Fusion board, grouped by column. |
| `fn_task_show` | Show full details for a task including steps, progress, and log entries. | | `fn_task_show` | Show full details for a task including steps, progress, and log entries. |
@@ -20,13 +20,13 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
| `fn_task_pause` | Pause a task — stops all automated agent and scheduler interaction for this task. | | `fn_task_pause` | Pause a task — stops all automated agent and scheduler interaction for this task. |
| `fn_task_unpause` | Unpause a task — resumes automated agent and scheduler interaction. | | `fn_task_unpause` | Unpause a task — resumes automated agent and scheduler interaction. |
| `fn_task_retry` | Retry a failed task — clears the error state and moves it back to the todo column for re-execution. | | `fn_task_retry` | Retry a failed task — clears the error state and moves it back to the todo column for re-execution. |
| `fn_task_duplicate` | Duplicate an existing task, creating a fresh copy in triage. Copies the title and description but resets all execution state. The AI triage agent will re-specify the new task. | | `fn_task_duplicate` | Duplicate an existing task, creating a fresh copy in planning. Copies the title and description but resets all execution state. The AI planning agent will replan the new task. |
| `fn_task_refine` | Request a refinement of a completed or in-review task. Creates a new follow-up task in triage that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. | | `fn_task_refine` | Request a refinement of a completed or in-review task. Creates a new follow-up task in planning that references the original task as a dependency. Use this when a done or in-review task needs additional work, improvements, or follow-up changes. |
| `fn_task_archive` | Archive a done task (move from done → archived). Archived tasks are preserved for historical reference but moved out of the main board view. | | `fn_task_archive` | Archive a done task (move from done → archived). Archived tasks are preserved for historical reference but moved out of the main board view. |
| `fn_task_unarchive` | Unarchive an archived task (move from archived → done). Restores the task to the done column. | | `fn_task_unarchive` | Unarchive an archived task (move from archived → done). Restores the task to the done column. |
| `fn_task_delete` | Permanently delete a task from the Fusion board. Tasks are deleted immediately and cannot be recovered. | | `fn_task_delete` | Permanently delete a task from the Fusion board. Tasks are deleted immediately and cannot be recovered. |
| `fn_task_import_github` | Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the triage column. Each task includes the issue title and body with a link to the source issue. | | `fn_task_import_github` | Import GitHub issues as Fusion tasks. Fetches open issues from a repository and creates tasks in the planning column. Each task includes the issue title and body with a link to the source issue. |
| `fn_task_import_github_issue` | Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the triage column with the issue title and body. | | `fn_task_import_github_issue` | Import a specific GitHub issue as a Fusion task. Fetches the issue by number and creates a single task in the planning column with the issue title and body. |
| `fn_task_browse_github_issues` | List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number. | | `fn_task_browse_github_issues` | List open GitHub issues from a repository to browse before importing. Returns issue numbers, titles, and URLs for selection. Use with fn_task_import_github_issue to import specific issues by number. |
| `fn_task_plan` | Create a task via AI-guided planning mode — interactive conversation to refine your idea into a well-specified task. | | `fn_task_plan` | Create a task via AI-guided planning mode — interactive conversation to refine your idea into a well-specified task. |
| `fn_mission_create` | Create a new mission — a high-level objective that can span multiple milestones. Missions contain milestones that break down work into phases. | | `fn_mission_create` | Create a new mission — a high-level objective that can span multiple milestones. Missions contain milestones that break down work into phases. |

View File

@@ -2315,7 +2315,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
console.log(` AI engine: ✗ disabled (dev mode)`); console.log(` AI engine: ✗ disabled (dev mode)`);
} else { } else {
console.log(` AI engine: ✓ active`); console.log(` AI engine: ✓ active`);
console.log(`triage: auto-specifying tasks`); console.log(`planning: auto-planning tasks`);
console.log(` • scheduler: dependency-aware execution`); console.log(` • scheduler: dependency-aware execution`);
console.log(` • cron: scheduled task execution`); console.log(` • cron: scheduled task execution`);
} }

View File

@@ -137,12 +137,12 @@ export default function kbExtension(pi: ExtensionAPI) {
name: "fn_task_create", name: "fn_task_create",
label: "fn: Create Task", label: "fn: Create Task",
description: description:
"Create a new task on the Fusion task board. The task enters the triage column " + "Create a new task on the Fusion task board. The task enters the planning column " +
"where the AI triage agent will specify it into a full prompt with steps, " + "where the AI planning agent will plan it into a full prompt with steps, " +
"file scope, and acceptance criteria.", "file scope, and acceptance criteria.",
promptSnippet: "Create a task on the Fusion AI-orchestrated task board", promptSnippet: "Create a task on the Fusion AI-orchestrated task board",
promptGuidelines: [ promptGuidelines: [
"Use fn_task_create for task tracking — be descriptive so the triage agent can write a good spec.", "Use fn_task_create for task tracking — be descriptive so the planning agent can write a good plan.",
"Include the problem AND desired outcome. For bugs, describe current vs expected behavior.", "Include the problem AND desired outcome. For bugs, describe current vs expected behavior.",
], ],
parameters: Type.Object({ parameters: Type.Object({
@@ -584,13 +584,13 @@ export default function kbExtension(pi: ExtensionAPI) {
name: "fn_task_duplicate", name: "fn_task_duplicate",
label: "fn: Duplicate Task", label: "fn: Duplicate Task",
description: description:
"Duplicate an existing task, creating a fresh copy in triage. " + "Duplicate an existing task, creating a fresh copy in planning. " +
"Copies the title and description but resets all execution state. " + "Copies the title and description but resets all execution state. " +
"The AI triage agent will re-specify the new task.", "The AI planning agent will replan the new task.",
promptSnippet: "Duplicate a Fusion task (creates copy in triage)", promptSnippet: "Duplicate a Fusion task (creates copy in planning)",
promptGuidelines: [ promptGuidelines: [
"Use when a task needs to be re-done, split, or used as a template", "Use when a task needs to be re-done, split, or used as a template",
"The duplicated task will be placed in triage for re-specification", "The duplicated task will be placed in planning for replanning",
"Dependencies, attachments, and execution state are NOT copied", "Dependencies, attachments, and execution state are NOT copied",
], ],
parameters: Type.Object({ parameters: Type.Object({
@@ -615,13 +615,13 @@ export default function kbExtension(pi: ExtensionAPI) {
label: "fn: Refine Task", label: "fn: Refine Task",
description: description:
"Request a refinement of a completed or in-review task. " + "Request a refinement of a completed or in-review task. " +
"Creates a new follow-up task in triage that references the original task as a dependency. " + "Creates a new follow-up task in planning that references the original task as a dependency. " +
"Use this when a done or in-review task needs additional work, improvements, or follow-up changes.", "Use this when a done or in-review task needs additional work, improvements, or follow-up changes.",
promptSnippet: "Create a refinement task for follow-up work on a completed task", promptSnippet: "Create a refinement task for follow-up work on a completed task",
promptGuidelines: [ promptGuidelines: [
"Use when a completed or in-review task needs follow-up work or improvements", "Use when a completed or in-review task needs follow-up work or improvements",
"The original task must be in 'done' or 'in-review' column", "The original task must be in 'done' or 'in-review' column",
"The refinement task will be created in triage and depend on the original task", "The refinement task will be created in planning and depend on the original task",
"Provide clear feedback about what needs to be refined or improved", "Provide clear feedback about what needs to be refined or improved",
], ],
parameters: Type.Object({ parameters: Type.Object({
@@ -739,7 +739,7 @@ export default function kbExtension(pi: ExtensionAPI) {
label: "fn: Import GitHub Issues", label: "fn: Import GitHub Issues",
description: description:
"Import GitHub issues as Fusion tasks. Fetches open issues from a repository " + "Import GitHub issues as Fusion tasks. Fetches open issues from a repository " +
"and creates tasks in the triage column. Each task includes the issue title " + "and creates tasks in the planning column. Each task includes the issue title " +
"and body with a link to the source issue.", "and body with a link to the source issue.",
promptSnippet: "Import GitHub issues as Fusion tasks", promptSnippet: "Import GitHub issues as Fusion tasks",
promptGuidelines: [ promptGuidelines: [
@@ -830,7 +830,7 @@ export default function kbExtension(pi: ExtensionAPI) {
label: "fn: Import GitHub Issue", label: "fn: Import GitHub Issue",
description: description:
"Import a specific GitHub issue as a Fusion task. Fetches the issue by number " + "Import a specific GitHub issue as a Fusion task. Fetches the issue by number " +
"and creates a single task in the triage column with the issue title and body.", "and creates a single task in the planning column with the issue title and body.",
promptSnippet: "Import a specific GitHub issue as a Fusion task", promptSnippet: "Import a specific GitHub issue as a Fusion task",
promptGuidelines: [ promptGuidelines: [
"Use for importing a single known issue by its number", "Use for importing a single known issue by its number",
@@ -1055,7 +1055,7 @@ export default function kbExtension(pi: ExtensionAPI) {
content: [ content: [
{ {
type: "text", type: "text",
text: summaryLine + (taskId ? `\n\nPlanning session completed. Task ${taskId} is now in triage and will be auto-specified by the AI triage agent.` : ""), text: summaryLine + (taskId ? `\n\nPlanning session completed. Task ${taskId} is now in planning and will be auto-planned by the AI planning agent.` : ""),
}, },
], ],
details: { taskId, logs }, details: { taskId, logs },

View File

@@ -70,7 +70,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
const [dragOver, setDragOver] = useState(false); const [dragOver, setDragOver] = useState(false);
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL); const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isRespecifying, setIsRespecifying] = useState(false); const [isReplanning, setIsReplanning] = useState(false);
const [isPausingAll, setIsPausingAll] = useState(false); const [isPausingAll, setIsPausingAll] = useState(false);
const [isMovingAllToTodo, setIsMovingAllToTodo] = useState(false); const [isMovingAllToTodo, setIsMovingAllToTodo] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null); const menuRef = useRef<HTMLDivElement | null>(null);
@@ -161,16 +161,16 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
setVisibleTaskCount((current) => Math.min(current + VISIBLE_TASKS_INCREMENT, tasks.length)); setVisibleTaskCount((current) => Math.min(current + VISIBLE_TASKS_INCREMENT, tasks.length));
}, [tasks.length]); }, [tasks.length]);
const handleRespecifyAll = useCallback(async () => { const handleReplanAll = useCallback(async () => {
setIsMenuOpen(false); setIsMenuOpen(false);
if (tasks.length === 0) return; if (tasks.length === 0) return;
const confirmed = window.confirm( const confirmed = window.confirm(
`Move all ${tasks.length} todo task${tasks.length === 1 ? "" : "s"} back to triage to be respecified?`, `Move all ${tasks.length} todo task${tasks.length === 1 ? "" : "s"} back to planning to be replanned?`,
); );
if (!confirmed) return; if (!confirmed) return;
setIsRespecifying(true); setIsReplanning(true);
try { try {
// Issue moves in parallel — onMoveTask is per-task, no bulk endpoint. // Issue moves in parallel — onMoveTask is per-task, no bulk endpoint.
const results = await Promise.allSettled( const results = await Promise.allSettled(
@@ -179,19 +179,19 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
const failed = results.filter((r) => r.status === "rejected").length; const failed = results.filter((r) => r.status === "rejected").length;
const moved = results.length - failed; const moved = results.length - failed;
if (failed === 0) { if (failed === 0) {
addToast(`Moved ${moved} task${moved === 1 ? "" : "s"} to triage for respec`, "success"); addToast(`Moved ${moved} task${moved === 1 ? "" : "s"} to planning for replanning`, "success");
} else { } else {
addToast(`Moved ${moved} of ${results.length} tasks; ${failed} failed`, "error"); addToast(`Moved ${moved} of ${results.length} tasks; ${failed} failed`, "error");
} }
} finally { } finally {
setIsRespecifying(false); setIsReplanning(false);
} }
}, [tasks, onMoveTask, addToast]); }, [tasks, onMoveTask, addToast]);
const pauseEligibleTasks = useMemo(() => tasks.filter((task) => !task.paused), [tasks]); const pauseEligibleTasks = useMemo(() => tasks.filter((task) => !task.paused), [tasks]);
const pauseEligibleCount = pauseEligibleTasks.length; const pauseEligibleCount = pauseEligibleTasks.length;
const hasColumnBulkActions = column === "todo" || column === "in-progress" || column === "in-review"; const hasColumnBulkActions = column === "todo" || column === "in-progress" || column === "in-review";
const isMenuBusy = isRespecifying || isPausingAll || isMovingAllToTodo; const isMenuBusy = isReplanning || isPausingAll || isMovingAllToTodo;
const handlePauseAll = useCallback(async () => { const handlePauseAll = useCallback(async () => {
if (!onPauseTask) return; if (!onPauseTask) return;
@@ -332,12 +332,12 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
type="button" type="button"
role="menuitem" role="menuitem"
className="column-menu-item" className="column-menu-item"
onClick={() => void handleRespecifyAll()} onClick={() => void handleReplanAll()}
disabled={tasks.length === 0 || isRespecifying} disabled={tasks.length === 0 || isReplanning}
> >
Respecify All Replan All
<span className="column-menu-item-hint"> <span className="column-menu-item-hint">
Move {tasks.length} task{tasks.length === 1 ? "" : "s"} to Triage Move {tasks.length} task{tasks.length === 1 ? "" : "s"} to Planning
</span> </span>
</button> </button>
)} )}

View File

@@ -22,7 +22,7 @@ const COLUMN_COLOR_MAP: Record<Column, string> = {
archived: "var(--text-secondary)", archived: "var(--text-secondary)",
}; };
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]); const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging"]);
type SortField = "id" | "title" | "status" | "column"; type SortField = "id" | "title" | "status" | "column";
type SortDirection = "asc" | "desc"; type SortDirection = "asc" | "desc";

View File

@@ -882,7 +882,7 @@ export function SettingsModal({
globalModelKey: "planningGlobalModelId", globalModelKey: "planningGlobalModelId",
projectProviderKey: "planningProvider", projectProviderKey: "planningProvider",
projectModelKey: "planningModelId", projectModelKey: "planningModelId",
helperText: "AI model used for task specification (triage).", helperText: "AI model used for task planning.",
fallbackOrder: "Project override → Global planning lane → Global default lane → Automatic resolution", fallbackOrder: "Project override → Global planning lane → Global default lane → Automatic resolution",
}, },
{ {
@@ -2102,7 +2102,7 @@ export function SettingsModal({
setForm((f) => ({ ...f, maxTriageConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState)); setForm((f) => ({ ...f, maxTriageConcurrent: val === "" ? undefined : Number(val) } as SettingsFormState));
}} }}
/> />
<small>Maximum concurrent triage/specification agents</small> <small>Maximum concurrent planning agents</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="pollIntervalMs">Poll Interval (ms)</label> <label htmlFor="pollIntervalMs">Poll Interval (ms)</label>
@@ -2144,9 +2144,9 @@ export function SettingsModal({
setForm((f) => ({ ...f, specStalenessEnabled: e.target.checked })) setForm((f) => ({ ...f, specStalenessEnabled: e.target.checked }))
} }
/> />
Enable specification staleness enforcement Enable plan staleness enforcement
</label> </label>
<small>When enabled, tasks with stale specifications (PROMPT.md older than the threshold) are automatically sent back to triage for re-specification</small> <small>When enabled, tasks with stale plans (PROMPT.md older than the threshold) are automatically sent back to planning for replanning</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="specStalenessMaxAgeMs">Stale Spec Threshold (hours)</label> <label htmlFor="specStalenessMaxAgeMs">Stale Spec Threshold (hours)</label>
@@ -2163,7 +2163,7 @@ export function SettingsModal({
}} }}
disabled={!form.specStalenessEnabled} disabled={!form.specStalenessEnabled}
/> />
<small>Maximum age in hours before a specification is considered stale. Default: 6 hours.</small> <small>Maximum age in hours before a plan is considered stale. Default: 6 hours.</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label"> <label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label">

View File

@@ -163,7 +163,7 @@ export function SpecEditor({
<div className="spec-editor-revision"> <div className="spec-editor-revision">
<h4>Ask AI to Revise</h4> <h4>Ask AI to Revise</h4>
<p className="spec-editor-revision-help"> <p className="spec-editor-revision-help">
Provide feedback for the AI to improve this specification. The task will move to triage for re-specification. Provide feedback for the AI to improve this specification. The task will move to planning for replanning.
</p> </p>
<textarea <textarea
ref={feedbackRef} ref={feedbackRef}

View File

@@ -81,7 +81,7 @@ function abbreviateBadge(text: string, max: number): string {
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]); const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]); const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging"]);
const COLUMN_PROGRESS_COLOR_MAP: Record<Column, string> = { const COLUMN_PROGRESS_COLOR_MAP: Record<Column, string> = {
triage: "var(--triage)", triage: "var(--triage)",

View File

@@ -299,8 +299,8 @@ export function TaskDetailModal({
const canRetryTask = const canRetryTask =
task.status === "failed" || task.status === "failed" ||
task.status === "stuck-killed" || task.status === "stuck-killed" ||
task.status === "specifying" || task.status === "planning" ||
task.status === "needs-respecify" || task.status === "needs-replan" ||
(task.stuckKillCount ?? 0) > 0 || (task.stuckKillCount ?? 0) > 0 ||
(task.recoveryRetryCount ?? 0) > 0 || (task.recoveryRetryCount ?? 0) > 0 ||
Boolean(task.nextRecoveryAt); Boolean(task.nextRecoveryAt);
@@ -903,7 +903,7 @@ export function TaskDetailModal({
if (!confirm("Reject this plan? The specification will be discarded and regenerated.")) return; if (!confirm("Reject this plan? The specification will be discarded and regenerated.")) return;
try { try {
await rejectPlan(task.id, projectId); await rejectPlan(task.id, projectId);
addToast(`Plan rejected — ${task.id} returned to Triage for re-specification`, "info"); addToast(`Plan rejected — ${task.id} returned to Planning for replanning`, "info");
onClose(); onClose();
} catch (err) { } catch (err) {
addToast(getErrorMessage(err), "error"); addToast(getErrorMessage(err), "error");
@@ -911,11 +911,11 @@ export function TaskDetailModal({
}, [task.id, onClose, addToast]); }, [task.id, onClose, addToast]);
const handleRespecify = useCallback(async () => { const handleRespecify = useCallback(async () => {
if (!confirm("Rebuild the specification for this task? The task will move to triage for re-specification.")) return; if (!confirm("Rebuild the plan for this task? The task will move to planning for replanning.")) return;
try { try {
await rebuildTaskSpec(task.id, projectId); await rebuildTaskSpec(task.id, projectId);
onClose(); onClose();
addToast(`Respecifying ${task.id}...`, "info"); addToast(`Replanning ${task.id}...`, "info");
} catch (err) { } catch (err) {
addToast(getErrorMessage(err), "error"); addToast(getErrorMessage(err), "error");
} }
@@ -1152,8 +1152,8 @@ export function TaskDetailModal({
setIsRequestingRevision(true); setIsRequestingRevision(true);
try { try {
await requestSpecRevision(task.id, feedback, projectId); await requestSpecRevision(task.id, feedback, projectId);
addToast("AI revision requested. Task moved to triage.", "success"); addToast("AI revision requested. Task moved to planning.", "success");
// Task has been moved to triage, close modal // Task has been moved to planning, close modal
onClose(); onClose();
} catch (err) { } catch (err) {
const msg = getErrorMessage(err); const msg = getErrorMessage(err);
@@ -1730,7 +1730,7 @@ export function TaskDetailModal({
<div className="spec-editor-revision"> <div className="spec-editor-revision">
<h4>Ask AI to Revise</h4> <h4>Ask AI to Revise</h4>
<p className="spec-editor-revision-help"> <p className="spec-editor-revision-help">
Provide feedback for the AI to improve this specification. The task will move to triage for re-specification. Provide feedback for the AI to improve this specification. The task will move to planning for replanning.
</p> </p>
<textarea <textarea
className="spec-editor-feedback" className="spec-editor-feedback"

View File

@@ -374,10 +374,10 @@ describe("TaskDetailModal", () => {
expect(retryButtons).toHaveLength(1); expect(retryButtons).toHaveLength(1);
}); });
it("shows Retry for a stranded specifying triage task", () => { it("shows Retry for a stranded planning triage task", () => {
render( render(
<TaskDetailModal <TaskDetailModal
task={makeTask({ column: "triage", status: "specifying", stuckKillCount: 6 })} task={makeTask({ column: "triage", status: "planning", stuckKillCount: 6 })}
onClose={noop} onClose={noop}
onMoveTask={noopMove} onMoveTask={noopMove}
onDeleteTask={noopDelete} onDeleteTask={noopDelete}
@@ -2992,7 +2992,7 @@ describe("TaskDetailModal", () => {
await waitFor(() => { await waitFor(() => {
expect(requestSpecRevision).toHaveBeenCalledWith("FN-099", "Please add more error handling details", undefined); expect(requestSpecRevision).toHaveBeenCalledWith("FN-099", "Please add more error handling details", undefined);
expect(addToast).toHaveBeenCalledWith("AI revision requested. Task moved to triage.", "success"); expect(addToast).toHaveBeenCalledWith("AI revision requested. Task moved to planning.", "success");
expect(onClose).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled();
}); });
}); });
@@ -3219,7 +3219,7 @@ describe("TaskDetailModal", () => {
<TaskDetailModal <TaskDetailModal
task={makeTask({ task={makeTask({
column: "triage", column: "triage",
status: "specifying", status: "planning",
prompt: "# Task Spec", prompt: "# Task Spec",
})} })}
onClose={noop} onClose={noop}
@@ -3325,7 +3325,7 @@ describe("TaskDetailModal", () => {
expect(mockRejectPlan).toHaveBeenCalledWith("FN-001", undefined); expect(mockRejectPlan).toHaveBeenCalledWith("FN-001", undefined);
}); });
expect(addToast).toHaveBeenCalledWith( expect(addToast).toHaveBeenCalledWith(
"Plan rejected — FN-001 returned to Triage for re-specification", "Plan rejected — FN-001 returned to Planning for replanning",
"info" "info"
); );
expect(onClose).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled();

View File

@@ -1946,30 +1946,30 @@ describe("POST /tasks/:id/retry", () => {
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (stuck kill budget reset)"); expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (stuck kill budget reset)");
}); });
it("retries a stranded specifying triage task in triage and removes stale prompt", async () => { it("retries a stranded planning triage task in triage and removes stale prompt", async () => {
const tempRoot = mkdtempSync(join(tmpdir(), "kb-task-retry-spec-")); const tempRoot = mkdtempSync(join(tmpdir(), "kb-task-retry-spec-"));
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001"); const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
mkdirSync(taskDir, { recursive: true }); mkdirSync(taskDir, { recursive: true });
writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n"); writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n");
const specifyingTask = { const planningTask = {
...FAKE_TASK_DETAIL, ...FAKE_TASK_DETAIL,
column: "triage" as const, column: "triage" as const,
status: "specifying", status: "planning",
stuckKillCount: 6, stuckKillCount: 6,
recoveryRetryCount: 2, recoveryRetryCount: 2,
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(), nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
}; };
const retriedTask = { const retriedTask = {
...specifyingTask, ...planningTask,
status: "needs-respecify", status: "needs-replan",
stuckKillCount: 0, stuckKillCount: 0,
recoveryRetryCount: undefined, recoveryRetryCount: undefined,
nextRecoveryAt: undefined, nextRecoveryAt: undefined,
}; };
(store.getTask as ReturnType<typeof vi.fn>) (store.getTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce(specifyingTask) .mockResolvedValueOnce(planningTask)
.mockResolvedValueOnce(retriedTask); .mockResolvedValueOnce(retriedTask);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(retriedTask); (store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(retriedTask);
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot); (store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
@@ -1981,7 +1981,7 @@ describe("POST /tasks/:id/retry", () => {
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", { expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
status: "needs-respecify", status: "needs-replan",
error: null, error: null,
worktree: null, worktree: null,
branch: null, branch: null,
@@ -1995,9 +1995,9 @@ describe("POST /tasks/:id/retry", () => {
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
expect(store.logEntry).toHaveBeenCalledWith( expect(store.logEntry).toHaveBeenCalledWith(
"KB-001", "KB-001",
"Retry requested from dashboard (specification retry budget reset)", "Retry requested from dashboard (planning retry budget reset)",
); );
expect(res.body.status).toBe("needs-respecify"); expect(res.body.status).toBe("needs-replan");
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }); rmSync(tempRoot, { recursive: true, force: true });
} }
@@ -6916,7 +6916,7 @@ describe("POST /tasks/:id/spec/revise", () => {
); );
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" }); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }); rmSync(tempRoot, { recursive: true, force: true });
} }
@@ -6944,7 +6944,7 @@ describe("POST /tasks/:id/spec/revise", () => {
it("allows spec revision for task already in triage", async () => { it("allows spec revision for task already in triage", async () => {
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const }; const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-respecify" as const }; const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-replan" as const };
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-revise-triage-")); const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-revise-triage-"));
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001"); const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
mkdirSync(taskDir, { recursive: true }); mkdirSync(taskDir, { recursive: true });
@@ -6973,7 +6973,7 @@ describe("POST /tasks/:id/spec/revise", () => {
); );
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" }); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }); rmSync(tempRoot, { recursive: true, force: true });
} }
@@ -7149,7 +7149,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
); );
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" }); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }); rmSync(tempRoot, { recursive: true, force: true });
} }
@@ -7167,7 +7167,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" }); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
}); });
it("rebuilds spec and moves task from done to triage", async () => { it("rebuilds spec and moves task from done to triage", async () => {
@@ -7186,7 +7186,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
it("allows rebuild for task already in triage", async () => { it("allows rebuild for task already in triage", async () => {
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const }; const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-respecify" as const }; const updatedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "needs-replan" as const };
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-rebuild-triage-")); const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-rebuild-triage-"));
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001"); const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
mkdirSync(taskDir, { recursive: true }); mkdirSync(taskDir, { recursive: true });
@@ -7208,7 +7208,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
); );
expect(store.moveTask).not.toHaveBeenCalled(); expect(store.moveTask).not.toHaveBeenCalled();
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-respecify" }); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
} finally { } finally {
rmSync(tempRoot, { recursive: true, force: true }); rmSync(tempRoot, { recursive: true, force: true });
} }
@@ -7288,7 +7288,7 @@ describe("POST /tasks/:id/approve-plan", () => {
}); });
it("returns 400 when task does not have awaiting-approval status", async () => { it("returns 400 when task does not have awaiting-approval status", async () => {
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "specifying" as const }; const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "planning" as const };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask); (store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/approve-plan"); const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/approve-plan");
@@ -7364,7 +7364,7 @@ describe("POST /tasks/:id/reject-plan", () => {
}); });
it("returns 400 when task does not have awaiting-approval status", async () => { it("returns 400 when task does not have awaiting-approval status", async () => {
const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "specifying" as const }; const triageTask = { ...FAKE_TASK_DETAIL, column: "triage" as const, status: "planning" as const };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask); (store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(triageTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/reject-plan"); const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/reject-plan");

View File

@@ -241,7 +241,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
} }
}); });
// Retry failed, stuck-killed, or stranded triage/specification task // Retry failed, stuck-killed, or stranded triage/planning task
router.post("/tasks/:id/retry", async (req, res) => { router.post("/tasks/:id/retry", async (req, res) => {
try { try {
const { store: scopedStore } = await getProjectContext(req); const { store: scopedStore } = await getProjectContext(req);
@@ -249,14 +249,14 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const retrySpecification = const retrySpecification =
task.column === "triage" && task.column === "triage" &&
(task.status === "failed" || (task.status === "failed" ||
task.status === "specifying" || task.status === "planning" ||
task.status === "needs-respecify" || task.status === "needs-replan" ||
(task.stuckKillCount ?? 0) > 0); (task.stuckKillCount ?? 0) > 0);
if (task.status !== "failed" && task.status !== "stuck-killed" && !retrySpecification) { if (task.status !== "failed" && task.status !== "stuck-killed" && !retrySpecification) {
throw badRequest(`Task is not in a retryable state (current status: ${task.status || 'none'})`); throw badRequest(`Task is not in a retryable state (current status: ${task.status || 'none'})`);
} }
await scopedStore.updateTask(req.params.id, { await scopedStore.updateTask(req.params.id, {
status: retrySpecification ? "needs-respecify" : null, status: retrySpecification ? "needs-replan" : null,
error: null, error: null,
worktree: null, worktree: null,
branch: null, branch: null,
@@ -272,7 +272,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const { join } = await import("node:path"); const { join } = await import("node:path");
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md"); const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
await rm(promptPath, { force: true }); await rm(promptPath, { force: true });
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (specification retry budget reset)"); await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (planning retry budget reset)");
const updated = await scopedStore.getTask(req.params.id); const updated = await scopedStore.getTask(req.params.id);
res.json(updated); res.json(updated);
return; return;
@@ -1110,20 +1110,20 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const task = await scopedStore.getTask(req.params.id); const task = await scopedStore.getTask(req.params.id);
// If task is already in triage, skip the transition check and moveTask. // If task is already in triage, skip the transition check and moveTask.
// Just reset for re-specification in place. // Just reset for replanning in place.
if (task.column === "triage") { if (task.column === "triage") {
// Log the revision request // Log the revision request
await scopedStore.logEntry(task.id, "AI spec revision requested", feedback); await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
// Remove the existing spec so re-specification starts from the task // Remove the existing spec so replanning starts from the task
// description and feedback rather than revising stale PROMPT.md content. // description and feedback rather than revising stale PROMPT.md content.
const { rm } = await import("node:fs/promises"); const { rm } = await import("node:fs/promises");
const { join } = await import("node:path"); const { join } = await import("node:path");
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md"); const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
await rm(promptPath, { force: true }); await rm(promptPath, { force: true });
// Update status to indicate needs re-specification // Update status to indicate needs replanning
await scopedStore.updateTask(task.id, { status: "needs-respecify" }); await scopedStore.updateTask(task.id, { status: "needs-replan" });
const updated = await scopedStore.getTask(task.id); const updated = await scopedStore.getTask(task.id);
res.json(updated); res.json(updated);
@@ -1141,18 +1141,18 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
// Log the revision request // Log the revision request
await scopedStore.logEntry(task.id, "AI spec revision requested", feedback); await scopedStore.logEntry(task.id, "AI spec revision requested", feedback);
// Move to triage for re-specification // Move to triage for replanning
const updated = await scopedStore.moveTask(task.id, "triage"); const updated = await scopedStore.moveTask(task.id, "triage");
// Remove the existing spec so re-specification starts from the task // Remove the existing spec so replanning starts from the task
// description and feedback rather than revising stale PROMPT.md content. // description and feedback rather than revising stale PROMPT.md content.
const { rm } = await import("node:fs/promises"); const { rm } = await import("node:fs/promises");
const { join } = await import("node:path"); const { join } = await import("node:path");
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md"); const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
await rm(promptPath, { force: true }); await rm(promptPath, { force: true });
// Update status to indicate needs re-specification // Update status to indicate needs replanning
await scopedStore.updateTask(task.id, { status: "needs-respecify" }); await scopedStore.updateTask(task.id, { status: "needs-replan" });
res.json(updated); res.json(updated);
} catch (err: unknown) { } catch (err: unknown) {
@@ -1176,7 +1176,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const task = await scopedStore.getTask(req.params.id); const task = await scopedStore.getTask(req.params.id);
// If task is already in triage, skip the transition check and moveTask. // If task is already in triage, skip the transition check and moveTask.
// Just reset for re-specification in place. // Just reset for replanning in place.
if (task.column === "triage") { if (task.column === "triage") {
// Log the rebuild request // Log the rebuild request
await scopedStore.logEntry(task.id, "Specification rebuild requested by user"); await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
@@ -1188,8 +1188,8 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md"); const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
await rm(promptPath, { force: true }); await rm(promptPath, { force: true });
// Update status to indicate needs re-specification // Update status to indicate needs replanning
await scopedStore.updateTask(task.id, { status: "needs-respecify" }); await scopedStore.updateTask(task.id, { status: "needs-replan" });
const updated = await scopedStore.getTask(task.id); const updated = await scopedStore.getTask(task.id);
res.json(updated); res.json(updated);
@@ -1205,7 +1205,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
// Log the rebuild request // Log the rebuild request
await scopedStore.logEntry(task.id, "Specification rebuild requested by user"); await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
// Move to triage for re-specification // Move to triage for replanning
const updated = await scopedStore.moveTask(task.id, "triage"); const updated = await scopedStore.moveTask(task.id, "triage");
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead // Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
@@ -1215,8 +1215,8 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md"); const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
await rm(promptPath, { force: true }); await rm(promptPath, { force: true });
// Update status to indicate needs re-specification // Update status to indicate needs replanning
await scopedStore.updateTask(task.id, { status: "needs-respecify" }); await scopedStore.updateTask(task.id, { status: "needs-replan" });
res.json(updated); res.json(updated);
} catch (err: unknown) { } catch (err: unknown) {