From d5f1ce7abd645c22fe5e8eb33b8db7bd96048ebf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 29 Jul 2026 20:58:52 -0700 Subject: [PATCH] U11 [writes]: stop CREATING cards into a column the workflow no longer declares (9 -> 0, engine+cli) (#2603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Taking: `engine/triage.ts`, `engine/pr-comment-handler.ts`, `engine/eval-followups.ts`, `cli/commands/task.ts`, `cli/extension.ts`** (write class — no collision with the comparison backlog). ## A class the census does not count The 48-guard work list tracks `=== "triage"` **comparisons**. These are `column: "triage"` **writes** — and post-#2515 every one creates a card directly into the state STALL 3 was about, except **manufactured continuously** rather than left behind by the upgrade. ## Why they bite `createTaskImpl` resolves the column as: ```ts column: input.column || options?.resolvedEntryColumn || fallbackIntakeColumn || "triage" ``` `input.column` **wins**, so an explicit `column: "triage"` overrides the workflow's resolved intake column entirely. `store-create-intake-column.test.ts` already pins that a create with **no** column lands in the default workflow's intake (now `todo`) — these callers opted out of it. The sharpest is `triage.ts`'s `fn_task_create` agent tool: it passed `workflowId: params.workflow_id` **and** `column: "triage"` in the same call. The caller chose a workflow and the column ignored it — a Coding (Ideas) create landed in `triage` instead of `ideas`. ## Counts **Comparison guards: unchanged by this PR.** This is the write class; conflating the two would misreport convergence toward the zero bar. | file | `column: "triage"` writes before | after | |---|---:|---:| | `packages/engine/src/triage.ts` | 1 | **0** | | `packages/engine/src/pr-comment-handler.ts` | 1 | **0** | | `packages/engine/src/eval-followups.ts` | 1 | **0** | | `packages/cli/src/commands/task.ts` | 3 | **0** | | `packages/cli/src/extension.ts` | 3 | **0** | | **total** | **9** | **0** | ## A test that pinned the defect `pr-comment-handler.test.ts` asserted `column: "triage"` in the createTask call — so it would have **failed the fix and passed the bug**. Rewritten to assert the invariant (the caller passes no column, so the workflow's intake wins) plus an explicit `Object.hasOwn(arg, "column") === false`, which is what actually catches a reintroduction. ## Interaction with #2591 My merged #2591 rescues these cards once created — they sit on a legacy planner id their workflow doesn't declare and are still in planning stage. So this isn't a *visible* stall today; the rescue absorbs it. **That's the reason to fix it rather than leave it:** a self-healing path silently absorbing a steady stream of malformed creates is exactly how the underlying defect stays invisible. ## Deliberately not touched - `{ id: "start", kind: "start", column: "triage" }` in the builtin coding / PR / lead-generation IRs — workflow-internal **node declarations** for workflows that still legitimately declare a `triage` column, not lifecycle writes. - Left for their owners: `core/task-store/project-store-ops.ts:210`, `core/task-store/update-task-deps.ts:111` (main worker), `dashboard/src/routes/register-gitlab.ts:108` (u12). Same defect, same one-line shape. ## Verification - 304 engine/CLI tests green across the affected suites - merge gate green (482 + 132 + 10), engine + CLI tsc clean, lint clean No changeset: `@fusion/engine` and `@fusion/core` are private; the CLI change is a bug fix with no user-facing API change — happy to add one if you'd rather it appear in release notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- packages/cli/src/commands/task.ts | 15 ++++++++++++--- packages/cli/src/extension.ts | 12 +++++++++--- .../src/__tests__/pr-comment-handler.test.ts | 14 +++++++++++++- packages/engine/src/eval-followups.ts | 5 ++++- packages/engine/src/pr-comment-handler.ts | 5 ++++- packages/engine/src/triage.ts | 5 ++++- 6 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/task.ts b/packages/cli/src/commands/task.ts index 6c3c16693f..cccc69f719 100644 --- a/packages/cli/src/commands/task.ts +++ b/packages/cli/src/commands/task.ts @@ -1604,7 +1604,10 @@ export async function runTaskImportGitHubInteractive( const task = await retryBoardCall(context, "import", "create task", () => store.createTask({ title: title || undefined, description, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [], sourceIssue: source.sourceIssue, source: { @@ -1777,7 +1780,10 @@ export async function runTaskImportFromGitHub( const task = await retryBoardCall(context, "import", "create task", () => store.createTask({ title: title || undefined, description, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [], sourceIssue: source.sourceIssue, source: { @@ -1852,7 +1858,10 @@ export async function runTaskImportFromGitLab( const task = await retryBoardCall(context, "import", "create task", () => store.createTask({ title: title || undefined, description: dashboard.buildGitLabTaskDescription(item), - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [], sourceIssue: provenance.sourceIssue, gitlabTracking: provenance.gitlabTracking, diff --git a/packages/cli/src/extension.ts b/packages/cli/src/extension.ts index 4515447f80..c6053172b3 100644 --- a/packages/cli/src/extension.ts +++ b/packages/cli/src/extension.ts @@ -2298,7 +2298,10 @@ export default function kbExtension(pi: ExtensionAPI) { const task = await store.createTask({ title: title || undefined, description, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [], sourceIssue: source.sourceIssue, source: { @@ -2393,7 +2396,10 @@ export default function kbExtension(pi: ExtensionAPI) { const task = await store.createTask({ title: title || undefined, description, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [], sourceIssue: source.sourceIssue, source: { @@ -2530,7 +2536,7 @@ export default function kbExtension(pi: ExtensionAPI) { const provenance = dashboard.buildGitLabTaskProvenance({ auth: client.auth, resourceType, item, projectInput: resourceType !== "group_issue" ? target : undefined, groupInput: resourceType === "group_issue" ? target : undefined }); if (existingTasks.some((task) => dashboard.isGitLabAlreadyImported(task, provenance))) continue; const title = resourceType === "merge_request" ? `Review MR !${item.iid}: ${item.title.slice(0, 180)}` : item.title.slice(0, 200); - const task = await store.createTask({ title: title || undefined, description: dashboard.buildGitLabTaskDescription(item), column: "triage", dependencies: [], sourceIssue: provenance.sourceIssue, gitlabTracking: provenance.gitlabTracking, source: { sourceType: "gitlab_import", sourceMetadata: provenance.sourceMetadata } }); + const task = await store.createTask({ title: title || undefined, description: dashboard.buildGitLabTaskDescription(item), dependencies: [], sourceIssue: provenance.sourceIssue, gitlabTracking: provenance.gitlabTracking, source: { sourceType: "gitlab_import", sourceMetadata: provenance.sourceMetadata } }); await store.logEntry(task.id, resourceType === "merge_request" ? "Imported merge request from GitLab" : "Imported from GitLab", item.webUrl); existingTasks.push(task); createdTasks.push({ id: task.id, title: task.title || item.title }); diff --git a/packages/engine/src/__tests__/pr-comment-handler.test.ts b/packages/engine/src/__tests__/pr-comment-handler.test.ts index 863fe9c9c8..f87c2ea0f4 100644 --- a/packages/engine/src/__tests__/pr-comment-handler.test.ts +++ b/packages/engine/src/__tests__/pr-comment-handler.test.ts @@ -245,10 +245,20 @@ describe("PrCommentHandler", () => { }, ]); + /* + FNXC:WorkflowLifecycleColumns 2026-07-29-20:40 (U11): + This asserted `column: "triage"` and so PINNED the defect. `createTaskImpl` + resolves the column as `input.column || resolvedEntryColumn || fallbackIntake + || "triage"`, so an explicit column OVERRIDES the workflow's intake — and + after #2515 `triage` is not a column the default lineage declares, so the + follow-up was created straight into the stranded state. + + Now asserts the invariant instead of the id: the caller passes NO column, so + whatever intake the task's workflow declares is what wins. + */ expect(mockStore.createTask).toHaveBeenCalledWith({ title: "Follow-up: Address PR #42 feedback", description: expect.stringContaining("FN-001"), - column: "triage", dependencies: ["FN-001"], source: { sourceType: "api", @@ -259,6 +269,8 @@ describe("PrCommentHandler", () => { }, }, }); + const [createArg] = (mockStore.createTask as unknown as { mock: { calls: [Record][] } }).mock.calls[0]; + expect(Object.hasOwn(createArg, "column")).toBe(false); }); it("does nothing when no unaddressed comments", async () => { diff --git a/packages/engine/src/eval-followups.ts b/packages/engine/src/eval-followups.ts index 7e9e0fdf91..8183823218 100644 --- a/packages/engine/src/eval-followups.ts +++ b/packages/engine/src/eval-followups.ts @@ -230,7 +230,10 @@ export async function materializeEvalFollowUps(input: MaterializeEvalFollowUpsIn `Rationale: ${followUp.rationale}`, `Evidence refs: ${followUp.evidenceRefs.map((ref) => ref.evidenceId).join(", ") || "none"}`, ].join("\n"), - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ priority: followUp.priority, source: { sourceType: "automation", diff --git a/packages/engine/src/pr-comment-handler.ts b/packages/engine/src/pr-comment-handler.ts index d8cd398dda..e0436db413 100644 --- a/packages/engine/src/pr-comment-handler.ts +++ b/packages/engine/src/pr-comment-handler.ts @@ -251,7 +251,10 @@ Please review the PR comments and address any remaining issues.`; const task = await this.store.createTask({ title: `Follow-up: Address PR #${prInfo.number} feedback`, description, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ dependencies: [originalTaskId], source: { sourceType: "api", diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 4144ac2bcb..73e4a0e452 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -3278,7 +3278,10 @@ export class TriageProcessor { title: params.title, description: params.description, dependencies: validDeps, - column: "triage", + /* FNXC:WorkflowLifecycleColumns 2026-07-29-20:15 (U11): no explicit column — + `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would + override it. Hard-coding `"triage"` created the card in a column the default + lineage no longer declares (#2515), i.e. straight into the stranded state. */ priority: params.priority, workflowId: params.workflow_id, noCommitsExpected: params.noCommitsExpected,