Fix workflow step execution wiring
This commit is contained in:
5
.changeset/fix-browser-verification-workflow.md
Normal file
5
.changeset/fix-browser-verification-workflow.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gsxdsm/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix browser verification workflow selection so built-in browser checks materialize into runnable workflow steps, persist on task edits, and execute with coding tools.
|
||||||
@@ -4762,6 +4762,40 @@ Task with acceptance criteria
|
|||||||
expect(task.enabledWorkflowSteps).toEqual([ws1.id, ws2.id]);
|
expect(task.enabledWorkflowSteps).toEqual([ws1.id, ws2.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should materialize built-in workflow templates when creating a task", async () => {
|
||||||
|
const task = await store.createTask({
|
||||||
|
description: "Task with browser verification",
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(task.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||||
|
|
||||||
|
const step = await store.getWorkflowStep("WS-001");
|
||||||
|
expect(step).toMatchObject({
|
||||||
|
id: "WS-001",
|
||||||
|
templateId: "browser-verification",
|
||||||
|
name: "Browser Verification",
|
||||||
|
toolMode: "coding",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reuse an existing materialized built-in workflow step", async () => {
|
||||||
|
const first = await store.createTask({
|
||||||
|
description: "First browser verification task",
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
});
|
||||||
|
const second = await store.createTask({
|
||||||
|
description: "Second browser verification task",
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||||
|
expect(second.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||||
|
|
||||||
|
const steps = await store.listWorkflowSteps();
|
||||||
|
expect(steps.filter((step) => step.templateId === "browser-verification")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("should not set enabledWorkflowSteps when empty array provided", async () => {
|
it("should not set enabledWorkflowSteps when empty array provided", async () => {
|
||||||
const task = await store.createTask({
|
const task = await store.createTask({
|
||||||
description: "Task without workflow steps",
|
description: "Task without workflow steps",
|
||||||
@@ -4994,6 +5028,32 @@ Task with acceptance criteria
|
|||||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001", "WS-002"]);
|
expect(task.enabledWorkflowSteps).toEqual(["WS-001", "WS-002"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update task workflow steps and materialize built-in templates", async () => {
|
||||||
|
const task = await store.createTask({ description: "Editable task" });
|
||||||
|
|
||||||
|
const updated = await store.updateTask(task.id, {
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updated.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||||
|
|
||||||
|
const persisted = await store.getTask(task.id);
|
||||||
|
expect(persisted.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve built-in workflow templates from getWorkflowStep", async () => {
|
||||||
|
const step = await store.getWorkflowStep("browser-verification");
|
||||||
|
|
||||||
|
expect(step).toMatchObject({
|
||||||
|
id: "browser-verification",
|
||||||
|
templateId: "browser-verification",
|
||||||
|
name: "Browser Verification",
|
||||||
|
mode: "prompt",
|
||||||
|
phase: "pre-merge",
|
||||||
|
toolMode: "coding",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Workflow Step Phase ──────────────────────────────────────────────
|
// ── Workflow Step Phase ──────────────────────────────────────────────
|
||||||
|
|
||||||
it("should default phase to 'pre-merge' when creating a workflow step", async () => {
|
it("should default phase to 'pre-merge' when creating a workflow step", async () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { appendFile, mkdir, readFile, writeFile, rename, unlink } from "node:fs/
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { existsSync, watch, type FSWatcher } from "node:fs";
|
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 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 { GlobalSettingsStore } from "./global-settings.js";
|
||||||
import { Database, toJson, toJsonNullable, fromJson } from "./db.js";
|
import { Database, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||||
import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js";
|
import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js";
|
||||||
@@ -657,6 +657,83 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
return join(this.tasksDir, id);
|
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(
|
async createTask(
|
||||||
input: TaskCreateInput,
|
input: TaskCreateInput,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -695,7 +772,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
|
// 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
|
// When enabledWorkflowSteps is not provided at all (undefined), auto-apply default-on workflow steps
|
||||||
if (input.enabledWorkflowSteps === undefined) {
|
if (input.enabledWorkflowSteps === undefined) {
|
||||||
@@ -1016,7 +1095,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
|
|
||||||
async updateTask(
|
async updateTask(
|
||||||
id: string,
|
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> {
|
): Promise<Task> {
|
||||||
return this.withTaskLock(id, async () => {
|
return this.withTaskLock(id, async () => {
|
||||||
// Validate that task doesn't depend on itself
|
// Validate that task doesn't depend on itself
|
||||||
@@ -1101,6 +1180,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
} else if (updates.nextRecoveryAt !== undefined) {
|
} else if (updates.nextRecoveryAt !== undefined) {
|
||||||
task.nextRecoveryAt = updates.nextRecoveryAt;
|
task.nextRecoveryAt = updates.nextRecoveryAt;
|
||||||
}
|
}
|
||||||
|
if (updates.enabledWorkflowSteps !== undefined) {
|
||||||
|
task.enabledWorkflowSteps = await this.resolveEnabledWorkflowSteps(updates.enabledWorkflowSteps);
|
||||||
|
}
|
||||||
if (updates.modelProvider === null) {
|
if (updates.modelProvider === null) {
|
||||||
task.modelProvider = undefined;
|
task.modelProvider = undefined;
|
||||||
} else if (updates.modelProvider !== undefined) {
|
} else if (updates.modelProvider !== undefined) {
|
||||||
@@ -2680,11 +2762,13 @@ ${stepsSection}`;
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const step: import("./types.js").WorkflowStep = {
|
const step: import("./types.js").WorkflowStep = {
|
||||||
id,
|
id,
|
||||||
|
templateId: input.templateId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
mode,
|
mode,
|
||||||
phase: input.phase || "pre-merge",
|
phase: input.phase || "pre-merge",
|
||||||
prompt: mode === "prompt" ? (input.prompt || "") : "",
|
prompt: mode === "prompt" ? (input.prompt || "") : "",
|
||||||
|
toolMode: mode === "prompt" ? (input.toolMode || "readonly") : undefined,
|
||||||
scriptName: mode === "script" ? input.scriptName : undefined,
|
scriptName: mode === "script" ? input.scriptName : undefined,
|
||||||
enabled: input.enabled !== undefined ? input.enabled : true,
|
enabled: input.enabled !== undefined ? input.enabled : true,
|
||||||
defaultOn: input.defaultOn === true ? true : undefined,
|
defaultOn: input.defaultOn === true ? true : undefined,
|
||||||
@@ -2718,7 +2802,11 @@ ${stepsSection}`;
|
|||||||
*/
|
*/
|
||||||
async getWorkflowStep(id: string): Promise<import("./types.js").WorkflowStep | undefined> {
|
async getWorkflowStep(id: string): Promise<import("./types.js").WorkflowStep | undefined> {
|
||||||
const config = await this.readConfig();
|
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
|
// When switching to script mode, clear prompt and model overrides
|
||||||
if (newMode === "script") {
|
if (newMode === "script") {
|
||||||
step.prompt = "";
|
step.prompt = "";
|
||||||
|
step.toolMode = undefined;
|
||||||
step.modelProvider = undefined;
|
step.modelProvider = undefined;
|
||||||
step.modelId = undefined;
|
step.modelId = undefined;
|
||||||
}
|
}
|
||||||
// When switching to prompt mode, clear scriptName
|
// When switching to prompt mode, clear scriptName
|
||||||
if (newMode === "prompt") {
|
if (newMode === "prompt") {
|
||||||
step.scriptName = undefined;
|
step.scriptName = undefined;
|
||||||
|
step.toolMode = step.toolMode || "readonly";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2761,6 +2851,7 @@ ${stepsSection}`;
|
|||||||
if (updates.description !== undefined) step.description = updates.description;
|
if (updates.description !== undefined) step.description = updates.description;
|
||||||
if (updates.phase !== undefined) step.phase = updates.phase;
|
if (updates.phase !== undefined) step.phase = updates.phase;
|
||||||
if (updates.prompt !== undefined && step.mode === "prompt") step.prompt = updates.prompt;
|
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.scriptName !== undefined && step.mode === "script") step.scriptName = updates.scriptName;
|
||||||
if (updates.enabled !== undefined) step.enabled = updates.enabled;
|
if (updates.enabled !== undefined) step.enabled = updates.enabled;
|
||||||
if (updates.defaultOn !== undefined) step.defaultOn = updates.defaultOn;
|
if (updates.defaultOn !== undefined) step.defaultOn = updates.defaultOn;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface ModelPreset {
|
|||||||
/** A reusable workflow step definition that can run after task implementation. */
|
/** A reusable workflow step definition that can run after task implementation. */
|
||||||
/** Execution mode for a workflow step. */
|
/** Execution mode for a workflow step. */
|
||||||
export type WorkflowStepMode = "prompt" | "script";
|
export type WorkflowStepMode = "prompt" | "script";
|
||||||
|
export type WorkflowStepToolMode = "readonly" | "coding";
|
||||||
|
|
||||||
/** Lifecycle phase for workflow step execution. */
|
/** Lifecycle phase for workflow step execution. */
|
||||||
export type WorkflowStepPhase = "pre-merge" | "post-merge";
|
export type WorkflowStepPhase = "pre-merge" | "post-merge";
|
||||||
@@ -66,6 +67,8 @@ export type WorkflowStepPhase = "pre-merge" | "post-merge";
|
|||||||
export interface WorkflowStep {
|
export interface WorkflowStep {
|
||||||
/** Unique identifier (e.g., "WS-001") */
|
/** Unique identifier (e.g., "WS-001") */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Built-in template source ID when this step was materialized from a template. */
|
||||||
|
templateId?: string;
|
||||||
/** Display name (e.g., "Documentation Review") */
|
/** Display name (e.g., "Documentation Review") */
|
||||||
name: string;
|
name: string;
|
||||||
/** Short description for UI display */
|
/** Short description for UI display */
|
||||||
@@ -76,6 +79,8 @@ export interface WorkflowStep {
|
|||||||
phase?: WorkflowStepPhase;
|
phase?: WorkflowStepPhase;
|
||||||
/** Full agent prompt to execute when this step runs (used when mode is "prompt") */
|
/** Full agent prompt to execute when this step runs (used when mode is "prompt") */
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
/** Tool set available to prompt-mode workflow agents. Defaults to readonly. */
|
||||||
|
toolMode?: WorkflowStepToolMode;
|
||||||
/** Name of a script from project settings `scripts` map to execute (required when mode is "script") */
|
/** Name of a script from project settings `scripts` map to execute (required when mode is "script") */
|
||||||
scriptName?: string;
|
scriptName?: string;
|
||||||
/** Whether this step is available for selection on new tasks */
|
/** Whether this step is available for selection on new tasks */
|
||||||
@@ -102,6 +107,8 @@ export interface WorkflowStep {
|
|||||||
export type NtfyNotificationEvent = "in-review" | "merged" | "failed";
|
export type NtfyNotificationEvent = "in-review" | "merged" | "failed";
|
||||||
|
|
||||||
export interface WorkflowStepInput {
|
export interface WorkflowStepInput {
|
||||||
|
/** Built-in template source ID when creating a concrete step from a template. */
|
||||||
|
templateId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
/** Execution mode — defaults to "prompt" if not specified */
|
/** Execution mode — defaults to "prompt" if not specified */
|
||||||
@@ -110,6 +117,8 @@ export interface WorkflowStepInput {
|
|||||||
phase?: WorkflowStepPhase;
|
phase?: WorkflowStepPhase;
|
||||||
/** Agent prompt (used when mode is "prompt"). Optional — can be AI-generated later via refinement. */
|
/** Agent prompt (used when mode is "prompt"). Optional — can be AI-generated later via refinement. */
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
/** Tool set available to prompt-mode workflow agents. Defaults to readonly. */
|
||||||
|
toolMode?: WorkflowStepToolMode;
|
||||||
/** Script name from project settings (required when mode is "script").
|
/** Script name from project settings (required when mode is "script").
|
||||||
* Must reference a named script in `settings.scripts` — no raw commands. */
|
* Must reference a named script in `settings.scripts` — no raw commands. */
|
||||||
scriptName?: string;
|
scriptName?: string;
|
||||||
@@ -152,6 +161,8 @@ export interface WorkflowStepTemplate {
|
|||||||
description: string;
|
description: string;
|
||||||
/** Full agent prompt template */
|
/** Full agent prompt template */
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
/** Tool set available when the template runs as a prompt-mode step. */
|
||||||
|
toolMode?: WorkflowStepToolMode;
|
||||||
/** Grouping category (e.g., "Quality", "Security") */
|
/** Grouping category (e.g., "Quality", "Security") */
|
||||||
category: string;
|
category: string;
|
||||||
/** Optional icon identifier for UI (e.g., "file-text", "shield") */
|
/** Optional icon identifier for UI (e.g., "file-text", "shield") */
|
||||||
@@ -166,6 +177,7 @@ export const WORKFLOW_STEP_TEMPLATES: WorkflowStepTemplate[] = [
|
|||||||
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
|
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
|
||||||
category: "Quality",
|
category: "Quality",
|
||||||
icon: "file-text",
|
icon: "file-text",
|
||||||
|
toolMode: "readonly",
|
||||||
prompt: `You are a documentation reviewer. Review the completed task and verify documentation quality.
|
prompt: `You are a documentation reviewer. Review the completed task and verify documentation quality.
|
||||||
|
|
||||||
Review Criteria:
|
Review Criteria:
|
||||||
@@ -191,6 +203,7 @@ Output Requirements:
|
|||||||
description: "Run tests and verify they pass, check for obvious bugs",
|
description: "Run tests and verify they pass, check for obvious bugs",
|
||||||
category: "Quality",
|
category: "Quality",
|
||||||
icon: "check-circle",
|
icon: "check-circle",
|
||||||
|
toolMode: "coding",
|
||||||
prompt: `You are a QA tester. Verify the task implementation by running tests and checking for bugs.
|
prompt: `You are a QA tester. Verify the task implementation by running tests and checking for bugs.
|
||||||
|
|
||||||
Test Execution:
|
Test Execution:
|
||||||
@@ -215,6 +228,7 @@ Output Requirements:
|
|||||||
description: "Check for common security vulnerabilities and anti-patterns",
|
description: "Check for common security vulnerabilities and anti-patterns",
|
||||||
category: "Security",
|
category: "Security",
|
||||||
icon: "shield",
|
icon: "shield",
|
||||||
|
toolMode: "readonly",
|
||||||
prompt: `You are a security auditor. Review the task changes for common security vulnerabilities.
|
prompt: `You are a security auditor. Review the task changes for common security vulnerabilities.
|
||||||
|
|
||||||
Security Checklist:
|
Security Checklist:
|
||||||
@@ -242,6 +256,7 @@ Output Requirements:
|
|||||||
description: "Check for performance anti-patterns and optimization opportunities",
|
description: "Check for performance anti-patterns and optimization opportunities",
|
||||||
category: "Quality",
|
category: "Quality",
|
||||||
icon: "zap",
|
icon: "zap",
|
||||||
|
toolMode: "readonly",
|
||||||
prompt: `You are a performance reviewer. Analyze the task changes for performance implications.
|
prompt: `You are a performance reviewer. Analyze the task changes for performance implications.
|
||||||
|
|
||||||
Performance Checklist:
|
Performance Checklist:
|
||||||
@@ -268,6 +283,7 @@ Output Requirements:
|
|||||||
description: "Verify UI changes meet accessibility standards (WCAG 2.1)",
|
description: "Verify UI changes meet accessibility standards (WCAG 2.1)",
|
||||||
category: "Quality",
|
category: "Quality",
|
||||||
icon: "eye",
|
icon: "eye",
|
||||||
|
toolMode: "readonly",
|
||||||
prompt: `You are an accessibility reviewer. Check UI changes for WCAG 2.1 compliance.
|
prompt: `You are an accessibility reviewer. Check UI changes for WCAG 2.1 compliance.
|
||||||
|
|
||||||
Accessibility Checklist:
|
Accessibility Checklist:
|
||||||
@@ -294,6 +310,7 @@ Output Requirements:
|
|||||||
description: "Verify web application functionality using browser automation",
|
description: "Verify web application functionality using browser automation",
|
||||||
category: "Quality",
|
category: "Quality",
|
||||||
icon: "globe",
|
icon: "globe",
|
||||||
|
toolMode: "coding",
|
||||||
prompt: `You are a browser verification specialist. Verify web application functionality after task implementation using the agent-browser CLI tool.
|
prompt: `You are a browser verification specialist. Verify web application functionality after task implementation using the agent-browser CLI tool.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|||||||
@@ -1402,6 +1402,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
dependencies: ["FN-002"],
|
dependencies: ["FN-002"],
|
||||||
|
enabledWorkflowSteps: undefined,
|
||||||
modelProvider: null,
|
modelProvider: null,
|
||||||
modelId: null,
|
modelId: null,
|
||||||
validatorModelProvider: null,
|
validatorModelProvider: null,
|
||||||
@@ -1423,6 +1424,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
|
enabledWorkflowSteps: undefined,
|
||||||
modelProvider: null,
|
modelProvider: null,
|
||||||
modelId: null,
|
modelId: null,
|
||||||
validatorModelProvider: null,
|
validatorModelProvider: null,
|
||||||
@@ -1454,6 +1456,7 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
|
enabledWorkflowSteps: undefined,
|
||||||
modelProvider: "anthropic",
|
modelProvider: "anthropic",
|
||||||
modelId: "claude-sonnet-4-5",
|
modelId: "claude-sonnet-4-5",
|
||||||
validatorModelProvider: "openai",
|
validatorModelProvider: "openai",
|
||||||
@@ -1503,12 +1506,50 @@ describe("PATCH /tasks/:id", () => {
|
|||||||
description: undefined,
|
description: undefined,
|
||||||
prompt: undefined,
|
prompt: undefined,
|
||||||
dependencies: undefined,
|
dependencies: undefined,
|
||||||
|
enabledWorkflowSteps: undefined,
|
||||||
modelProvider: null,
|
modelProvider: null,
|
||||||
modelId: null,
|
modelId: null,
|
||||||
validatorModelProvider: null,
|
validatorModelProvider: null,
|
||||||
validatorModelId: null,
|
validatorModelId: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards enabledWorkflowSteps to store.updateTask", async () => {
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
...FAKE_TASK_DETAIL,
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
}), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||||
|
title: undefined,
|
||||||
|
description: undefined,
|
||||||
|
prompt: undefined,
|
||||||
|
dependencies: undefined,
|
||||||
|
enabledWorkflowSteps: ["browser-verification"],
|
||||||
|
modelProvider: null,
|
||||||
|
modelId: null,
|
||||||
|
validatorModelProvider: null,
|
||||||
|
validatorModelId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 for invalid enabledWorkflowSteps type", async () => {
|
||||||
|
const res = await REQUEST(buildApp(), "PATCH", "/api/tasks/KB-001", JSON.stringify({
|
||||||
|
enabledWorkflowSteps: [123],
|
||||||
|
}), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body.error).toContain("enabledWorkflowSteps must be an array of strings");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Attachment routes", () => {
|
describe("Attachment routes", () => {
|
||||||
@@ -6941,9 +6982,11 @@ describe("POST /workflow-step-templates/:id/create", () => {
|
|||||||
expect(res.body.id).toBe("WS-001");
|
expect(res.body.id).toBe("WS-001");
|
||||||
expect(res.body.name).toBe("Documentation Review");
|
expect(res.body.name).toBe("Documentation Review");
|
||||||
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
||||||
|
templateId: "documentation-review",
|
||||||
name: "Documentation Review",
|
name: "Documentation Review",
|
||||||
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
|
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
|
||||||
prompt: expect.stringContaining("documentation reviewer"),
|
prompt: expect.stringContaining("documentation reviewer"),
|
||||||
|
toolMode: "readonly",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -6968,9 +7011,11 @@ describe("POST /workflow-step-templates/:id/create", () => {
|
|||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
expect(res.body.name).toBe("QA Check");
|
expect(res.body.name).toBe("QA Check");
|
||||||
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
expect(store.createWorkflowStep).toHaveBeenCalledWith({
|
||||||
|
templateId: "qa-check",
|
||||||
name: "QA Check",
|
name: "QA Check",
|
||||||
description: "Run tests and verify they pass, check for obvious bugs",
|
description: "Run tests and verify they pass, check for obvious bugs",
|
||||||
prompt: expect.stringContaining("QA tester"),
|
prompt: expect.stringContaining("QA tester"),
|
||||||
|
toolMode: "coding",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1744,7 +1744,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
);
|
);
|
||||||
res.status(201).json(task);
|
res.status(201).json(task);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const status = err.message?.includes("must be a string") ? 400 : 500;
|
const status = err.message?.includes("must be a string") || err.message?.includes("must be an array of strings") ? 400 : 500;
|
||||||
res.status(status).json({ error: err.message });
|
res.status(status).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2457,7 +2457,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
router.patch("/tasks/:id", async (req, res) => {
|
router.patch("/tasks/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scopedStore = await getScopedStore(req);
|
const scopedStore = await getScopedStore(req);
|
||||||
const { title, description, prompt, dependencies, modelProvider, modelId, validatorModelProvider, validatorModelId } = req.body;
|
const { title, description, prompt, dependencies, enabledWorkflowSteps, modelProvider, modelId, validatorModelProvider, validatorModelId } = req.body;
|
||||||
|
|
||||||
// Validate model fields are strings or undefined/null
|
// Validate model fields are strings or undefined/null
|
||||||
const validateModelField = (value: unknown, name: string): string | null | undefined => {
|
const validateModelField = (value: unknown, name: string): string | null | undefined => {
|
||||||
@@ -2473,11 +2473,18 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
const validatedValidatorModelProvider = validateModelField(validatorModelProvider, "validatorModelProvider");
|
const validatedValidatorModelProvider = validateModelField(validatorModelProvider, "validatorModelProvider");
|
||||||
const validatedValidatorModelId = validateModelField(validatorModelId, "validatorModelId");
|
const validatedValidatorModelId = validateModelField(validatorModelId, "validatorModelId");
|
||||||
|
|
||||||
|
if (enabledWorkflowSteps !== undefined) {
|
||||||
|
if (!Array.isArray(enabledWorkflowSteps) || !enabledWorkflowSteps.every((id: unknown) => typeof id === "string")) {
|
||||||
|
throw new Error("enabledWorkflowSteps must be an array of strings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const task = await scopedStore.updateTask(req.params.id, {
|
const task = await scopedStore.updateTask(req.params.id, {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
prompt,
|
prompt,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
enabledWorkflowSteps,
|
||||||
modelProvider: validatedModelProvider,
|
modelProvider: validatedModelProvider,
|
||||||
modelId: validatedModelId,
|
modelId: validatedModelId,
|
||||||
validatorModelProvider: validatedValidatorModelProvider,
|
validatorModelProvider: validatedValidatorModelProvider,
|
||||||
@@ -2485,7 +2492,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
});
|
});
|
||||||
res.json(task);
|
res.json(task);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const status = err.message?.includes("must be a string") ? 400 : 500;
|
const status = err.message?.includes("must be a string") || err.message?.includes("must be an array of strings") ? 400 : 500;
|
||||||
res.status(status).json({ error: err.message });
|
res.status(status).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -6205,7 +6212,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
router.post("/workflow-steps", async (req, res) => {
|
router.post("/workflow-steps", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scopedStore = await getScopedStore(req);
|
const scopedStore = await getScopedStore(req);
|
||||||
const { name, description, mode, phase, prompt, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
const { name, description, mode, phase, prompt, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||||
|
|
||||||
if (!name || typeof name !== "string" || !name.trim()) {
|
if (!name || typeof name !== "string" || !name.trim()) {
|
||||||
res.status(400).json({ error: "name is required" });
|
res.status(400).json({ error: "name is required" });
|
||||||
@@ -6233,6 +6240,10 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
res.status(400).json({ error: "prompt must be a string" });
|
res.status(400).json({ error: "prompt must be a string" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (toolMode !== undefined && toolMode !== "readonly" && toolMode !== "coding") {
|
||||||
|
res.status(400).json({ error: "toolMode must be 'readonly' or 'coding'" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (scriptName !== undefined && typeof scriptName !== "string") {
|
if (scriptName !== undefined && typeof scriptName !== "string") {
|
||||||
res.status(400).json({ error: "scriptName must be a string" });
|
res.status(400).json({ error: "scriptName must be a string" });
|
||||||
return;
|
return;
|
||||||
@@ -6276,6 +6287,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
mode: resolvedMode,
|
mode: resolvedMode,
|
||||||
phase,
|
phase,
|
||||||
prompt: prompt?.trim(),
|
prompt: prompt?.trim(),
|
||||||
|
toolMode,
|
||||||
scriptName: scriptName?.trim(),
|
scriptName: scriptName?.trim(),
|
||||||
enabled,
|
enabled,
|
||||||
defaultOn: defaultOn === true,
|
defaultOn: defaultOn === true,
|
||||||
@@ -6298,7 +6310,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
router.patch("/workflow-steps/:id", async (req, res) => {
|
router.patch("/workflow-steps/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scopedStore = await getScopedStore(req);
|
const scopedStore = await getScopedStore(req);
|
||||||
const { name, description, mode, phase, prompt, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
const { name, description, mode, phase, prompt, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||||
|
|
||||||
const updates: Record<string, unknown> = {};
|
const updates: Record<string, unknown> = {};
|
||||||
if (name !== undefined) {
|
if (name !== undefined) {
|
||||||
@@ -6336,6 +6348,13 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
}
|
}
|
||||||
updates.prompt = prompt;
|
updates.prompt = prompt;
|
||||||
}
|
}
|
||||||
|
if (toolMode !== undefined) {
|
||||||
|
if (toolMode !== "readonly" && toolMode !== "coding") {
|
||||||
|
res.status(400).json({ error: "toolMode must be 'readonly' or 'coding'" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updates.toolMode = toolMode;
|
||||||
|
}
|
||||||
if (scriptName !== undefined) {
|
if (scriptName !== undefined) {
|
||||||
if (typeof scriptName !== "string") {
|
if (typeof scriptName !== "string") {
|
||||||
res.status(400).json({ error: "scriptName must be a string" });
|
res.status(400).json({ error: "scriptName must be a string" });
|
||||||
@@ -6537,9 +6556,11 @@ Output ONLY the prompt text (no markdown, no explanations).`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const step = await scopedStore.createWorkflowStep({
|
const step = await scopedStore.createWorkflowStep({
|
||||||
|
templateId: template.id,
|
||||||
name: template.name,
|
name: template.name,
|
||||||
description: template.description,
|
description: template.description,
|
||||||
prompt: template.prompt,
|
prompt: template.prompt,
|
||||||
|
toolMode: template.toolMode,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5474,6 +5474,168 @@ describe("Workflow Steps Execution", () => {
|
|||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs browser verification workflow steps with coding tools", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Browser task",
|
||||||
|
description: "Verify browser behavior",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
store.getWorkflowStep.mockResolvedValue({
|
||||||
|
id: "WS-001",
|
||||||
|
templateId: "browser-verification",
|
||||||
|
name: "Browser Verification",
|
||||||
|
description: "Verify with browser automation",
|
||||||
|
mode: "prompt",
|
||||||
|
toolMode: "coding",
|
||||||
|
prompt: "Use browser automation to verify the app.",
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let callIdx = 0;
|
||||||
|
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
|
||||||
|
callIdx++;
|
||||||
|
if (callIdx === 1) {
|
||||||
|
const customTools = opts.customTools || [];
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn().mockImplementation(async () => {
|
||||||
|
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
|
||||||
|
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
return { session };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
state: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Browser task",
|
||||||
|
description: "Verify browser behavior",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||||
|
const secondCall = mockedCreateHaiAgent.mock.calls[1];
|
||||||
|
expect(secondCall[0].tools).toBe("coding");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs QA workflow steps with coding tools", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "QA task",
|
||||||
|
description: "Verify tests pass",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
store.getWorkflowStep.mockResolvedValue({
|
||||||
|
id: "WS-001",
|
||||||
|
templateId: "qa-check",
|
||||||
|
name: "QA Check",
|
||||||
|
description: "Run tests and verify they pass",
|
||||||
|
mode: "prompt",
|
||||||
|
toolMode: "coding",
|
||||||
|
prompt: "Run the test suite and report results.",
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let callIdx = 0;
|
||||||
|
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
|
||||||
|
callIdx++;
|
||||||
|
if (callIdx === 1) {
|
||||||
|
const customTools = opts.customTools || [];
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn().mockImplementation(async () => {
|
||||||
|
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
|
||||||
|
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
return { session };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
state: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "QA task",
|
||||||
|
description: "Verify tests pass",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
|
||||||
|
const secondCall = mockedCreateHaiAgent.mock.calls[1];
|
||||||
|
expect(secondCall[0].tools).toBe("coding");
|
||||||
|
});
|
||||||
|
|
||||||
it("skips workflow steps with no prompt", async () => {
|
it("skips workflow steps with no prompt", async () => {
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
|
|
||||||
|
|||||||
@@ -1902,6 +1902,7 @@ export class TaskExecutor {
|
|||||||
worktreePath: string,
|
worktreePath: string,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
): Promise<{ success: boolean; output?: string; error?: string }> {
|
): Promise<{ success: boolean; output?: string; error?: string }> {
|
||||||
|
const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly";
|
||||||
const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
|
const systemPrompt = `You are a workflow step agent executing: ${workflowStep.name}
|
||||||
|
|
||||||
Task Context:
|
Task Context:
|
||||||
@@ -1937,7 +1938,7 @@ If issues are found that need attention, describe them clearly.`;
|
|||||||
const { session } = await createKbAgent({
|
const { session } = await createKbAgent({
|
||||||
cwd: worktreePath,
|
cwd: worktreePath,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
tools: "readonly",
|
tools: toolMode,
|
||||||
defaultProvider: stepProvider,
|
defaultProvider: stepProvider,
|
||||||
defaultModelId: stepModelId,
|
defaultModelId: stepModelId,
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
|
|||||||
@@ -1539,6 +1539,7 @@ async function executePostMergePromptStep(
|
|||||||
rootDir: string,
|
rootDir: string,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
): Promise<{ success: boolean; output?: string; error?: string }> {
|
): Promise<{ success: boolean; output?: string; error?: string }> {
|
||||||
|
const toolMode: "coding" | "readonly" = workflowStep.toolMode || "readonly";
|
||||||
const systemPrompt = `You are a post-merge workflow step agent executing: ${workflowStep.name}
|
const systemPrompt = `You are a post-merge workflow step agent executing: ${workflowStep.name}
|
||||||
|
|
||||||
Task Context:
|
Task Context:
|
||||||
@@ -1567,7 +1568,7 @@ If issues are found that need attention, describe them clearly.`;
|
|||||||
const { session } = await createKbAgent({
|
const { session } = await createKbAgent({
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
tools: "readonly",
|
tools: toolMode,
|
||||||
defaultProvider: stepProvider,
|
defaultProvider: stepProvider,
|
||||||
defaultModelId: stepModelId,
|
defaultModelId: stepModelId,
|
||||||
fallbackProvider: settings.fallbackProvider,
|
fallbackProvider: settings.fallbackProvider,
|
||||||
|
|||||||
Reference in New Issue
Block a user