intentdna 1.5.21 → 1.6.1
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 +135 -21
- 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.1",
|
|
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.1"
|
|
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)) {
|
|
@@ -134,6 +134,7 @@ context_files:
|
|
|
134
134
|
- ".dna/specs/diagnosis-{{ARGUMENTS}}.md"
|
|
135
135
|
surgeon:
|
|
136
136
|
- ".dna/specs/diagnosis-{{ARGUMENTS}}.md"
|
|
137
|
+
- ".dna/specs/diagnosis-{{ARGUMENTS}}.review.json"
|
|
137
138
|
fix_reviewer:
|
|
138
139
|
- "docs/behavior/{{ARGUMENTS}}.md"
|
|
139
140
|
- ".dna/specs/diagnosis-{{ARGUMENTS}}.md"
|
|
@@ -239,19 +240,20 @@ roles:
|
|
|
239
240
|
- "DO NOT run tests. DO NOT edit app code. Analysis only."
|
|
240
241
|
|
|
241
242
|
analysis_reviewer:
|
|
242
|
-
description: "Reviews analyzer output quality
|
|
243
|
+
description: "Reviews analyzer output quality and writes durable diagnosis review verdict."
|
|
243
244
|
tool_permissions:
|
|
244
|
-
allow: [Read, Grep, Glob]
|
|
245
|
-
deny: [Bash, Edit,
|
|
245
|
+
allow: [Read, Grep, Glob, Write]
|
|
246
|
+
deny: [Bash, Edit, NotebookEdit]
|
|
246
247
|
scope:
|
|
247
248
|
read: ["**/*"]
|
|
248
|
-
write: []
|
|
249
|
+
write: [".dna/specs/diagnosis-*.review.json"]
|
|
249
250
|
instructions:
|
|
250
251
|
- "REQUIRED FIRST: Read all context files listed in SKILL.md"
|
|
251
252
|
- "Verify analyzer spec: v1 references exist? Classifications sound?"
|
|
252
253
|
- "Check missing: any failing test not covered?"
|
|
253
254
|
- "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
|
|
255
|
+
- "Output a structured verdict block with verdict, artifact_reviewed, sections_to_fix, evidence_paths, confidence, summary, and updated_at"
|
|
256
|
+
- "Write the same structured verdict JSON to .dna/specs/diagnosis-$ARGUMENTS.review.json before finishing"
|
|
255
257
|
|
|
256
258
|
fix_reviewer:
|
|
257
259
|
description: "Reviews surgeon changes against approved diagnosis, v1 evidence, and v2 architecture. Writes only the structured review verdict artifact."
|
|
@@ -283,6 +285,26 @@ roles:
|
|
|
283
285
|
- "Classify red tests by type (compile/logic/widget/hung)"
|
|
284
286
|
- "Output progress delta, not test content"
|
|
285
287
|
|
|
288
|
+
controllers:
|
|
289
|
+
fix-loop:
|
|
290
|
+
name: Fix Loop
|
|
291
|
+
description: "Run diagnosis once if needed, then loop fix/review/verify until PASS or stop condition"
|
|
292
|
+
kind: flutter_rewrite_fix_loop
|
|
293
|
+
max_rounds: 10
|
|
294
|
+
progress_artifact_path: ".dna/fix-progress/$ARGUMENTS.json"
|
|
295
|
+
diagnosis_workflow: diagnosis
|
|
296
|
+
fix_workflow: fix
|
|
297
|
+
diagnosis_artifact_path: ".dna/specs/diagnosis-$ARGUMENTS.md"
|
|
298
|
+
diagnosis_review_artifact_path: ".dna/specs/diagnosis-$ARGUMENTS.review.json"
|
|
299
|
+
review_artifact_path: ".dna/reviews/$ARGUMENTS-review.json"
|
|
300
|
+
roles:
|
|
301
|
+
analyzer: analyzer
|
|
302
|
+
analysis_reviewer: analysis_reviewer
|
|
303
|
+
surgeon: surgeon
|
|
304
|
+
fix_reviewer: fix_reviewer
|
|
305
|
+
test_runner: test_runner
|
|
306
|
+
stop_format: zh_three_part
|
|
307
|
+
|
|
286
308
|
workflows:
|
|
287
309
|
behavior-lock:
|
|
288
310
|
name: Behavior Lock
|
|
@@ -411,10 +433,13 @@ workflows:
|
|
|
411
433
|
"sections_to_fix": ["section ids or titles; empty when approved"],
|
|
412
434
|
"evidence_paths": ["paths that justify the verdict"],
|
|
413
435
|
"confidence": "high" | "medium" | "low",
|
|
414
|
-
"summary": "brief reason"
|
|
436
|
+
"summary": "brief reason",
|
|
437
|
+
"updated_at": "ISO-8601 timestamp"
|
|
415
438
|
}
|
|
416
439
|
```
|
|
417
440
|
|
|
441
|
+
Write the same verdict JSON to .dna/specs/diagnosis-$ARGUMENTS.review.json before finishing.
|
|
442
|
+
|
|
418
443
|
APPROVE → diagnosis complete.
|
|
419
444
|
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
445
|
handoff:
|
|
@@ -423,6 +448,9 @@ workflows:
|
|
|
423
448
|
path: ".dna/specs/diagnosis-$ARGUMENTS.md"
|
|
424
449
|
description: "Diagnosis spec from analyzer"
|
|
425
450
|
produces:
|
|
451
|
+
- type: file
|
|
452
|
+
path: ".dna/specs/diagnosis-$ARGUMENTS.review.json"
|
|
453
|
+
description: "Durable diagnosis review verdict (APPROVE or REQUEST_REANALYSIS)"
|
|
426
454
|
- type: summary
|
|
427
455
|
required: false
|
|
428
456
|
description: "Review verdict (APPROVE or REQUEST_REANALYSIS)"
|
|
@@ -438,13 +466,20 @@ workflows:
|
|
|
438
466
|
prompt: |
|
|
439
467
|
This invocation is one fix round.
|
|
440
468
|
|
|
441
|
-
Before editing,
|
|
442
|
-
- If it
|
|
443
|
-
-
|
|
444
|
-
-
|
|
445
|
-
|
|
469
|
+
Before editing, read .dna/specs/diagnosis-$ARGUMENTS.review.json.
|
|
470
|
+
- If it is missing, invalid, or verdict is not APPROVE, stop immediately; do not edit.
|
|
471
|
+
- Manual fix invocations must obey this same gate.
|
|
472
|
+
- If verdict is REQUEST_REANALYSIS, run diagnosis reanalysis first instead of fixing.
|
|
473
|
+
|
|
474
|
+
Read context + diagnosis spec only after the diagnosis review gate passes.
|
|
446
475
|
|
|
447
|
-
|
|
476
|
+
Progress artifacts:
|
|
477
|
+
- .dna/fix-progress/$ARGUMENTS.json is the machine-readable source of truth for controller scheduling.
|
|
478
|
+
- If JSON exists and provenance matches this module, use its next_round_focus/verdict for the current round.
|
|
479
|
+
- .dna/fix-progress/$ARGUMENTS.md is human-readable legacy/manual context only; read it for Failed approaches / Do not repeat and evidence, but never use it as the controller verdict or source of truth.
|
|
480
|
+
- If .md exists without .json, treat it as legacy context only; do not infer PASS/CONTINUE/BLOCKED from it.
|
|
481
|
+
- If progress records REQUEST_CHANGES or BLOCKED_BY_REVIEW, address those review issues before starting unrelated diagnosis items.
|
|
482
|
+
- If neither progress artifact exists, treat this as round 1.
|
|
448
483
|
|
|
449
484
|
Count diagnosis classifications before editing. If UNIMPLEMENTED count is greater than 100, this round is implementation-first: fix UNIMPLEMENTED items by priority only, and leave TEST_BUG / BUG / INFRA / REMOVED for later rounds unless they block implementation.
|
|
450
485
|
|
|
@@ -466,6 +501,9 @@ workflows:
|
|
|
466
501
|
- type: file
|
|
467
502
|
path: ".dna/specs/diagnosis-$ARGUMENTS.md"
|
|
468
503
|
description: "Diagnosis spec"
|
|
504
|
+
- type: file
|
|
505
|
+
path: ".dna/specs/diagnosis-$ARGUMENTS.review.json"
|
|
506
|
+
description: "Approved diagnosis review gate"
|
|
469
507
|
produces:
|
|
470
508
|
- type: git_commit
|
|
471
509
|
description: "Fix commit"
|
|
@@ -488,16 +526,26 @@ workflows:
|
|
|
488
526
|
```json
|
|
489
527
|
{
|
|
490
528
|
"verdict": "APPROVE" | "REQUEST_CHANGES",
|
|
529
|
+
"module": "$ARGUMENTS",
|
|
530
|
+
"round": 1,
|
|
531
|
+
"run_id": "controller run id or manual-run",
|
|
491
532
|
"issues": ["specific issue with evidence path; empty when approved"],
|
|
492
533
|
"evidence_paths": ["paths that justify the verdict"],
|
|
493
534
|
"failure_reason": "why changes need another round; empty when approved",
|
|
494
535
|
"next_round_focus": "specific priority/classification/test file for the next fix invocation; empty when approved",
|
|
495
|
-
"summary": "brief reason"
|
|
536
|
+
"summary": "brief reason",
|
|
537
|
+
"updated_at": "ISO-8601 timestamp"
|
|
496
538
|
}
|
|
497
539
|
```
|
|
498
540
|
|
|
499
541
|
Write the same verdict JSON to .dna/reviews/$ARGUMENTS-review.json before finishing.
|
|
500
542
|
|
|
543
|
+
JSON field rules:
|
|
544
|
+
- module is required and must equal $ARGUMENTS.
|
|
545
|
+
- round is required and must be the provided current_round, or 1 for manual fix invocation.
|
|
546
|
+
- run_id is required and must equal the provided controller_run_id, or "manual-run" for manual fix invocation.
|
|
547
|
+
- updated_at is a required ISO timestamp string.
|
|
548
|
+
|
|
501
549
|
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
550
|
handoff:
|
|
503
551
|
consumes:
|
|
@@ -526,33 +574,96 @@ workflows:
|
|
|
526
574
|
|
|
527
575
|
If the review verdict is APPROVE:
|
|
528
576
|
- Run full module test suite for $ARGUMENTS.
|
|
577
|
+
- Read diagnosis classification summary.
|
|
578
|
+
- Read current test summary.
|
|
529
579
|
- Compare with diagnosis spec baseline.
|
|
580
|
+
- Compare skipped tests against remaining UNIMPLEMENTED diagnosis items.
|
|
581
|
+
|
|
582
|
+
Hard PASS rules:
|
|
583
|
+
- 0 failed is not sufficient for PASS.
|
|
584
|
+
- PASS is forbidden if any approved diagnosis UNIMPLEMENTED item remains skipped.
|
|
585
|
+
- Diagnosis-backed UNIMPLEMENTED skipped tests count as Remaining, not Blocked.
|
|
586
|
+
- UNIMPLEMENTED skipped tests count as Remaining.
|
|
587
|
+
- 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.
|
|
588
|
+
|
|
589
|
+
Verdict mapping:
|
|
590
|
+
- review REQUEST_CHANGES -> Verdict: BLOCKED, Failure reason: BLOCKED_BY_REVIEW.
|
|
591
|
+
- 0 failed + remaining UNIMPLEMENTED skipped > 0 -> Verdict: CONTINUE, Module complete: no, Next Round Focus: next diagnosis-backed UNIMPLEMENTED test/evidence path.
|
|
592
|
+
- progress contradicts diagnosis/test evidence -> Verdict: BLOCKED, Failure reason: PROGRESS_DIAGNOSIS_CONFLICT.
|
|
593
|
+
- new red caused by current round -> Verdict: REQUEST_CHANGES.
|
|
594
|
+
- new red appears to be newly exposed baseline gap AND remaining count decreases -> Verdict: CONTINUE.
|
|
595
|
+
- ambiguous new red -> Verdict: BLOCKED, Failure reason: AMBIGUOUS_NEW_RED.
|
|
596
|
+
- 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
597
|
|
|
531
598
|
Output report:
|
|
532
599
|
- Verdict: PASS / CONTINUE / REQUEST_CHANGES / BLOCKED
|
|
533
600
|
- Review: APPROVE / REQUEST_CHANGES
|
|
601
|
+
- Module complete: yes/no
|
|
534
602
|
- Fixed: N tests now green
|
|
535
603
|
- New red: M tests that regressed
|
|
536
|
-
- Blocked: K tests skipped
|
|
537
|
-
- Remaining: R tests still failing
|
|
604
|
+
- Blocked: K tests skipped for TEST_BUG / INFRA / REMOVED / user-approved blocked reasons
|
|
605
|
+
- Remaining: R tests still failing or diagnosis-backed UNIMPLEMENTED skipped
|
|
606
|
+
- Remaining UNIMPLEMENTED skipped: U diagnosis-backed skipped tests still remaining
|
|
538
607
|
- Failure reason: why progress stopped, if any
|
|
539
608
|
- Next round focus: the priority/classification/test file to continue with, if verdict is CONTINUE or REQUEST_CHANGES
|
|
540
609
|
|
|
541
610
|
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
|
|
611
|
+
- Current verdict: Verdict, Review, Module complete, Last fix commit, Updated at
|
|
612
|
+
- Round summary: Fixed, New red, Blocked, Remaining, Remaining UNIMPLEMENTED skipped, Failure reason
|
|
544
613
|
- Next Round Focus: non-empty when Verdict is CONTINUE or REQUEST_CHANGES
|
|
545
614
|
- Review Issues To Address First: reviewer issues when Review is REQUEST_CHANGES
|
|
546
615
|
- Failed approaches / Do not repeat: approaches that failed or caused regressions
|
|
547
616
|
- Evidence: commands run and relevant paths
|
|
548
617
|
|
|
549
|
-
|
|
618
|
+
Always write or update .dna/fix-progress/$ARGUMENTS.json before finishing, even when review blocks verification. The JSON must match this contract exactly:
|
|
619
|
+
```json
|
|
620
|
+
{
|
|
621
|
+
"verdict": "PASS | CONTINUE | REQUEST_CHANGES | BLOCKED",
|
|
622
|
+
"review": "APPROVE | REQUEST_CHANGES",
|
|
623
|
+
"module": "$ARGUMENTS",
|
|
624
|
+
"round": 1,
|
|
625
|
+
"run_id": "controller run id or manual-run",
|
|
626
|
+
"diagnosis_artifact": ".dna/specs/diagnosis-$ARGUMENTS.md",
|
|
627
|
+
"review_artifact": ".dna/reviews/$ARGUMENTS-review.json",
|
|
628
|
+
"git_head": null,
|
|
629
|
+
"module_complete": false,
|
|
630
|
+
"fixed_count": 0,
|
|
631
|
+
"new_red_count": 0,
|
|
632
|
+
"blocked_count": 0,
|
|
633
|
+
"remaining_count": 0,
|
|
634
|
+
"remaining_unimplemented_skipped_count": 0,
|
|
635
|
+
"next_round_focus": "",
|
|
636
|
+
"failure_reason": "",
|
|
637
|
+
"last_fix_commit": null,
|
|
638
|
+
"evidence_paths": [],
|
|
639
|
+
"updated_at": "ISO-8601 timestamp"
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
JSON field rules:
|
|
644
|
+
- module is required and must equal $ARGUMENTS.
|
|
645
|
+
- round is required and must be the current fix-loop round number, or 1 for manual fix invocation.
|
|
646
|
+
- run_id is required and must identify the current controller run, or "manual-run" for manual fix invocation.
|
|
647
|
+
- diagnosis_artifact is required and must equal .dna/specs/diagnosis-$ARGUMENTS.md.
|
|
648
|
+
- review_artifact is required and must equal .dna/reviews/$ARGUMENTS-review.json.
|
|
649
|
+
- git_head is the current git HEAD after verification if available, otherwise null.
|
|
650
|
+
- verdict is required and must be PASS, CONTINUE, REQUEST_CHANGES, or BLOCKED.
|
|
651
|
+
- review is required and must be APPROVE or REQUEST_CHANGES.
|
|
652
|
+
- module_complete is a required boolean and must match the Markdown Module complete value.
|
|
653
|
+
- fixed_count, new_red_count, blocked_count, remaining_count, and remaining_unimplemented_skipped_count are required non-negative integers.
|
|
654
|
+
- next_round_focus is required and non-empty when verdict is CONTINUE or REQUEST_CHANGES; empty string is allowed when verdict is PASS.
|
|
655
|
+
- failure_reason is required and non-empty when verdict is BLOCKED or REQUEST_CHANGES; empty string is allowed when verdict is PASS or CONTINUE.
|
|
656
|
+
- last_fix_commit is the commit hash string if available, otherwise null.
|
|
657
|
+
- evidence_paths is a required array of command/test/diagnosis/review evidence paths.
|
|
658
|
+
- updated_at is a required ISO timestamp string.
|
|
659
|
+
|
|
660
|
+
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
661
|
|
|
551
662
|
Verdict meanings:
|
|
552
|
-
- PASS: all
|
|
663
|
+
- 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
664
|
- 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
|
|
665
|
+
- REQUEST_CHANGES: verification found issues in this round's changes
|
|
666
|
+
- BLOCKED: review blocks verification, user confirmation is needed, evidence conflicts, or repeated no-progress prevents safe continuation
|
|
556
667
|
handoff:
|
|
557
668
|
consumes:
|
|
558
669
|
- type: file
|
|
@@ -569,6 +680,9 @@ workflows:
|
|
|
569
680
|
- type: file
|
|
570
681
|
path: ".dna/fix-progress/$ARGUMENTS.md"
|
|
571
682
|
description: "Durable cumulative fix-progress artifact for the next fix invocation"
|
|
683
|
+
- type: file
|
|
684
|
+
path: ".dna/fix-progress/$ARGUMENTS.json"
|
|
685
|
+
description: "Machine-readable fix-progress verdict sidecar for controllers"
|
|
572
686
|
- type: summary
|
|
573
687
|
required: false
|
|
574
688
|
description: "Verification report with test delta"
|