@danielblomma/cortex-mcp 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import {
4
+ composeStageEnvelope,
5
+ createRun,
6
+ advanceStage,
7
+ getRunState,
8
+ type StageStatus,
9
+ type WorkflowDefinition,
10
+ } from "../core/workflow/index.js";
11
+ import { DEFAULT_WORKFLOWS } from "../core/workflow/default-workflows.js";
12
+
13
+ /**
14
+ * `cortex stage` CLI surface. Each subcommand is a thin shell wrapper
15
+ * around the workflow primitives, mirroring the cortex.workflow.* MCP
16
+ * tools so shell scripts and CI can drive the harness without an MCP
17
+ * client.
18
+ *
19
+ * Subcommands:
20
+ * start --task-id <id> --description "..." [--workflow <id>]
21
+ * status --task-id <id>
22
+ * envelope --task-id <id> [--stage <name>]
23
+ * advance --task-id <id> --stage <name> --body-file <path>
24
+ * [--frontmatter-file <path>] [--status <complete|blocked|failed>]
25
+ * run --task-id <id> -- <command> [args...]
26
+ * Sets CORTEX_ACTIVE_TASK_ID and execs the command. Use this to
27
+ * spawn an agent that runs under the harness's pre-tool-use gate.
28
+ *
29
+ * All commands resolve cwd from CORTEX_PROJECT_ROOT (preferred) or
30
+ * process.cwd() so they work both inside an MCP server context and as
31
+ * standalone shell calls.
32
+ */
33
+
34
+ export async function runStageCommand(args: string[]): Promise<void> {
35
+ const sub = args[0] ?? "help";
36
+ const rest = args.slice(1);
37
+
38
+ switch (sub) {
39
+ case "start":
40
+ return runStart(rest);
41
+ case "status":
42
+ return runStatus(rest);
43
+ case "envelope":
44
+ return runEnvelope(rest);
45
+ case "advance":
46
+ return runAdvance(rest);
47
+ case "run":
48
+ return runRun(rest);
49
+ case "help":
50
+ case "--help":
51
+ case "-h":
52
+ printHelp();
53
+ return;
54
+ default:
55
+ printHelp();
56
+ throw new Error(`Unknown stage subcommand: ${sub}`);
57
+ }
58
+ }
59
+
60
+ function printHelp(): void {
61
+ const lines = [
62
+ "Usage:",
63
+ " cortex stage start --task-id <id> --description \"...\" [--workflow <id>]",
64
+ " cortex stage status --task-id <id>",
65
+ " cortex stage envelope --task-id <id> [--stage <name>]",
66
+ " cortex stage advance --task-id <id> --stage <name> --body-file <path>",
67
+ " [--frontmatter-file <path>] [--status <s>] [--outcome-file <path>]",
68
+ " cortex stage run --task-id <id> -- <command> [args...]",
69
+ "",
70
+ "Status values: complete (default) | blocked | failed",
71
+ "All commands operate on .agents/<task-id>/ in the current project root.",
72
+ ];
73
+ process.stdout.write(lines.join("\n") + "\n");
74
+ }
75
+
76
+ function projectRoot(): string {
77
+ return process.env.CORTEX_PROJECT_ROOT?.trim() || process.cwd();
78
+ }
79
+
80
+ function resolveWorkflow(workflowId: string): WorkflowDefinition {
81
+ const wf = DEFAULT_WORKFLOWS[workflowId];
82
+ if (!wf) {
83
+ throw new Error(
84
+ `Unknown workflow_id: ${workflowId}. Available: ${
85
+ Object.keys(DEFAULT_WORKFLOWS).join(", ") || "<none>"
86
+ }`,
87
+ );
88
+ }
89
+ return wf;
90
+ }
91
+
92
+ type Flags = Record<string, string | boolean>;
93
+
94
+ function parseFlags(args: string[]): { flags: Flags; rest: string[] } {
95
+ const flags: Flags = {};
96
+ const rest: string[] = [];
97
+ let i = 0;
98
+ while (i < args.length) {
99
+ const arg = args[i];
100
+ if (arg === "--") {
101
+ rest.push(...args.slice(i + 1));
102
+ break;
103
+ }
104
+ if (arg.startsWith("--")) {
105
+ const next = args[i + 1];
106
+ if (next === undefined || next.startsWith("--")) {
107
+ flags[arg.slice(2)] = true;
108
+ i += 1;
109
+ } else {
110
+ flags[arg.slice(2)] = next;
111
+ i += 2;
112
+ }
113
+ continue;
114
+ }
115
+ rest.push(arg);
116
+ i += 1;
117
+ }
118
+ return { flags, rest };
119
+ }
120
+
121
+ function requireFlag(flags: Flags, name: string): string {
122
+ const value = flags[name];
123
+ if (typeof value !== "string" || value.length === 0) {
124
+ throw new Error(`Missing required flag: --${name}`);
125
+ }
126
+ return value;
127
+ }
128
+
129
+ function emitJson(data: unknown): void {
130
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
131
+ }
132
+
133
+ async function runStart(args: string[]): Promise<void> {
134
+ const { flags } = parseFlags(args);
135
+ const taskId = requireFlag(flags, "task-id");
136
+ const description = requireFlag(flags, "description");
137
+ const workflowId =
138
+ typeof flags.workflow === "string" ? flags.workflow : "secure-build";
139
+
140
+ const workflow = resolveWorkflow(workflowId);
141
+ const state = createRun({
142
+ cwd: projectRoot(),
143
+ taskId,
144
+ workflow,
145
+ taskDescription: description,
146
+ });
147
+ const envelope = composeStageEnvelope({
148
+ cwd: projectRoot(),
149
+ taskId,
150
+ workflow,
151
+ });
152
+ emitJson({ state, envelope });
153
+ }
154
+
155
+ async function runStatus(args: string[]): Promise<void> {
156
+ const { flags } = parseFlags(args);
157
+ const taskId = requireFlag(flags, "task-id");
158
+ const state = getRunState(projectRoot(), taskId);
159
+ emitJson({ state });
160
+ }
161
+
162
+ async function runEnvelope(args: string[]): Promise<void> {
163
+ const { flags } = parseFlags(args);
164
+ const taskId = requireFlag(flags, "task-id");
165
+ const stageName = typeof flags.stage === "string" ? flags.stage : undefined;
166
+
167
+ const state = getRunState(projectRoot(), taskId);
168
+ if (!state) {
169
+ throw new Error(
170
+ `No run state for task ${taskId}. Start one with 'cortex stage start'.`,
171
+ );
172
+ }
173
+ const workflow = resolveWorkflow(state.workflow_id);
174
+ const envelope = composeStageEnvelope({
175
+ cwd: projectRoot(),
176
+ taskId,
177
+ workflow,
178
+ stageName,
179
+ });
180
+ emitJson({ envelope });
181
+ }
182
+
183
+ async function runAdvance(args: string[]): Promise<void> {
184
+ const { flags } = parseFlags(args);
185
+ const taskId = requireFlag(flags, "task-id");
186
+ const stageName = requireFlag(flags, "stage");
187
+ const bodyPath = requireFlag(flags, "body-file");
188
+ const frontmatterPath =
189
+ typeof flags["frontmatter-file"] === "string"
190
+ ? flags["frontmatter-file"]
191
+ : null;
192
+ const outcomePath =
193
+ typeof flags["outcome-file"] === "string" ? flags["outcome-file"] : null;
194
+ const statusFlag =
195
+ typeof flags.status === "string" ? (flags.status as StageStatus) : undefined;
196
+
197
+ const body = readFileSync(bodyPath, "utf8");
198
+ const frontmatter: Record<string, unknown> = frontmatterPath
199
+ ? parseJsonObject(frontmatterPath)
200
+ : {};
201
+ const outcome = outcomePath ? parseJsonObject(outcomePath) : undefined;
202
+
203
+ const state = getRunState(projectRoot(), taskId);
204
+ if (!state) {
205
+ throw new Error(
206
+ `No run state for task ${taskId}. Start one with 'cortex stage start'.`,
207
+ );
208
+ }
209
+ const workflow = resolveWorkflow(state.workflow_id);
210
+ const stage = workflow.stages.find((s) => s.name === stageName);
211
+ if (!stage) {
212
+ throw new Error(
213
+ `Stage ${stageName} is not defined in workflow ${workflow.id}`,
214
+ );
215
+ }
216
+
217
+ const finalStatus: StageStatus = statusFlag ?? "complete";
218
+ const next = advanceStage({
219
+ cwd: projectRoot(),
220
+ taskId,
221
+ workflow,
222
+ stageName,
223
+ artifactName: stage.artifact,
224
+ frontmatter: {
225
+ ...frontmatter,
226
+ stage: stageName,
227
+ status: finalStatus,
228
+ references:
229
+ (Array.isArray((frontmatter as Record<string, unknown>).references)
230
+ ? ((frontmatter as Record<string, unknown>).references as unknown[])
231
+ .filter((v): v is string => typeof v === "string")
232
+ : null) ?? deriveReferencesFromReads(stage.reads, workflow),
233
+ },
234
+ body,
235
+ status: finalStatus,
236
+ outcome,
237
+ });
238
+
239
+ let nextEnvelope = null;
240
+ if (next.outcome === "in_progress" && next.current_stage) {
241
+ nextEnvelope = composeStageEnvelope({
242
+ cwd: projectRoot(),
243
+ taskId,
244
+ workflow,
245
+ });
246
+ }
247
+ emitJson({ state: next, next_envelope: nextEnvelope });
248
+ }
249
+
250
+ function parseJsonObject(filePath: string): Record<string, unknown> {
251
+ const raw = readFileSync(filePath, "utf8");
252
+ let value: unknown;
253
+ try {
254
+ value = JSON.parse(raw);
255
+ } catch (err) {
256
+ throw new Error(
257
+ `Failed to parse JSON in ${filePath}: ${
258
+ err instanceof Error ? err.message : String(err)
259
+ }`,
260
+ );
261
+ }
262
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
263
+ throw new Error(`Expected JSON object in ${filePath}, got ${typeof value}`);
264
+ }
265
+ return value as Record<string, unknown>;
266
+ }
267
+
268
+ function deriveReferencesFromReads(
269
+ reads: string[],
270
+ workflow: WorkflowDefinition,
271
+ ): string[] {
272
+ const refs: string[] = [];
273
+ for (const readName of reads) {
274
+ const stage = workflow.stages.find((s) => s.name === readName);
275
+ if (stage) refs.push(stage.artifact);
276
+ }
277
+ return refs;
278
+ }
279
+
280
+ async function runRun(args: string[]): Promise<void> {
281
+ const { flags, rest } = parseFlags(args);
282
+ const taskId = requireFlag(flags, "task-id");
283
+ if (rest.length === 0) {
284
+ throw new Error(
285
+ "cortex stage run requires a command after --, e.g. 'cortex stage run --task-id task-1 -- claude'",
286
+ );
287
+ }
288
+
289
+ const state = getRunState(projectRoot(), taskId);
290
+ if (!state) {
291
+ throw new Error(
292
+ `No run state for task ${taskId}. Start one with 'cortex stage start'.`,
293
+ );
294
+ }
295
+ if (state.outcome !== "in_progress" || !state.current_stage) {
296
+ throw new Error(
297
+ `Run ${taskId} is not in progress (outcome=${state.outcome}). Cannot spawn agent.`,
298
+ );
299
+ }
300
+
301
+ const [command, ...commandArgs] = rest;
302
+
303
+ const child = spawn(command, commandArgs, {
304
+ stdio: "inherit",
305
+ env: {
306
+ ...process.env,
307
+ CORTEX_ACTIVE_TASK_ID: taskId,
308
+ },
309
+ });
310
+
311
+ await new Promise<void>((resolve, reject) => {
312
+ child.on("error", reject);
313
+ child.on("exit", (code, signal) => {
314
+ if (signal) {
315
+ reject(new Error(`spawned process terminated by signal ${signal}`));
316
+ return;
317
+ }
318
+ if (code !== 0) {
319
+ reject(new Error(`spawned process exited with code ${code}`));
320
+ return;
321
+ }
322
+ resolve();
323
+ });
324
+ });
325
+ }
@@ -0,0 +1,156 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ } from "node:fs";
7
+ import { join } from "node:path";
8
+ import * as yaml from "js-yaml";
9
+ import {
10
+ runStateSchema,
11
+ stageArtifactFrontmatterSchema,
12
+ type RunState,
13
+ type StageArtifactFrontmatter,
14
+ } from "./schemas.js";
15
+
16
+ /**
17
+ * Filesystem layout for one workflow run:
18
+ *
19
+ * <cwd>/.agents/<task-id>/
20
+ * plan.md
21
+ * review.md
22
+ * ...
23
+ * state.json
24
+ *
25
+ * All paths in this module are relative to the project's <cwd>. The caller
26
+ * is responsible for choosing cwd; we never assume process.cwd() here so
27
+ * tests can target a tmp directory.
28
+ */
29
+
30
+ export const AGENTS_DIR = ".agents";
31
+ export const STATE_FILENAME = "state.json";
32
+
33
+ export function runDir(cwd: string, taskId: string): string {
34
+ return join(cwd, AGENTS_DIR, taskId);
35
+ }
36
+
37
+ export function stateFilePath(cwd: string, taskId: string): string {
38
+ return join(runDir(cwd, taskId), STATE_FILENAME);
39
+ }
40
+
41
+ export function artifactPath(
42
+ cwd: string,
43
+ taskId: string,
44
+ artifactName: string,
45
+ ): string {
46
+ return join(runDir(cwd, taskId), artifactName);
47
+ }
48
+
49
+ const FRONTMATTER_OPEN = /^---\s*\r?\n/;
50
+ const FRONTMATTER_CLOSE = /\r?\n---\s*\r?\n/;
51
+
52
+ export type ParsedArtifact = {
53
+ frontmatter: StageArtifactFrontmatter;
54
+ body: string;
55
+ };
56
+
57
+ export function parseStageArtifact(text: string): ParsedArtifact {
58
+ if (!FRONTMATTER_OPEN.test(text)) {
59
+ throw new Error("Stage artifact is missing YAML frontmatter (--- ... ---)");
60
+ }
61
+ const afterOpen = text.replace(FRONTMATTER_OPEN, "");
62
+ const closeMatch = afterOpen.match(FRONTMATTER_CLOSE);
63
+ if (!closeMatch || closeMatch.index === undefined) {
64
+ throw new Error("Stage artifact frontmatter is not terminated (--- ... ---)");
65
+ }
66
+ const yamlText = afterOpen.slice(0, closeMatch.index);
67
+ const body = afterOpen
68
+ .slice(closeMatch.index + closeMatch[0].length)
69
+ .replace(/^\s+/, "")
70
+ .replace(/\s+$/, "");
71
+
72
+ let raw: unknown;
73
+ try {
74
+ raw = yaml.load(yamlText);
75
+ } catch (err) {
76
+ throw new Error(
77
+ `Failed to parse stage artifact frontmatter as YAML: ${
78
+ err instanceof Error ? err.message : String(err)
79
+ }`,
80
+ );
81
+ }
82
+
83
+ const parsed = stageArtifactFrontmatterSchema.safeParse(raw);
84
+ if (!parsed.success) {
85
+ throw new Error(
86
+ `Stage artifact frontmatter does not match schema: ${parsed.error.message}`,
87
+ );
88
+ }
89
+ return { frontmatter: parsed.data, body };
90
+ }
91
+
92
+ export function readStageArtifact(
93
+ cwd: string,
94
+ taskId: string,
95
+ artifactName: string,
96
+ ): ParsedArtifact {
97
+ const path = artifactPath(cwd, taskId, artifactName);
98
+ const text = readFileSync(path, "utf8");
99
+ return parseStageArtifact(text);
100
+ }
101
+
102
+ export function renderStageArtifact(
103
+ frontmatter: StageArtifactFrontmatter,
104
+ body: string,
105
+ ): string {
106
+ const yamlText = yaml.dump(frontmatter, {
107
+ lineWidth: 100,
108
+ noRefs: true,
109
+ sortKeys: false,
110
+ });
111
+ const trimmedBody = body.trim();
112
+ return `---\n${yamlText}---\n\n${trimmedBody}\n`;
113
+ }
114
+
115
+ export function writeStageArtifact(
116
+ cwd: string,
117
+ taskId: string,
118
+ artifactName: string,
119
+ frontmatter: StageArtifactFrontmatter,
120
+ body: string,
121
+ ): string {
122
+ const dir = runDir(cwd, taskId);
123
+ mkdirSync(dir, { recursive: true });
124
+ const path = artifactPath(cwd, taskId, artifactName);
125
+ writeFileSync(path, renderStageArtifact(frontmatter, body), "utf8");
126
+ return path;
127
+ }
128
+
129
+ export function readRunState(cwd: string, taskId: string): RunState | null {
130
+ const path = stateFilePath(cwd, taskId);
131
+ if (!existsSync(path)) return null;
132
+ let raw: unknown;
133
+ try {
134
+ raw = JSON.parse(readFileSync(path, "utf8"));
135
+ } catch (err) {
136
+ throw new Error(
137
+ `Failed to parse run state at ${path}: ${
138
+ err instanceof Error ? err.message : String(err)
139
+ }`,
140
+ );
141
+ }
142
+ const parsed = runStateSchema.safeParse(raw);
143
+ if (!parsed.success) {
144
+ throw new Error(`Run state at ${path} does not match schema: ${parsed.error.message}`);
145
+ }
146
+ return parsed.data;
147
+ }
148
+
149
+ export function writeRunState(cwd: string, state: RunState): string {
150
+ const validated = runStateSchema.parse(state);
151
+ const dir = runDir(cwd, validated.task_id);
152
+ mkdirSync(dir, { recursive: true });
153
+ const path = stateFilePath(cwd, validated.task_id);
154
+ writeFileSync(path, JSON.stringify(validated, null, 2) + "\n", "utf8");
155
+ return path;
156
+ }
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Capability registry for the harness. A capability is a least-privilege
5
+ * profile referenced by a workflow stage's `capability` field. The
6
+ * pre-tool-use hook reads the active stage's capability and uses it to
7
+ * gate the agent's tool calls.
8
+ *
9
+ * The capability defines:
10
+ * - read_globs paths the agent may read (empty = no restriction)
11
+ * - write_globs paths the agent may modify (empty = read-only)
12
+ * - tools_allowed which tool names the agent may call (empty = all)
13
+ *
14
+ * Glob patterns use minimatch syntax. The harness ships a default set
15
+ * keyed by the names referenced in default-workflows.ts; orgs can ship
16
+ * additional capabilities later via cortex-web sync.
17
+ */
18
+
19
+ const slugSchema = z
20
+ .string()
21
+ .min(1)
22
+ .max(80)
23
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/);
24
+
25
+ export const capabilityDefinitionSchema = z.object({
26
+ name: slugSchema,
27
+ description: z.string().min(1).max(500),
28
+ read_globs: z.array(z.string().min(1)).default([]),
29
+ write_globs: z.array(z.string().min(1)).default([]),
30
+ tools_allowed: z.array(z.string().min(1)).default([]),
31
+ });
32
+
33
+ export type CapabilityDefinition = z.infer<typeof capabilityDefinitionSchema>;
34
+
35
+ /**
36
+ * Default capability profiles that ship with Cortex. Names match the
37
+ * `capability` fields referenced by SECURE_BUILD_WORKFLOW.
38
+ *
39
+ * tools_allowed is intentionally empty for most profiles to mean
40
+ * "no per-tool restriction beyond what the file globs already imply".
41
+ * The hook layer checks file paths first, tool name second.
42
+ */
43
+ export const DEFAULT_CAPABILITIES: Record<string, CapabilityDefinition> = {
44
+ planner: {
45
+ name: "planner",
46
+ description:
47
+ "Read-only profile for stages that produce planning artifacts. " +
48
+ "Can read the whole repo and call context tools, cannot modify any files.",
49
+ read_globs: ["**"],
50
+ write_globs: [],
51
+ tools_allowed: [],
52
+ },
53
+ reviewer: {
54
+ name: "reviewer",
55
+ description:
56
+ "Read-only profile for review stages. Same access as planner; the " +
57
+ "review artifact itself is written by the harness, not by an Edit tool.",
58
+ read_globs: ["**"],
59
+ write_globs: [],
60
+ tools_allowed: [],
61
+ },
62
+ builder: {
63
+ name: "builder",
64
+ description:
65
+ "Build profile. May edit source and test files but not config, " +
66
+ "lockfiles, CI workflows, or anything outside the obvious app surface.",
67
+ read_globs: ["**"],
68
+ write_globs: ["src/**", "tests/**", "test/**", "lib/**", "app/**", "components/**"],
69
+ tools_allowed: [],
70
+ },
71
+ tester: {
72
+ name: "tester",
73
+ description:
74
+ "Mutation/test profile. Read-only on production code, may edit only " +
75
+ "test files. Used by mutation-testing or coverage-improvement stages.",
76
+ read_globs: ["**"],
77
+ write_globs: ["tests/**", "test/**", "**/*.test.ts", "**/*.test.tsx", "**/*.test.mjs", "**/*.spec.ts"],
78
+ tools_allowed: [],
79
+ },
80
+ "security-reviewer": {
81
+ name: "security-reviewer",
82
+ description:
83
+ "Security review profile. Read-only across the repo. Produces a " +
84
+ "report artifact; no source modifications allowed.",
85
+ read_globs: ["**"],
86
+ write_globs: [],
87
+ tools_allowed: [],
88
+ },
89
+ human: {
90
+ name: "human",
91
+ description:
92
+ "Sentinel capability for human approval stages. The harness does not " +
93
+ "invoke an agent for this stage; the human writes the artifact directly. " +
94
+ "If a tool call somehow reaches the hook under this capability, it is " +
95
+ "blocked because no automation should be running.",
96
+ read_globs: [],
97
+ write_globs: [],
98
+ tools_allowed: [],
99
+ },
100
+ };
@@ -0,0 +1,83 @@
1
+ import type { WorkflowDefinition } from "./schemas.js";
2
+
3
+ /**
4
+ * The default secure-build workflow that ships with Cortex. Organizations
5
+ * can override this from cortex-web later (Phase 2 of the harness rollout);
6
+ * until then this is the workflow every project gets out of the box.
7
+ */
8
+ export const SECURE_BUILD_WORKFLOW: WorkflowDefinition = {
9
+ id: "secure-build",
10
+ description:
11
+ "Plan → Review → Build → Review → Mutation Tests → Security Review → Human Approval. " +
12
+ "The default Cortex Harness workflow for AI-driven development on production code.",
13
+ version: 1,
14
+ stages: [
15
+ {
16
+ name: "plan",
17
+ artifact: "plan.md",
18
+ reads: [],
19
+ required_fields: ["files_targeted", "constraints"],
20
+ capability: "planner",
21
+ description:
22
+ "Produce a step-by-step implementation plan grounded in the repo's rules and memory.",
23
+ },
24
+ {
25
+ name: "plan-review",
26
+ artifact: "plan-review.md",
27
+ reads: ["plan"],
28
+ required_fields: ["approved", "blocking_comments"],
29
+ capability: "reviewer",
30
+ description:
31
+ "Review the plan for architectural fit and rule compliance before any code is written.",
32
+ },
33
+ {
34
+ name: "build",
35
+ artifact: "changes.md",
36
+ reads: ["plan", "plan-review"],
37
+ required_fields: ["files_changed"],
38
+ capability: "builder",
39
+ description:
40
+ "Implement the approved plan. Produces the diff manifest used by the downstream reviewers.",
41
+ },
42
+ {
43
+ name: "build-review",
44
+ artifact: "build-review.md",
45
+ reads: ["plan", "changes"],
46
+ required_fields: ["approved", "blocking_comments"],
47
+ capability: "reviewer",
48
+ description:
49
+ "Review the implementation against the plan and the project's rules.",
50
+ },
51
+ {
52
+ name: "mutation",
53
+ artifact: "mutation-report.md",
54
+ reads: ["changes"],
55
+ required_fields: ["score", "survived"],
56
+ capability: "tester",
57
+ description:
58
+ "Run mutation tests on the changed files. Report score + surviving mutants.",
59
+ },
60
+ {
61
+ name: "security",
62
+ artifact: "security-report.md",
63
+ reads: ["changes"],
64
+ required_fields: ["findings", "severity_summary"],
65
+ capability: "security-reviewer",
66
+ description:
67
+ "Security review focused on the diff: injection, authn/authz, secrets, dependency risk.",
68
+ },
69
+ {
70
+ name: "approval",
71
+ artifact: "approval.md",
72
+ reads: ["plan", "changes", "build-review", "mutation", "security"],
73
+ required_fields: ["approved", "approver"],
74
+ capability: "human",
75
+ description:
76
+ "Human sign-off. Pulls every prior artifact for the approver to read; the approver writes the artifact.",
77
+ },
78
+ ],
79
+ };
80
+
81
+ export const DEFAULT_WORKFLOWS: Record<string, WorkflowDefinition> = {
82
+ [SECURE_BUILD_WORKFLOW.id]: SECURE_BUILD_WORKFLOW,
83
+ };