refactor(FN-7039): delete WORKFLOW_STEP_TEMPLATES + legacy workflow-step management surface
U6: delete the built-in WORKFLOW_STEP_TEMPLATES catalog + its materializer (getBuiltInWorkflowTemplate/ensureWorkflowStepForTemplate/toBuiltInWorkflowStep); inline the browser-verification + code-review name/prompt/toolMode/gateMode into their optional-group IR builders (node bytes unchanged); simplify resolveEnabledWorkflowSteps to an identity-stable pass-through (no materialization, so the optionalGroupIdSet collision guard is no longer needed). Plugin-contributed step templates are kept as the editor palette. U5: remove the legacy /api/workflow-steps REST surface (GET/POST/PATCH/DELETE + /refine + /workflow-step-templates/:id/create), the dead client fns, and the Settings management UI; GET /api/workflow-step-templates now serves plugin templates only. The create-time optional-step toggles remain. Scope: the workflow_steps store CRUD + table are intentionally KEPT — still consumed by the engine (merger/recovery) and needed by U7's migration; their removal + the table drop land in U7. Plan U5 + U6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
} from "../builtin-code-review-group.js";
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||
import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "../builtin-stepwise-coding-workflow-ir.js";
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "../types.js";
|
||||
import { parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js";
|
||||
import {
|
||||
resolveDefaultOnOptionalGroupIds,
|
||||
@@ -15,37 +14,33 @@ import {
|
||||
|
||||
/*
|
||||
FNXC:CodeReviewStep 2026-06-25-15:00:
|
||||
Coverage for the DEFAULT-ON but TOGGLEABLE "Code Review" pre-merge step: the catalog
|
||||
template fields, the `optional-group` node (defaultOn:true) built from it, and its wiring
|
||||
into the coding + stepwise built-ins as a default-on optional group. Code review is a
|
||||
WORKFLOW prompt-gate (shared verdict machinery), not engine verification code.
|
||||
Coverage for the DEFAULT-ON but TOGGLEABLE "Code Review" pre-merge step: the
|
||||
`optional-group` node (defaultOn:true) and its wiring into the coding + stepwise
|
||||
built-ins as a default-on optional group. Code review is a WORKFLOW prompt-gate (shared
|
||||
verdict machinery), not engine verification code.
|
||||
|
||||
FNXC:WorkflowStepTemplate 2026-06-25-00:00:
|
||||
U6 deleted the `WORKFLOW_STEP_TEMPLATES` catalog. The former "code-review catalog
|
||||
fields" assertions are gone; the inlined literal values (name/toolMode/gateMode/prompt
|
||||
verdict convention) are now asserted directly on the built group node below, which is the
|
||||
parity oracle.
|
||||
*/
|
||||
|
||||
describe("code-review WORKFLOW_STEP_TEMPLATE", () => {
|
||||
const template = WORKFLOW_STEP_TEMPLATES.find((t) => t.id === "code-review");
|
||||
|
||||
it("exists with the expected catalog fields", () => {
|
||||
expect(template).toBeTruthy();
|
||||
expect(template!.name).toBe("Code Review");
|
||||
expect(template!.toolMode).toBe("readonly");
|
||||
// Advisory → non-blocking by default (like the existing review); operators can promote.
|
||||
expect(template!.gateMode).toBe("advisory");
|
||||
expect(template!.phase).toBe("pre-merge");
|
||||
expect(template!.description.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("ends with the shared trailing verdict convention and reads the diff", () => {
|
||||
const prompt = template!.prompt;
|
||||
describe("codeReviewOptionalGroupNode", () => {
|
||||
it("carries the inlined catalog literals (name/toolMode/gateMode/prompt)", () => {
|
||||
const node = codeReviewOptionalGroupNode("in-progress");
|
||||
expect(node.config?.name).toBe("Code Review");
|
||||
const inner = (node.config?.template as { nodes: { config?: Record<string, unknown> }[] }).nodes[0];
|
||||
expect(inner.config?.toolMode).toBe("readonly");
|
||||
expect(inner.config?.gateMode).toBe("advisory");
|
||||
const prompt = String(inner.config?.prompt);
|
||||
expect(prompt).toMatch(/"verdict":"APPROVE\|APPROVE_WITH_NOTES\|REVISE"/);
|
||||
expect(prompt).not.toContain('"verdict":"PASS"');
|
||||
expect(prompt).not.toContain('"verdict":"FAIL"');
|
||||
// Focused on the value tests miss + reads the diff against the base.
|
||||
expect(prompt).toMatch(/git diff/);
|
||||
expect(prompt).toMatch(/out of scope/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("codeReviewOptionalGroupNode", () => {
|
||||
it("builds a DEFAULT-ON optional-group with the stable group id and distinct inner id", () => {
|
||||
const node = codeReviewOptionalGroupNode("in-progress");
|
||||
expect(node.id).toBe(CODE_REVIEW_GROUP_ID);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
applyFrontendUxCriteria,
|
||||
matchesFrontendUxPath,
|
||||
} from "../frontend-ux-policy.js";
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "../types.js";
|
||||
|
||||
const EXACT_FRONTEND_UX_CRITERIA = `## Frontend UX Criteria
|
||||
|
||||
@@ -103,14 +102,12 @@ Implement dashboard UI.
|
||||
expect(injected.match(/## Frontend UX Criteria/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("keeps checklist tokens aligned with the frontend UX design persona", () => {
|
||||
const persona = WORKFLOW_STEP_TEMPLATES.find((template) => template.id === "frontend-ux-design");
|
||||
expect(persona?.name).toBe("Frontend UX Design");
|
||||
expect(persona?.prompt).toContain("design tokens");
|
||||
expect(persona?.prompt).toContain("Component Reuse");
|
||||
expect(persona?.prompt).toContain("Responsive Behavior");
|
||||
expect(persona?.prompt).toContain("Visual Hierarchy");
|
||||
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: the `frontend-ux-design`
|
||||
// WORKFLOW_STEP_TEMPLATES persona was deleted in U6 (the built-in catalog is gone;
|
||||
// only browser-verification + code-review survive, inlined into their group builders).
|
||||
// The criteria-section token assertions that did not depend on the deleted persona are
|
||||
// kept below.
|
||||
it("keeps checklist tokens aligned with the injected frontend UX criteria section", () => {
|
||||
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Design tokens only");
|
||||
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Component reuse");
|
||||
expect(FRONTEND_UX_CRITERIA_SECTION).toContain("Responsive scaffolding");
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("TaskStore Workflow Steps", () => {
|
||||
expect(task.enabledWorkflowSteps).toEqual(["plugin:my-plugin:disabled-step"]);
|
||||
});
|
||||
|
||||
it("should keep plugin workflow IDs unchanged while materializing built-in templates", async () => {
|
||||
it("keeps plugin and former-built-in workflow ids unchanged (all pass through)", async () => {
|
||||
store.setPluginWorkflowStepTemplates([
|
||||
{
|
||||
pluginId: "my-plugin",
|
||||
@@ -318,16 +318,19 @@ describe("TaskStore Workflow Steps", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
// frontend-ux-design is a built-in WORKFLOW_STEP_TEMPLATE that is NOT a
|
||||
// builtin:coding optional-group node, so it still materializes into a WS row.
|
||||
// (browser-verification can no longer be used here — it is a builtin:coding
|
||||
// optional-group id that now passes through untouched; see FN-7039 regression.)
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: U6 deleted the built-in catalog +
|
||||
// template materializer, so resolveEnabledWorkflowSteps is a pure pass-through. Both
|
||||
// the plugin id AND a former built-in template id (frontend-ux-design) are kept
|
||||
// verbatim — nothing materializes into a WS row.
|
||||
const task = await store.createTask({
|
||||
description: "Task with mixed workflow steps",
|
||||
enabledWorkflowSteps: ["plugin:my-plugin:my-step", "frontend-ux-design"],
|
||||
});
|
||||
|
||||
expect(task.enabledWorkflowSteps).toEqual(["plugin:my-plugin:my-step", "WS-001"]);
|
||||
expect(task.enabledWorkflowSteps).toEqual(["plugin:my-plugin:my-step", "frontend-ux-design"]);
|
||||
|
||||
const steps = await store.listWorkflowSteps();
|
||||
expect(steps.filter((step) => step.templateId === "frontend-ux-design")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should update a workflow step", async () => {
|
||||
@@ -491,22 +494,21 @@ describe("TaskStore Workflow Steps", () => {
|
||||
expect(task.enabledWorkflowSteps).toEqual([ws1.id, ws2.id]);
|
||||
});
|
||||
|
||||
it("should materialize built-in workflow templates when creating a task", async () => {
|
||||
// frontend-ux-design is a plain built-in template (not a builtin:coding
|
||||
// optional-group), so it materializes into a WS row.
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: U6 deleted the built-in
|
||||
// WORKFLOW_STEP_TEMPLATES catalog + the template materializer, so
|
||||
// resolveEnabledWorkflowSteps is now a pure identity-stable pass-through. A former
|
||||
// built-in template id (frontend-ux-design) no longer materializes into a WS row — it
|
||||
// passes through verbatim, exactly like any other enable id.
|
||||
it("passes a former built-in template id (frontend-ux-design) through untouched without materializing", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Task with frontend ux design",
|
||||
enabledWorkflowSteps: ["frontend-ux-design"],
|
||||
});
|
||||
|
||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
expect(task.enabledWorkflowSteps).toEqual(["frontend-ux-design"]);
|
||||
|
||||
const step = await store.getWorkflowStep("WS-001");
|
||||
expect(step).toMatchObject({
|
||||
id: "WS-001",
|
||||
templateId: "frontend-ux-design",
|
||||
name: "Frontend UX Design",
|
||||
});
|
||||
const steps = await store.listWorkflowSteps();
|
||||
expect(steps.filter((step) => step.templateId === "frontend-ux-design")).toHaveLength(0);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -542,7 +544,10 @@ describe("TaskStore Workflow Steps", () => {
|
||||
expect(task.enabledWorkflowSteps).toEqual(["code-review"]);
|
||||
});
|
||||
|
||||
it("should reuse an existing materialized built-in workflow step", async () => {
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: with pass-through resolution, the same
|
||||
// former-built-in id used across two tasks stays identical and creates no rows (the
|
||||
// old "reuse the materialized row" semantics no longer apply — nothing is materialized).
|
||||
it("keeps a former built-in template id identical across tasks without materializing any row", async () => {
|
||||
const first = await store.createTask({
|
||||
description: "First frontend ux design task",
|
||||
enabledWorkflowSteps: ["frontend-ux-design"],
|
||||
@@ -552,28 +557,11 @@ describe("TaskStore Workflow Steps", () => {
|
||||
enabledWorkflowSteps: ["frontend-ux-design"],
|
||||
});
|
||||
|
||||
expect(first.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
expect(second.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
expect(first.enabledWorkflowSteps).toEqual(["frontend-ux-design"]);
|
||||
expect(second.enabledWorkflowSteps).toEqual(["frontend-ux-design"]);
|
||||
|
||||
const steps = await store.listWorkflowSteps();
|
||||
expect(steps.filter((step) => step.templateId === "frontend-ux-design")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should materialize frontend-ux-design built-in template when creating a task", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Task with frontend UX design review",
|
||||
enabledWorkflowSteps: ["frontend-ux-design"],
|
||||
});
|
||||
|
||||
expect(task.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
|
||||
const step = await store.getWorkflowStep("WS-001");
|
||||
expect(step).toMatchObject({
|
||||
id: "WS-001",
|
||||
templateId: "frontend-ux-design",
|
||||
name: "Frontend UX Design",
|
||||
toolMode: "readonly",
|
||||
});
|
||||
expect(steps.filter((step) => step.templateId === "frontend-ux-design")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not set enabledWorkflowSteps when empty array provided", async () => {
|
||||
@@ -833,17 +821,23 @@ describe("TaskStore Workflow Steps", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should update task workflow steps and materialize built-in templates", async () => {
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: U6 made resolveEnabledWorkflowSteps a
|
||||
// pure pass-through on the update path too — a former built-in template id is kept
|
||||
// verbatim and never materialized into a WS row.
|
||||
it("passes a former built-in template id through updateTask untouched (no materialization)", async () => {
|
||||
const task = await store.createTask({ description: "Editable task" });
|
||||
|
||||
const updated = await store.updateTask(task.id, {
|
||||
enabledWorkflowSteps: ["frontend-ux-design"],
|
||||
});
|
||||
|
||||
expect(updated.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
expect(updated.enabledWorkflowSteps).toEqual(["frontend-ux-design"]);
|
||||
|
||||
const persisted = await store.getTask(task.id);
|
||||
expect(persisted.enabledWorkflowSteps).toEqual(["WS-001"]);
|
||||
expect(persisted.enabledWorkflowSteps).toEqual(["frontend-ux-design"]);
|
||||
|
||||
const steps = await store.listWorkflowSteps();
|
||||
expect(steps.filter((step) => step.templateId === "frontend-ux-design")).toHaveLength(0);
|
||||
});
|
||||
|
||||
// FNXC:WorkflowOptionalGroup 2026-06-26-04:30: FN-7039 update-path surface — a
|
||||
@@ -865,30 +859,14 @@ describe("TaskStore Workflow Steps", () => {
|
||||
expect(steps.filter((step) => step.templateId === "browser-verification")).toHaveLength(0);
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve frontend-ux-design built-in template from getWorkflowStep", async () => {
|
||||
const step = await store.getWorkflowStep("frontend-ux-design");
|
||||
|
||||
expect(step).toMatchObject({
|
||||
id: "frontend-ux-design",
|
||||
templateId: "frontend-ux-design",
|
||||
name: "Frontend UX Design",
|
||||
mode: "prompt",
|
||||
phase: "pre-merge",
|
||||
toolMode: "readonly",
|
||||
});
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: U6 deleted the built-in
|
||||
// WORKFLOW_STEP_TEMPLATES catalog and the getWorkflowStep built-in-synthesis
|
||||
// fallback. Built-in quality gates (browser-verification, code-review) are now graph
|
||||
// optional-group nodes, not synthesized WorkflowStep rows — so getWorkflowStep returns
|
||||
// undefined for a built-in id that has no stored row.
|
||||
it("returns undefined for built-in optional-group ids (no longer synthesized)", async () => {
|
||||
expect(await store.getWorkflowStep("browser-verification")).toBeUndefined();
|
||||
expect(await store.getWorkflowStep("frontend-ux-design")).toBeUndefined();
|
||||
});
|
||||
|
||||
// ── Workflow Step Phase ──────────────────────────────────────────────
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "../types";
|
||||
|
||||
const TARGET_IDS = [
|
||||
"documentation-review",
|
||||
"qa-check",
|
||||
"security-audit",
|
||||
"performance-review",
|
||||
"accessibility-check",
|
||||
"browser-verification",
|
||||
"code-review",
|
||||
"frontend-ux-design",
|
||||
] as const;
|
||||
|
||||
describe("workflow step template verdict contracts", () => {
|
||||
it.each(TARGET_IDS)("%s uses canonical structured verdict output", (id) => {
|
||||
const template = WORKFLOW_STEP_TEMPLATES.find((entry) => entry.id === id);
|
||||
expect(template).toBeTruthy();
|
||||
|
||||
const prompt = template!.prompt;
|
||||
expect(prompt).toMatch(/"verdict":"APPROVE\|APPROVE_WITH_NOTES\|REVISE"/);
|
||||
expect(prompt).not.toContain('"verdict":"PASS"');
|
||||
expect(prompt).not.toContain('"verdict":"FAIL"');
|
||||
expect(prompt).not.toContain("task_done(");
|
||||
expect(prompt).not.toContain("task_log(");
|
||||
expect(prompt).toMatch(/Diff Scope|out of scope/i);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { WorkflowIrNode } from "./workflow-ir-types.js";
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "./types.js";
|
||||
|
||||
/*
|
||||
FNXC:WorkflowOptionalGroup 2026-06-21-15:10:
|
||||
@@ -18,45 +17,77 @@ keeping it identical to the prior `optionalSteps` templateId preserves any persi
|
||||
(`browser-verification-step`) because a template node id may not collide with the
|
||||
group/top-level node id (U1 validation).
|
||||
|
||||
The inner node mirrors the dashboard's `stepTemplateToNode` projection of the
|
||||
canonical `browser-verification` WORKFLOW_STEP_TEMPLATE: a `prompt` node carrying the
|
||||
template's prompt, `toolMode` (coding), and `gateMode` (advisory default). Sourcing
|
||||
prompt/toolMode from the catalog keeps the built-in byte-identical to the template a
|
||||
human would insert from the palette (KTD-5).
|
||||
FNXC:WorkflowOptionalGroup 2026-06-25-00:00:
|
||||
U6 deleted the built-in step-template catalog; the inner node's literal
|
||||
name/description/prompt/toolMode/gateMode are now inlined here directly (byte-identical
|
||||
to the former `browser-verification` catalog entry). These built-ins are the parity
|
||||
oracle, so the produced node bytes must NOT change. Plugin-contributed templates still
|
||||
use the `WorkflowStepTemplate` shape via the editor palette, but built-ins no longer
|
||||
read from a shared array.
|
||||
*/
|
||||
|
||||
function resolveBrowserVerificationTemplate() {
|
||||
const tpl = WORKFLOW_STEP_TEMPLATES.find((t) => t.id === "browser-verification");
|
||||
if (!tpl) {
|
||||
throw new Error("browser-verification WORKFLOW_STEP_TEMPLATE is missing");
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
|
||||
const BROWSER_VERIFICATION_TEMPLATE = resolveBrowserVerificationTemplate();
|
||||
|
||||
/** Stable per-task enable key + group node id (preserved from the prior templateId). */
|
||||
export const BROWSER_VERIFICATION_GROUP_ID = "browser-verification";
|
||||
|
||||
/** Inner template node id — distinct from the group id (template-node-id collision rule, U1). */
|
||||
export const BROWSER_VERIFICATION_STEP_NODE_ID = "browser-verification-step";
|
||||
|
||||
/** Display name (inlined from the former `browser-verification` catalog template). */
|
||||
const BROWSER_VERIFICATION_NAME = "Browser Verification";
|
||||
|
||||
/** Short description (inlined from the former catalog template). */
|
||||
const BROWSER_VERIFICATION_DESCRIPTION = "Verify web application functionality using browser automation";
|
||||
|
||||
/** Agent prompt (inlined verbatim from the former catalog template — parity oracle). */
|
||||
const BROWSER_VERIFICATION_PROMPT = `You are a browser verification specialist. Verify web application functionality after task implementation using the agent-browser CLI tool.
|
||||
|
||||
## Prerequisites
|
||||
First, determine the URL to verify. Check the task PROMPT.md for any URLs mentioned, or look at the code changes to identify the local development server URL (typically http://localhost:3000, http://localhost:5173, http://localhost:8080, etc.).
|
||||
|
||||
## Verification Commands
|
||||
Use these agent-browser commands for verification:
|
||||
- \`agent-browser open <url>\` — Navigate to the page
|
||||
- \`agent-browser snapshot -i\` — Get interactive elements with refs (@e1, @e2, etc.)
|
||||
- \`agent-browser click @e1\` — Click an element
|
||||
- \`agent-browser fill @e1 "text"\` — Fill an input field
|
||||
- \`agent-browser get text @e1\` — Get element text content
|
||||
- \`agent-browser screenshot\` — Capture screenshot to file
|
||||
- \`agent-browser wait --load networkidle\` — Wait for page to fully load
|
||||
|
||||
## Verification Checklist
|
||||
1. Page loads without JavaScript errors or blank screens
|
||||
2. Navigation between pages/sections works
|
||||
3. Forms accept input and submit correctly
|
||||
4. Interactive elements (buttons, links) respond to clicks
|
||||
5. Error states are handled gracefully
|
||||
6. Screenshots capture expected content
|
||||
|
||||
## Output Requirements
|
||||
- Fast-bail: if Diff Scope contains no browser-verification-relevant UI files, output {"verdict":"APPROVE","notes":"out of scope: browser verification"} immediately.
|
||||
- APPROVE: verification succeeds.
|
||||
- APPROVE_WITH_NOTES: verification succeeds with non-blocking advisory findings; include evidence references in notes.
|
||||
- REVISE: verification failures or regressions require changes; include failing behavior and actionable file paths in notes.
|
||||
- Screenshots/artifacts referenced in notes are evidence only; verdict must be conveyed by the final JSON line.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}
|
||||
|
||||
Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after clicking links or form submissions.`;
|
||||
|
||||
/**
|
||||
* Build the `browser-verification` optional-group node placed on a workflow's
|
||||
* pre-merge path. `column` matches where the legacy `workflow-step` seam sat
|
||||
* (in-progress) so the editor renders the group in the implementation column.
|
||||
*
|
||||
* Mirrors `stepTemplateToNode(browser-verification)`: a single `prompt` node whose
|
||||
* config carries the catalog prompt + `toolMode: "coding"` + `gateMode: "advisory"`.
|
||||
* config carries the inlined prompt + `toolMode: "coding"` + `gateMode: "advisory"`.
|
||||
*/
|
||||
export function browserVerificationOptionalGroupNode(column: string): WorkflowIrNode {
|
||||
const tpl = BROWSER_VERIFICATION_TEMPLATE;
|
||||
return {
|
||||
id: BROWSER_VERIFICATION_GROUP_ID,
|
||||
kind: "optional-group",
|
||||
column,
|
||||
config: {
|
||||
name: tpl.name,
|
||||
name: BROWSER_VERIFICATION_NAME,
|
||||
defaultOn: false,
|
||||
template: {
|
||||
nodes: [
|
||||
@@ -64,11 +95,11 @@ export function browserVerificationOptionalGroupNode(column: string): WorkflowIr
|
||||
id: BROWSER_VERIFICATION_STEP_NODE_ID,
|
||||
kind: "prompt",
|
||||
config: {
|
||||
name: tpl.name,
|
||||
description: tpl.description,
|
||||
prompt: tpl.prompt ?? "",
|
||||
toolMode: tpl.toolMode === "coding" ? "coding" : "readonly",
|
||||
gateMode: tpl.gateMode ?? "advisory",
|
||||
name: BROWSER_VERIFICATION_NAME,
|
||||
description: BROWSER_VERIFICATION_DESCRIPTION,
|
||||
prompt: BROWSER_VERIFICATION_PROMPT,
|
||||
toolMode: "coding",
|
||||
gateMode: "advisory",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { WorkflowIrNode } from "./workflow-ir-types.js";
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "./types.js";
|
||||
|
||||
/*
|
||||
FNXC:CodeReviewStep 2026-06-25-15:00:
|
||||
@@ -16,29 +15,55 @@ key; the inner template node carries a DISTINCT id (`code-review-step`) because
|
||||
node id may not collide with the group/top-level node id (U1 validation).
|
||||
|
||||
The inner node mirrors the dashboard's `stepTemplateToNode` projection of the canonical
|
||||
`code-review` WORKFLOW_STEP_TEMPLATE: a `prompt` node carrying the template's prompt,
|
||||
`toolMode` (readonly — review reads the diff, never mutates), and `gateMode` (advisory —
|
||||
non-blocking, like the existing review; operators can promote to a gate). Sourcing
|
||||
prompt/toolMode/gateMode from the catalog keeps the built-in byte-identical to the
|
||||
template a human would insert from the palette (KTD-5).
|
||||
`code-review` step: a `prompt` node carrying the prompt, `toolMode` (readonly — review
|
||||
reads the diff, never mutates), and `gateMode` (advisory — non-blocking, like the
|
||||
existing review; operators can promote to a gate).
|
||||
|
||||
FNXC:CodeReviewStep 2026-06-25-00:00:
|
||||
U6 deleted the built-in step-template catalog; the inner node's literal
|
||||
name/description/prompt/toolMode/gateMode are now inlined here directly (byte-identical
|
||||
to the former `code-review` catalog entry). These built-ins are the parity oracle, so
|
||||
the produced node bytes must NOT change.
|
||||
*/
|
||||
|
||||
function resolveCodeReviewTemplate() {
|
||||
const tpl = WORKFLOW_STEP_TEMPLATES.find((t) => t.id === "code-review");
|
||||
if (!tpl) {
|
||||
throw new Error("code-review WORKFLOW_STEP_TEMPLATE is missing");
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
|
||||
const CODE_REVIEW_TEMPLATE = resolveCodeReviewTemplate();
|
||||
|
||||
/** Stable per-task enable key + group node id. */
|
||||
export const CODE_REVIEW_GROUP_ID = "code-review";
|
||||
|
||||
/** Inner template node id — distinct from the group id (template-node-id collision rule, U1). */
|
||||
export const CODE_REVIEW_STEP_NODE_ID = "code-review-step";
|
||||
|
||||
/** Display name (inlined from the former `code-review` catalog template). */
|
||||
const CODE_REVIEW_NAME = "Code Review";
|
||||
|
||||
/** Short description (inlined from the former catalog template). */
|
||||
const CODE_REVIEW_DESCRIPTION =
|
||||
"Diff-review the task's changes for correctness bugs, regressions, and intent mismatches that tests miss";
|
||||
|
||||
/** Agent prompt (inlined verbatim from the former catalog template — parity oracle). */
|
||||
const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the task's diff for the correctness value automated tests do NOT catch.
|
||||
|
||||
## Step 1: Read the change
|
||||
1. Read the full diff against the base branch: \`git diff <base>...HEAD\` (or \`git diff <base>\`). Determine the base from the task context / merge target.
|
||||
2. Read the changed files in full where the diff is non-trivial, so you see the surrounding code paths the change touches — not just the hunks.
|
||||
|
||||
## Step 2: Review focus (the value tests miss)
|
||||
1. **Correctness / logic bugs** — wrong conditions, inverted boolean/comparison logic, off-by-one, incorrect operator precedence, mishandled return values.
|
||||
2. **Broken edge cases** — empty/undefined/null inputs, zero/duplicate/boundary values, concurrency and ordering assumptions.
|
||||
3. **Intent vs implementation** — does the code actually do what the task/PROMPT.md describes? Flag silent scope drift or partial implementations.
|
||||
4. **Regressions in touched code paths** — does the change break or weaken an existing behavior in the files it edits or their callers?
|
||||
5. **Error handling** — swallowed errors, unhandled rejections/exceptions, missing validation at trust boundaries, misleading error messages.
|
||||
6. **Contract / signature changes** — changed function/exported-type signatures, API request/response shapes, or serialization that breaks existing callers.
|
||||
|
||||
Be specific: cite \`file:line\` for every finding and explain the concrete failure it causes.
|
||||
|
||||
## Output Requirements
|
||||
- Fast-bail: if the diff is trivial, generated, or out-of-scope for code review (e.g. pure docs/config/formatting with no logic), output {"verdict":"APPROVE","notes":"out of scope: code review"} immediately and stop.
|
||||
- APPROVE: no correctness concerns; use empty or brief notes.
|
||||
- APPROVE_WITH_NOTES: shippable, but include non-blocking advisories (with file:line) in notes.
|
||||
- REVISE: a correctness bug, regression, or contract break requires changes; include file:line and the concrete failure plus remediation in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`;
|
||||
|
||||
/**
|
||||
* Build the `code-review` optional-group node placed on a workflow's pre-merge path.
|
||||
* `defaultOn: true` makes it run by default while remaining togglable per task. `column`
|
||||
@@ -46,16 +71,15 @@ export const CODE_REVIEW_STEP_NODE_ID = "code-review-step";
|
||||
* the group in the implementation column.
|
||||
*
|
||||
* Mirrors `stepTemplateToNode(code-review)`: a single `prompt` node whose config carries
|
||||
* the catalog prompt + `toolMode: "readonly"` + `gateMode: "advisory"`.
|
||||
* the inlined prompt + `toolMode: "readonly"` + `gateMode: "advisory"`.
|
||||
*/
|
||||
export function codeReviewOptionalGroupNode(column: string): WorkflowIrNode {
|
||||
const tpl = CODE_REVIEW_TEMPLATE;
|
||||
return {
|
||||
id: CODE_REVIEW_GROUP_ID,
|
||||
kind: "optional-group",
|
||||
column,
|
||||
config: {
|
||||
name: tpl.name,
|
||||
name: CODE_REVIEW_NAME,
|
||||
// Default-ON: runs for every coding task by default, but operators can toggle it
|
||||
// off per task (remove `code-review` from enabledWorkflowSteps).
|
||||
defaultOn: true,
|
||||
@@ -65,11 +89,11 @@ export function codeReviewOptionalGroupNode(column: string): WorkflowIrNode {
|
||||
id: CODE_REVIEW_STEP_NODE_ID,
|
||||
kind: "prompt",
|
||||
config: {
|
||||
name: tpl.name,
|
||||
description: tpl.description,
|
||||
prompt: tpl.prompt ?? "",
|
||||
toolMode: tpl.toolMode === "coding" ? "coding" : "readonly",
|
||||
gateMode: tpl.gateMode ?? "advisory",
|
||||
name: CODE_REVIEW_NAME,
|
||||
description: CODE_REVIEW_DESCRIPTION,
|
||||
prompt: CODE_REVIEW_PROMPT,
|
||||
toolMode: "readonly",
|
||||
gateMode: "advisory",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES } from "./types.js";
|
||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES } from "./types.js";
|
||||
export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskTokenUsagePerModel, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, ArtifactType, Artifact, ArtifactCreateInput, ArtifactWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, Locale, ExecutionMode, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY, assertNotWorkspaceTaskMerge, isWorkspaceTask, WorkspaceTaskMergeError } from "./types.js";
|
||||
export {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { existsSync, watch, type Dirent, type FSWatcher } from "node:fs";
|
||||
import { detectWorkspaceRepos, saveWorkspaceConfig, loadWorkspaceConfig } from "./git-repository.js";
|
||||
import type { Task, TaskDetail, TaskCreateInput, TaskAttachment, AgentLogEntry, BoardConfig, Column, ColumnId, CheckoutClaimPrecondition, MergeResult, Settings, GlobalSettings, ProjectSettings, ActivityLogEntry, ActivityEventType, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, Artifact, ArtifactCreateInput, ArtifactType, ArtifactWithTask, InboxTask, TaskLogEntry, RunMutationContext, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, ArchivedTaskEntry, ArchiveAgentLogMode, TaskPriority, SourceType, WorkflowStepTemplate, Agent, AutostashOrphanRecord, TaskCommitAssociation, TaskCommitAssociationMatchSource, TaskCommitAssociationConfidence, CommitAssociationDiffBackfillReport, GithubIssueAction, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, HandoffToReviewOptions, GoalCitation, GoalCitationFilter, GoalCitationInput, GoalCitationSurface, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, TaskBranchAssignmentMode, MergeRequestRecord, MergeRequestState, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, PrEntity, PrEntityCreateInput, PrEntityUpdate, PrEntityState, PrThreadState, PrThreadOutcome, PrConflictState, PrChecksRollup, PrReviewDecision, PluginActivation, PluginActivationInput } from "./types.js";
|
||||
import { createActivityLogSnapshot, createRunAuditSnapshot, createTaskMetadataSnapshot, toTaskMetadataRecord, validateSnapshotEnvelope, type ActivityLogSnapshot, type RunAuditSnapshot, type TaskMetadataSnapshot } from "./shared-mesh-state.js";
|
||||
import { VALID_TRANSITIONS, COLUMNS, DEFAULT_SETTINGS, isColumn, isGlobalOnlySettingsKey, WORKFLOW_STEP_TEMPLATES, validateDocumentKey, assertNotWorkspaceTaskMerge } from "./types.js";
|
||||
import { VALID_TRANSITIONS, COLUMNS, DEFAULT_SETTINGS, isColumn, isGlobalOnlySettingsKey, validateDocumentKey, assertNotWorkspaceTaskMerge } from "./types.js";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "./settings-schema.js";
|
||||
import {
|
||||
MOVED_SETTINGS_KEYS,
|
||||
@@ -116,7 +116,7 @@ import type {
|
||||
WorkflowNodeLayout,
|
||||
} from "./workflow-definition-types.js";
|
||||
import { compileWorkflowToSteps, isInterpreterDeferredWorkflowCompileError } from "./workflow-compiler.js";
|
||||
import { resolveDefaultOnOptionalGroupIds, resolveAllOptionalGroupIds } from "./workflow-optional-steps.js";
|
||||
import { resolveDefaultOnOptionalGroupIds } from "./workflow-optional-steps.js";
|
||||
import {
|
||||
BUILTIN_WORKFLOWS,
|
||||
getBuiltinWorkflow,
|
||||
@@ -4217,28 +4217,6 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
return `${Date.now()}-${id}-${sanitized}`;
|
||||
}
|
||||
|
||||
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",
|
||||
gateMode: "advisory",
|
||||
prompt: template.prompt,
|
||||
toolMode: template.toolMode || "readonly",
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
private toStoredWorkflowStep(row: {
|
||||
id: string;
|
||||
templateId: string | null;
|
||||
@@ -4317,59 +4295,22 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private async ensureWorkflowStepForTemplate(templateId: string): Promise<import("./types.js").WorkflowStep> {
|
||||
const template = this.getBuiltInWorkflowTemplate(templateId);
|
||||
if (!template) {
|
||||
throw new Error(`Workflow step template '${templateId}' not found`);
|
||||
}
|
||||
|
||||
const existing = await this.getWorkflowStep(templateId);
|
||||
if (existing && existing.id !== templateId) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const allSteps = await this.listWorkflowSteps();
|
||||
const byName = allSteps.find((step) => step.name.toLowerCase() === template.name.toLowerCase());
|
||||
if (byName) {
|
||||
return byName;
|
||||
}
|
||||
|
||||
return this.createWorkflowStep({
|
||||
templateId: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
mode: "prompt",
|
||||
phase: "pre-merge",
|
||||
prompt: template.prompt,
|
||||
gateMode: "advisory",
|
||||
toolMode: template.toolMode || "readonly",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowOptionalGroup 2026-06-21-16:30:
|
||||
`optionalGroupIds` are the optional-group node ids of the task's workflow. They are executor toggle keys (matched by node id in `enabledWorkflowSteps`), NOT legacy `WorkflowStep` template ids. A built-in group id can deliberately collide with a `WORKFLOW_STEP_TEMPLATES` id (e.g. "browser-verification"); without this pass-through the colliding id is materialized into a step row whose id differs from the group node id, so the executor's `enabledWorkflowSteps.includes(node.id)` check fails and an enabled group is silently bypassed (P1 from code review). Editor-authored group ids never collide (they come from `newNodeId()`), so they already passed through; this guards the built-in collision.
|
||||
FNXC:WorkflowOptionalGroup 2026-06-25-00:00:
|
||||
U6 deleted the built-in step-template catalog and its template
|
||||
materializer (`getBuiltInWorkflowTemplate`/`ensureWorkflowStepForTemplate`/
|
||||
`toBuiltInWorkflowStep`). `resolveEnabledWorkflowSteps` is now a pure pass-through:
|
||||
enable ids are trimmed + de-duplicated but otherwise pass through UNCHANGED, keeping
|
||||
them identity-stable (KTD-6). There is no longer any built-in template to materialize
|
||||
into a `WS-xxx` row, so the prior `optionalGroupIdSet` collision guard (which kept
|
||||
built-in group ids out of materialization) is no longer needed and was removed — a
|
||||
group id like "browser-verification" now passes straight through, exactly matching the
|
||||
optional-group node id the executor toggles on `enabledWorkflowSteps.includes(node.id)`.
|
||||
Plugin (`plugin:`-prefixed) ids also pass through. Workflow-compiled step rows are still
|
||||
materialized separately via `materializeWorkflowSteps` (unchanged).
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowOptionalGroup 2026-06-26-04:30:
|
||||
Resolution order MUST mirror the executor's workflow resolution for the namespaces to line up: explicit `workflowId` → project default → `builtin:coding`. An unselected task with NO project default still runs `builtin:coding` (its optional-group nodes are `browser-verification` + `code-review`), so the toggle-key set must resolve there too. The earlier `?? getDefaultWorkflowId()` with an empty-set bail-out left this group-id set EMPTY for that common case, so a toggled `browser-verification` (which collides with a `WORKFLOW_STEP_TEMPLATES` id) got materialized into a `WS-NNN` step row the executor never matches against `enabledWorkflowSteps.includes(node.id)` — the optional step silently never ran and never appeared in the unified step progress bar (FN-7039 repro). Falling back to `builtin:coding` keeps the stored id equal to the group node id, so the executor runs it and the UI renders it.
|
||||
*/
|
||||
/** Optional-group node ids for a workflow (its `enabledWorkflowSteps` toggle
|
||||
* keys). Resolves explicit `workflowId` → project default → `builtin:coding`,
|
||||
* matching the executor's unselected-task resolution; empty for missing/fragment
|
||||
* workflows. Used to keep group ids out of the legacy step-template
|
||||
* materialization in {@link resolveEnabledWorkflowSteps}. */
|
||||
private async optionalGroupIdSet(workflowId?: string | null): Promise<Set<string>> {
|
||||
const wfId = workflowId ?? (await this.getDefaultWorkflowId()) ?? "builtin:coding";
|
||||
const def = await this.getWorkflowDefinition(wfId);
|
||||
if (!def || def.kind === "fragment") return new Set();
|
||||
return new Set(resolveAllOptionalGroupIds(def.ir));
|
||||
}
|
||||
|
||||
private async resolveEnabledWorkflowSteps(
|
||||
stepIds?: string[],
|
||||
optionalGroupIds?: Set<string>,
|
||||
): Promise<string[] | undefined> {
|
||||
if (!stepIds?.length) return undefined;
|
||||
|
||||
@@ -4379,26 +4320,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
for (const rawId of stepIds) {
|
||||
const stepId = rawId.trim();
|
||||
if (!stepId) continue;
|
||||
|
||||
if (stepId.startsWith("plugin:")) {
|
||||
if (!seen.has(stepId)) {
|
||||
seen.add(stepId);
|
||||
resolved.push(stepId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional-group toggle ids pass through raw — never materialized as legacy step rows.
|
||||
const template = optionalGroupIds?.has(stepId)
|
||||
? undefined
|
||||
: this.getBuiltInWorkflowTemplate(stepId);
|
||||
const resolvedId = template
|
||||
? (await this.ensureWorkflowStepForTemplate(stepId)).id
|
||||
: stepId;
|
||||
|
||||
if (!seen.has(resolvedId)) {
|
||||
seen.add(resolvedId);
|
||||
resolved.push(resolvedId);
|
||||
// Identity-stable pass-through: plugin ids, built-in optional-group ids, and any
|
||||
// other enable id are kept verbatim so the executor's node-id toggle check matches.
|
||||
if (!seen.has(stepId)) {
|
||||
seen.add(stepId);
|
||||
resolved.push(stepId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4531,10 +4457,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
|
||||
// Determine enabledWorkflowSteps: explicit input takes precedence, otherwise auto-apply default-on steps
|
||||
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length
|
||||
? await this.resolveEnabledWorkflowSteps(
|
||||
input.enabledWorkflowSteps,
|
||||
await this.optionalGroupIdSet(input.workflowId),
|
||||
)
|
||||
? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps)
|
||||
: undefined;
|
||||
|
||||
// When a project default workflow is configured, new tasks inherit it
|
||||
@@ -4729,10 +4652,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
|
||||
const title = input.title?.trim() || undefined;
|
||||
let resolvedWorkflowSteps: string[] | undefined = input.enabledWorkflowSteps?.length
|
||||
? await this.resolveEnabledWorkflowSteps(
|
||||
input.enabledWorkflowSteps,
|
||||
await this.optionalGroupIdSet(input.workflowId),
|
||||
)
|
||||
? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps)
|
||||
: undefined;
|
||||
|
||||
let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined;
|
||||
@@ -8805,13 +8725,12 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
|
||||
task.nextRecoveryAt = updates.nextRecoveryAt;
|
||||
}
|
||||
if (updates.enabledWorkflowSteps !== undefined) {
|
||||
// Pass the task's own workflow optional-group ids through untouched so a
|
||||
// toggled built-in group id (e.g. "browser-verification") is not remapped
|
||||
// to a materialized step row the executor never matches (code-review P1).
|
||||
const taskWorkflowId = this.getTaskWorkflowSelection(task.id)?.workflowId;
|
||||
// Enable ids pass through untouched (identity-stable, KTD-6) so a toggled
|
||||
// built-in group id (e.g. "browser-verification") matches the optional-group
|
||||
// node id the executor checks. U6 removed the template materializer, so there
|
||||
// is no longer any remapping to guard against.
|
||||
task.enabledWorkflowSteps = await this.resolveEnabledWorkflowSteps(
|
||||
updates.enabledWorkflowSteps,
|
||||
await this.optionalGroupIdSet(taskWorkflowId),
|
||||
);
|
||||
}
|
||||
if (updates.noCommitsExpected === null) {
|
||||
@@ -14742,10 +14661,25 @@ ${stepsSection}`;
|
||||
return this.applyLegacyWorkflowStepOverrides(this.toStoredWorkflowStep(byTemplate));
|
||||
}
|
||||
|
||||
const template = this.getBuiltInWorkflowTemplate(id);
|
||||
return template ? this.toBuiltInWorkflowStep(template) : undefined;
|
||||
// U6: the built-in step-template catalog was deleted. Built-in quality
|
||||
// gates (browser-verification, code-review) are now graph optional-group nodes, not
|
||||
// `workflow_steps` rows, so there is no built-in template to synthesize a step from.
|
||||
// An id with no stored row resolves to undefined (callers treat that as "no step").
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowStepCRUD 2026-06-25-00:00:
|
||||
U5 removed the `/api/workflow-steps` REST surface (GET/POST/PATCH/DELETE + refine), the
|
||||
`/workflow-step-templates/:id/create` route, the dead dashboard client mutations, and
|
||||
(already absent) the Settings management UI. The store-level `workflow_steps` CRUD is
|
||||
INTENTIONALLY KEPT in full: the table is retained (its drop is U7), `createWorkflowStep`
|
||||
drives the workflow-compilation materializer (`materializeWorkflowSteps`),
|
||||
`getWorkflowStep` is consumed by the engine (merger post-merge steps + executor
|
||||
recovery), `listWorkflowSteps` backs `readConfig`, and `update`/`deleteWorkflowStep`
|
||||
round-trip the table for those execution/read paths and their store tests. Removing the
|
||||
store methods belongs with U7's table drop, not the management-surface removal.
|
||||
*/
|
||||
/**
|
||||
* Update a workflow step definition.
|
||||
* @throws Error if the workflow step is not found
|
||||
|
||||
@@ -874,7 +874,17 @@ export interface WorkflowRunStepInstance {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** A built-in workflow step template for one-click creation. */
|
||||
/*
|
||||
FNXC:WorkflowStepTemplate 2026-06-25-00:00:
|
||||
U6 deleted the built-in step-template catalog array (the former value export). The
|
||||
`WorkflowStepTemplate` SHAPE is KEPT because plugin-contributed step templates still use
|
||||
it (they feed the
|
||||
workflow-editor optional-group palette via `getPluginWorkflowStepTemplates`). It is no
|
||||
longer backed by any built-in catalog: the former built-in `browser-verification` /
|
||||
`code-review` literals now live inlined in their optional-group node builders
|
||||
(`builtin-browser-verification-group.ts` / `builtin-code-review-group.ts`).
|
||||
*/
|
||||
/** A workflow step template shape used by plugin-contributed steps (palette entries). */
|
||||
export interface WorkflowStepTemplate {
|
||||
/** Unique template identifier (e.g., "documentation-review") */
|
||||
id: string;
|
||||
@@ -908,290 +918,6 @@ export interface WorkflowStepTemplate {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** Built-in workflow step templates available for one-click creation. */
|
||||
export const WORKFLOW_STEP_TEMPLATES: WorkflowStepTemplate[] = [
|
||||
{
|
||||
id: "documentation-review",
|
||||
name: "Documentation Review",
|
||||
description: "Verify all public APIs, functions, and complex logic have appropriate documentation",
|
||||
category: "Quality",
|
||||
icon: "file-text",
|
||||
toolMode: "readonly",
|
||||
prompt: `You are a documentation reviewer. Review the completed task and verify documentation quality.
|
||||
|
||||
Review Criteria:
|
||||
1. All new public functions, classes, and modules have JSDoc comments or equivalent documentation
|
||||
2. Complex logic has inline comments explaining the "why" not just the "what"
|
||||
3. README files are updated if the task changes user-facing behavior
|
||||
4. CHANGELOG or release notes are considered for significant changes
|
||||
5. Type definitions are documented for public APIs
|
||||
|
||||
Files to Review:
|
||||
- Review all files modified in the task worktree
|
||||
- Focus on public API surface area
|
||||
- Check test files for test documentation
|
||||
|
||||
Output Requirements:
|
||||
- Fast-bail: if Diff Scope contains no documentation-relevant files, output {"verdict":"APPROVE","notes":"out of scope: documentation"} immediately.
|
||||
- APPROVE: documentation is adequate; use empty or brief notes.
|
||||
- APPROVE_WITH_NOTES: documentation is adequate with advisory improvements; include concise suggestions in notes.
|
||||
- REVISE: documentation is missing or incorrect; include actionable file paths/functions in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "qa-check",
|
||||
name: "QA Check",
|
||||
description: "Run lint, tests, and typecheck; verify they pass and check for obvious bugs",
|
||||
category: "Quality",
|
||||
icon: "check-circle",
|
||||
toolMode: "coding",
|
||||
prompt: `You are a QA tester. Verify the task implementation by running lint, tests, and typecheck, and checking for bugs.
|
||||
|
||||
Quality Gate Execution (all three must pass):
|
||||
1. Run the project's lint command (e.g. \`pnpm lint\`, \`npm run lint\`)
|
||||
2. Run the project's test suite (e.g. \`pnpm test\`, \`npm test\`, or the configured test command)
|
||||
3. Run the project's typecheck command if one exists (e.g. \`pnpm typecheck\`, \`tsc --noEmit\`)
|
||||
4. Verify lint, tests, and typecheck all pass
|
||||
5. If any gate fails, analyze whether failures are related to the task changes
|
||||
|
||||
Code Review:
|
||||
1. Review the changes for obvious bugs or edge cases
|
||||
2. Check error handling is appropriate
|
||||
3. Verify input validation is present where needed
|
||||
4. Look for common issues: null pointer risks, off-by-one errors, race conditions
|
||||
|
||||
Output Requirements:
|
||||
- Fast-bail: if Diff Scope contains no QA-relevant files, output {"verdict":"APPROVE","notes":"out of scope: QA"} immediately.
|
||||
- APPROVE: lint/tests/typecheck pass and no actionable bugs.
|
||||
- APPROVE_WITH_NOTES: quality gates pass but include non-blocking advisories in notes.
|
||||
- REVISE: any gate fails or actionable bugs are found; include failing commands and affected file paths in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "security-audit",
|
||||
name: "Security Audit",
|
||||
description: "Check for common security vulnerabilities and anti-patterns",
|
||||
category: "Security",
|
||||
icon: "shield",
|
||||
toolMode: "readonly",
|
||||
prompt: `You are a security auditor. Review the task changes for common security vulnerabilities.
|
||||
|
||||
Security Checklist:
|
||||
1. **Injection vulnerabilities** — Check for SQL injection, command injection, XSS via unsanitized user input
|
||||
2. **Secrets and credentials** — Ensure no hardcoded passwords, API keys, tokens, or private keys
|
||||
3. **Unsafe eval** — Check for eval(), new Function(), or similar dangerous patterns
|
||||
4. **Path traversal** — Verify file path handling prevents directory traversal attacks
|
||||
5. **Insecure deserialization** — Check for unsafe parsing of untrusted data
|
||||
6. **Authentication/Authorization** — Verify access controls are properly implemented
|
||||
7. **Dependency risks** — Note any new dependencies that might have known vulnerabilities
|
||||
|
||||
Files to Review:
|
||||
- All modified files in the task
|
||||
- Configuration files that might contain secrets
|
||||
- Areas handling user input or external data
|
||||
|
||||
Output Requirements:
|
||||
- Fast-bail: if Diff Scope contains no security-relevant files, output {"verdict":"APPROVE","notes":"out of scope: security"} immediately.
|
||||
- APPROVE: no security issues found.
|
||||
- APPROVE_WITH_NOTES: no blocking issues, but include advisory hardening opportunities in notes.
|
||||
- REVISE: vulnerabilities require changes; include file paths, severity, and remediation guidance in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "performance-review",
|
||||
name: "Performance Review",
|
||||
description: "Check for performance anti-patterns and optimization opportunities",
|
||||
category: "Quality",
|
||||
icon: "zap",
|
||||
toolMode: "readonly",
|
||||
prompt: `You are a performance reviewer. Analyze the task changes for performance implications.
|
||||
|
||||
Performance Checklist:
|
||||
1. **Algorithmic complexity** — Check for O(n²) or worse patterns that could bottleneck
|
||||
2. **N+1 queries** — Look for database queries in loops
|
||||
3. **Memory leaks** — Check for unclosed resources, event listeners, or accumulating caches
|
||||
4. **Unnecessary re-renders** — For UI code, check for inefficient React/Angular/Vue patterns
|
||||
5. **Bundle size** — Note if large dependencies are added unnecessarily
|
||||
6. **Async patterns** — Verify proper use of async/await, Promise.all for parallel work
|
||||
7. **Caching opportunities** — Identify where caching could improve performance
|
||||
|
||||
Files to Review:
|
||||
- All modified files, focusing on hot paths and frequently executed code
|
||||
- Database query files
|
||||
- API endpoints and route handlers
|
||||
|
||||
Output Requirements:
|
||||
- Fast-bail: if Diff Scope contains no performance-relevant files, output {"verdict":"APPROVE","notes":"out of scope: performance"} immediately.
|
||||
- APPROVE: performance impact is acceptable.
|
||||
- APPROVE_WITH_NOTES: acceptable overall, but include optimization advisories in notes.
|
||||
- REVISE: performance risks require changes; include actionable file paths and optimization guidance in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "accessibility-check",
|
||||
name: "Accessibility Check",
|
||||
description: "Verify UI changes meet accessibility standards (WCAG 2.1)",
|
||||
category: "Quality",
|
||||
icon: "eye",
|
||||
toolMode: "readonly",
|
||||
prompt: `You are an accessibility reviewer. Check UI changes for WCAG 2.1 compliance.
|
||||
|
||||
Accessibility Checklist:
|
||||
1. **Keyboard navigation** — Ensure all interactive elements are keyboard accessible
|
||||
2. **ARIA labels** — Check that screen reader announcements are appropriate
|
||||
3. **Color contrast** — Verify text meets minimum contrast ratios (4.5:1 for normal text)
|
||||
4. **Focus indicators** — Ensure visible focus states for keyboard navigation
|
||||
5. **Alt text** — Check that images have meaningful alternative text
|
||||
6. **Form labels** — Verify all inputs have associated labels
|
||||
7. **Semantic HTML** — Check that proper HTML elements are used (buttons not divs)
|
||||
|
||||
Files to Review:
|
||||
- Modified UI components
|
||||
- CSS/styling changes
|
||||
- New HTML templates or JSX
|
||||
|
||||
Output Requirements:
|
||||
- Fast-bail: if Diff Scope contains no accessibility-relevant UI files, output {"verdict":"APPROVE","notes":"out of scope: accessibility"} immediately.
|
||||
- APPROVE: accessibility requirements are met.
|
||||
- APPROVE_WITH_NOTES: compliant overall, with advisory improvements in notes.
|
||||
- REVISE: accessibility issues require changes; include file paths, WCAG references, and remediation steps in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "browser-verification",
|
||||
name: "Browser Verification",
|
||||
description: "Verify web application functionality using browser automation",
|
||||
category: "Quality",
|
||||
icon: "globe",
|
||||
toolMode: "coding",
|
||||
prompt: `You are a browser verification specialist. Verify web application functionality after task implementation using the agent-browser CLI tool.
|
||||
|
||||
## Prerequisites
|
||||
First, determine the URL to verify. Check the task PROMPT.md for any URLs mentioned, or look at the code changes to identify the local development server URL (typically http://localhost:3000, http://localhost:5173, http://localhost:8080, etc.).
|
||||
|
||||
## Verification Commands
|
||||
Use these agent-browser commands for verification:
|
||||
- \`agent-browser open <url>\` — Navigate to the page
|
||||
- \`agent-browser snapshot -i\` — Get interactive elements with refs (@e1, @e2, etc.)
|
||||
- \`agent-browser click @e1\` — Click an element
|
||||
- \`agent-browser fill @e1 "text"\` — Fill an input field
|
||||
- \`agent-browser get text @e1\` — Get element text content
|
||||
- \`agent-browser screenshot\` — Capture screenshot to file
|
||||
- \`agent-browser wait --load networkidle\` — Wait for page to fully load
|
||||
|
||||
## Verification Checklist
|
||||
1. Page loads without JavaScript errors or blank screens
|
||||
2. Navigation between pages/sections works
|
||||
3. Forms accept input and submit correctly
|
||||
4. Interactive elements (buttons, links) respond to clicks
|
||||
5. Error states are handled gracefully
|
||||
6. Screenshots capture expected content
|
||||
|
||||
## Output Requirements
|
||||
- Fast-bail: if Diff Scope contains no browser-verification-relevant UI files, output {"verdict":"APPROVE","notes":"out of scope: browser verification"} immediately.
|
||||
- APPROVE: verification succeeds.
|
||||
- APPROVE_WITH_NOTES: verification succeeds with non-blocking advisory findings; include evidence references in notes.
|
||||
- REVISE: verification failures or regressions require changes; include failing behavior and actionable file paths in notes.
|
||||
- Screenshots/artifacts referenced in notes are evidence only; verdict must be conveyed by the final JSON line.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}
|
||||
|
||||
Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after clicking links or form submissions.`,
|
||||
},
|
||||
{
|
||||
/*
|
||||
FNXC:CodeReviewStep 2026-06-25-12:00:
|
||||
Built-in "Code Review" catalog template: a configurable pre-merge prompt-gate that
|
||||
diff-reviews the task's changes for the correctness value automated tests miss
|
||||
(logic bugs, broken edge cases, intent-vs-implementation drift, regressions in
|
||||
touched paths, error handling, contract/signature breaks). This is the WORKFLOW-layer
|
||||
code review — it reuses the shared prompt-gate verdict machinery, NOT engine
|
||||
verification code. gateMode defaults to "advisory" (non-blocking) exactly like
|
||||
browser-verification, so it is opt-in/non-blocking until an operator promotes it to a
|
||||
blocking gate. phase "pre-merge" places it before merge. toolMode "readonly": review
|
||||
reads the diff/files, it does not mutate the worktree.
|
||||
*/
|
||||
id: "code-review",
|
||||
name: "Code Review",
|
||||
description: "Diff-review the task's changes for correctness bugs, regressions, and intent mismatches that tests miss",
|
||||
category: "Quality",
|
||||
icon: "git-pull-request",
|
||||
toolMode: "readonly",
|
||||
gateMode: "advisory",
|
||||
phase: "pre-merge",
|
||||
prompt: `You are a senior code reviewer. Review the task's diff for the correctness value automated tests do NOT catch.
|
||||
|
||||
## Step 1: Read the change
|
||||
1. Read the full diff against the base branch: \`git diff <base>...HEAD\` (or \`git diff <base>\`). Determine the base from the task context / merge target.
|
||||
2. Read the changed files in full where the diff is non-trivial, so you see the surrounding code paths the change touches — not just the hunks.
|
||||
|
||||
## Step 2: Review focus (the value tests miss)
|
||||
1. **Correctness / logic bugs** — wrong conditions, inverted boolean/comparison logic, off-by-one, incorrect operator precedence, mishandled return values.
|
||||
2. **Broken edge cases** — empty/undefined/null inputs, zero/duplicate/boundary values, concurrency and ordering assumptions.
|
||||
3. **Intent vs implementation** — does the code actually do what the task/PROMPT.md describes? Flag silent scope drift or partial implementations.
|
||||
4. **Regressions in touched code paths** — does the change break or weaken an existing behavior in the files it edits or their callers?
|
||||
5. **Error handling** — swallowed errors, unhandled rejections/exceptions, missing validation at trust boundaries, misleading error messages.
|
||||
6. **Contract / signature changes** — changed function/exported-type signatures, API request/response shapes, or serialization that breaks existing callers.
|
||||
|
||||
Be specific: cite \`file:line\` for every finding and explain the concrete failure it causes.
|
||||
|
||||
## Output Requirements
|
||||
- Fast-bail: if the diff is trivial, generated, or out-of-scope for code review (e.g. pure docs/config/formatting with no logic), output {"verdict":"APPROVE","notes":"out of scope: code review"} immediately and stop.
|
||||
- APPROVE: no correctness concerns; use empty or brief notes.
|
||||
- APPROVE_WITH_NOTES: shippable, but include non-blocking advisories (with file:line) in notes.
|
||||
- REVISE: a correctness bug, regression, or contract break requires changes; include file:line and the concrete failure plus remediation in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}`,
|
||||
},
|
||||
{
|
||||
id: "frontend-ux-design",
|
||||
name: "Frontend UX Design",
|
||||
description: "Verify visual polish and consistency with existing UI patterns and design tokens",
|
||||
category: "Quality",
|
||||
icon: "layout-grid",
|
||||
toolMode: "readonly",
|
||||
prompt: `You are a UX design reviewer. Verify frontend changes maintain visual polish and consistency with existing UI patterns and design tokens.
|
||||
|
||||
## Step 1: Scope Check (MANDATORY FIRST)
|
||||
|
||||
The task harness provides a "Diff Scope" listing files this task actually changed.
|
||||
|
||||
If the Diff Scope contains ZERO frontend/UI files (no .tsx/.jsx/.ts/.js component files, no .css/.scss/.sass/.styl, no .html/.vue/.svelte/.astro, no design-token/theme files), output ONLY:
|
||||
{"verdict":"APPROVE","notes":"out of scope: frontend UX design"}
|
||||
Then STOP. Do not browse the worktree. Do not read any files.
|
||||
|
||||
If there ARE frontend/UI files in scope, proceed to Step 2.
|
||||
|
||||
## Step 2: Design Review
|
||||
|
||||
Restrict your review to ONLY the UI files in the diff scope.
|
||||
|
||||
Check:
|
||||
1. **Visual Hierarchy** — heading levels, content flow, information architecture
|
||||
2. **Spacing and Typography** — consistent margins, padding, gaps, type scale
|
||||
3. **Color and Token Consistency** — CSS custom properties and design tokens used; no hardcoded colors
|
||||
4. **Component Reuse** — existing components reused; no one-off styling or duplication
|
||||
5. **Responsive Behavior** — layouts adapt across viewports
|
||||
6. **Fit with Design Language** — border radius, shadows, transitions, icon style match patterns
|
||||
|
||||
## Output Format
|
||||
|
||||
- APPROVE: visual quality is acceptable; use empty or brief notes.
|
||||
- APPROVE_WITH_NOTES: acceptable with non-blocking polish advisories; include specific notes.
|
||||
- REVISE: issues require code changes; include specific files and required changes in notes.
|
||||
- Final output: output exactly one trailing JSON object on the final line (no markdown fences, no surrounding prose):
|
||||
{"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE","notes":"..."}
|
||||
|
||||
Prioritize: layout breaks > visual inconsistency > style preferences.
|
||||
Do NOT spend time on nits when no real issues exist.`,
|
||||
},
|
||||
];
|
||||
|
||||
export type PrConflictState = "clean" | "conflicting" | "behind" | "blocked" | "unknown";
|
||||
|
||||
export interface PrConflictDiagnostics {
|
||||
|
||||
@@ -80,7 +80,7 @@ export function resolveDefaultOnOptionalGroupIds(ir: WorkflowIr): string[] {
|
||||
|
||||
/*
|
||||
FNXC:WorkflowOptionalGroup 2026-06-21-16:30:
|
||||
Every optional-group node id in a workflow, regardless of `defaultOn`. These ids are executor toggle keys (the per-task `enabledWorkflowSteps` set), NOT legacy `WorkflowStep` template ids. A built-in group id can deliberately equal a `WORKFLOW_STEP_TEMPLATES` id (e.g. "browser-verification"), so the store must pass these through `resolveEnabledWorkflowSteps` untouched instead of materializing them into a step row whose id the executor would never match.
|
||||
Every optional-group node id in a workflow, regardless of `defaultOn`. These ids are executor toggle keys (the per-task `enabledWorkflowSteps` set), NOT legacy `WorkflowStep` template ids. A built-in group id (e.g. "browser-verification") is passed through `resolveEnabledWorkflowSteps` untouched. (Historically it could collide with an id in the now-deleted built-in step-template catalog, which would wrongly materialize it into a step row the executor never matched; U6 removed that catalog + the materializer, so resolution is a pure identity-stable pass-through.)
|
||||
*/
|
||||
export function resolveAllOptionalGroupIds(ir: WorkflowIr): string[] {
|
||||
return resolveWorkflowOptionalSteps(ir).map((step) => step.templateId);
|
||||
|
||||
@@ -5215,38 +5215,22 @@ export function clearActivityLog(projectId?: string): Promise<{ success: boolean
|
||||
|
||||
// ── Workflow Steps ─────────────────────────────────────────────────────
|
||||
|
||||
/** Fetch all workflow step definitions */
|
||||
export function fetchWorkflowSteps(projectId?: string): Promise<WorkflowStep[]> {
|
||||
const path = withProjectId("/workflow-steps", projectId);
|
||||
return dedupe(path, () => api<WorkflowStep[]>(path));
|
||||
}
|
||||
|
||||
/** Create a new workflow step */
|
||||
export function createWorkflowStep(input: WorkflowStepInput, projectId?: string): Promise<WorkflowStep> {
|
||||
return api<WorkflowStep>(withProjectId("/workflow-steps", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
/** Update a workflow step */
|
||||
export function updateWorkflowStep(id: string, updates: Partial<WorkflowStepInput>, projectId?: string): Promise<WorkflowStep> {
|
||||
return api<WorkflowStep>(withProjectId(`/workflow-steps/${id}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a workflow step */
|
||||
export function deleteWorkflowStep(id: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/workflow-steps/${id}`, projectId), { method: "DELETE" });
|
||||
}
|
||||
|
||||
/** Refine a workflow step's prompt using AI */
|
||||
export function refineWorkflowStepPrompt(id: string, projectId?: string): Promise<{ prompt: string; workflowStep: WorkflowStep }> {
|
||||
return api<{ prompt: string; workflowStep: WorkflowStep }>(withProjectId(`/workflow-steps/${id}/refine`, projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowStepCRUD 2026-06-25-00:00:
|
||||
U5 removed the legacy `/workflow-steps` CRUD/REST surface (GET list, POST create,
|
||||
PATCH update, DELETE, refine) along with its Settings management UI. The client
|
||||
mutation helpers (`createWorkflowStep`/`updateWorkflowStep`/`deleteWorkflowStep`/
|
||||
`refineWorkflowStepPrompt`/`createWorkflowStepFromTemplate`) had no remaining callers
|
||||
and were deleted. `fetchWorkflowSteps` is retained as a stable, no-network shim
|
||||
returning `[]`: its only remaining consumers are the plugin dashboard context's
|
||||
`workflowSteps` field and the WorkflowResultsTab option list, both of which now source
|
||||
step state from the graph (optional-group nodes) — the legacy definition list no longer
|
||||
exists. Removing the field outright is graph-native U3 plumbing work, out of scope here.
|
||||
*/
|
||||
/** Legacy workflow-step definition list (removed in U5). Resolves to an empty list:
|
||||
* built-in/custom step definitions are now graph optional-group nodes, not DB rows. */
|
||||
export function fetchWorkflowSteps(_projectId?: string): Promise<WorkflowStep[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
/** Fetch workflow step results for a task */
|
||||
@@ -5594,7 +5578,9 @@ export function setProjectDefaultWorkflow(
|
||||
/** Re-export WorkflowStepTemplate type from core */
|
||||
export type { WorkflowStepTemplate } from "@fusion/core";
|
||||
|
||||
/** Fetch all built-in workflow step templates */
|
||||
/** Fetch the workflow step templates that feed the editor palette. The built-in
|
||||
* built-in step-template catalog was deleted in U6, so this now returns only
|
||||
* plugin-contributed templates. */
|
||||
export function fetchWorkflowStepTemplates(): Promise<{ templates: import("@fusion/core").WorkflowStepTemplate[] }> {
|
||||
return api<{ templates: import("@fusion/core").WorkflowStepTemplate[] }>("/workflow-step-templates");
|
||||
}
|
||||
@@ -5608,13 +5594,6 @@ export function fetchPluginWorkflowStepTemplates(): Promise<{
|
||||
}>("/plugin-workflow-step-templates");
|
||||
}
|
||||
|
||||
/** Create a workflow step from a built-in or plugin template */
|
||||
export function createWorkflowStepFromTemplate(templateId: string, projectId?: string): Promise<WorkflowStep> {
|
||||
return api<WorkflowStep>(withProjectId(`/workflow-step-templates/${encodeURIComponent(templateId)}/create`, projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scripts API ────────────────────────────────────────────────────────
|
||||
|
||||
/** Script entry returned from the API */
|
||||
|
||||
@@ -2,7 +2,23 @@ import { readFileSync } from "node:fs";
|
||||
import { useState } from "react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, waitFor, cleanup, within } from "@testing-library/react";
|
||||
import { parseWorkflowIr, WORKFLOW_STEP_TEMPLATES, type WorkflowDefinition, type Settings } from "@fusion/core";
|
||||
import { parseWorkflowIr, type WorkflowDefinition, type Settings } from "@fusion/core";
|
||||
|
||||
// FNXC:WorkflowStepTemplate 2026-06-25-00:00: U6 deleted the built-in
|
||||
// WORKFLOW_STEP_TEMPLATES catalog. These palette tests only need an arbitrary set of
|
||||
// step templates returned by `fetchWorkflowStepTemplates` to verify palette rendering +
|
||||
// insertion; this local fixture replaces the deleted catalog (the editor is
|
||||
// template-agnostic — it renders whatever the API returns). `WorkflowStepTemplate` is
|
||||
// imported below (type-only imports hoist).
|
||||
const STEP_TEMPLATE_FIXTURES: WorkflowStepTemplate[] = [
|
||||
{ id: "documentation-review", name: "Documentation Review", description: "doc review", prompt: "You review docs.", category: "Quality", toolMode: "readonly" },
|
||||
{ id: "qa-check", name: "QA Check", description: "qa", prompt: "You run QA.", category: "Quality", toolMode: "coding" },
|
||||
{ id: "security-audit", name: "Security Audit", description: "sec", prompt: "You audit security.", category: "Security", toolMode: "readonly" },
|
||||
{ id: "performance-review", name: "Performance Review", description: "perf", prompt: "You review perf.", category: "Quality", toolMode: "readonly" },
|
||||
{ id: "accessibility-check", name: "Accessibility Check", description: "a11y", prompt: "You check a11y.", category: "Quality", toolMode: "readonly" },
|
||||
{ id: "browser-verification", name: "Browser Verification", description: "browser", prompt: "You verify in a browser.", category: "Quality", toolMode: "coding" },
|
||||
{ id: "frontend-ux-design", name: "Frontend UX Design", description: "ux", prompt: "You review UX.", category: "Quality", toolMode: "readonly" },
|
||||
];
|
||||
import type { Agent, BoardWorkflowDefinition } from "../../api";
|
||||
import {
|
||||
irToFlow,
|
||||
@@ -391,8 +407,10 @@ describe("workflow-flow-mapping", () => {
|
||||
const { edges } = edgeRenderableAssertion(builtinDef());
|
||||
const failuresToEnd = edges.filter((edge) => edge.target === "end" && edge.data?.condition === "failure");
|
||||
// FNXC:WorkflowOptionalGroup 2026-06-21-15:30: the coding built-in's pre-merge `workflow-step` seam was migrated to a `browser-verification` optional-group (U6), which now carries the failure->end edge in its place.
|
||||
// FNXC:CodeReviewStep 2026-06-25-00:00: the default-on `code-review` optional-group is also on the pre-merge success path with its own failure->end edge (see builtin-code-review-group.test.ts), so it is an expected failure->end source too. This corrected a stale assertion that predated the code-review group's addition.
|
||||
expect(failuresToEnd.map((edge) => edge.source).sort()).toEqual([
|
||||
"browser-verification",
|
||||
"code-review",
|
||||
"execute",
|
||||
"merge-attempt",
|
||||
"planning",
|
||||
@@ -3239,7 +3257,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
it("surfaces all seven built-in add-ons in the palette", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
|
||||
vi.mocked(fetchWorkflowStepTemplates).mockResolvedValue({
|
||||
templates: WORKFLOW_STEP_TEMPLATES,
|
||||
templates: STEP_TEMPLATE_FIXTURES,
|
||||
});
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
@@ -3247,13 +3265,13 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
|
||||
// Every add-on id is present as a primary "insert as node" button AND offers
|
||||
// the "as optional group" sibling variant.
|
||||
for (const tpl of WORKFLOW_STEP_TEMPLATES) {
|
||||
for (const tpl of STEP_TEMPLATE_FIXTURES) {
|
||||
expect(screen.getByTestId(`wf-tpl-step-${tpl.id}`)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId(`wf-tpl-step-${tpl.id}-optional-group`),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
expect(WORKFLOW_STEP_TEMPLATES).toHaveLength(7);
|
||||
expect(STEP_TEMPLATE_FIXTURES).toHaveLength(7);
|
||||
});
|
||||
|
||||
it("inserts an add-on as a single node carrying its template config", async () => {
|
||||
@@ -3261,7 +3279,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
vi.mocked(updateWorkflow).mockImplementation(async (_id, updates) => ({ ...def(), ...(updates as object) }));
|
||||
vi.mocked(compileWorkflow).mockResolvedValue({ steps: [] });
|
||||
vi.mocked(fetchWorkflowStepTemplates).mockResolvedValue({
|
||||
templates: WORKFLOW_STEP_TEMPLATES,
|
||||
templates: STEP_TEMPLATE_FIXTURES,
|
||||
});
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
@@ -3279,7 +3297,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
await waitFor(() => expect(updateWorkflow).toHaveBeenCalled());
|
||||
const [, updates] = vi.mocked(updateWorkflow).mock.calls[0];
|
||||
const ir = (updates as { ir: { nodes: { kind: string; config?: Record<string, unknown> }[] } }).ir;
|
||||
const docTpl = WORKFLOW_STEP_TEMPLATES.find((tpl) => tpl.id === "documentation-review")!;
|
||||
const docTpl = STEP_TEMPLATE_FIXTURES.find((tpl) => tpl.id === "documentation-review")!;
|
||||
const inserted = ir.nodes.find((n) => n.config?.name === docTpl.name);
|
||||
expect(inserted).toBeTruthy();
|
||||
expect(inserted!.kind).toBe(docTpl.mode === "script" ? "script" : "prompt");
|
||||
@@ -3290,7 +3308,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
vi.mocked(updateWorkflow).mockImplementation(async (_id, updates) => ({ ...def(), ...(updates as object) }));
|
||||
vi.mocked(compileWorkflow).mockResolvedValue({ steps: [] });
|
||||
vi.mocked(fetchWorkflowStepTemplates).mockResolvedValue({
|
||||
templates: WORKFLOW_STEP_TEMPLATES,
|
||||
templates: STEP_TEMPLATE_FIXTURES,
|
||||
});
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
@@ -3308,7 +3326,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
await waitFor(() => expect(updateWorkflow).toHaveBeenCalled());
|
||||
const [, updates] = vi.mocked(updateWorkflow).mock.calls[0];
|
||||
const ir = (updates as { ir: { nodes: { kind: string; config?: Record<string, unknown> }[] } }).ir;
|
||||
const secTpl = WORKFLOW_STEP_TEMPLATES.find((tpl) => tpl.id === "security-audit")!;
|
||||
const secTpl = STEP_TEMPLATE_FIXTURES.find((tpl) => tpl.id === "security-audit")!;
|
||||
const group = ir.nodes.find((n) => n.kind === "optional-group");
|
||||
expect(group).toBeTruthy();
|
||||
expect(group!.config!.defaultOn).toBe(secTpl.defaultOn ?? false);
|
||||
@@ -3322,7 +3340,7 @@ describe("WorkflowNodeEditor — U9 palette Templates section", () => {
|
||||
vi.mocked(updateWorkflow).mockImplementation(async (_id, updates) => ({ ...def(), ...(updates as object) }));
|
||||
vi.mocked(compileWorkflow).mockResolvedValue({ steps: [] });
|
||||
vi.mocked(fetchWorkflowStepTemplates).mockResolvedValue({
|
||||
templates: WORKFLOW_STEP_TEMPLATES,
|
||||
templates: STEP_TEMPLATE_FIXTURES,
|
||||
});
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import * as nodeFs from "node:fs";
|
||||
import os from "node:os";
|
||||
import v8 from "node:v8";
|
||||
|
||||
import type { TaskStore, ScheduleType, ActivityEventType, ModelPreset, RoutineTriggerType, WorkflowStepTemplate } from "@fusion/core";
|
||||
import type { TaskStore, ScheduleType, ActivityEventType, ModelPreset, RoutineTriggerType } from "@fusion/core";
|
||||
import {
|
||||
type Task,
|
||||
type PiExtensionEntry,
|
||||
@@ -247,17 +247,6 @@ export interface AuthStorageLike {
|
||||
get?(providerId: string): { type?: string; key?: string; access?: string; refresh?: string; expires?: number; [key: string]: unknown } | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended session interface for workflow step refinement.
|
||||
* The AgentSession from @earendil-works/pi-coding-agent has on() and prompt() methods
|
||||
* but the local AgentSession type is minimal.
|
||||
*/
|
||||
interface RefineAgentSession {
|
||||
on(event: "text", listener: (delta: string) => void): void;
|
||||
prompt(text: string): Promise<void>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
||||
@@ -383,43 +372,6 @@ export function __setCreateFnAgentForRefine(mock: typeof createFnAgentForRefine)
|
||||
createFnAgentForRefine = mock;
|
||||
}
|
||||
|
||||
// Default system prompt for workflow step refinement (fallback when overrides unavailable)
|
||||
|
||||
let resolveWorkflowStepRefinePrompt: (key: string, overrides?: Record<string, string | null>) => string = () => DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
|
||||
let promptOverridesReady = false;
|
||||
|
||||
async function initPromptOverrides() {
|
||||
if (promptOverridesReady) return;
|
||||
try {
|
||||
const core = await import("@fusion/core");
|
||||
resolveWorkflowStepRefinePrompt = (key: string, overrides?: Record<string, string | null>) =>
|
||||
core.resolvePrompt(key as keyof typeof core.PROMPT_KEY_CATALOG, overrides);
|
||||
promptOverridesReady = true;
|
||||
} catch {
|
||||
resolveWorkflowStepRefinePrompt = () => DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
|
||||
promptOverridesReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on module load
|
||||
initPromptOverrides();
|
||||
|
||||
/** Default system prompt for workflow step refinement */
|
||||
const DEFAULT_WORKFLOW_STEP_REFINE_PROMPT = `You are an expert at creating detailed agent prompts for workflow steps.
|
||||
|
||||
A workflow step is a quality gate that runs after a task is implemented but before it's marked complete.
|
||||
|
||||
Given a rough description, create a detailed prompt that an AI agent can follow to execute this workflow step.
|
||||
|
||||
The prompt should:
|
||||
1. Define the purpose clearly
|
||||
2. Specify what files/context to examine
|
||||
3. List specific criteria to check
|
||||
4. Describe what "success" looks like
|
||||
5. Include guidance on handling common edge cases
|
||||
|
||||
Output ONLY the prompt text (no markdown, no explanations).`;
|
||||
|
||||
function validateOptionalModelField(value: unknown, name: string): string | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (typeof value !== "string") {
|
||||
@@ -2886,396 +2838,29 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
});
|
||||
|
||||
// ── Workflow Step Routes ──────────────────────────────────────────────
|
||||
// ── Workflow Step Templates (palette) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /api/workflow-steps
|
||||
* List all workflow step definitions.
|
||||
* Returns: WorkflowStep[]
|
||||
*/
|
||||
router.get("/workflow-steps", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const steps = await scopedStore.listWorkflowSteps();
|
||||
res.json(steps);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/workflow-steps
|
||||
* Create a new workflow step.
|
||||
* Body: { name: string, description: string, mode?: "prompt"|"script", prompt?: string, scriptName?: string, enabled?: boolean, modelProvider?: string, modelId?: string }
|
||||
* Returns: WorkflowStep
|
||||
*/
|
||||
router.post("/workflow-steps", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const { name, description, mode, phase, prompt, gateMode, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||
|
||||
if (!name || typeof name !== "string" || !name.trim()) {
|
||||
throw badRequest("name is required");
|
||||
}
|
||||
if (!description || typeof description !== "string" || !description.trim()) {
|
||||
throw badRequest("description is required");
|
||||
}
|
||||
|
||||
// Validate mode
|
||||
const resolvedMode: "prompt" | "script" = mode || "prompt";
|
||||
if (resolvedMode !== "prompt" && resolvedMode !== "script") {
|
||||
throw badRequest("mode must be 'prompt' or 'script'");
|
||||
}
|
||||
|
||||
// Validate phase
|
||||
if (phase !== undefined && phase !== "pre-merge" && phase !== "post-merge") {
|
||||
throw badRequest("phase must be 'pre-merge' or 'post-merge'");
|
||||
}
|
||||
|
||||
if (prompt !== undefined && typeof prompt !== "string") {
|
||||
throw badRequest("prompt must be a string");
|
||||
}
|
||||
if (gateMode !== undefined && gateMode !== "gate" && gateMode !== "advisory") {
|
||||
throw badRequest("gateMode must be 'gate' or 'advisory'");
|
||||
}
|
||||
if (toolMode !== undefined && toolMode !== "readonly" && toolMode !== "coding") {
|
||||
throw badRequest("toolMode must be 'readonly' or 'coding'");
|
||||
}
|
||||
if (scriptName !== undefined && typeof scriptName !== "string") {
|
||||
throw badRequest("scriptName must be a string");
|
||||
}
|
||||
if (enabled !== undefined && typeof enabled !== "boolean") {
|
||||
throw badRequest("enabled must be a boolean");
|
||||
}
|
||||
if (defaultOn !== undefined && typeof defaultOn !== "boolean") {
|
||||
throw badRequest("defaultOn must be a boolean");
|
||||
}
|
||||
|
||||
// Validate script mode: scriptName must reference a named script in settings
|
||||
if (resolvedMode === "script") {
|
||||
if (!scriptName?.trim()) {
|
||||
throw badRequest("scriptName is required when mode is 'script'");
|
||||
}
|
||||
const settings = await scopedStore.getSettings();
|
||||
const scripts = settings.scripts || {};
|
||||
if (!(scriptName.trim() in scripts)) {
|
||||
throw badRequest(`Script '${scriptName.trim()}' not found in project settings. Available scripts: ${Object.keys(scripts).join(", ") || "none"}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate model override pair (only relevant for prompt mode)
|
||||
const modelPair = assertConsistentOptionalPair(modelProvider, modelId, "workflow step model");
|
||||
|
||||
// Check for name conflicts
|
||||
const existing = await scopedStore.listWorkflowSteps();
|
||||
if (existing.some((ws) => ws.name.toLowerCase() === name.trim().toLowerCase())) {
|
||||
throw conflict(`A workflow step named '${name.trim()}' already exists`);
|
||||
}
|
||||
|
||||
const step = await scopedStore.createWorkflowStep({
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
mode: resolvedMode,
|
||||
phase,
|
||||
prompt: prompt?.trim(),
|
||||
gateMode,
|
||||
toolMode,
|
||||
scriptName: scriptName?.trim(),
|
||||
enabled,
|
||||
defaultOn: defaultOn === true,
|
||||
modelProvider: modelPair.provider,
|
||||
modelId: modelPair.modelId,
|
||||
});
|
||||
res.status(201).json(step);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
const status = typeof (err instanceof Error ? err.message : String(err)) === "string" && ((err instanceof Error ? err.message : String(err)).includes("must include both provider and modelId") || (err instanceof Error ? err.message : String(err)).includes("Script mode requires")) ? 400 : 500;
|
||||
throw new ApiError(status, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/workflow-steps/:id
|
||||
* Update a workflow step.
|
||||
* Body: Partial<{ name, description, mode, prompt, scriptName, enabled, modelProvider, modelId }>
|
||||
* Returns: WorkflowStep
|
||||
*/
|
||||
router.patch("/workflow-steps/:id", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const { name, description, mode, phase, prompt, gateMode, toolMode, scriptName, enabled, defaultOn, modelProvider, modelId } = req.body;
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (name !== undefined) {
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
throw badRequest("name must be a non-empty string");
|
||||
}
|
||||
updates.name = name.trim();
|
||||
}
|
||||
if (description !== undefined) {
|
||||
if (typeof description !== "string" || !description.trim()) {
|
||||
throw badRequest("description must be a non-empty string");
|
||||
}
|
||||
updates.description = description.trim();
|
||||
}
|
||||
if (mode !== undefined) {
|
||||
if (mode !== "prompt" && mode !== "script") {
|
||||
throw badRequest("mode must be 'prompt' or 'script'");
|
||||
}
|
||||
updates.mode = mode;
|
||||
}
|
||||
if (phase !== undefined) {
|
||||
if (phase !== "pre-merge" && phase !== "post-merge") {
|
||||
throw badRequest("phase must be 'pre-merge' or 'post-merge'");
|
||||
}
|
||||
updates.phase = phase;
|
||||
}
|
||||
if (prompt !== undefined) {
|
||||
if (typeof prompt !== "string") {
|
||||
throw badRequest("prompt must be a string");
|
||||
}
|
||||
updates.prompt = prompt;
|
||||
}
|
||||
if (gateMode !== undefined) {
|
||||
if (gateMode !== "gate" && gateMode !== "advisory") {
|
||||
throw badRequest("gateMode must be 'gate' or 'advisory'");
|
||||
}
|
||||
updates.gateMode = gateMode;
|
||||
}
|
||||
if (toolMode !== undefined) {
|
||||
if (toolMode !== "readonly" && toolMode !== "coding") {
|
||||
throw badRequest("toolMode must be 'readonly' or 'coding'");
|
||||
}
|
||||
updates.toolMode = toolMode;
|
||||
}
|
||||
if (scriptName !== undefined) {
|
||||
if (typeof scriptName !== "string") {
|
||||
throw badRequest("scriptName must be a string");
|
||||
}
|
||||
updates.scriptName = scriptName;
|
||||
}
|
||||
if (enabled !== undefined) {
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw badRequest("enabled must be a boolean");
|
||||
}
|
||||
updates.enabled = enabled;
|
||||
}
|
||||
if (defaultOn !== undefined) {
|
||||
if (typeof defaultOn !== "boolean") {
|
||||
throw badRequest("defaultOn must be a boolean");
|
||||
}
|
||||
updates.defaultOn = defaultOn;
|
||||
}
|
||||
|
||||
// Validate script-mode requirements against the resulting state (existing + updates)
|
||||
// This catches cases where an existing script-mode step has its scriptName updated
|
||||
// without the mode field being explicitly sent.
|
||||
const existingStep = await scopedStore.getWorkflowStep(req.params.id);
|
||||
const resultingMode: string | undefined = updates.mode !== undefined ? (updates.mode as string) : existingStep?.mode;
|
||||
const resultingScriptName: string | undefined = updates.scriptName !== undefined ? (updates.scriptName as string) : existingStep?.scriptName;
|
||||
|
||||
if (resultingMode === "script") {
|
||||
if (!resultingScriptName?.trim()) {
|
||||
throw badRequest("scriptName is required when mode is 'script'");
|
||||
}
|
||||
const settings = await scopedStore.getSettings();
|
||||
const scripts = settings.scripts || {};
|
||||
if (!(resultingScriptName.trim() in scripts)) {
|
||||
throw badRequest(`Script '${resultingScriptName.trim()}' not found in project settings. Available scripts: ${Object.keys(scripts).join(", ") || "none"}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and apply model override pair
|
||||
if (modelProvider !== undefined || modelId !== undefined) {
|
||||
const modelPair = assertConsistentOptionalPair(modelProvider, modelId, "workflow step model");
|
||||
updates.modelProvider = modelPair.provider;
|
||||
updates.modelId = modelPair.modelId;
|
||||
}
|
||||
|
||||
const step = await scopedStore.updateWorkflowStep(req.params.id, updates);
|
||||
res.json(step);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
if ((err instanceof Error ? err.message : String(err)).includes("not found")) {
|
||||
throw notFound(err instanceof Error ? err.message : String(err));
|
||||
} else {
|
||||
const status = typeof (err instanceof Error ? err.message : String(err)) === "string" && ((err instanceof Error ? err.message : String(err)).includes("must include both provider and modelId") || (err instanceof Error ? err.message : String(err)).includes("Script mode requires")) ? 400 : 500;
|
||||
throw new ApiError(status, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/workflow-steps/:id
|
||||
* Delete a workflow step.
|
||||
* Returns: 204 No Content
|
||||
*/
|
||||
router.delete("/workflow-steps/:id", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
await scopedStore.deleteWorkflowStep(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
if ((err instanceof Error ? err.message : String(err)).includes("not found")) {
|
||||
throw notFound(err instanceof Error ? err.message : String(err));
|
||||
} else {
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/workflow-steps/:id/refine
|
||||
* Use AI to refine the workflow step's description into a detailed agent prompt.
|
||||
* Only available for prompt-mode steps.
|
||||
* Returns: { prompt: string, workflowStep: WorkflowStep }
|
||||
*/
|
||||
router.post("/workflow-steps/:id/refine", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const step = await scopedStore.getWorkflowStep(req.params.id);
|
||||
if (!step) {
|
||||
throw notFound(`Workflow step '${req.params.id}' not found`);
|
||||
}
|
||||
|
||||
if (step.mode === "script") {
|
||||
throw badRequest("Cannot refine prompt for script-mode workflow steps");
|
||||
}
|
||||
|
||||
if (!step.description?.trim()) {
|
||||
throw badRequest("Workflow step has no description to refine");
|
||||
}
|
||||
|
||||
// Use AI to refine the description into a detailed agent prompt
|
||||
let refinedPrompt: string;
|
||||
try {
|
||||
const createFnAgent = createFnAgentForRefine;
|
||||
|
||||
const settings = await scopedStore.getSettings();
|
||||
|
||||
// Resolve the system prompt using prompt overrides (with fallback to default)
|
||||
const systemPrompt = resolveWorkflowStepRefinePrompt(
|
||||
"workflow-step-refine",
|
||||
settings.promptOverrides
|
||||
) || DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
|
||||
|
||||
if (!createFnAgent) {
|
||||
throw new Error("createFnAgent is not available");
|
||||
}
|
||||
const planningModel = resolvePlanningSettingsModel(settings);
|
||||
const { session } = await createFnAgent({
|
||||
cwd: scopedStore.getRootDir(),
|
||||
systemPrompt,
|
||||
tools: "readonly",
|
||||
// Resolve planning model using canonical lane hierarchy:
|
||||
// 1. Project planning lane
|
||||
// 2. Global planning lane
|
||||
// 3. Project default override
|
||||
// 4. Global default
|
||||
defaultProvider: planningModel.provider,
|
||||
defaultModelId: planningModel.modelId,
|
||||
defaultThinkingLevel: settings.defaultThinkingLevel,
|
||||
});
|
||||
|
||||
const refineSession = session as unknown as RefineAgentSession;
|
||||
let output = "";
|
||||
refineSession.on("text", (delta: string) => {
|
||||
output += delta;
|
||||
});
|
||||
|
||||
await refineSession.prompt(
|
||||
`Refine this workflow step description into a detailed agent prompt:\n\nName: ${step.name}\nDescription: ${step.description}`
|
||||
);
|
||||
refineSession.dispose();
|
||||
|
||||
refinedPrompt = output.trim();
|
||||
} catch {
|
||||
// Fallback: return the description as-is if AI is unavailable
|
||||
refinedPrompt = step.description;
|
||||
}
|
||||
|
||||
// Update the workflow step with the refined prompt
|
||||
const updated = await scopedStore.updateWorkflowStep(step.id, { prompt: refinedPrompt });
|
||||
res.json({ prompt: refinedPrompt, workflowStep: updated });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Workflow Step Templates ───────────────────────────────────────────
|
||||
/*
|
||||
FNXC:WorkflowStepCRUD 2026-06-25-00:00:
|
||||
U5/U6 removed the legacy workflow-step management surface: the
|
||||
GET/POST/PATCH/DELETE `/workflow-steps` CRUD routes, the `/workflow-steps/:id/refine`
|
||||
route, and the `/workflow-step-templates/:id/create` route are gone (their Settings
|
||||
manager UI and the built-in step-template catalog were deleted). Workflow
|
||||
quality gates now live as graph optional-group nodes, authored in the workflow editor.
|
||||
Only the plugin-contributed step-template palette survives below.
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /api/workflow-step-templates
|
||||
* List all built-in workflow step templates.
|
||||
* List the plugin-contributed workflow step templates that feed the workflow
|
||||
* editor's optional-group palette. The built-in step-template catalog
|
||||
* was deleted in U6, so only plugin templates remain.
|
||||
* Returns: { templates: WorkflowStepTemplate[] }
|
||||
*/
|
||||
router.get("/workflow-step-templates", async (_req, res) => {
|
||||
router.get("/workflow-step-templates", (_req, res) => {
|
||||
try {
|
||||
const { WORKFLOW_STEP_TEMPLATES } = await import("@fusion/core");
|
||||
const pluginTemplates = options?.pluginRunner?.getPluginWorkflowStepTemplates?.() ?? [];
|
||||
res.json({
|
||||
templates: [
|
||||
...WORKFLOW_STEP_TEMPLATES,
|
||||
...pluginTemplates.map(({ template }) => template),
|
||||
],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/workflow-step-templates/:id/create
|
||||
* Create a workflow step from a built-in template.
|
||||
* Returns: WorkflowStep
|
||||
*/
|
||||
router.post("/workflow-step-templates/:id/create", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const { WORKFLOW_STEP_TEMPLATES } = await import("@fusion/core");
|
||||
let template: WorkflowStepTemplate | undefined = WORKFLOW_STEP_TEMPLATES.find((t) => t.id === req.params.id);
|
||||
|
||||
if (!template) {
|
||||
const pluginTemplates = options?.pluginRunner?.getPluginWorkflowStepTemplates?.() ?? [];
|
||||
template = pluginTemplates.find(({ template: pluginTemplate }) => pluginTemplate.id === req.params.id)?.template;
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
throw notFound(`Template '${req.params.id}' not found`);
|
||||
}
|
||||
|
||||
// Check for name conflicts with existing workflow steps
|
||||
const existing = await scopedStore.listWorkflowSteps();
|
||||
if (existing.some((ws) => ws.name.toLowerCase() === template.name.toLowerCase())) {
|
||||
throw conflict(`A workflow step named '${template.name}' already exists`);
|
||||
}
|
||||
|
||||
const step = await scopedStore.createWorkflowStep({
|
||||
templateId: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
prompt: template.prompt,
|
||||
toolMode: template.toolMode,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
res.status(201).json(step);
|
||||
res.json({ templates: pluginTemplates.map(({ template }) => template) });
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { WORKFLOW_STEP_TEMPLATES } from "@fusion/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { inferWorkflowStepVerdictFromProse, parseWorkflowStepVerdict } from "../executor.js";
|
||||
|
||||
const TARGET_TEMPLATE_IDS = [
|
||||
"documentation-review",
|
||||
"qa-check",
|
||||
"security-audit",
|
||||
"performance-review",
|
||||
"accessibility-check",
|
||||
"browser-verification",
|
||||
"frontend-ux-design",
|
||||
] as const;
|
||||
|
||||
describe("workflow step template verdict interoperability", () => {
|
||||
it.each(TARGET_TEMPLATE_IDS)("%s supports canonical JSON and prose fallback", (id) => {
|
||||
const template = WORKFLOW_STEP_TEMPLATES.find((entry) => entry.id === id);
|
||||
expect(template).toBeTruthy();
|
||||
|
||||
const promptBody = template!.prompt;
|
||||
expect(promptBody).toContain('"verdict":"APPROVE|APPROVE_WITH_NOTES|REVISE"');
|
||||
|
||||
/*
|
||||
FNXC:WorkflowStepResults 2026-06-26: the WORKFLOW_STEP_TEMPLATES catalog was deleted
|
||||
(graph-native cutover, plan U6) — built-in quality gates now live as optional-group IR
|
||||
nodes whose prompt envelopes are asserted by the core builtin-group tests. This suite
|
||||
retains the executor-owned VERDICT PARSER coverage (`parseWorkflowStepVerdict` /
|
||||
`inferWorkflowStepVerdictFromProse`), which the graph path still uses to interpret
|
||||
prompt-mode workflow-step output, independent of any template catalog.
|
||||
*/
|
||||
describe("workflow step verdict parsing", () => {
|
||||
it("parses canonical structured verdicts", () => {
|
||||
expect(parseWorkflowStepVerdict('{"verdict":"APPROVE","notes":""}')).toEqual({
|
||||
verdict: "APPROVE",
|
||||
notes: "",
|
||||
});
|
||||
expect(parseWorkflowStepVerdict(`{"verdict":"APPROVE","notes":"out of scope: ${id}"}`)).toEqual({
|
||||
expect(parseWorkflowStepVerdict('{"verdict":"APPROVE","notes":"out of scope"}')).toEqual({
|
||||
verdict: "APPROVE",
|
||||
notes: `out of scope: ${id}`,
|
||||
notes: "out of scope",
|
||||
});
|
||||
expect(parseWorkflowStepVerdict(`{"verdict":"APPROVE_WITH_NOTES","notes":"advisory only: ${id}"}`)).toEqual({
|
||||
expect(parseWorkflowStepVerdict('{"verdict":"APPROVE_WITH_NOTES","notes":"advisory only"}')).toEqual({
|
||||
verdict: "APPROVE_WITH_NOTES",
|
||||
notes: `advisory only: ${id}`,
|
||||
notes: "advisory only",
|
||||
});
|
||||
expect(parseWorkflowStepVerdict('{"verdict":"REVISE","notes":"fix auth"}')).toEqual({
|
||||
verdict: "REVISE",
|
||||
notes: "fix auth",
|
||||
});
|
||||
});
|
||||
|
||||
it("infers REVISE from the legacy prose fallback", () => {
|
||||
expect(inferWorkflowStepVerdictFromProse("REQUEST REVISION\nfix packages/foo.ts")).toEqual({
|
||||
verdict: "REVISE",
|
||||
notes: "fix packages/foo.ts",
|
||||
|
||||
Reference in New Issue
Block a user