From ecbbb29c2d5e64b647472428e9d3d598c5938992 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 4 Jul 2026 00:12:39 -0700 Subject: [PATCH] feat: add Coding (Ideas) workflow with manual Ideas intake and merged Todo planner column Add builtin:coding-ideas, a capture-first variant of the default coding pipeline. New cards land in a manual Ideas intake (autoTriage:false) and are not auto-planned until an operator promotes them into the merged Todo planner+capacity column, where the triage service plans them in place. Engine foundation: - createTask lands cards in the workflow intake column (resolvedEntryColumn) instead of hardcoding triage; default workflow is byte-identical. - Triage poll discovers unplanned todo tasks (bootstrap-stub prompt) and plans them in place; finalizeApprovedTask skips the redundant move. - Scheduler skips todo tasks that are planning or still carry a bootstrap prompt, so unplanned cards are never dispatched. Dashboard: - Start button on ideas cards (ideas -> todo move triggers planning). - Ready badge on planned todo tasks waiting for an in-progress slot. - ideas column label in board-workflows. Tests: workflow IR round-trip/column/node-placement, createTask intake wiring, and updated builtin catalog order assertion. --- .changeset/fn-coding-ideas-workflow.md | 7 ++ .../builtin-coding-ideas-workflow-ir.test.ts | 102 ++++++++++++++++++ .../src/__tests__/builtin-workflows.test.ts | 2 +- .../store-create-intake-column.test.ts | 51 +++++++++ .../src/builtin-coding-ideas-workflow-ir.ts | 93 ++++++++++++++++ packages/core/src/builtin-workflows.ts | 37 +++++++ packages/core/src/index.ts | 1 + packages/core/src/store.ts | 28 ++++- .../dashboard/app/components/TaskCard.tsx | 40 ++++++- .../dashboard/src/routes/board-workflows.ts | 1 + packages/engine/src/scheduler.ts | 21 ++++ packages/engine/src/triage.ts | 36 ++++++- 12 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 .changeset/fn-coding-ideas-workflow.md create mode 100644 packages/core/src/__tests__/builtin-coding-ideas-workflow-ir.test.ts create mode 100644 packages/core/src/__tests__/store-create-intake-column.test.ts create mode 100644 packages/core/src/builtin-coding-ideas-workflow-ir.ts diff --git a/.changeset/fn-coding-ideas-workflow.md b/.changeset/fn-coding-ideas-workflow.md new file mode 100644 index 0000000000..4f8fa8f9e1 --- /dev/null +++ b/.changeset/fn-coding-ideas-workflow.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. +category: feature +dev: New `builtin:coding-ideas` clones the default stepwise pipeline with an `ideas` intake (autoTriage:false) in front of a merged `todo` planner+capacity column. createTask lands cards in the workflow's intake column; the triage service plans unplanned todo tasks in place; the scheduler skips bootstrap-prompt todo tasks; TaskCard gains a Start button and a Ready badge. diff --git a/packages/core/src/__tests__/builtin-coding-ideas-workflow-ir.test.ts b/packages/core/src/__tests__/builtin-coding-ideas-workflow-ir.test.ts new file mode 100644 index 0000000000..89f3500fbf --- /dev/null +++ b/packages/core/src/__tests__/builtin-coding-ideas-workflow-ir.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { + BUILTIN_CODING_IDEAS_WORKFLOW_IR, + parseWorkflowIr, + serializeWorkflowIr, + getBuiltinWorkflow, + resolveEntryColumnId, +} from "../index.js"; +import { resolveColumnFlags } from "../trait-registry.js"; +import type { WorkflowIrV2 } from "../workflow-ir-types.js"; + +describe("builtin coding-ideas workflow ir", () => { + it("parses and round-trips", () => { + const parsed = parseWorkflowIr(BUILTIN_CODING_IDEAS_WORKFLOW_IR); + const reparsed = parseWorkflowIr(serializeWorkflowIr(parsed)); + expect(reparsed).toEqual(parsed); + expect(parsed.version).toBe("v2"); + }); + + it("is registered in the builtin catalog as a selectable workflow", () => { + const workflow = getBuiltinWorkflow("builtin:coding-ideas"); + expect(workflow).toBeDefined(); + expect(workflow!.id).toBe("builtin:coding-ideas"); + expect(workflow!.name).toBe("Coding (Ideas)"); + expect(workflow!.kind).toBe("workflow"); + expect(workflow!.ir).toBe(BUILTIN_CODING_IDEAS_WORKFLOW_IR); + }); + + it("declares the five-stage Ideas → Todo → In-progress → In-review → Done board shape plus archived", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + expect(ir.columns.map((c) => c.id)).toEqual([ + "ideas", + "todo", + "in-progress", + "in-review", + "done", + "archived", + ]); + }); + + it("makes the ideas column the manual (autoTriage:false) intake", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + const ideas = ir.columns.find((c) => c.id === "ideas")!; + expect(resolveColumnFlags(ideas).intake).toBe(true); + const intakeTrait = ideas.traits.find((t) => t.trait === "intake")!; + expect(intakeTrait.config).toEqual({ autoTriage: false }); + // The entry column resolves to ideas (the intake column). + expect(resolveEntryColumnId(ir)).toBe("ideas"); + }); + + it("merges the planner and capacity-hold stages into the todo column", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + const todo = ir.columns.find((c) => c.id === "todo")!; + const flags = resolveColumnFlags(todo); + expect(flags.hold).toBe(true); + expect(flags.resetOnEntry).toBe(true); + }); + + it("keeps the in-progress / in-review / done column traits from the default pipeline", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + expect(resolveColumnFlags(ir.columns.find((c) => c.id === "in-progress")!)).toMatchObject({ + countsTowardWip: true, + abortOnExit: true, + timing: true, + }); + expect(resolveColumnFlags(ir.columns.find((c) => c.id === "in-review")!)).toMatchObject({ + mergeBlocker: true, + humanReview: true, + stallDetection: true, + mergeOrchestration: true, + }); + expect(resolveColumnFlags(ir.columns.find((c) => c.id === "done")!).complete).toBe(true); + }); + + it("places the start node in ideas and the planning nodes in the merged todo column", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + const nodeColumn = (id: string) => ir.nodes.find((n) => n.id === id)?.column; + expect(nodeColumn("start")).toBe("ideas"); + expect(nodeColumn("plan")).toBe("todo"); + expect(nodeColumn("plan-review")).toBe("todo"); + expect(nodeColumn("plan-replan")).toBe("todo"); + }); + + it("retains the default-on optional plan/code review groups from the default coding graph", () => { + const workflow = getBuiltinWorkflow("builtin:coding-ideas")!; + const byId = new Map(workflow.ir.nodes.map((n) => [n.id, n])); + const planReview = byId.get("plan-review"); + expect(planReview?.kind).toBe("optional-group"); + expect(planReview?.config?.defaultOn).toBe(true); + const codeReview = byId.get("code-review"); + expect(codeReview?.kind).toBe("optional-group"); + expect(codeReview?.config?.defaultOn).toBe(true); + }); + + it("never leaves a node in a column the workflow does not declare", () => { + const ir = BUILTIN_CODING_IDEAS_WORKFLOW_IR as WorkflowIrV2; + const declared = new Set(ir.columns.map((c) => c.id)); + for (const node of ir.nodes) { + expect(declared.has(node.column!), `node ${node.id} in undeclared column ${node.column}`).toBe(true); + } + }); +}); diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index b52d4a5146..c695958ccf 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -672,10 +672,10 @@ describe("built-in workflows", () => { expect(defaultEnabledBuiltinWorkflowIds().length).toBeGreaterThanOrEqual(5); expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([ "builtin:coding", + "builtin:coding-ideas", "builtin:legacy-coding", "builtin:quick-fix", "builtin:review-heavy", - "builtin:marketing", ]); expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:stepwise-coding"); }); diff --git a/packages/core/src/__tests__/store-create-intake-column.test.ts b/packages/core/src/__tests__/store-create-intake-column.test.ts new file mode 100644 index 0000000000..faf6160bbf --- /dev/null +++ b/packages/core/src/__tests__/store-create-intake-column.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { Task } from "../types.js"; +import { createTaskStoreTestHarness } from "./store-test-helpers.js"; + +/* +FNXC:CodingIdeasWorkflow 2026-07-04-11:30: +Pin the createTask intake-column wiring: a task created against the Coding (Ideas) workflow (manual autoTriage:false intake) must land in the "ideas" column, not the legacy "triage" default, while the default Coding workflow keeps landing cards in "triage". +*/ +describe("createTask intake-column wiring (Coding (Ideas))", () => { + const harness = createTaskStoreTestHarness(); + + beforeEach(harness.beforeEach); + afterEach(harness.afterEach); + + it("lands a default-workflow task in triage (byte-identical regression guard)", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "default workflow task" }); + expect(task.column).toBe("triage"); + }); + + it("lands a Coding (Ideas) task in the ideas intake column when selected explicitly", async () => { + const store = harness.store(); + const task = await store.createTask({ + description: "ideas workflow task", + workflowId: "builtin:coding-ideas", + }); + expect(task.column).toBe("ideas"); + }); + + it("lands a Coding (Ideas) task in ideas when it is the project default workflow", async () => { + const store = harness.store(); + await store.setDefaultWorkflowId("builtin:coding-ideas"); + const task = await store.createTask({ description: "default ideas task" }); + expect(task.column).toBe("ideas"); + }); + + it("writes a bootstrap PROMPT.md for an ideas-column task (unplanned)", async () => { + const store = harness.store(); + const task: Task = await store.createTask({ + description: "ideas bootstrap prompt task", + workflowId: "builtin:coding-ideas", + }); + const prompt = await readFile( + join(harness.rootDir(), ".fusion", "tasks", task.id, "PROMPT.md"), + "utf-8", + ); + expect(prompt).toBe(`# ${task.id}\n\n${task.description}\n`); + }); +}); diff --git a/packages/core/src/builtin-coding-ideas-workflow-ir.ts b/packages/core/src/builtin-coding-ideas-workflow-ir.ts new file mode 100644 index 0000000000..6e38478b5c --- /dev/null +++ b/packages/core/src/builtin-coding-ideas-workflow-ir.ts @@ -0,0 +1,93 @@ +import type { WorkflowIr, WorkflowIrColumn, WorkflowIrV2 } from "./workflow-ir-types.js"; +import { parseWorkflowIr } from "./workflow-ir.js"; +import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "./builtin-stepwise-final-review-coding-workflow-ir.js"; +import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; + +/* +FNXC:CodingIdeasWorkflow 2026-07-04-09:15: +Operators need a manual-capture intake ("Ideas") in front of the default coding pipeline so they can park tasks without the engine auto-planning them. This workflow clones the current default Coding graph (stepwise execution + final review) and swaps the board columns to a five-stage Ideas → Todo → In-progress → In-review → Done shape. + +FNXC:CodingIdeasWorkflow 2026-07-04-09:18: +The "Ideas" column is the intake column with autoTriage disabled. Tasks created into this workflow land there and are NOT picked up by the triage service until an operator moves them to "Todo" (the merged planner + capacity column). Planning then runs in place inside "Todo"; a "ready" badge distinguishes planned (real PROMPT.md) tasks from unplanned (bootstrap stub) ones while they wait for an in-progress slot. See createTask intake-column wiring (store.ts) and the triage todo-discovery extension (triage.ts). +*/ + +/** The board columns for the Coding (Ideas) workflow. The "ideas" intake carries + * `autoTriage: false` so the engine's createTask intake-column wiring lands new + * cards there and the triage service leaves them alone until they are promoted + * into "todo". "todo" merges the legacy triage (planner) and todo (capacity + * hold) stages into one agent-staffed column. */ +const CODING_IDEAS_COLUMNS: WorkflowIrColumn[] = [ + { + id: "ideas", + name: "Ideas", + traits: [{ trait: "intake", config: { autoTriage: false } }], + }, + { + id: "todo", + name: "Todo", + traits: [{ trait: "hold", config: { release: "capacity" } }, { trait: "reset-on-entry" }], + }, + { + id: "in-progress", + name: "In progress", + traits: [ + { trait: "wip", config: { limitSetting: "maxConcurrent", countPending: true } }, + { trait: "abort-on-exit" }, + { trait: "timing" }, + ], + }, + { + id: "in-review", + name: "In review", + traits: [{ trait: "merge-blocker" }, { trait: "human-review" }, { trait: "stall-detection" }, { trait: "merge" }], + }, + { id: "done", name: "Done", traits: [{ trait: "complete" }] }, + { id: "archived", name: "Archived", traits: [{ trait: "archived" }] }, +]; + +/** Planning-stage node ids that sit in the legacy "triage" / "in-progress" + * columns in the cloned default graph. They are re-homed to the merged "todo" + * planner column so an agent is visibly working while the spec is produced. */ +const PLANNING_NODE_IDS: Record = { + plan: true, + "plan-review": true, + "plan-replan": true, +}; + +const RAW_BUILTIN_CODING_IDEAS_WORKFLOW_IR: WorkflowIr = (() => { + const ir = JSON.parse(JSON.stringify(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)) as WorkflowIr; + ir.name = "builtin-coding-ideas"; + + const v2 = ir as WorkflowIrV2; + v2.columns = CODING_IDEAS_COLUMNS.map((column) => ({ + ...column, + traits: column.traits.map((trait) => ({ + ...trait, + config: trait.config ? { ...trait.config } : undefined, + })), + })); + + /* + FNXC:CodingIdeasWorkflow 2026-07-04-09:30: + Re-home graph nodes to the new column shape: the start node becomes the "ideas" intake anchor; planning-stage nodes move to the merged "todo" column; every execution / review / merge / done node keeps its existing column id (in-progress / in-review / done), which still exists in the new column set. Unknown legacy columns (e.g. a leftover "triage" placement) default to "todo" so no node is ever left dangling in a column the workflow no longer declares. + */ + const knownColumnIds = new Set(v2.columns.map((c) => c.id)); + for (const node of v2.nodes) { + if (node.kind === "start") { + node.column = "ideas"; + continue; + } + if (PLANNING_NODE_IDS[node.id]) { + node.column = "todo"; + continue; + } + if (!node.column || !knownColumnIds.has(node.column)) { + node.column = "todo"; + } + } + + v2.settings = BUILTIN_WORKFLOW_SETTINGS; + return ir; +})(); + +export const BUILTIN_CODING_IDEAS_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_CODING_IDEAS_WORKFLOW_IR); diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 6f4062f6cd..1575033940 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -1,4 +1,5 @@ import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js"; +import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js"; import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js"; import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js"; import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js"; @@ -356,6 +357,42 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ createdAt: BUILTIN_TS, updatedAt: BUILTIN_TS, }, + /* + * FNXC:CodingIdeasWorkflow 2026-07-04-09:40: + * The Coding (Ideas) variant adds a manual "Ideas" intake in front of the default stepwise pipeline. New cards land in "ideas" (autoTriage off) and are not planned until an operator promotes them into the merged "todo" planner column; from there the graph is identical to the default Coding workflow. + */ + { + id: "builtin:coding-ideas", + name: "Coding (Ideas)", + description: + "Capture-first coding pipeline: park ideas in a manual intake, then plan, execute per step, run the optional final code review, and merge.", + kind: "workflow", + ir: BUILTIN_CODING_IDEAS_WORKFLOW_IR, + layout: { + start: { x: 60, y: 160 }, + plan: { x: 230, y: 160 }, + "plan-review": { x: 400, y: 160 }, + "plan-replan": { x: 400, y: 320 }, + parse: { x: 570, y: 160 }, + steps: { x: 740, y: 160 }, + "browser-verification": { x: 910, y: 160 }, + "browser-verification-remediation": { x: 910, y: 320 }, + "code-review": { x: 1080, y: 160 }, + "code-review-remediation": { x: 1080, y: 320 }, + "completion-summary": { x: 1250, y: 160 }, + "merge-gate": { x: 1420, y: 160 }, + "branch-group-member-integration": { x: 1590, y: 80 }, + "branch-group-promotion": { x: 1760, y: 80 }, + "merge-attempt": { x: 1930, y: 160 }, + "merge-retry": { x: 2100, y: 80 }, + "recovery-router": { x: 2100, y: 240 }, + "merge-manual-hold": { x: 1590, y: 240 }, + "post-merge-verification": { x: 2270, y: 160 }, + end: { x: 2440, y: 160 }, + }, + createdAt: BUILTIN_TS, + updatedAt: BUILTIN_TS, + }, { id: "builtin:legacy-coding", name: "Legacy coding", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 78d589d7d4..2f923aaf83 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -165,6 +165,7 @@ export type { EffectiveAgentResult, } from "./column-agent-resolver.js"; export { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js"; +export { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js"; export { PLAN_REVIEW_GROUP_ID } from "./builtin-plan-review-group.js"; export { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js"; export { diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index bf543ed627..6157c6ffec 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -4639,6 +4639,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} // When a project default workflow is configured, new tasks inherit it // (compiled to steps) ahead of the legacy default-on step behavior. let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined; + let resolvedEntryColumn: string | undefined; /* FNXC:WorkflowCreation 2026-06-28-23:09: User-facing task creation can submit a selected workflowId and optional-group toggles together. The visible workflow selection is operator intent and must persist as task_workflow_selection; enabledWorkflowSteps only overrides that workflow's default optional-group seed. @@ -4657,6 +4658,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} ? (resolvedWorkflowSteps ?? []) : undefined; resolvedWorkflowSteps = explicitStepIds ?? selected.stepIds; + resolvedEntryColumn = selected.entryColumnId; pendingWorkflowSelection = { workflowId: selected.workflowId, stepIds: explicitStepIds ?? selected.stepIds, @@ -4667,6 +4669,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} const inherited = await this.materializeDefaultWorkflowSteps(); if (inherited) { resolvedWorkflowSteps = inherited.stepIds; + resolvedEntryColumn = inherited.entryColumnId; pendingWorkflowSelection = inherited; } } catch (err) { @@ -4708,7 +4711,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} title, resolvedWorkflowSteps, taskId, - { invokeTaskCreatedHook: shouldInvokeTaskCreatedHook && !hasPendingSummarization, reservationCommit }, + { invokeTaskCreatedHook: shouldInvokeTaskCreatedHook && !hasPendingSummarization, reservationCommit, resolvedEntryColumn }, ); }, }); @@ -4833,6 +4836,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} : undefined; let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined; + let resolvedEntryColumn: string | undefined; /* FNXC:WorkflowCreation 2026-06-28-23:09: Reserved-id task creation must match normal task creation: workflowId and enabledWorkflowSteps are independent create controls, so explicit optional toggles do not erase the selected workflow row. @@ -4850,6 +4854,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} ? (resolvedWorkflowSteps ?? []) : undefined; resolvedWorkflowSteps = explicitStepIds ?? selected.stepIds; + resolvedEntryColumn = selected.entryColumnId; pendingWorkflowSelection = { workflowId: selected.workflowId, stepIds: explicitStepIds ?? selected.stepIds, @@ -4862,6 +4867,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} const inherited = await this.materializeDefaultWorkflowSteps(); if (inherited) { resolvedWorkflowSteps = inherited.stepIds; + resolvedEntryColumn = inherited.entryColumnId; pendingWorkflowSelection = inherited; } } catch (err) { @@ -4893,14 +4899,13 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} resolvedWorkflowSteps = []; } - // U7c: selection seeds are optional-group node ids (not materialized - // `workflow_steps` rows), so a failed task creation strands nothing to clean. const createdTask: Task = await this._createTaskInternal(input, title, resolvedWorkflowSteps, id, { createdAt: options.createdAt, updatedAt: options.updatedAt, promptOverride: options.prompt, invokeTaskCreatedHook: options.invokeTaskCreatedHook, reservationCommit: options.reservationCommit, + resolvedEntryColumn, }); // Record the inherited workflow selection now that the task row exists. @@ -4974,6 +4979,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} promptOverride?: string; invokeTaskCreatedHook?: boolean; reservationCommit?: { reservationId: string; nodeId: string }; + /* + FNXC:CodingIdeasWorkflow 2026-07-04-10:02: + The resolved workflow's intake column id. When the caller omits an explicit `input.column`, the task lands here instead of the legacy "triage" default so workflows with a manual intake (e.g. Coding (Ideas) → "ideas") capture new cards without auto-planning them. Defaults to "triage" when unset, preserving byte-identical behavior for the default workflow. + */ + resolvedEntryColumn?: string; }, ): Promise { const now = options?.createdAt ?? new Date().toISOString(); @@ -4999,7 +5009,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} branchContext: input.branchContext, autoMerge: input.autoMerge, autoMergeProvenance: input.autoMerge === undefined ? undefined : "user", - column: input.column || "triage", + column: input.column || options?.resolvedEntryColumn || "triage", dependencies: input.dependencies || [], breakIntoSubtasks: input.breakIntoSubtasks === true ? true : undefined, noCommitsExpected: input.noCommitsExpected === true ? true : undefined, @@ -5049,8 +5059,16 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} // Update cache if watcher is active 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. + */ + const isPrePlanningColumn = task.column !== "in-progress" + && task.column !== "in-review" + && task.column !== "done" + && task.column !== "archived"; const prompt = options?.promptOverride - ?? (task.column === "triage" + ?? (isPrePlanningColumn ? 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 1e94adaf94..d7c53dede1 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -777,6 +777,7 @@ function TaskCardComponent({ const [isRetrying, setIsRetrying] = useState(false); const [isPrCreateOpen, setIsPrCreateOpen] = useState(false); const [isAddressingPrFeedback, setIsAddressingPrFeedback] = useState(false); + const [isStarting, setIsStarting] = useState(false); const [timeIndicatorNowMs, setTimeIndicatorNowMs] = useState(() => Date.now()); const descTextareaRef = useRef(null); @@ -1406,7 +1407,8 @@ function TaskCardComponent({ || Boolean(task.blockedBy) || Boolean(task.overlapBlockedBy) || Boolean(fanout && fanout.totalCount > 0); - const shouldRenderActionRow = Boolean(onPromote) || showCreatePrQuickAction || showAddressPrFeedbackAction || (showInReviewMoveControl && !metaRowVisible); + const showStartAction = task.column === "ideas" && Boolean(onMoveTask); + const shouldRenderActionRow = Boolean(onPromote) || showCreatePrQuickAction || showAddressPrFeedbackAction || showStartAction || (showInReviewMoveControl && !metaRowVisible); const renderInReviewMoveControl = () => (
@@ -2184,6 +2186,19 @@ function TaskCardComponent({ if (!onPromote || isPromoting) return; void onPromote(task.id); }, [isPromoting, onPromote, task.id]); + const handleStartClick = useCallback(async (e: React.MouseEvent) => { + e.stopPropagation(); + if (!onMoveTask || isStarting) return; + setIsStarting(true); + try { + await onMoveTask(task.id, "todo"); + addToast(t("tasks.startedPlanning", "Started planning {{taskId}}", { taskId: task.id }), "success"); + } catch (err) { + addToast(getErrorMessage(err), "error"); + } finally { + setIsStarting(false); + } + }, [addToast, isStarting, onMoveTask, t, task.id]); const handleAddressPrFeedbackClick = useCallback(async (e: React.MouseEvent) => { e.stopPropagation(); @@ -2400,6 +2415,15 @@ function TaskCardComponent({ {isStuck ? t("tasks.stuck", "Stuck") : isAwaitingApproval ? t("tasks.awaitingApproval", "Awaiting Approval") : isAwaitingInput ? t("tasks.needsInput", "Needs input") : visualStatus === "merging-fix" ? t("tasks.statusMergingFix", "Merging fixes…") : getTaskStatusLabel(visualStatus, t)} )} + {/* + FNXC:CodingIdeasWorkflow 2026-07-04-11:10: + In the merged planner/capacity "todo" column (Coding (Ideas)), a planned task with no active status is ready and waiting for an in-progress slot. Show a "Ready" badge so operators can distinguish planned cards from freshly promoted unplanned ones. Tasks still being planned surface the "planning" status badge above instead. + */} + {!isPaused && task.column === "todo" && !visualStatus && (task.steps?.length ?? 0) > 0 && ( + + {t("tasks.ready", "Ready")} + + )} {hasInReviewStall && stallCopy && ( )} + {showStartAction && ( + + )} {onPromote && (