intentdna 1.5.20 → 1.6.0

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.
@@ -10,6 +10,170 @@ import { mkdir, writeFile, readdir, readFile, rm } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
  import { toKebabCase } from "./agent-md.js";
12
12
  const SENTINEL = "<!-- intentdna:managed — do not edit manually -->";
13
+ // ── Compile Controller → Skill ─────────────────────────────
14
+ export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
15
+ if (controller.kind !== "flutter_rewrite_fix_loop") {
16
+ throw new Error(`Unsupported controller kind: ${controller.kind}`);
17
+ }
18
+ const skillName = `dna-${toKebabCase(controllerKey)}`;
19
+ const analyzerAgent = `dna-${toKebabCase(controller.roles.analyzer)}`;
20
+ const analysisReviewerAgent = `dna-${toKebabCase(controller.roles.analysis_reviewer)}`;
21
+ const surgeonAgent = `dna-${toKebabCase(controller.roles.surgeon)}`;
22
+ const fixReviewerAgent = `dna-${toKebabCase(controller.roles.fix_reviewer)}`;
23
+ const testRunnerAgent = `dna-${toKebabCase(controller.roles.test_runner)}`;
24
+ const diagnosisWorkflow = workflows?.[controller.diagnosis_workflow];
25
+ const fixWorkflow = workflows?.[controller.fix_workflow];
26
+ const diagnosisAnalyzeStep = requireStepByRole(diagnosisWorkflow, controller.roles.analyzer, controller.diagnosis_workflow);
27
+ const diagnosisReviewStep = requireStepByRole(diagnosisWorkflow, controller.roles.analysis_reviewer, controller.diagnosis_workflow);
28
+ const fixBugsStep = requireStepByRole(fixWorkflow, controller.roles.surgeon, controller.fix_workflow);
29
+ const reviewChangesStep = requireStepByRole(fixWorkflow, controller.roles.fix_reviewer, controller.fix_workflow);
30
+ const verifyReportStep = requireStepByRole(fixWorkflow, controller.roles.test_runner, controller.fix_workflow);
31
+ const stepPrompt = (step) => step.prompt || step.description || "";
32
+ const lines = [];
33
+ lines.push("---");
34
+ lines.push(`name: ${skillName}`);
35
+ lines.push(`description: "Use when user says /${skillName}. ${escapeYaml(controller.description || controller.name)}"`);
36
+ lines.push("user-invocable: true");
37
+ lines.push("triggers:");
38
+ lines.push(` - "${toKebabCase(controllerKey)}"`);
39
+ lines.push(` - "run ${toKebabCase(controllerKey)}"`);
40
+ lines.push("---");
41
+ lines.push("");
42
+ lines.push(SENTINEL);
43
+ lines.push(`<!-- Compiled: ${new Date().toISOString()} -->`);
44
+ lines.push("");
45
+ lines.push(`# /${skillName} <module>`);
46
+ lines.push("");
47
+ lines.push("<Purpose>");
48
+ lines.push(controller.description || controller.name);
49
+ lines.push("</Purpose>");
50
+ lines.push("");
51
+ lines.push("<Controller>");
52
+ lines.push("kind: flutter_rewrite_fix_loop");
53
+ lines.push(`max_rounds: ${controller.max_rounds}`);
54
+ lines.push(`diagnosis_workflow: ${controller.diagnosis_workflow}`);
55
+ lines.push(`fix_workflow: ${controller.fix_workflow}`);
56
+ lines.push(`progress_artifact_path: ${controller.progress_artifact_path}`);
57
+ lines.push("</Controller>");
58
+ lines.push("");
59
+ lines.push("<Steps>");
60
+ lines.push("0. Validate the module argument before using it in any artifact path.");
61
+ lines.push(" - The module argument must match /^[A-Za-z0-9_-]+$/.");
62
+ lines.push(" - If it contains '/', '.', '..', spaces, braces, glob characters, or path separators, stop with the Chinese report format.");
63
+ lines.push(" - Do not read or write controller artifacts until the module argument is safe.");
64
+ lines.push("1. Check diagnosis artifact freshness before any fix round.");
65
+ lines.push(` - Diagnosis artifact: ${controller.diagnosis_artifact_path}`);
66
+ lines.push(` - Diagnosis review artifact: ${controller.diagnosis_review_artifact_path}`);
67
+ lines.push("2. Create a stable controller_run_id once for this invocation before using any artifact path.");
68
+ lines.push(" - Reuse the same controller_run_id in every child Agent prompt and in every progress validation.");
69
+ lines.push(" - Do not regenerate controller_run_id inside fix rounds.");
70
+ lines.push("3. Reuse diagnosis only when all diagnosis reuse criteria pass:");
71
+ lines.push(` - ${controller.diagnosis_artifact_path} exists`);
72
+ lines.push(` - ${controller.diagnosis_review_artifact_path} exists and verdict is APPROVE`);
73
+ lines.push(" - behavior doc is not newer than diagnosis spec");
74
+ lines.push(" - baseline artifact is not newer than diagnosis spec");
75
+ lines.push(" - user did not explicitly pass reanalyze or scope changed");
76
+ lines.push(" - progress JSON does not say diagnosis_invalid");
77
+ lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run diagnosis once with fresh context and sequential gates:`);
78
+ lines.push(" a. Spawn fresh analyzer agent with controller_run_id, then wait for completion.");
79
+ lines.push(" ```");
80
+ lines.push(" Agent(");
81
+ lines.push(` subagent_type="${analyzerAgent}",`);
82
+ lines.push(` prompt="controller_run_id=<controller_run_id>. ${escapePrompt(stepPrompt(diagnosisAnalyzeStep))}"`);
83
+ lines.push(" )");
84
+ lines.push(" ```");
85
+ lines.push(` b. Diagnosis artifact gate: read ${controller.diagnosis_artifact_path}; validate it exists, is non-empty, names the safe module argument, and is current for this module before continuing.`);
86
+ lines.push(" c. Spawn fresh analysis_reviewer agent with controller_run_id, then wait for completion.");
87
+ lines.push(" ```");
88
+ lines.push(" Agent(");
89
+ lines.push(` subagent_type="${analysisReviewerAgent}",`);
90
+ lines.push(` prompt="controller_run_id=<controller_run_id>; expected_review_artifact=${controller.diagnosis_review_artifact_path}. ${escapePrompt(stepPrompt(diagnosisReviewStep))}"`);
91
+ lines.push(" )");
92
+ lines.push(" ```");
93
+ lines.push(` d. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
94
+ lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the Chinese report format; do NOT enter fix rounds.");
95
+ lines.push(`5. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
96
+ lines.push(" a. Spawn fresh surgeon agent with controller_run_id and current_round, then wait for completion.");
97
+ lines.push(" ```");
98
+ lines.push(" Agent(");
99
+ lines.push(` subagent_type="${surgeonAgent}",`);
100
+ lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context round for $ARGUMENTS. ${escapePrompt(stepPrompt(fixBugsStep))}"`);
101
+ lines.push(" )");
102
+ lines.push(" ```");
103
+ lines.push(" b. Fix handoff gate: validate the current round handoff and fix commit, or an explicit no-commit handoff for current_round, before review.");
104
+ lines.push(" c. Spawn fresh fix_reviewer agent with controller_run_id, current_round, and expected review artifact path, then wait for completion.");
105
+ lines.push(" ```");
106
+ lines.push(" Agent(");
107
+ lines.push(` subagent_type="${fixReviewerAgent}",`);
108
+ lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>; expected_review_artifact=${controller.review_artifact_path}. Fresh context review for $ARGUMENTS. ${escapePrompt(stepPrompt(reviewChangesStep))}"`);
109
+ lines.push(" )");
110
+ lines.push(" ```");
111
+ lines.push(` d. Review JSON gate: read ${controller.review_artifact_path}; validate it exists, is valid JSON, module equals the safe module argument, round equals the provided current_round, run_id equals the provided controller_run_id, updated_at is present, and verdict is APPROVE or REQUEST_CHANGES before verification.`);
112
+ lines.push(" e. Spawn fresh test_runner agent only after the review gate passes; include controller_run_id and current_round, then wait for completion.");
113
+ lines.push(" ```");
114
+ lines.push(" Agent(");
115
+ lines.push(` subagent_type="${testRunnerAgent}",`);
116
+ lines.push(` prompt="controller_run_id=<controller_run_id>; current_round=<current_round>. Fresh context verification for $ARGUMENTS. The progress JSON run_id must equal the provided controller_run_id and round must equal the provided current_round; do not use manual-run during controller execution. ${escapePrompt(stepPrompt(verifyReportStep))}"`);
117
+ lines.push(" )");
118
+ lines.push(" ```");
119
+ lines.push(` f. Progress JSON read/provenance validation: read ${controller.progress_artifact_path}. The controller must read this JSON sidecar, not Markdown prose.`);
120
+ lines.push(" g. Verify progress JSON provenance before branching:");
121
+ lines.push(" - module equals the safe module argument");
122
+ lines.push(" - round equals the provided current_round");
123
+ lines.push(" - run_id equals the provided controller_run_id");
124
+ lines.push(` - diagnosis_artifact equals ${controller.diagnosis_artifact_path}`);
125
+ lines.push(` - review_artifact equals ${controller.review_artifact_path}`);
126
+ lines.push(" - git_head or last_fix_commit references the current round's fix commit when available");
127
+ lines.push(" - updated_at is not earlier than the current round start time");
128
+ lines.push(" - if provenance is missing, stale, or mismatched, stop with Verdict BLOCKED and failure_reason PROGRESS_PROVENANCE_MISMATCH");
129
+ lines.push(" h. Branch only from the JSON verdict:");
130
+ lines.push(" - PASS -> success stop");
131
+ lines.push(" - CONTINUE -> next fresh context fix round");
132
+ lines.push(" - REQUEST_CHANGES -> stop with Chinese report");
133
+ lines.push(" - BLOCKED -> stop with Chinese report");
134
+ lines.push(`6. If max ${controller.max_rounds} rounds is reached, stop with Chinese report.`);
135
+ lines.push("</Steps>");
136
+ lines.push("");
137
+ lines.push("<Verdict_Mapping>");
138
+ lines.push("PASS / CONTINUE / REQUEST_CHANGES / BLOCKED mapping:");
139
+ lines.push("- PASS: success stop");
140
+ lines.push("- CONTINUE: start the next fresh context round");
141
+ lines.push("- REQUEST_CHANGES: stop, report reviewer/verifier requested changes");
142
+ lines.push("- BLOCKED: stop, report blocker");
143
+ lines.push("- MAX_ROUNDS: stop, report max rounds reached and next focus");
144
+ lines.push("</Verdict_Mapping>");
145
+ lines.push("");
146
+ lines.push("<Stop_Report>");
147
+ lines.push("On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, output exactly three Chinese sections:");
148
+ lines.push("结论:...");
149
+ lines.push("卡点:...");
150
+ lines.push("下一步:...");
151
+ lines.push("</Stop_Report>");
152
+ lines.push("");
153
+ lines.push("<Constraints>");
154
+ lines.push("- Do not infer module completion from raw test output.");
155
+ lines.push(`- The controller must schedule only from ${controller.progress_artifact_path}.`);
156
+ lines.push("- Manual /dna-frw-diagnosis and /dna-frw-fix flows remain separate and available.");
157
+ lines.push("- Do not use the generic workflow policy block; branch only from the progress JSON verdict.");
158
+ lines.push("</Constraints>");
159
+ lines.push("");
160
+ let content = lines.join("\n") + "\n";
161
+ if (variables) {
162
+ for (const [key, value] of Object.entries(variables)) {
163
+ content = content.replaceAll(`{{${key}}}`, value);
164
+ }
165
+ }
166
+ const dirName = skillName;
167
+ const fileName = join(dirName, "SKILL.md");
168
+ return { name: skillName, fileName, dirName, content };
169
+ }
170
+ function requireStepByRole(workflow, role, workflowName) {
171
+ const step = workflow?.steps.find((candidate) => candidate.role === role);
172
+ if (!step) {
173
+ throw new Error(`Controller workflow '${workflowName}' must include a step for role '${role}'`);
174
+ }
175
+ return step;
176
+ }
13
177
  // ── Compile Workflow → Skill ───────────────────────────────
14
178
  /**
15
179
  * Compile a WorkflowPlan + roles + IR into a SKILL.md file.
@@ -42,6 +206,12 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
42
206
  lines.push("</Purpose>");
43
207
  lines.push("");
44
208
  }
209
+ lines.push("<Workflow_State>");
210
+ lines.push("At workflow start, persist runtime inputs in DNA workflow state before executing steps.");
211
+ lines.push("Record the invocation argument as inputs.ARGUMENTS so handoff artifact paths like $ARGUMENTS can be resolved deterministically.");
212
+ lines.push("If template variables are shown below, persist their resolved values as resolved_variables.");
213
+ lines.push("</Workflow_State>");
214
+ lines.push("");
45
215
  // Required Context — context files that must be read before any work
46
216
  const workflowKeys = [plan.workflow_key, plan.source_workflow, plan.name].filter((key, index, keys) => Boolean(key) && keys.indexOf(key) === index);
47
217
  const requiredReadAssets = [];
@@ -94,9 +94,38 @@ export type HandoffType = "file" | "directory" | "test_result" | "git_commit" |
94
94
  export interface HandoffArtifact {
95
95
  type: HandoffType;
96
96
  path?: string;
97
+ name?: string;
98
+ artifact_id?: string;
97
99
  from?: string;
100
+ required?: boolean;
98
101
  description: string;
99
102
  }
103
+ export interface ArtifactKey {
104
+ workflow: string;
105
+ session_id: string;
106
+ producer_step: string;
107
+ type: HandoffType;
108
+ identity: string;
109
+ }
110
+ export interface ArtifactManifest {
111
+ id: string;
112
+ key: ArtifactKey;
113
+ type: HandoffType;
114
+ workflow: string;
115
+ session_id: string;
116
+ producer_step: string;
117
+ identity: string;
118
+ path?: string;
119
+ contract_path?: string;
120
+ name?: string;
121
+ artifact_id?: string;
122
+ metadata?: Record<string, unknown>;
123
+ recorded_at: string;
124
+ resolver_version: number;
125
+ }
126
+ export interface ArtifactFact extends ArtifactManifest {
127
+ manifest_path: string;
128
+ }
100
129
  /** Handoff declaration for a workflow step */
101
130
  export interface StepHandoff {
102
131
  consumes?: HandoffArtifact[];
@@ -171,6 +200,27 @@ export interface WorkflowDef {
171
200
  default_isolation?: "none" | "worktree" | "auto";
172
201
  merge_strategy?: "escalate";
173
202
  }
203
+ export type ControllerKind = "flutter_rewrite_fix_loop";
204
+ export interface ControllerDef {
205
+ name: string;
206
+ description?: string;
207
+ kind: ControllerKind;
208
+ max_rounds: number;
209
+ progress_artifact_path: string;
210
+ diagnosis_workflow: string;
211
+ fix_workflow: string;
212
+ diagnosis_artifact_path: string;
213
+ diagnosis_review_artifact_path: string;
214
+ review_artifact_path: string;
215
+ roles: {
216
+ analyzer: string;
217
+ analysis_reviewer: string;
218
+ surgeon: string;
219
+ fix_reviewer: string;
220
+ test_runner: string;
221
+ };
222
+ stop_format: "zh_three_part";
223
+ }
174
224
  /** Parallel block syntax sugar — expanded by compiler into steps with depends_on */
175
225
  export interface ParallelBlockDef {
176
226
  parallel: {
@@ -247,6 +297,7 @@ export interface IntentDNA {
247
297
  roles?: Record<string, RoleDef>;
248
298
  workflow?: WorkflowDef;
249
299
  workflows?: Record<string, WorkflowDef>;
300
+ controllers?: Record<string, ControllerDef>;
250
301
  variables?: Record<string, string | VariableDef>;
251
302
  mcp?: Record<string, MCPServerDef>;
252
303
  legibility_assets?: LegibilityAssetMap;
@@ -344,13 +395,19 @@ export interface WorkflowIR {
344
395
  step_enforce_rules?: StepEnforceRule[];
345
396
  }
346
397
  /** Record of artifacts completed by a step — used for cross-session resume */
398
+ export interface CompletedArtifactMetadata {
399
+ [key: string]: unknown;
400
+ }
401
+ export interface CompletedArtifactRecord {
402
+ type: string;
403
+ path: string;
404
+ verified_at: string;
405
+ artifact_id?: string;
406
+ metadata?: CompletedArtifactMetadata;
407
+ }
347
408
  export interface CompletedArtifactEntry {
348
409
  step_id: string;
349
- artifacts: {
350
- type: string;
351
- path: string;
352
- verified_at: string;
353
- }[];
410
+ artifacts: CompletedArtifactRecord[];
354
411
  }
355
412
  /** Runtime workflow state written to .dna/state/workflow.json */
356
413
  export interface WorkflowState {
@@ -361,6 +418,8 @@ export interface WorkflowState {
361
418
  iteration: number;
362
419
  session_id: string;
363
420
  started_at: string;
421
+ inputs?: Record<string, string>;
422
+ resolved_variables?: Record<string, string>;
364
423
  completed_artifacts?: CompletedArtifactEntry[];
365
424
  }
366
425
  export interface ConstraintIR {
@@ -299,6 +299,99 @@ function validateCompletionCheck(check, path) {
299
299
  }
300
300
  return errors;
301
301
  }
302
+ function isSafeControllerArtifactPath(path) {
303
+ return path.startsWith(".dna/") &&
304
+ !path.startsWith("/") &&
305
+ !path.includes("..") &&
306
+ !/[*?[\]{}]/.test(path) &&
307
+ !path.includes("{{") &&
308
+ !path.includes("}}") &&
309
+ path.includes("$ARGUMENTS");
310
+ }
311
+ function validateController(controller, roleNames, workflows, path) {
312
+ const errors = [];
313
+ if (!controller.name) {
314
+ errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
315
+ }
316
+ if (controller.kind !== "flutter_rewrite_fix_loop") {
317
+ errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
318
+ }
319
+ if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
320
+ errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
321
+ }
322
+ const requiredStringFields = [
323
+ "progress_artifact_path",
324
+ "diagnosis_workflow",
325
+ "fix_workflow",
326
+ "diagnosis_artifact_path",
327
+ "diagnosis_review_artifact_path",
328
+ "review_artifact_path",
329
+ ];
330
+ for (const field of requiredStringFields) {
331
+ if (typeof controller[field] !== "string" || !controller[field]) {
332
+ errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
333
+ }
334
+ }
335
+ const artifactPathFields = [
336
+ "progress_artifact_path",
337
+ "diagnosis_artifact_path",
338
+ "diagnosis_review_artifact_path",
339
+ "review_artifact_path",
340
+ ];
341
+ for (const field of artifactPathFields) {
342
+ if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
343
+ errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
344
+ }
345
+ }
346
+ const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
347
+ const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
348
+ if (controller.diagnosis_workflow && !diagnosisWorkflow) {
349
+ errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
350
+ }
351
+ if (controller.fix_workflow && !fixWorkflow) {
352
+ errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
353
+ }
354
+ const roleMap = controller.roles;
355
+ const requiredRoles = ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
356
+ if (!roleMap || typeof roleMap !== "object") {
357
+ errors.push({ path: `${path}.roles`, message: "controller requires roles" });
358
+ }
359
+ else {
360
+ for (const roleKey of requiredRoles) {
361
+ const roleName = roleMap[roleKey];
362
+ if (typeof roleName !== "string" || !roleName) {
363
+ errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
364
+ }
365
+ else if (!roleNames.has(roleName)) {
366
+ errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
367
+ }
368
+ }
369
+ }
370
+ if (roleMap && typeof roleMap === "object") {
371
+ validateControllerWorkflowRoles(diagnosisWorkflow, ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
372
+ validateControllerWorkflowRoles(fixWorkflow, ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
373
+ }
374
+ if (controller.stop_format !== "zh_three_part") {
375
+ errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'zh_three_part'" });
376
+ }
377
+ return errors;
378
+ }
379
+ function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
380
+ if (!workflow)
381
+ return;
382
+ const workflowRoles = new Set(workflow.steps.map((step) => step.role));
383
+ for (const roleKey of requiredRoleKeys) {
384
+ const roleName = roleMap[roleKey];
385
+ if (typeof roleName !== "string" || !roleName)
386
+ continue;
387
+ if (!workflowRoles.has(roleName)) {
388
+ errors.push({
389
+ path: `${path}.${roleKey}`,
390
+ message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
391
+ });
392
+ }
393
+ }
394
+ }
302
395
  function validateWorkflow(workflow, roleNames, pathPrefix) {
303
396
  const errors = [];
304
397
  const path = pathPrefix ?? "workflow";
@@ -382,6 +475,16 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
382
475
  }
383
476
  }
384
477
  }
478
+ for (const direction of ["consumes", "produces"]) {
479
+ const artifacts = step.handoff?.[direction] ?? [];
480
+ for (let j = 0; j < artifacts.length; j++) {
481
+ const artifact = artifacts[j];
482
+ const artifactPath = `${stepPath}.handoff.${direction}[${j}]`;
483
+ if (artifact.required !== false && artifact.type !== "git_commit" && !artifact.path && !artifact.name && !artifact.artifact_id) {
484
+ errors.push({ path: artifactPath, message: "required handoff artifacts must define path, name, or artifact_id" });
485
+ }
486
+ }
487
+ }
385
488
  // checkpoints validation
386
489
  if (step.checkpoints) {
387
490
  if (!Array.isArray(step.checkpoints)) {
@@ -572,6 +675,14 @@ export function validateDNA(dna) {
572
675
  errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
573
676
  }
574
677
  }
678
+ if (dna.controllers) {
679
+ const workflowsForControllers = { ...(dna.workflows ?? {}) };
680
+ if (dna.workflow)
681
+ workflowsForControllers[dna.workflow.name] = dna.workflow;
682
+ for (const [controllerName, controllerDef] of Object.entries(dna.controllers)) {
683
+ errors.push(...validateController(controllerDef, roleNamesSet, workflowsForControllers, `controllers.${controllerName}`));
684
+ }
685
+ }
575
686
  // Validate mcp
576
687
  if (dna.mcp) {
577
688
  for (const [name, server] of Object.entries(dna.mcp)) {
@@ -99,6 +99,7 @@ workflow:
99
99
  description: "Implementation to review"
100
100
  produces:
101
101
  - type: summary
102
+ required: false
102
103
  description: "Review verdict (PASS/FAIL)"
103
104
  transitions:
104
105
  - from: review
@@ -133,6 +133,7 @@ workflows:
133
133
  handoff:
134
134
  produces:
135
135
  - type: summary
136
+ name: breakpoint_analysis
136
137
  description: "Breakpoint analysis with file paths and line numbers"
137
138
  - id: fix
138
139
  role: surgeon
@@ -143,6 +144,7 @@ workflows:
143
144
  consumes:
144
145
  - type: summary
145
146
  from: trace
147
+ name: breakpoint_analysis
146
148
  description: "Breakpoint analysis from investigator"
147
149
  produces:
148
150
  - type: git_commit
@@ -159,4 +161,5 @@ workflows:
159
161
  description: "Fix commit to verify"
160
162
  produces:
161
163
  - type: summary
164
+ required: false
162
165
  description: "Verification result with screenshots"