U11 [writes]: stop CREATING cards into a column the workflow no longer declares (9 -> 0, engine+cli) (#2603)

**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)
This commit is contained in:
gsxdsm
2026-07-29 20:58:52 -07:00
committed by GitHub
parent 9c1c6f7479
commit d5f1ce7abd
6 changed files with 46 additions and 10 deletions

View File

@@ -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,

View File

@@ -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 });

View File

@@ -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<string, unknown>][] } }).mock.calls[0];
expect(Object.hasOwn(createArg, "column")).toBe(false);
});
it("does nothing when no unaddressed comments", async () => {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,