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