Address PR review feedback (#1890)

- Gate bootstrap prompt to entry column/triage only, not every non-execution
  column, so direct createTask({column:'todo'}) keeps generateSpecifiedPrompt.
- Guard the workflow-column hold-release dispatch path (reserveSlot) against
  planning-status and bootstrap-stub todo tasks, matching the legacy filter.
- Extend clearStaleSpecifyingStatuses startup sweep to the todo column so a
  restarted in-place planning task does not hold a maxTriageConcurrent slot.
- Gate the Start button on the intake column flag instead of the literal
  'ideas' id, so any manual-intake workflow gets the affordance.
- Add regression test: direct todo create must not get a bootstrap stub.
This commit is contained in:
gsxdsm
2026-07-04 15:12:55 -07:00
parent ecbbb29c2d
commit 3ba7b08ccb
5 changed files with 43 additions and 10 deletions

View File

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

View File

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

View File

@@ -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 = () => (

View File

@@ -2252,6 +2252,24 @@ export class Scheduler {
reserveSlot: async (task): Promise<SlotReservation | null> => {
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, {

View File

@@ -336,8 +336,13 @@ export class TriageProcessor {
}
private async clearStaleSpecifyingStatuses(): Promise<void> {
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) {