intentdna 1.5.21 → 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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
12
- "version": "1.5.21",
12
+ "version": "1.6.0",
13
13
  "author": {
14
14
  "name": "Samuel"
15
15
  },
@@ -25,5 +25,5 @@
25
25
  ]
26
26
  }
27
27
  ],
28
- "version": "1.5.21"
28
+ "version": "1.6.0"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.21",
3
+ "version": "1.6.0",
4
4
  "description": "Declarative policy layer for AI agent behavior",
5
5
  "author": {
6
6
  "name": "Samuel"
@@ -20,7 +20,7 @@ import { parseYAML } from "../../schema/yaml-parser.js";
20
20
  import { compileToMarkdown, injectIntoFile, removeFromFile } from "../../runtime/markdown.js";
21
21
  import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
22
22
  import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
23
- import { compileWorkflowToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
23
+ import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
24
24
  import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "../../runtime/settings-adapter.js";
25
25
  import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
26
26
  import { cascadeDNA } from "../../compiler/cascade.js";
@@ -127,11 +127,9 @@ export async function cleanStaleDNAFiles(dir, type, activeNamespaces) {
127
127
  const entries = await readdir(dir, { withFileTypes: true });
128
128
  for (const entry of entries) {
129
129
  const fullPath = resolve(dir, entry.name);
130
- // Extract namespace from filename: dna-{namespace}-*
131
- const nsMatch = entry.name.match(/^dna-([a-z0-9_-]+)-/);
132
- const fileNamespace = nsMatch ? nsMatch[1] : null;
133
- // Skip if namespace doesn't match active namespaces
134
- if (!fileNamespace || !activeNamespaces.includes(fileNamespace)) {
130
+ // Match active namespace prefixes directly so multi-hyphen names like
131
+ // dna-frw-fix-loop still resolve to namespace frw.
132
+ if (!activeNamespaces.some(ns => entry.name.startsWith(`dna-${ns}-`))) {
135
133
  continue;
136
134
  }
137
135
  if (type === "agents" && entry.isFile() && entry.name.endsWith(".md")) {
@@ -653,6 +651,7 @@ export async function runSync(opts) {
653
651
  const dnas = loadedDNAs;
654
652
  const cascadedForSkills = cascadeDNA(dnas);
655
653
  const workflows = cascadedForSkills.workflows;
654
+ const controllers = cascadedForSkills.controllers;
656
655
  const roles = cascadedForSkills.roles;
657
656
  // Collect and resolve variables from all DNA files
658
657
  const rawVars = {};
@@ -676,8 +675,8 @@ export async function runSync(opts) {
676
675
  process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
677
676
  }
678
677
  }
679
- if (Object.keys(workflows).length === 0) {
680
- process.stderr.write("No workflow defined — no skills to generate\n");
678
+ if (Object.keys(workflows).length === 0 && Object.keys(controllers).length === 0) {
679
+ process.stderr.write("No workflow or controller defined — no skills to generate\n");
681
680
  }
682
681
  else {
683
682
  const { compileWorkflow } = await import("../../compiler/workflow.js");
@@ -688,6 +687,9 @@ export async function runSync(opts) {
688
687
  skillResults.push(compileWorkflowToSkill(result.plan, roles, ir, variables));
689
688
  }
690
689
  }
690
+ for (const [name, controllerDef] of Object.entries(controllers)) {
691
+ skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
692
+ }
691
693
  if (skillResults.length > 0) {
692
694
  const written = await writeSkillFiles(skillResults, opts.skillsDir);
693
695
  process.stderr.write(`Generated ${written.length} skill file(s) in ${opts.skillsDir}\n`);
@@ -10,12 +10,13 @@
10
10
  * - attract/repel: same target accumulates, different targets independent
11
11
  * - amplify/suppress: factors multiply (1.5x × 2.0x = 3.0x)
12
12
  */
13
- import type { IntentDNA, Gene, ContextRegion, EpigeneticMarker, RoleDef, WorkflowDef, LegibilityAssetMap, VerifierCommandPolicy } from "../schema/types.js";
13
+ import type { IntentDNA, Gene, ContextRegion, EpigeneticMarker, RoleDef, WorkflowDef, LegibilityAssetMap, VerifierCommandPolicy, ControllerDef } from "../schema/types.js";
14
14
  export interface CascadedDNA {
15
15
  genes: Record<string, Gene>;
16
16
  contexts: Record<string, ContextRegion>;
17
17
  roles: Record<string, RoleDef>;
18
18
  workflows: Record<string, WorkflowDef>;
19
+ controllers: Record<string, ControllerDef>;
19
20
  epigenetic_markers: EpigeneticMarker[];
20
21
  source_ids: string[];
21
22
  /** Warnings from cascade process (e.g., cross-namespace gene collisions) */
@@ -91,6 +91,20 @@ function prefixRecordKeys(record, ns) {
91
91
  return record;
92
92
  return Object.fromEntries(Object.entries(record).map(([key, value]) => [`${ns}_${key}`, value]));
93
93
  }
94
+ function prefixController(controller, ns) {
95
+ return {
96
+ ...controller,
97
+ diagnosis_workflow: `${ns}_${controller.diagnosis_workflow}`,
98
+ fix_workflow: `${ns}_${controller.fix_workflow}`,
99
+ roles: {
100
+ analyzer: `${ns}_${controller.roles.analyzer}`,
101
+ analysis_reviewer: `${ns}_${controller.roles.analysis_reviewer}`,
102
+ surgeon: `${ns}_${controller.roles.surgeon}`,
103
+ fix_reviewer: `${ns}_${controller.roles.fix_reviewer}`,
104
+ test_runner: `${ns}_${controller.roles.test_runner}`,
105
+ },
106
+ };
107
+ }
94
108
  function prefixLegibilityAssets(map, ns) {
95
109
  if (!map)
96
110
  return undefined;
@@ -176,6 +190,7 @@ export function cascadeDNA(layers) {
176
190
  const mergedContexts = {};
177
191
  const mergedRoles = {};
178
192
  const mergedWorkflows = {};
193
+ const mergedControllers = {};
179
194
  const allMarkers = [];
180
195
  const sourceIds = [];
181
196
  const warnings = [];
@@ -225,6 +240,15 @@ export function cascadeDNA(layers) {
225
240
  mergedWorkflows[name] = wf;
226
241
  }
227
242
  }
243
+ // Merge controllers: namespace-prefixed keys + workflow/role references
244
+ for (const [name, controller] of Object.entries(dna.controllers ?? {})) {
245
+ if (ns) {
246
+ mergedControllers[`${ns}_${name}`] = prefixController(controller, ns);
247
+ }
248
+ else {
249
+ mergedControllers[name] = controller;
250
+ }
251
+ }
228
252
  // Collect epigenetic markers
229
253
  if (dna.epigenetic?.markers) {
230
254
  allMarkers.push(...dna.epigenetic.markers);
@@ -250,6 +274,7 @@ export function cascadeDNA(layers) {
250
274
  contexts: mergedContexts,
251
275
  roles: mergedRoles,
252
276
  workflows: mergedWorkflows,
277
+ controllers: mergedControllers,
253
278
  epigenetic_markers: allMarkers,
254
279
  source_ids: sourceIds,
255
280
  warnings: warnings.length > 0 ? warnings : undefined,
@@ -114,7 +114,7 @@ export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
114
114
  workflow: string;
115
115
  artifact_facts?: ArtifactFact[];
116
116
  }): EnforceResult | null;
117
- /** Check if a file path is allowed by a list of write globs (prefix matching). */
117
+ /** Check if a file path is allowed by a list of write globs. */
118
118
  export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
119
119
  /**
120
120
  * Extract directory prefix from a glob pattern.
@@ -439,7 +439,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
439
439
  const filePath = extractFilePath(input);
440
440
  if (!filePath)
441
441
  return null;
442
- return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths);
442
+ return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths, true);
443
443
  }
444
444
  // Bash: extract potential write targets from command
445
445
  if (input.tool_name === "Bash") {
@@ -450,7 +450,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
450
450
  if (writePaths.length === 0)
451
451
  return null;
452
452
  for (const p of writePaths) {
453
- const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths);
453
+ const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths, false);
454
454
  if (result)
455
455
  return result;
456
456
  }
@@ -459,7 +459,7 @@ function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
459
459
  return null;
460
460
  }
461
461
  /** Check a single file path against role scope. Shared by Write tools and Bash. */
462
- function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = []) {
462
+ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = [], blockViolation = true) {
463
463
  const cwd = input.cwd;
464
464
  let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
465
465
  // Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
@@ -474,10 +474,16 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePa
474
474
  ? [...writeGlobs, ...additionalWritePaths]
475
475
  : writeGlobs;
476
476
  if (allWriteGlobs.length === 0) {
477
- return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' has no write permission (path: ${filePath}). Merge-time scope gate will filter.`);
477
+ const message = `Role '${entry.role_name}' has no write permission (path: ${filePath}).`;
478
+ return blockViolation
479
+ ? blockOutput(`[Intent DNA]: ${message}`)
480
+ : allowOutput(`WARN [Intent DNA]: ${message}`);
478
481
  }
479
482
  if (!checkWriteAllowed(relativePath, allWriteGlobs)) {
480
- return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}). Merge-time scope gate will filter.`);
483
+ const message = `Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}).`;
484
+ return blockViolation
485
+ ? blockOutput(`[Intent DNA]: ${message}`)
486
+ : allowOutput(`WARN [Intent DNA]: ${message}`);
481
487
  }
482
488
  return null; // Role matched, write allowed
483
489
  }
@@ -565,20 +571,48 @@ function enforceOutputSchema(roles, input) {
565
571
  return null;
566
572
  }
567
573
  // ── Public Helpers ──────────────────────────────────────────
568
- /** Check if a file path is allowed by a list of write globs (prefix matching). */
574
+ /** Check if a file path is allowed by a list of write globs. */
569
575
  export function checkWriteAllowed(filePath, allowedGlobs) {
576
+ const normalizedPath = normalize(filePath).replace(/\\/g, "/");
570
577
  for (const glob of allowedGlobs) {
571
- const prefix = globToPrefix(glob);
572
- if (prefix.length === 0)
573
- return true; // Root glob like "*.md" — allow all
574
- if (filePath.startsWith(prefix))
575
- return true;
576
- // Also check exact match for non-glob patterns
577
- if (!glob.includes("*") && filePath === glob)
578
+ const normalizedGlob = normalize(glob).replace(/\\/g, "/");
579
+ if (normalizedGlob.includes("*")) {
580
+ if (globToRegExp(normalizedGlob).test(normalizedPath))
581
+ return true;
582
+ continue;
583
+ }
584
+ if (normalizedPath.startsWith(normalizedGlob))
578
585
  return true;
579
586
  }
580
587
  return false;
581
588
  }
589
+ function globToRegExp(glob) {
590
+ let source = "^";
591
+ for (let i = 0; i < glob.length; i++) {
592
+ const char = glob[i];
593
+ if (char === "*") {
594
+ if (glob[i + 1] === "*") {
595
+ if (glob[i + 2] === "/") {
596
+ source += "(?:.*/)?";
597
+ i += 2;
598
+ }
599
+ else {
600
+ source += ".*";
601
+ i += 1;
602
+ }
603
+ }
604
+ else {
605
+ source += "[^/]*";
606
+ }
607
+ continue;
608
+ }
609
+ source += escapeRegExp(char);
610
+ }
611
+ return new RegExp(`${source}$`);
612
+ }
613
+ function escapeRegExp(char) {
614
+ return char.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
615
+ }
582
616
  /**
583
617
  * Extract directory prefix from a glob pattern.
584
618
  * "src/**\/*.ts" → "src/"
@@ -649,7 +683,9 @@ function isWriteTool(toolName) {
649
683
  return WRITE_TOOLS.has(toolName);
650
684
  }
651
685
  function extractFilePath(input) {
652
- const fp = input.tool_input.file_path;
686
+ const fp = input.tool_name === "NotebookEdit"
687
+ ? input.tool_input.notebook_path
688
+ : input.tool_input.file_path;
653
689
  if (typeof fp === "string" && fp)
654
690
  return fp;
655
691
  return null;
@@ -6,13 +6,14 @@
6
6
  *
7
7
  * Skills are compiled views of DNA's structured definitions — not stored content.
8
8
  */
9
- import type { WorkflowPlan, RoleDef, ConstraintIR } from "../schema/types.js";
9
+ import type { WorkflowPlan, WorkflowDef, RoleDef, ConstraintIR, ControllerDef } from "../schema/types.js";
10
10
  export interface SkillResult {
11
11
  name: string;
12
12
  fileName: string;
13
13
  dirName: string;
14
14
  content: string;
15
15
  }
16
+ export declare function compileControllerToSkill(controllerKey: string, controller: ControllerDef, variables?: Record<string, string>, workflows?: Record<string, WorkflowDef>): SkillResult;
16
17
  /**
17
18
  * Compile a WorkflowPlan + roles + IR into a SKILL.md file.
18
19
  * Variables from DNA config are substituted into prompts and descriptions.
@@ -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.
@@ -200,6 +200,27 @@ export interface WorkflowDef {
200
200
  default_isolation?: "none" | "worktree" | "auto";
201
201
  merge_strategy?: "escalate";
202
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
+ }
203
224
  /** Parallel block syntax sugar — expanded by compiler into steps with depends_on */
204
225
  export interface ParallelBlockDef {
205
226
  parallel: {
@@ -276,6 +297,7 @@ export interface IntentDNA {
276
297
  roles?: Record<string, RoleDef>;
277
298
  workflow?: WorkflowDef;
278
299
  workflows?: Record<string, WorkflowDef>;
300
+ controllers?: Record<string, ControllerDef>;
279
301
  variables?: Record<string, string | VariableDef>;
280
302
  mcp?: Record<string, MCPServerDef>;
281
303
  legibility_assets?: LegibilityAssetMap;
@@ -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";
@@ -582,6 +675,14 @@ export function validateDNA(dna) {
582
675
  errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
583
676
  }
584
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
+ }
585
686
  // Validate mcp
586
687
  if (dna.mcp) {
587
688
  for (const [name, server] of Object.entries(dna.mcp)) {
@@ -239,19 +239,20 @@ roles:
239
239
  - "DO NOT run tests. DO NOT edit app code. Analysis only."
240
240
 
241
241
  analysis_reviewer:
242
- description: "Reviews analyzer output quality. Read-only."
242
+ description: "Reviews analyzer output quality and writes durable diagnosis review verdict."
243
243
  tool_permissions:
244
- allow: [Read, Grep, Glob]
245
- deny: [Bash, Edit, Write, NotebookEdit]
244
+ allow: [Read, Grep, Glob, Write]
245
+ deny: [Bash, Edit, NotebookEdit]
246
246
  scope:
247
247
  read: ["**/*"]
248
- write: []
248
+ write: [".dna/specs/diagnosis-*.review.json"]
249
249
  instructions:
250
250
  - "REQUIRED FIRST: Read all context files listed in SKILL.md"
251
251
  - "Verify analyzer spec: v1 references exist? Classifications sound?"
252
252
  - "Check missing: any failing test not covered?"
253
253
  - "Verify diagnosis remains evidence-only: no fix prescriptions, implementation order, call-site instructions, or Notes for Surgeon"
254
- - "Output a structured verdict block with verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, and summary"
254
+ - "Output a structured verdict block with verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, summary, and updated_at"
255
+ - "Write the same structured verdict JSON to .dna/specs/diagnosis-$ARGUMENTS.review.json before finishing"
255
256
 
256
257
  fix_reviewer:
257
258
  description: "Reviews surgeon changes against approved diagnosis, v1 evidence, and v2 architecture. Writes only the structured review verdict artifact."
@@ -283,6 +284,26 @@ roles:
283
284
  - "Classify red tests by type (compile/logic/widget/hung)"
284
285
  - "Output progress delta, not test content"
285
286
 
287
+ controllers:
288
+ fix-loop:
289
+ name: Fix Loop
290
+ description: "Run diagnosis once if needed, then loop fix/review/verify until PASS or stop condition"
291
+ kind: flutter_rewrite_fix_loop
292
+ max_rounds: 10
293
+ progress_artifact_path: ".dna/fix-progress/$ARGUMENTS.json"
294
+ diagnosis_workflow: diagnosis
295
+ fix_workflow: fix
296
+ diagnosis_artifact_path: ".dna/specs/diagnosis-$ARGUMENTS.md"
297
+ diagnosis_review_artifact_path: ".dna/specs/diagnosis-$ARGUMENTS.review.json"
298
+ review_artifact_path: ".dna/reviews/$ARGUMENTS-review.json"
299
+ roles:
300
+ analyzer: analyzer
301
+ analysis_reviewer: analysis_reviewer
302
+ surgeon: surgeon
303
+ fix_reviewer: fix_reviewer
304
+ test_runner: test_runner
305
+ stop_format: zh_three_part
306
+
286
307
  workflows:
287
308
  behavior-lock:
288
309
  name: Behavior Lock
@@ -411,10 +432,13 @@ workflows:
411
432
  "sections_to_fix": ["section ids or titles; empty when approved"],
412
433
  "evidence_paths": ["paths that justify the verdict"],
413
434
  "confidence": "high" | "medium" | "low",
414
- "summary": "brief reason"
435
+ "summary": "brief reason",
436
+ "updated_at": "ISO-8601 timestamp"
415
437
  }
416
438
  ```
417
439
 
440
+ Write the same verdict JSON to .dna/specs/diagnosis-$ARGUMENTS.review.json before finishing.
441
+
418
442
  APPROVE → diagnosis complete.
419
443
  REQUEST_REANALYSIS → sections_to_fix must be passed back to analyze. docs/behavior/blocked_items.md is historical context only and must not be treated as the current run verdict.
420
444
  handoff:
@@ -423,6 +447,9 @@ workflows:
423
447
  path: ".dna/specs/diagnosis-$ARGUMENTS.md"
424
448
  description: "Diagnosis spec from analyzer"
425
449
  produces:
450
+ - type: file
451
+ path: ".dna/specs/diagnosis-$ARGUMENTS.review.json"
452
+ description: "Durable diagnosis review verdict (APPROVE or REQUEST_REANALYSIS)"
426
453
  - type: summary
427
454
  required: false
428
455
  description: "Review verdict (APPROVE or REQUEST_REANALYSIS)"
@@ -488,16 +515,26 @@ workflows:
488
515
  ```json
489
516
  {
490
517
  "verdict": "APPROVE" | "REQUEST_CHANGES",
518
+ "module": "$ARGUMENTS",
519
+ "round": 1,
520
+ "run_id": "controller run id or manual-run",
491
521
  "issues": ["specific issue with evidence path; empty when approved"],
492
522
  "evidence_paths": ["paths that justify the verdict"],
493
523
  "failure_reason": "why changes need another round; empty when approved",
494
524
  "next_round_focus": "specific priority/classification/test file for the next fix invocation; empty when approved",
495
- "summary": "brief reason"
525
+ "summary": "brief reason",
526
+ "updated_at": "ISO-8601 timestamp"
496
527
  }
497
528
  ```
498
529
 
499
530
  Write the same verdict JSON to .dna/reviews/$ARGUMENTS-review.json before finishing.
500
531
 
532
+ JSON field rules:
533
+ - module is required and must equal $ARGUMENTS.
534
+ - round is required and must be the provided current_round, or 1 for manual fix invocation.
535
+ - run_id is required and must equal the provided controller_run_id, or "manual-run" for manual fix invocation.
536
+ - updated_at is a required ISO timestamp string.
537
+
501
538
  REQUEST_CHANGES → include failure_reason and concrete next_round_focus for the next fix invocation. The next_round_focus must name the priority/classification and specific test file or evidence path to address first.
502
539
  handoff:
503
540
  consumes:
@@ -526,33 +563,96 @@ workflows:
526
563
 
527
564
  If the review verdict is APPROVE:
528
565
  - Run full module test suite for $ARGUMENTS.
566
+ - Read diagnosis classification summary.
567
+ - Read current test summary.
529
568
  - Compare with diagnosis spec baseline.
569
+ - Compare skipped tests against remaining UNIMPLEMENTED diagnosis items.
570
+
571
+ Hard PASS rules:
572
+ - 0 failed is not sufficient for PASS.
573
+ - PASS is forbidden if any approved diagnosis UNIMPLEMENTED item remains skipped.
574
+ - Diagnosis-backed UNIMPLEMENTED skipped tests count as Remaining, not Blocked.
575
+ - UNIMPLEMENTED skipped tests count as Remaining.
576
+ - Before writing Module complete: yes, read diagnosis classification summary, read current test summary, compare skipped tests against remaining UNIMPLEMENTED diagnosis items, and if any UNIMPLEMENTED skipped remains, Module complete must be no.
577
+
578
+ Verdict mapping:
579
+ - review REQUEST_CHANGES -> Verdict: BLOCKED, Failure reason: BLOCKED_BY_REVIEW.
580
+ - 0 failed + remaining UNIMPLEMENTED skipped > 0 -> Verdict: CONTINUE, Module complete: no, Next Round Focus: next diagnosis-backed UNIMPLEMENTED test/evidence path.
581
+ - progress contradicts diagnosis/test evidence -> Verdict: BLOCKED, Failure reason: PROGRESS_DIAGNOSIS_CONFLICT.
582
+ - new red caused by current round -> Verdict: REQUEST_CHANGES.
583
+ - new red appears to be newly exposed baseline gap AND remaining count decreases -> Verdict: CONTINUE.
584
+ - ambiguous new red -> Verdict: BLOCKED, Failure reason: AMBIGUOUS_NEW_RED.
585
+ - 0 failed + no remaining diagnosis-backed UNIMPLEMENTED skipped + no unresolved actionable BUG / UNIMPLEMENTED items + only TEST_BUG / INFRA / REMOVED / explicitly user-approved blocked items remain -> Verdict: PASS.
530
586
 
531
587
  Output report:
532
588
  - Verdict: PASS / CONTINUE / REQUEST_CHANGES / BLOCKED
533
589
  - Review: APPROVE / REQUEST_CHANGES
590
+ - Module complete: yes/no
534
591
  - Fixed: N tests now green
535
592
  - New red: M tests that regressed
536
- - Blocked: K tests skipped (see blocked_items.md)
537
- - Remaining: R tests still failing
593
+ - Blocked: K tests skipped for TEST_BUG / INFRA / REMOVED / user-approved blocked reasons
594
+ - Remaining: R tests still failing or diagnosis-backed UNIMPLEMENTED skipped
595
+ - Remaining UNIMPLEMENTED skipped: U diagnosis-backed skipped tests still remaining
538
596
  - Failure reason: why progress stopped, if any
539
597
  - Next round focus: the priority/classification/test file to continue with, if verdict is CONTINUE or REQUEST_CHANGES
540
598
 
541
599
  Always write or update .dna/fix-progress/$ARGUMENTS.md before finishing, even when review blocks verification. Include these sections:
542
- - Current verdict: Verdict, Review, Last fix commit, Updated at
543
- - Round summary: Fixed, New red, Blocked, Remaining, Failure reason
600
+ - Current verdict: Verdict, Review, Module complete, Last fix commit, Updated at
601
+ - Round summary: Fixed, New red, Blocked, Remaining, Remaining UNIMPLEMENTED skipped, Failure reason
544
602
  - Next Round Focus: non-empty when Verdict is CONTINUE or REQUEST_CHANGES
545
603
  - Review Issues To Address First: reviewer issues when Review is REQUEST_CHANGES
546
604
  - Failed approaches / Do not repeat: approaches that failed or caused regressions
547
605
  - Evidence: commands run and relevant paths
548
606
 
549
- If Verdict is PASS, mark the module complete in .dna/fix-progress/$ARGUMENTS.md.
607
+ Always write or update .dna/fix-progress/$ARGUMENTS.json before finishing, even when review blocks verification. The JSON must match this contract exactly:
608
+ ```json
609
+ {
610
+ "verdict": "PASS | CONTINUE | REQUEST_CHANGES | BLOCKED",
611
+ "review": "APPROVE | REQUEST_CHANGES",
612
+ "module": "$ARGUMENTS",
613
+ "round": 1,
614
+ "run_id": "controller run id or manual-run",
615
+ "diagnosis_artifact": ".dna/specs/diagnosis-$ARGUMENTS.md",
616
+ "review_artifact": ".dna/reviews/$ARGUMENTS-review.json",
617
+ "git_head": null,
618
+ "module_complete": false,
619
+ "fixed_count": 0,
620
+ "new_red_count": 0,
621
+ "blocked_count": 0,
622
+ "remaining_count": 0,
623
+ "remaining_unimplemented_skipped_count": 0,
624
+ "next_round_focus": "",
625
+ "failure_reason": "",
626
+ "last_fix_commit": null,
627
+ "evidence_paths": [],
628
+ "updated_at": "ISO-8601 timestamp"
629
+ }
630
+ ```
631
+
632
+ JSON field rules:
633
+ - module is required and must equal $ARGUMENTS.
634
+ - round is required and must be the current fix-loop round number, or 1 for manual fix invocation.
635
+ - run_id is required and must identify the current controller run, or "manual-run" for manual fix invocation.
636
+ - diagnosis_artifact is required and must equal .dna/specs/diagnosis-$ARGUMENTS.md.
637
+ - review_artifact is required and must equal .dna/reviews/$ARGUMENTS-review.json.
638
+ - git_head is the current git HEAD after verification if available, otherwise null.
639
+ - verdict is required and must be PASS, CONTINUE, REQUEST_CHANGES, or BLOCKED.
640
+ - review is required and must be APPROVE or REQUEST_CHANGES.
641
+ - module_complete is a required boolean and must match the Markdown Module complete value.
642
+ - fixed_count, new_red_count, blocked_count, remaining_count, and remaining_unimplemented_skipped_count are required non-negative integers.
643
+ - next_round_focus is required and non-empty when verdict is CONTINUE or REQUEST_CHANGES; empty string is allowed when verdict is PASS.
644
+ - failure_reason is required and non-empty when verdict is BLOCKED or REQUEST_CHANGES; empty string is allowed when verdict is PASS or CONTINUE.
645
+ - last_fix_commit is the commit hash string if available, otherwise null.
646
+ - evidence_paths is a required array of command/test/diagnosis/review evidence paths.
647
+ - updated_at is a required ISO timestamp string.
648
+
649
+ If Verdict is PASS, mark Module complete: yes in .dna/fix-progress/$ARGUMENTS.md and set module_complete true in .dna/fix-progress/$ARGUMENTS.json.
550
650
 
551
651
  Verdict meanings:
552
- - PASS: all approved diagnosis items are fixed or intentionally skipped
652
+ - PASS: all actionable BUG / UNIMPLEMENTED items are fixed, with no diagnosis-backed UNIMPLEMENTED skipped tests remaining; only TEST_BUG / INFRA / REMOVED / explicitly user-approved blocked items may remain
553
653
  - CONTINUE: this round made progress and remaining approved items should continue in the next fix invocation
554
- - REQUEST_CHANGES: review or verification found issues in this round's changes
555
- - BLOCKED: user confirmation is needed or repeated no-progress prevents safe continuation
654
+ - REQUEST_CHANGES: verification found issues in this round's changes
655
+ - BLOCKED: review blocks verification, user confirmation is needed, evidence conflicts, or repeated no-progress prevents safe continuation
556
656
  handoff:
557
657
  consumes:
558
658
  - type: file
@@ -569,6 +669,9 @@ workflows:
569
669
  - type: file
570
670
  path: ".dna/fix-progress/$ARGUMENTS.md"
571
671
  description: "Durable cumulative fix-progress artifact for the next fix invocation"
672
+ - type: file
673
+ path: ".dna/fix-progress/$ARGUMENTS.json"
674
+ description: "Machine-readable fix-progress verdict sidecar for controllers"
572
675
  - type: summary
573
676
  required: false
574
677
  description: "Verification report with test delta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.21",
3
+ "version": "1.6.0",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",