Fix workflow step execution wiring

This commit is contained in:
gsxdsm
2026-04-05 22:28:22 -07:00
parent ef3136d601
commit 6ff6e69e60
9 changed files with 414 additions and 11 deletions

View File

@@ -4,7 +4,7 @@ import { appendFile, mkdir, readFile, writeFile, rename, unlink } from "node:fs/
import { join } from "node:path";
import { existsSync, watch, type FSWatcher } from "node:fs";
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType } from "./types.js";
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, GLOBAL_SETTINGS_KEYS } from "./types.js";
import { VALID_TRANSITIONS, DEFAULT_SETTINGS, GLOBAL_SETTINGS_KEYS, WORKFLOW_STEP_TEMPLATES } from "./types.js";
import { GlobalSettingsStore } from "./global-settings.js";
import { Database, toJson, toJsonNullable, fromJson } from "./db.js";
import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js";
@@ -657,6 +657,83 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
return join(this.tasksDir, id);
}
private getBuiltInWorkflowTemplate(templateId: string): import("./types.js").WorkflowStepTemplate | undefined {
return WORKFLOW_STEP_TEMPLATES.find((template) => template.id === templateId);
}
private toBuiltInWorkflowStep(template: import("./types.js").WorkflowStepTemplate): import("./types.js").WorkflowStep {
const now = new Date().toISOString();
return {
id: template.id,
templateId: template.id,
name: template.name,
description: template.description,
mode: "prompt",
phase: "pre-merge",
prompt: template.prompt,
toolMode: template.toolMode || "readonly",
enabled: true,
createdAt: now,
updatedAt: now,
};
}
private async ensureWorkflowStepForTemplate(templateId: string): Promise<import("./types.js").WorkflowStep> {
return this.withConfigLock(async () => {
const template = this.getBuiltInWorkflowTemplate(templateId);
if (!template) {
throw new Error(`Workflow step template '${templateId}' not found`);
}
const config = await this.readConfig();
const existing = (config.workflowSteps || []).find((ws) =>
ws.id === templateId
|| ws.templateId === templateId
|| ws.name.toLowerCase() === template.name.toLowerCase(),
);
if (existing) {
return existing;
}
const nextWsId = config.nextWorkflowStepId || 1;
const step = this.toBuiltInWorkflowStep(template);
step.id = `WS-${String(nextWsId).padStart(3, "0")}`;
if (!config.workflowSteps) {
config.workflowSteps = [];
}
config.workflowSteps.push(step);
config.nextWorkflowStepId = nextWsId + 1;
await this.writeConfig(config);
return step;
});
}
private async resolveEnabledWorkflowSteps(stepIds?: string[]): Promise<string[] | undefined> {
if (!stepIds?.length) return undefined;
const resolved: string[] = [];
const seen = new Set<string>();
for (const rawId of stepIds) {
const stepId = rawId.trim();
if (!stepId) continue;
const template = this.getBuiltInWorkflowTemplate(stepId);
const resolvedId = template
? (await this.ensureWorkflowStepForTemplate(stepId)).id
: stepId;
if (!seen.has(resolvedId)) {
seen.add(resolvedId);
resolved.push(resolvedId);
}
}
return resolved.length > 0 ? resolved : undefined;
}
async createTask(
input: TaskCreateInput,
options?: {
@@ -695,7 +772,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length ? input.enabledWorkflowSteps : undefined;
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length
? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps)
: undefined;
// When enabledWorkflowSteps is not provided at all (undefined), auto-apply default-on workflow steps
if (input.enabledWorkflowSteps === undefined) {
@@ -1016,7 +1095,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask(
id: string,
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; blockedBy?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
): Promise<Task> {
return this.withTaskLock(id, async () => {
// Validate that task doesn't depend on itself
@@ -1101,6 +1180,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} else if (updates.nextRecoveryAt !== undefined) {
task.nextRecoveryAt = updates.nextRecoveryAt;
}
if (updates.enabledWorkflowSteps !== undefined) {
task.enabledWorkflowSteps = await this.resolveEnabledWorkflowSteps(updates.enabledWorkflowSteps);
}
if (updates.modelProvider === null) {
task.modelProvider = undefined;
} else if (updates.modelProvider !== undefined) {
@@ -2680,11 +2762,13 @@ ${stepsSection}`;
const now = new Date().toISOString();
const step: import("./types.js").WorkflowStep = {
id,
templateId: input.templateId,
name: input.name,
description: input.description,
mode,
phase: input.phase || "pre-merge",
prompt: mode === "prompt" ? (input.prompt || "") : "",
toolMode: mode === "prompt" ? (input.toolMode || "readonly") : undefined,
scriptName: mode === "script" ? input.scriptName : undefined,
enabled: input.enabled !== undefined ? input.enabled : true,
defaultOn: input.defaultOn === true ? true : undefined,
@@ -2718,7 +2802,11 @@ ${stepsSection}`;
*/
async getWorkflowStep(id: string): Promise<import("./types.js").WorkflowStep | undefined> {
const config = await this.readConfig();
return (config.workflowSteps || []).find((ws) => ws.id === id);
const stored = (config.workflowSteps || []).find((ws) => ws.id === id);
if (stored) return stored;
const template = this.getBuiltInWorkflowTemplate(id);
return template ? this.toBuiltInWorkflowStep(template) : undefined;
}
/**
@@ -2748,12 +2836,14 @@ ${stepsSection}`;
// When switching to script mode, clear prompt and model overrides
if (newMode === "script") {
step.prompt = "";
step.toolMode = undefined;
step.modelProvider = undefined;
step.modelId = undefined;
}
// When switching to prompt mode, clear scriptName
if (newMode === "prompt") {
step.scriptName = undefined;
step.toolMode = step.toolMode || "readonly";
}
}
@@ -2761,6 +2851,7 @@ ${stepsSection}`;
if (updates.description !== undefined) step.description = updates.description;
if (updates.phase !== undefined) step.phase = updates.phase;
if (updates.prompt !== undefined && step.mode === "prompt") step.prompt = updates.prompt;
if (updates.toolMode !== undefined && step.mode === "prompt") step.toolMode = updates.toolMode;
if (updates.scriptName !== undefined && step.mode === "script") step.scriptName = updates.scriptName;
if (updates.enabled !== undefined) step.enabled = updates.enabled;
if (updates.defaultOn !== undefined) step.defaultOn = updates.defaultOn;