diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index a26d3c82e2..5c3a651233 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -1031,7 +1031,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?: ): Promise { if (!flagOn) return {}; try { - const selection = projectStore.getTaskWorkflowSelection(task.id); + /* + FNXC:WorkflowSelection 2026-07-14-17:06: + The dashboard TUI must resolve task workflow selections through the asynchronous store API so PostgreSQL-backed projects retain custom workflow column names and trait flags. The synchronous compatibility method has no backend result and is reserved for legacy test doubles. + */ + const selection = await projectStore.getTaskWorkflowSelectionAsync(task.id); const workflowId = selection?.workflowId; let columns = workflowIrCache.get(workflowId); if (columns === undefined) { @@ -3175,7 +3179,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?: try { const values = (t as { customFields?: Record }).customFields; if (values && Object.keys(values).length > 0) { - const selection = projectStore.getTaskWorkflowSelection(t.id); + /* + FNXC:WorkflowSelection 2026-07-14-17:06: + Task-detail custom-field chips must use the asynchronous workflow-selection read so PostgreSQL tasks render fields declared by their selected workflow. + */ + const selection = await projectStore.getTaskWorkflowSelectionAsync(t.id); const def = selection?.workflowId ? await projectStore.getWorkflowDefinition(selection.workflowId) : undefined; diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index f639819d5d..0e7192d89b 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -5135,7 +5135,10 @@ export class TaskExecutor { */ settings = { ...settings }; let selection: { workflowId: string; stepIds: string[] } | undefined; - if (typeof this.store.getTaskWorkflowSelection !== "function") { + if ( + typeof this.store.getTaskWorkflowSelectionAsync !== "function" + && typeof this.store.getTaskWorkflowSelection !== "function" + ) { /* FNXC:WorkflowExecution 2026-06-23-22:01: Graph execution is the default for production TaskStore implementations, which expose workflow-selection APIs. Minimal test stores and older embedded adapters can lack that API; fall back to the legacy executor instead of half-entering graph routing with no workflow persistence surface. @@ -5159,7 +5162,7 @@ export class TaskExecutor { disposition: "failed", outcome: "failure", reason: - "workflow-selection-api-unavailable: store lacks getTaskWorkflowSelection so the workflow graph cannot run " + "workflow-selection-api-unavailable: store lacks a workflow-selection reader so the workflow graph cannot run " + `${gateTask.enabledWorkflowSteps?.length ?? 0} enabled pre-merge workflow step(s); the legacy runWorkflowSteps path was removed (U4). Failing closed rather than skipping gates (KTD-5).`, visitedNodeIds: [], }); @@ -5249,8 +5252,12 @@ export class TaskExecutor { const runner = new WorkflowGraphTaskRunner({ store: { ...this.store, - getTaskWorkflowSelection: (taskId: string) => - this.store.getTaskWorkflowSelection?.(taskId) ?? { workflowId: "builtin:coding", stepIds: [] }, + /* + FNXC:WorkflowSelection 2026-07-14-17:06: + Graph execution must reuse the asynchronously resolved selection. A PostgreSQL TaskStore cannot provide that selection through the synchronous compatibility method, and substituting builtin:coding here would silently execute the wrong graph. + */ + getTaskWorkflowSelection: () => selection, + getTaskWorkflowSelectionAsync: async () => selection, getWorkflowDefinition: async (id: string) => (await this.store.getWorkflowDefinition?.(id)) ?? (id === "builtin:coding" ? getBuiltinWorkflow("builtin:coding") : undefined), @@ -5940,7 +5947,10 @@ export class TaskExecutor { */ private async maybeObserveWorkflowParity(taskId: string, settings: Settings): Promise { if (!isExperimentalFeatureEnabled(settings, WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG)) return; - if (typeof this.store.getTaskWorkflowSelection !== "function") return; + if ( + typeof this.store.getTaskWorkflowSelectionAsync !== "function" + && typeof this.store.getTaskWorkflowSelection !== "function" + ) return; try { const selection = typeof this.store.getTaskWorkflowSelectionAsync === "function" ? await this.store.getTaskWorkflowSelectionAsync(taskId) diff --git a/packages/engine/src/workflow-authoritative-driver.ts b/packages/engine/src/workflow-authoritative-driver.ts index d1a130dc61..787fdafd4d 100644 --- a/packages/engine/src/workflow-authoritative-driver.ts +++ b/packages/engine/src/workflow-authoritative-driver.ts @@ -22,6 +22,7 @@ export interface WorkflowAuthoritativeDriverStore { getSettings(): Promise; getTask(taskId: string): Promise; getTaskWorkflowSelection?(taskId: string): { workflowId: string; stepIds: string[] } | undefined; + getTaskWorkflowSelectionAsync?(taskId: string): Promise<{ workflowId: string; stepIds: string[] } | undefined>; getWorkflowParitySummary?(options?: { since?: string; limit?: number }): WorkflowParitySummary; } @@ -160,7 +161,13 @@ export class WorkflowAuthoritativeDriver { }; } - const existingSelection = this.deps.store.getTaskWorkflowSelection?.(task.id); + /* + FNXC:WorkflowSelection 2026-07-14-17:06: + The authoritative cutover guard must await PostgreSQL workflow-selection storage before deciding a task is unselected. Keep the synchronous branch only for narrow legacy/in-memory driver doubles. + */ + const existingSelection = this.deps.store.getTaskWorkflowSelectionAsync + ? await this.deps.store.getTaskWorkflowSelectionAsync(task.id) + : this.deps.store.getTaskWorkflowSelection?.(task.id); if (existingSelection) { return { handled: false,