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:
@@ -10,7 +10,7 @@ All tools are registered via the pi extension. They are available in any pi agen
|
||||
|
||||
### 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 |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -82,7 +82,7 @@ Retry a failed task — clears the error state and moves it back to the todo col
|
||||
|
||||
### 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 |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -90,7 +90,7 @@ Duplicate an existing task, creating a fresh copy in triage. Copies the title an
|
||||
|
||||
### 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 |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -133,7 +133,7 @@ Create a task via AI-guided planning mode — interactive conversation to refine
|
||||
|
||||
### 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 |
|
||||
|-----------|------|----------|-------------|
|
||||
@@ -143,7 +143,7 @@ Import GitHub issues as Fusion tasks. Fetches open issues from a repository and
|
||||
|
||||
### 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 |
|
||||
|-----------|------|----------|-------------|
|
||||
|
||||
@@ -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) -->
|
||||
| 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_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. |
|
||||
@@ -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_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_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_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_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 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_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_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_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` | 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 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_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. |
|
||||
|
||||
@@ -2315,7 +2315,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
console.log(` AI engine: ✗ disabled (dev mode)`);
|
||||
} else {
|
||||
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(` • cron: scheduled task execution`);
|
||||
}
|
||||
|
||||
@@ -137,12 +137,12 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
name: "fn_task_create",
|
||||
label: "fn: Create Task",
|
||||
description:
|
||||
"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, " +
|
||||
"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.",
|
||||
promptSnippet: "Create a task on the Fusion AI-orchestrated task board",
|
||||
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.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
@@ -584,13 +584,13 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
name: "fn_task_duplicate",
|
||||
label: "fn: Duplicate Task",
|
||||
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. " +
|
||||
"The AI triage agent will re-specify the new task.",
|
||||
promptSnippet: "Duplicate a Fusion task (creates copy in triage)",
|
||||
"The AI planning agent will replan the new task.",
|
||||
promptSnippet: "Duplicate a Fusion task (creates copy in planning)",
|
||||
promptGuidelines: [
|
||||
"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",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
@@ -615,13 +615,13 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
label: "fn: Refine Task",
|
||||
description:
|
||||
"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.",
|
||||
promptSnippet: "Create a refinement task for follow-up work on a completed task",
|
||||
promptGuidelines: [
|
||||
"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 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",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
@@ -739,7 +739,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
label: "fn: Import GitHub Issues",
|
||||
description:
|
||||
"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.",
|
||||
promptSnippet: "Import GitHub issues as Fusion tasks",
|
||||
promptGuidelines: [
|
||||
@@ -830,7 +830,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
label: "fn: Import GitHub Issue",
|
||||
description:
|
||||
"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",
|
||||
promptGuidelines: [
|
||||
"Use for importing a single known issue by its number",
|
||||
@@ -1055,7 +1055,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
content: [
|
||||
{
|
||||
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 },
|
||||
|
||||
@@ -70,7 +70,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isRespecifying, setIsRespecifying] = useState(false);
|
||||
const [isReplanning, setIsReplanning] = useState(false);
|
||||
const [isPausingAll, setIsPausingAll] = useState(false);
|
||||
const [isMovingAllToTodo, setIsMovingAllToTodo] = useState(false);
|
||||
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));
|
||||
}, [tasks.length]);
|
||||
|
||||
const handleRespecifyAll = useCallback(async () => {
|
||||
const handleReplanAll = useCallback(async () => {
|
||||
setIsMenuOpen(false);
|
||||
if (tasks.length === 0) return;
|
||||
|
||||
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;
|
||||
|
||||
setIsRespecifying(true);
|
||||
setIsReplanning(true);
|
||||
try {
|
||||
// Issue moves in parallel — onMoveTask is per-task, no bulk endpoint.
|
||||
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 moved = results.length - failed;
|
||||
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 {
|
||||
addToast(`Moved ${moved} of ${results.length} tasks; ${failed} failed`, "error");
|
||||
}
|
||||
} finally {
|
||||
setIsRespecifying(false);
|
||||
setIsReplanning(false);
|
||||
}
|
||||
}, [tasks, onMoveTask, addToast]);
|
||||
|
||||
const pauseEligibleTasks = useMemo(() => tasks.filter((task) => !task.paused), [tasks]);
|
||||
const pauseEligibleCount = pauseEligibleTasks.length;
|
||||
const hasColumnBulkActions = column === "todo" || column === "in-progress" || column === "in-review";
|
||||
const isMenuBusy = isRespecifying || isPausingAll || isMovingAllToTodo;
|
||||
const isMenuBusy = isReplanning || isPausingAll || isMovingAllToTodo;
|
||||
|
||||
const handlePauseAll = useCallback(async () => {
|
||||
if (!onPauseTask) return;
|
||||
@@ -332,12 +332,12 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="column-menu-item"
|
||||
onClick={() => void handleRespecifyAll()}
|
||||
disabled={tasks.length === 0 || isRespecifying}
|
||||
onClick={() => void handleReplanAll()}
|
||||
disabled={tasks.length === 0 || isReplanning}
|
||||
>
|
||||
Respecify All
|
||||
Replan All
|
||||
<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>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ const COLUMN_COLOR_MAP: Record<Column, string> = {
|
||||
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 SortDirection = "asc" | "desc";
|
||||
|
||||
@@ -882,7 +882,7 @@ export function SettingsModal({
|
||||
globalModelKey: "planningGlobalModelId",
|
||||
projectProviderKey: "planningProvider",
|
||||
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",
|
||||
},
|
||||
{
|
||||
@@ -2102,7 +2102,7 @@ export function SettingsModal({
|
||||
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 className="form-group">
|
||||
<label htmlFor="pollIntervalMs">Poll Interval (ms)</label>
|
||||
@@ -2144,9 +2144,9 @@ export function SettingsModal({
|
||||
setForm((f) => ({ ...f, specStalenessEnabled: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Enable specification staleness enforcement
|
||||
Enable plan staleness enforcement
|
||||
</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 className="form-group">
|
||||
<label htmlFor="specStalenessMaxAgeMs">Stale Spec Threshold (hours)</label>
|
||||
@@ -2163,7 +2163,7 @@ export function SettingsModal({
|
||||
}}
|
||||
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 className="form-group">
|
||||
<label htmlFor="autoArchiveDoneTasksEnabled" className="checkbox-label">
|
||||
|
||||
@@ -163,7 +163,7 @@ export function SpecEditor({
|
||||
<div className="spec-editor-revision">
|
||||
<h4>Ask AI to Revise</h4>
|
||||
<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>
|
||||
<textarea
|
||||
ref={feedbackRef}
|
||||
|
||||
@@ -81,7 +81,7 @@ function abbreviateBadge(text: string, max: number): string {
|
||||
|
||||
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> = {
|
||||
triage: "var(--triage)",
|
||||
|
||||
@@ -299,8 +299,8 @@ export function TaskDetailModal({
|
||||
const canRetryTask =
|
||||
task.status === "failed" ||
|
||||
task.status === "stuck-killed" ||
|
||||
task.status === "specifying" ||
|
||||
task.status === "needs-respecify" ||
|
||||
task.status === "planning" ||
|
||||
task.status === "needs-replan" ||
|
||||
(task.stuckKillCount ?? 0) > 0 ||
|
||||
(task.recoveryRetryCount ?? 0) > 0 ||
|
||||
Boolean(task.nextRecoveryAt);
|
||||
@@ -903,7 +903,7 @@ export function TaskDetailModal({
|
||||
if (!confirm("Reject this plan? The specification will be discarded and regenerated.")) return;
|
||||
try {
|
||||
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();
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err), "error");
|
||||
@@ -911,11 +911,11 @@ export function TaskDetailModal({
|
||||
}, [task.id, onClose, addToast]);
|
||||
|
||||
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 {
|
||||
await rebuildTaskSpec(task.id, projectId);
|
||||
onClose();
|
||||
addToast(`Respecifying ${task.id}...`, "info");
|
||||
addToast(`Replanning ${task.id}...`, "info");
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err), "error");
|
||||
}
|
||||
@@ -1152,8 +1152,8 @@ export function TaskDetailModal({
|
||||
setIsRequestingRevision(true);
|
||||
try {
|
||||
await requestSpecRevision(task.id, feedback, projectId);
|
||||
addToast("AI revision requested. Task moved to triage.", "success");
|
||||
// Task has been moved to triage, close modal
|
||||
addToast("AI revision requested. Task moved to planning.", "success");
|
||||
// Task has been moved to planning, close modal
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const msg = getErrorMessage(err);
|
||||
@@ -1730,7 +1730,7 @@ export function TaskDetailModal({
|
||||
<div className="spec-editor-revision">
|
||||
<h4>Ask AI to Revise</h4>
|
||||
<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>
|
||||
<textarea
|
||||
className="spec-editor-feedback"
|
||||
|
||||
@@ -374,10 +374,10 @@ describe("TaskDetailModal", () => {
|
||||
expect(retryButtons).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("shows Retry for a stranded specifying triage task", () => {
|
||||
it("shows Retry for a stranded planning triage task", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "triage", status: "specifying", stuckKillCount: 6 })}
|
||||
task={makeTask({ column: "triage", status: "planning", stuckKillCount: 6 })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -2992,7 +2992,7 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -3219,7 +3219,7 @@ describe("TaskDetailModal", () => {
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
column: "triage",
|
||||
status: "specifying",
|
||||
status: "planning",
|
||||
prompt: "# Task Spec",
|
||||
})}
|
||||
onClose={noop}
|
||||
@@ -3325,7 +3325,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(mockRejectPlan).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
"Plan rejected — FN-001 returned to Triage for re-specification",
|
||||
"Plan rejected — FN-001 returned to Planning for replanning",
|
||||
"info"
|
||||
);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
|
||||
@@ -1946,30 +1946,30 @@ describe("POST /tasks/:id/retry", () => {
|
||||
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 taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||
mkdirSync(taskDir, { recursive: true });
|
||||
writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n");
|
||||
|
||||
const specifyingTask = {
|
||||
const planningTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
column: "triage" as const,
|
||||
status: "specifying",
|
||||
status: "planning",
|
||||
stuckKillCount: 6,
|
||||
recoveryRetryCount: 2,
|
||||
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
};
|
||||
const retriedTask = {
|
||||
...specifyingTask,
|
||||
status: "needs-respecify",
|
||||
...planningTask,
|
||||
status: "needs-replan",
|
||||
stuckKillCount: 0,
|
||||
recoveryRetryCount: undefined,
|
||||
nextRecoveryAt: undefined,
|
||||
};
|
||||
|
||||
(store.getTask as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce(specifyingTask)
|
||||
.mockResolvedValueOnce(planningTask)
|
||||
.mockResolvedValueOnce(retriedTask);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(retriedTask);
|
||||
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
|
||||
@@ -1981,7 +1981,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: "needs-respecify",
|
||||
status: "needs-replan",
|
||||
error: null,
|
||||
worktree: null,
|
||||
branch: null,
|
||||
@@ -1995,9 +1995,9 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"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 {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -6916,7 +6916,7 @@ describe("POST /tasks/:id/spec/revise", () => {
|
||||
);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
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 {
|
||||
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 () => {
|
||||
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 taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||
mkdirSync(taskDir, { recursive: true });
|
||||
@@ -6973,7 +6973,7 @@ describe("POST /tasks/:id/spec/revise", () => {
|
||||
);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
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 {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -7149,7 +7149,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
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 {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -7167,7 +7167,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
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 () => {
|
||||
@@ -7186,7 +7186,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
|
||||
it("allows rebuild for task already in triage", async () => {
|
||||
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 taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||
mkdirSync(taskDir, { recursive: true });
|
||||
@@ -7208,7 +7208,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
);
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
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 {
|
||||
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 () => {
|
||||
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);
|
||||
|
||||
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 () => {
|
||||
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);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/reject-plan");
|
||||
|
||||
@@ -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) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
@@ -249,14 +249,14 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const retrySpecification =
|
||||
task.column === "triage" &&
|
||||
(task.status === "failed" ||
|
||||
task.status === "specifying" ||
|
||||
task.status === "needs-respecify" ||
|
||||
task.status === "planning" ||
|
||||
task.status === "needs-replan" ||
|
||||
(task.stuckKillCount ?? 0) > 0);
|
||||
if (task.status !== "failed" && task.status !== "stuck-killed" && !retrySpecification) {
|
||||
throw badRequest(`Task is not in a retryable state (current status: ${task.status || 'none'})`);
|
||||
}
|
||||
await scopedStore.updateTask(req.params.id, {
|
||||
status: retrySpecification ? "needs-respecify" : null,
|
||||
status: retrySpecification ? "needs-replan" : null,
|
||||
error: null,
|
||||
worktree: null,
|
||||
branch: null,
|
||||
@@ -272,7 +272,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const { join } = await import("node:path");
|
||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
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);
|
||||
res.json(updated);
|
||||
return;
|
||||
@@ -1110,20 +1110,20 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
|
||||
// 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") {
|
||||
// Log the revision request
|
||||
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.
|
||||
const { rm } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
// Update status to indicate needs replanning
|
||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
||||
|
||||
const updated = await scopedStore.getTask(task.id);
|
||||
res.json(updated);
|
||||
@@ -1141,18 +1141,18 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
// Log the revision request
|
||||
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");
|
||||
|
||||
// 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.
|
||||
const { rm } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
// Update status to indicate needs replanning
|
||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
||||
|
||||
res.json(updated);
|
||||
} catch (err: unknown) {
|
||||
@@ -1176,7 +1176,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
|
||||
// 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") {
|
||||
// Log the rebuild request
|
||||
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");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
// Update status to indicate needs replanning
|
||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
||||
|
||||
const updated = await scopedStore.getTask(task.id);
|
||||
res.json(updated);
|
||||
@@ -1205,7 +1205,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
// Log the rebuild request
|
||||
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");
|
||||
|
||||
// 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");
|
||||
await rm(promptPath, { force: true });
|
||||
|
||||
// Update status to indicate needs re-specification
|
||||
await scopedStore.updateTask(task.id, { status: "needs-respecify" });
|
||||
// Update status to indicate needs replanning
|
||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
||||
|
||||
res.json(updated);
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user