diff --git a/packages/core/src/__tests__/store-create-intake-column.test.ts b/packages/core/src/__tests__/store-create-intake-column.test.ts index faf6160bbf..02b9a14452 100644 --- a/packages/core/src/__tests__/store-create-intake-column.test.ts +++ b/packages/core/src/__tests__/store-create-intake-column.test.ts @@ -48,4 +48,16 @@ describe("createTask intake-column wiring (Coding (Ideas))", () => { ); expect(prompt).toBe(`# ${task.id}\n\n${task.description}\n`); }); + + it("keeps generateSpecifiedPrompt for a direct create into todo (not bootstrap)", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "direct todo create", column: "todo" }); + expect(task.column).toBe("todo"); + const prompt = await readFile( + join(harness.rootDir(), ".fusion", "tasks", task.id, "PROMPT.md"), + "utf-8", + ); + // A direct todo create is NOT an intake column, so it must NOT get the bootstrap stub. + expect(prompt).not.toBe(`# ${task.id}\n\n${task.description}\n`); + }); }); diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 6157c6ffec..19c9807837 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -5060,15 +5060,13 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} if (this.isWatching) this.taskCache.set(id, { ...task }); /* - FNXC:CodingIdeasWorkflow 2026-07-04-10:10: - A freshly created task has no specification yet regardless of which intake/planning column it lands in (triage, ideas, or a merged todo). Use the bootstrap stub for every pre-execution column so the spec-detection helpers (isBootstrapPromptStub) treat the card as unplanned until triage replaces it with a real PROMPT.md. Execution/review/done/archived columns keep the generated specified prompt for the legacy direct-create paths. + FNXC:CodingIdeasWorkflow 2026-07-04-10:10 (revised): + A freshly created task needs the bootstrap stub only when it lands in a column the triage service will plan from — the legacy "triage" intake or a workflow's resolved manual intake (e.g. Coding (Ideas) → "ideas"). Gate on those two ids so legacy direct-create callers that pass an explicit column like "todo" still get the generated specified prompt. A task whose column is neither the entry column nor "triage" (custom hold/backlog columns, direct todo creates) keeps generateSpecifiedPrompt. */ - const isPrePlanningColumn = task.column !== "in-progress" - && task.column !== "in-review" - && task.column !== "done" - && task.column !== "archived"; + const isIntakeColumn = task.column === "triage" + || (options?.resolvedEntryColumn !== undefined && task.column === options.resolvedEntryColumn); const prompt = options?.promptOverride - ?? (isPrePlanningColumn + ?? (isIntakeColumn ? buildBootstrapPrompt(id, task.title, task.description) : this.generateSpecifiedPrompt(task)); const validation = validateFileScopeInPromptContent(prompt); diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index d7c53dede1..50566bd3c6 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1407,7 +1407,7 @@ function TaskCardComponent({ || Boolean(task.blockedBy) || Boolean(task.overlapBlockedBy) || Boolean(fanout && fanout.totalCount > 0); - const showStartAction = task.column === "ideas" && Boolean(onMoveTask); + const showStartAction = taskColumnFlags?.intake === true && task.column !== "triage" && Boolean(onMoveTask); const shouldRenderActionRow = Boolean(onPromote) || showCreatePrQuickAction || showAddressPrFeedbackAction || showStartAction || (showInReviewMoveControl && !metaRowVisible); const renderInReviewMoveControl = () => ( diff --git a/packages/engine/src/scheduler.ts b/packages/engine/src/scheduler.ts index a8d3d927d5..283b8c594d 100644 --- a/packages/engine/src/scheduler.ts +++ b/packages/engine/src/scheduler.ts @@ -2252,6 +2252,24 @@ export class Scheduler { reserveSlot: async (task): Promise => { let reservedScope = false; + /* + FNXC:CodingIdeasWorkflow 2026-07-04-12:10: + The workflow-column dispatch path is the only dispatcher when the flag is on, so the planning/bootstrap guards from the legacy todo filter must also apply here. A todo task being specified in place (status "planning") or still carrying the bootstrap stub PROMPT.md must not be released into an execution slot. + */ + if (task.status === "planning") { + return null; + } + if (task.column === "todo") { + try { + const promptContent = await readFile(getPromptPath(this.store.getTasksDir(), task.id), "utf-8"); + if (promptContent === buildBootstrapPrompt(task.id, task.title, task.description)) { + return null; + } + } catch { + // Missing prompt handled by filesystem validation below. + } + } + const unmetDeps = getUnmetSchedulingDependencies(task, tasks, schedulingDependencyOptions); if (unmetDeps.length > 0) { await this.store.updateTask(task.id, { diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index a63867dd75..76d8fd41b8 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -336,8 +336,13 @@ export class TriageProcessor { } private async clearStaleSpecifyingStatuses(): Promise { - const tasks = await this.store.listTasks({ column: "triage", slim: true }); - const stale = tasks.filter( + /* + FNXC:CodingIdeasWorkflow 2026-07-04-12:00: + In the merged planner/capacity "todo" column a task can carry status "planning" when the triage service is specifying it in place. A crash/restart before planning completes leaves that status set, so the startup sweep must clear it from BOTH triage and todo — otherwise a stale planning todo task permanently occupies a maxTriageConcurrent slot and blocks new triage work. + */ + const triageTasks = await this.store.listTasks({ column: "triage", slim: true }); + const todoTasks = await this.store.listTasks({ column: "todo", slim: true }); + const stale = [...triageTasks, ...todoTasks].filter( (t) => t.status === "planning" && !this.processing.has(t.id), ); for (const t of stale) {