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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/sync.js +10 -8
- package/dist/compiler/cascade.d.ts +2 -1
- package/dist/compiler/cascade.js +25 -0
- package/dist/hooks/enforce.d.ts +1 -1
- package/dist/hooks/enforce.js +50 -14
- package/dist/runtime/skill-adapter.d.ts +2 -1
- package/dist/runtime/skill-adapter.js +164 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema/validate.js +101 -0
- package/dist/templates/flutter-rewrite.dna.yaml +118 -15
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
28
|
+
"version": "1.6.0"
|
|
29
29
|
}
|
|
@@ -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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
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) */
|
package/dist/compiler/cascade.js
CHANGED
|
@@ -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,
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (
|
|
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.
|
|
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.
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/schema/validate.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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"
|