diff --git a/packages/cli/skill/fusion/references/engine-tools.md b/packages/cli/skill/fusion/references/engine-tools.md index 49e4cfe492..bf1ce2eddc 100644 --- a/packages/cli/skill/fusion/references/engine-tools.md +++ b/packages/cli/skill/fusion/references/engine-tools.md @@ -17,6 +17,11 @@ These tools are **not** part of the user-invokable extension surface. They are i | `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) | | `fn_workflow_list` | executor | List the project's custom workflows (read-only built-ins plus user definitions) | none | | `fn_workflow_select` | executor | Assign a custom workflow to a task (defaults to the current task) | `workflow_id` (string), `task_id?` (string) | +| `fn_workflow_create` | executor | Create a custom workflow definition from a graph IR (validated server-side) | `name` (string), `description?` (string), `ir` (object), `layout?` (object) | +| `fn_workflow_update` | executor | Update a custom workflow definition's name/description/ir/layout (built-ins cannot be edited) | `workflow_id` (string), `name?` (string), `description?` (string), `ir?` (object), `layout?` (object), `rehome_to?` (string) | +| `fn_workflow_delete` | executor | Delete a custom workflow definition (built-ins cannot be deleted); selecting tasks are re-homed to the default workflow's entry column | `workflow_id` (string) | +| `fn_task_promote` | executor | Promote a held task out of a manual-release hold column (defaults to the current task) | `task_id?` (string) | +| `fn_trait_list` | executor | List the registered column trait catalog (built-in and plugin traits) | none | | `fn_memory_search` | triage, executor, heartbeat | Search project memory plus per-agent layered memory snippets | `query` (string), `limit?` (number) | | `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window (including bounded per-agent layered paths) | `path` (string), `startLine?` (number), `lineCount?` (number) | | `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append memory notes with explicit scope: `scope="agent"` for private operating context, `scope="project"` for workspace-wide durable knowledge | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) | diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index e50dcc9130..896d611b7c 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -144,7 +144,7 @@ function isAgentCreatedTask(task: Task): boolean { // ── Constants ─────────────────────────────────────────────────────────────── -// #1403: widened to ColumnId so `.has(task.column)` accepts custom column ids +// Issue 1403: widened to ColumnId so `.has(task.column)` accepts custom column ids // (which are not members and correctly resolve to false). const EDITABLE_COLUMNS: Set = new Set(["triage", "todo"]); @@ -2004,7 +2004,7 @@ function TaskCardComponent({ className="card-progress-fill" style={{ width: `${progressPercent}%`, - // #1403: custom columns have no legacy progress color → fall back to accent. + // Issue 1403: custom columns have no legacy progress color → fall back to accent. backgroundColor: (COLUMN_PROGRESS_COLOR_MAP as Record)[task.column] ?? "var(--accent)", }} diff --git a/packages/engine/src/__tests__/project-runtime.test.ts b/packages/engine/src/__tests__/project-runtime.test.ts index b0d06cbeda..ac766aecdb 100644 --- a/packages/engine/src/__tests__/project-runtime.test.ts +++ b/packages/engine/src/__tests__/project-runtime.test.ts @@ -10,6 +10,12 @@ vi.mock("@earendil-works/pi-ai", () => ({ Array: (schema: unknown, opts?: unknown) => ({ type: "array", items: schema, ...((opts as object) ?? {}) }), Union: (schemas: unknown[], opts?: unknown) => ({ anyOf: schemas, ...((opts as object) ?? {}) }), Literal: (value: unknown) => ({ const: value }), + Unknown: (opts?: unknown) => ({ ...((opts as object) ?? {}) }), + Record: (_key: unknown, value: unknown, opts?: unknown) => ({ + type: "object", + additionalProperties: value, + ...((opts as object) ?? {}), + }), }, })); diff --git a/packages/engine/src/__tests__/reliability-interactions/executor-no-task-done-vs-worktree-reclaim.test.ts b/packages/engine/src/__tests__/reliability-interactions/executor-no-task-done-vs-worktree-reclaim.test.ts index 20fdc28c62..4010c44eb6 100644 --- a/packages/engine/src/__tests__/reliability-interactions/executor-no-task-done-vs-worktree-reclaim.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/executor-no-task-done-vs-worktree-reclaim.test.ts @@ -101,7 +101,7 @@ describe("reliability interactions: executor no-fn_task_done vs worktree reclaim branch: null, worktreeSessionRetryCount: 1, })); - expect(store.moveTask).toHaveBeenCalledWith("FN-4601", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-4601", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); // FN-4806: session-start missing-worktree is engine self-heal, must not burn retry budget // and must not mark the task failed. expect(store.updateTask).not.toHaveBeenCalledWith( diff --git a/packages/engine/src/__tests__/reliability-interactions/in-progress-limbo-recovery.test.ts b/packages/engine/src/__tests__/reliability-interactions/in-progress-limbo-recovery.test.ts index 664950f3a5..de592e2dd2 100644 --- a/packages/engine/src/__tests__/reliability-interactions/in-progress-limbo-recovery.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/in-progress-limbo-recovery.test.ts @@ -82,7 +82,7 @@ describe("FN-5219 reliability interactions: in-progress limbo recovery", () => { expect(first).toBe(1); expect(second).toBe(0); - expect(mockStore.moveTask).toHaveBeenCalledWith("FN-5149", "todo", { preserveProgress: true }); + expect(mockStore.moveTask).toHaveBeenCalledWith("FN-5149", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); expect(mockStore.recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({ mutationType: "task:auto-recover-in-progress-limbo", target: "FN-5149", diff --git a/packages/engine/src/__tests__/reliability-interactions/worktree-incomplete-session-start.test.ts b/packages/engine/src/__tests__/reliability-interactions/worktree-incomplete-session-start.test.ts index cd7d2bb0e3..7a2ec1993a 100644 --- a/packages/engine/src/__tests__/reliability-interactions/worktree-incomplete-session-start.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/worktree-incomplete-session-start.test.ts @@ -87,8 +87,7 @@ describe("reliability interactions: FN-4917 worktree incomplete session-start", }), })); - expect(store.moveTask.mock.calls).toContainEqual(["FN-4917-T", "todo"]); - expect(store.moveTask.mock.calls.some((call: any[]) => call.length > 2)).toBe(false); + expect(store.moveTask.mock.calls).toContainEqual(["FN-4917-T", "todo", { moveSource: "engine", recoveryRehome: true }]); for (const call of store.logEntry.mock.calls) { const leaked = call.some((arg: unknown) => typeof arg === "string" && /Refusing to start coding agent/.test(arg)); expect(leaked).toBe(false); @@ -113,7 +112,7 @@ describe("reliability interactions: FN-4917 worktree incomplete session-start", await runRecovery(store, task, "Refusing to start coding agent in incomplete worktree: /tmp/wt", events); - expect(store.moveTask).toHaveBeenCalledWith("FN-4917-T", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-4917-T", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); expect(store.moveTask.mock.calls).not.toContainEqual(["FN-4917-T", "todo"]); for (const call of store.logEntry.mock.calls) { const leaked = call.some((arg: unknown) => typeof arg === "string" && /Refusing to start coding agent/.test(arg)); diff --git a/packages/engine/src/__tests__/restart.integration.test.ts b/packages/engine/src/__tests__/restart.integration.test.ts index cbdcc1ece6..9df703eab2 100644 --- a/packages/engine/src/__tests__/restart.integration.test.ts +++ b/packages/engine/src/__tests__/restart.integration.test.ts @@ -235,6 +235,12 @@ vi.mock("@earendil-works/pi-ai", () => ({ Array: (schema: unknown, opts?: unknown) => ({ type: "array", items: schema, ...((opts as object) ?? {}) }), Union: (schemas: unknown[], opts?: unknown) => ({ anyOf: schemas, ...((opts as object) ?? {}) }), Literal: (value: unknown) => ({ const: value }), + Unknown: (opts?: unknown) => ({ ...((opts as object) ?? {}) }), + Record: (_key: unknown, value: unknown, opts?: unknown) => ({ + type: "object", + additionalProperties: value, + ...((opts as object) ?? {}), + }), }, })); vi.mock("@earendil-works/pi-coding-agent", () => { diff --git a/packages/engine/src/__tests__/self-healing-in-progress-limbo.test.ts b/packages/engine/src/__tests__/self-healing-in-progress-limbo.test.ts index fb0d501f20..5ef1222233 100644 --- a/packages/engine/src/__tests__/self-healing-in-progress-limbo.test.ts +++ b/packages/engine/src/__tests__/self-healing-in-progress-limbo.test.ts @@ -118,7 +118,7 @@ describe("recoverInProgressLimbo", () => { taskDoneRetryCount: null, sessionFile: null, })); - expect(store.moveTask).toHaveBeenCalledWith("FN-5149", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-5149", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); expect(store.recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({ domain: "database", mutationType: "task:auto-recover-in-progress-limbo", diff --git a/packages/engine/src/__tests__/self-healing.test.ts b/packages/engine/src/__tests__/self-healing.test.ts index 6121e773f9..12813ab527 100644 --- a/packages/engine/src/__tests__/self-healing.test.ts +++ b/packages/engine/src/__tests__/self-healing.test.ts @@ -458,6 +458,8 @@ describe("SelfHealingManager", () => { expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true, preserveStatus: true, + moveSource: "engine", + recoveryRehome: true, }); expect(store.updateTask).toHaveBeenLastCalledWith("FN-001", expect.objectContaining({ stuckKillCount: 7, @@ -494,6 +496,8 @@ describe("SelfHealingManager", () => { expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true, preserveStatus: true, + moveSource: "engine", + recoveryRehome: true, }); expect(store.updateTask).not.toHaveBeenCalledWith("FN-001", expect.objectContaining({ paused: false, @@ -530,6 +534,8 @@ describe("SelfHealingManager", () => { expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { preserveProgress: true, preserveStatus: true, + moveSource: "engine", + recoveryRehome: true, }); expect(store.logEntry).toHaveBeenCalledWith( "FN-001", @@ -1364,7 +1370,7 @@ describe("SelfHealingManager", () => { "FN-1473", expect.stringContaining("no-progress no-task_done failure"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-1473", "todo"); + expect(store.moveTask).toHaveBeenCalledWith("FN-1473", "todo", { moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -2233,7 +2239,7 @@ describe("SelfHealingManager", () => { "FN-3900", expect.stringContaining("session-start unusable-worktree assertion"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-3900", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-3900", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -2277,7 +2283,7 @@ describe("SelfHealingManager", () => { "FN-4559", expect.stringContaining("session-start unusable-worktree assertion"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-4559", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-4559", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -2309,7 +2315,7 @@ describe("SelfHealingManager", () => { "FN-4560", expect.stringContaining("session-start unusable-worktree assertion"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-4560", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-4560", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -2349,7 +2355,7 @@ describe("SelfHealingManager", () => { "FN-4651", expect.stringContaining("Auto-recovered (no-progress): session-start refused unusable worktree"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-4651", "todo"); + expect(store.moveTask).toHaveBeenCalledWith("FN-4651", "todo", { moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -2574,7 +2580,7 @@ describe("SelfHealingManager", () => { "FN-2164", expect.stringContaining("Auto-retry 1/3"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-2164", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-2164", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -3912,7 +3918,7 @@ describe("SelfHealingManager", () => { "FN-1572", expect.stringContaining("in-review task still had incomplete steps"), ); - expect(store.moveTask).toHaveBeenCalledWith("FN-1572", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-1572", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -3971,7 +3977,7 @@ describe("SelfHealingManager", () => { const result = await managerWithRecovery.recoverStaleIncompleteReviewTasks(); expect(result).toBe(1); - expect(store.moveTask).toHaveBeenCalledWith("FN-407-test-1", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-407-test-1", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -3999,7 +4005,7 @@ describe("SelfHealingManager", () => { const result = await managerWithRecovery.recoverStaleIncompleteReviewTasks(); expect(result).toBe(1); - expect(store.moveTask).toHaveBeenCalledWith("FN-407-test-2", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-407-test-2", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); }); @@ -5808,7 +5814,7 @@ describe("SelfHealingManager", () => { expect(result).toBe(1); expect(store.updateTask).not.toHaveBeenCalled(); - expect(store.moveTask).toHaveBeenCalledWith("FN-9003", "todo", { preserveProgress: true }); + expect(store.moveTask).toHaveBeenCalledWith("FN-9003", "todo", { preserveProgress: true, moveSource: "engine", recoveryRehome: true }); managerWithRecovery.stop(); });