intentdna 1.6.4 → 1.7.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.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/cli/commands/compile.js +2 -47
  4. package/dist/cli/commands/context.d.ts +8 -0
  5. package/dist/cli/commands/context.js +63 -0
  6. package/dist/cli/commands/feedback.d.ts +1 -0
  7. package/dist/cli/commands/feedback.js +11 -0
  8. package/dist/cli/commands/generate.js +5 -33
  9. package/dist/cli/commands/init.js +11 -63
  10. package/dist/cli/commands/run.js +5 -4
  11. package/dist/cli/commands/show.js +2 -38
  12. package/dist/cli/commands/sync.d.ts +9 -6
  13. package/dist/cli/commands/sync.js +209 -190
  14. package/dist/cli/commands/templates.d.ts +10 -1
  15. package/dist/cli/commands/templates.js +50 -1
  16. package/dist/cli/commands/validate.js +15 -9
  17. package/dist/cli/commands/verify.js +97 -25
  18. package/dist/cli/index.js +76 -11
  19. package/dist/compiler/cascade.d.ts +3 -1
  20. package/dist/compiler/cascade.js +51 -0
  21. package/dist/compiler/compile.js +37 -0
  22. package/dist/compiler/diagnostics.d.ts +17 -0
  23. package/dist/compiler/diagnostics.js +30 -0
  24. package/dist/compiler/index.d.ts +3 -0
  25. package/dist/compiler/index.js +8 -11
  26. package/dist/compiler/input-resolver.d.ts +25 -0
  27. package/dist/compiler/input-resolver.js +175 -0
  28. package/dist/hooks/cli.d.ts +10 -1
  29. package/dist/hooks/cli.js +39 -26
  30. package/dist/hooks/event-registry.d.ts +17 -0
  31. package/dist/hooks/event-registry.js +89 -0
  32. package/dist/hooks/protocol.d.ts +1 -1
  33. package/dist/hooks/schema.js +5 -1
  34. package/dist/hooks/state.d.ts +2 -0
  35. package/dist/hooks/state.js +23 -2
  36. package/dist/mcp/index.js +2 -0
  37. package/dist/mcp/tools-compile.js +18 -49
  38. package/dist/mcp/tools-context.d.ts +2 -0
  39. package/dist/mcp/tools-context.js +85 -0
  40. package/dist/mcp/tools-enforce.d.ts +2 -2
  41. package/dist/mcp/tools-enforce.js +21 -53
  42. package/dist/mcp/tools-observability.js +24 -0
  43. package/dist/report/kernel-signals.js +3 -0
  44. package/dist/report/report-package.d.ts +56 -0
  45. package/dist/report/report-package.js +85 -0
  46. package/dist/runtime/agent-md.d.ts +1 -0
  47. package/dist/runtime/agent-md.js +21 -3
  48. package/dist/runtime/context-sources.d.ts +14 -0
  49. package/dist/runtime/context-sources.js +60 -0
  50. package/dist/runtime/markdown-target-registry.d.ts +17 -0
  51. package/dist/runtime/markdown-target-registry.js +37 -0
  52. package/dist/runtime/markdown.d.ts +2 -1
  53. package/dist/runtime/markdown.js +9 -12
  54. package/dist/runtime/output-artifact-registry.d.ts +11 -0
  55. package/dist/runtime/output-artifact-registry.js +30 -0
  56. package/dist/runtime/settings-adapter.d.ts +2 -1
  57. package/dist/runtime/settings-adapter.js +4 -12
  58. package/dist/runtime/skill-adapter.d.ts +32 -4
  59. package/dist/runtime/skill-adapter.js +187 -10
  60. package/dist/runtime/workflow-runner.d.ts +1 -1
  61. package/dist/runtime/workflow-runner.js +1 -1
  62. package/dist/schema/controller-registry.d.ts +29 -0
  63. package/dist/schema/controller-registry.js +35 -0
  64. package/dist/schema/types.d.ts +33 -0
  65. package/dist/schema/validate.js +157 -138
  66. package/dist/schema/validators/controllers.d.ts +3 -0
  67. package/dist/schema/validators/controllers.js +156 -0
  68. package/dist/signals/index.d.ts +10 -0
  69. package/dist/signals/index.js +90 -5
  70. package/dist/templates/catalog.d.ts +19 -0
  71. package/dist/templates/catalog.js +57 -0
  72. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  73. package/dist/templates/metadata.d.ts +6 -0
  74. package/dist/templates/metadata.js +32 -0
  75. package/package.json +1 -1
  76. package/spec/foundation-hardening.md +2 -1
@@ -0,0 +1,35 @@
1
+ export const CONTROLLER_ROLE_KEYS = [
2
+ "analyzer",
3
+ "analysis_reviewer",
4
+ "surgeon",
5
+ "fix_reviewer",
6
+ "test_runner",
7
+ ];
8
+ export const CONTROLLER_KIND_REGISTRY = [
9
+ {
10
+ kind: "fix_loop",
11
+ label: "Fix loop",
12
+ supportsSkillGeneration: true,
13
+ requiredRoleKeys: CONTROLLER_ROLE_KEYS,
14
+ diagnosisRoleKeys: ["analyzer", "analysis_reviewer"],
15
+ fixRoleKeys: ["surgeon", "fix_reviewer", "test_runner"],
16
+ },
17
+ {
18
+ kind: "flutter_rewrite_fix_loop",
19
+ label: "Flutter rewrite fix loop",
20
+ supportsSkillGeneration: true,
21
+ requiredRoleKeys: CONTROLLER_ROLE_KEYS,
22
+ diagnosisRoleKeys: ["analyzer", "analysis_reviewer"],
23
+ fixRoleKeys: ["surgeon", "fix_reviewer", "test_runner"],
24
+ },
25
+ ];
26
+ const CONTROLLER_KIND_BY_ID = new Map(CONTROLLER_KIND_REGISTRY.map((entry) => [entry.kind, entry]));
27
+ export function getControllerKindMetadata(kind) {
28
+ return typeof kind === "string" ? CONTROLLER_KIND_BY_ID.get(kind) : undefined;
29
+ }
30
+ export function isSupportedControllerKind(kind) {
31
+ return getControllerKindMetadata(kind) !== undefined;
32
+ }
33
+ export function supportedControllerKinds() {
34
+ return CONTROLLER_KIND_REGISTRY.map((entry) => entry.kind);
35
+ }
@@ -295,6 +295,35 @@ export interface LegibilityAssetMap {
295
295
  per_role?: Record<string, LegibilityAsset[]>;
296
296
  per_workflow?: Record<string, LegibilityAsset[]>;
297
297
  }
298
+ export type AdvisoryContextSourceType = "file" | "url" | "doc" | "note";
299
+ export interface AdvisoryContextSource {
300
+ id: string;
301
+ type: AdvisoryContextSourceType;
302
+ path?: string;
303
+ url?: string;
304
+ title?: string;
305
+ description?: string;
306
+ tags?: string[];
307
+ }
308
+ export interface PlanningContext {
309
+ sources: string[];
310
+ instructions?: string[];
311
+ require_quotes?: boolean;
312
+ require_attribution?: boolean;
313
+ }
314
+ export interface CompiledAdvisoryContextSource extends AdvisoryContextSource {
315
+ usage: "planning_only";
316
+ non_enforcing: true;
317
+ requires_quotes: boolean;
318
+ requires_attribution: boolean;
319
+ }
320
+ export interface CompiledPlanningContext {
321
+ sources: string[];
322
+ instructions?: string[];
323
+ require_quotes: boolean;
324
+ require_attribution: boolean;
325
+ non_enforcing: true;
326
+ }
298
327
  export interface IntentDNA {
299
328
  $schema?: string;
300
329
  version: string;
@@ -311,6 +340,8 @@ export interface IntentDNA {
311
340
  variables?: Record<string, string | VariableDef>;
312
341
  mcp?: Record<string, MCPServerDef>;
313
342
  legibility_assets?: LegibilityAssetMap;
343
+ context_sources?: AdvisoryContextSource[];
344
+ planning_context?: PlanningContext;
314
345
  context_files?: {
315
346
  mandatory?: string[];
316
347
  per_role?: Record<string, string[]>;
@@ -439,6 +470,8 @@ export interface ConstraintIR {
439
470
  post_execution_validators: PostExecutionValidator[];
440
471
  verifier_specs?: VerifierSpec[];
441
472
  context_injections: ContextInjection[];
473
+ context_sources?: CompiledAdvisoryContextSource[];
474
+ planning_context?: CompiledPlanningContext;
442
475
  source_dna_ids: string[];
443
476
  compiled_at: string;
444
477
  active_context?: string;
@@ -7,6 +7,25 @@
7
7
  * - Codon values in valid ranges
8
8
  * - No circular inheritance
9
9
  */
10
+ import { isAbsolute } from "node:path";
11
+ import { validateController } from "./validators/controllers.js";
12
+ const SAFE_GENERATED_IDENTIFIER = /^[A-Za-z0-9_-]+$/;
13
+ const SAFE_GENERATED_DISPLAY_NAME = /^[A-Za-z0-9][A-Za-z0-9 _-]*$/;
14
+ function isSafeGeneratedIdentifier(value) {
15
+ return SAFE_GENERATED_IDENTIFIER.test(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\");
16
+ }
17
+ function isSafeGeneratedDisplayName(value) {
18
+ return SAFE_GENERATED_DISPLAY_NAME.test(value) && !value.includes("..");
19
+ }
20
+ function isSafeProjectTemplatePath(value) {
21
+ const withoutPlaceholders = value.replace(/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/g, "placeholder");
22
+ return value.trim().length > 0 &&
23
+ !isAbsolute(value) &&
24
+ !value.split(/[\\/]+/).includes("..") &&
25
+ !/[*?[\]{}]/.test(withoutPlaceholders) &&
26
+ !value.includes("${") &&
27
+ !value.includes("$(`");
28
+ }
10
29
  function isLegibilityAssetArray(value) {
11
30
  return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
12
31
  typeof item.type === "string" &&
@@ -71,6 +90,126 @@ function validateLegibilityAssets(dna, roleNamesSet, errors, warnings) {
71
90
  }
72
91
  }
73
92
  }
93
+ function validateContextSources(dna, errors) {
94
+ const sources = dna.context_sources;
95
+ if (sources !== undefined) {
96
+ if (!Array.isArray(sources)) {
97
+ errors.push({ path: "context_sources", message: "must be an array" });
98
+ }
99
+ else {
100
+ const seen = new Set();
101
+ for (let i = 0; i < sources.length; i++) {
102
+ const path = `context_sources[${i}]`;
103
+ if (typeof sources[i] !== "object" || sources[i] === null || Array.isArray(sources[i])) {
104
+ errors.push({ path, message: "context source must be an object" });
105
+ continue;
106
+ }
107
+ const source = sources[i];
108
+ if (!source.id || typeof source.id !== "string") {
109
+ errors.push({ path: `${path}.id`, message: "context source requires non-empty id" });
110
+ }
111
+ else if (seen.has(source.id)) {
112
+ errors.push({ path: `${path}.id`, message: `duplicate context source id '${source.id}'` });
113
+ }
114
+ else {
115
+ seen.add(source.id);
116
+ }
117
+ if (!["file", "url", "doc", "note"].includes(source.type)) {
118
+ errors.push({ path: `${path}.type`, message: "must be one of: file, url, doc, note" });
119
+ }
120
+ if (source.type === "file" || source.type === "doc") {
121
+ if (typeof source.path !== "string" || !source.path) {
122
+ errors.push({ path: `${path}.path`, message: `${source.type} context source requires non-empty path` });
123
+ }
124
+ else if (isAbsolute(source.path) || source.path.split(/[\\/]+/).includes("..")) {
125
+ errors.push({ path: `${path}.path`, message: "context source path must stay within the project" });
126
+ }
127
+ }
128
+ if (source.type === "url" && (typeof source.url !== "string" || !source.url)) {
129
+ errors.push({ path: `${path}.url`, message: "url context source requires non-empty url" });
130
+ }
131
+ if (source.tags !== undefined && (!Array.isArray(source.tags) || source.tags.some((tag) => typeof tag !== "string" || !tag))) {
132
+ errors.push({ path: `${path}.tags`, message: "tags must be an array of non-empty strings" });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (dna.planning_context) {
138
+ const pc = dna.planning_context;
139
+ if (!Array.isArray(pc.sources) || pc.sources.some((sourceId) => typeof sourceId !== "string" || !sourceId)) {
140
+ errors.push({ path: "planning_context.sources", message: "must be an array of non-empty context source ids" });
141
+ }
142
+ else {
143
+ const sourceList = Array.isArray(sources) ? sources : [];
144
+ const declared = new Set(sourceList.map((source) => source.id));
145
+ for (const sourceId of pc.sources) {
146
+ if (!declared.has(sourceId)) {
147
+ errors.push({ path: "planning_context.sources", message: `references unknown context source '${sourceId}'` });
148
+ }
149
+ }
150
+ }
151
+ if (pc.instructions !== undefined && (!Array.isArray(pc.instructions) || pc.instructions.some((instruction) => typeof instruction !== "string" || !instruction))) {
152
+ errors.push({ path: "planning_context.instructions", message: "must be an array of non-empty strings" });
153
+ }
154
+ if (pc.require_quotes !== undefined && typeof pc.require_quotes !== "boolean") {
155
+ errors.push({ path: "planning_context.require_quotes", message: "must be a boolean" });
156
+ }
157
+ if (pc.require_attribution !== undefined && typeof pc.require_attribution !== "boolean") {
158
+ errors.push({ path: "planning_context.require_attribution", message: "must be a boolean" });
159
+ }
160
+ }
161
+ }
162
+ function contextSourceIdentities(source) {
163
+ const identities = [source.id];
164
+ if (source.type === "file" || source.type === "doc")
165
+ identities.push(source.path);
166
+ if (source.type === "url")
167
+ identities.push(source.url);
168
+ return identities.filter((identity) => typeof identity === "string" && identity.length > 0);
169
+ }
170
+ function handoffArtifactIdentities(artifact) {
171
+ return [artifact.artifact_id, artifact.name, artifact.path, artifact.type === "git_commit" ? "git_commit" : undefined]
172
+ .filter((identity) => typeof identity === "string" && identity.length > 0);
173
+ }
174
+ function validateContextSourceHandoffOverlap(dna, errors) {
175
+ const contextSources = Array.isArray(dna.context_sources) ? dna.context_sources : [];
176
+ if (contextSources.length === 0)
177
+ return;
178
+ const advisoryIdentities = new Set(contextSources.flatMap(contextSourceIdentities));
179
+ if (advisoryIdentities.size === 0)
180
+ return;
181
+ const checkArtifact = (artifact, path) => {
182
+ if (artifact.required === false)
183
+ return;
184
+ for (const identity of handoffArtifactIdentities(artifact)) {
185
+ if (!advisoryIdentities.has(identity))
186
+ continue;
187
+ errors.push({
188
+ path,
189
+ message: `planning-only context source '${identity}' cannot be used as a required handoff artifact`,
190
+ });
191
+ return;
192
+ }
193
+ };
194
+ const checkWorkflow = (workflow, path) => {
195
+ for (let i = 0; i < (workflow.steps ?? []).length; i++) {
196
+ const step = workflow.steps[i];
197
+ for (const direction of ["consumes", "produces"]) {
198
+ const artifacts = step.handoff?.[direction] ?? [];
199
+ for (let j = 0; j < artifacts.length; j++) {
200
+ checkArtifact(artifacts[j], `${path}.steps[${i}].handoff.${direction}[${j}]`);
201
+ }
202
+ }
203
+ }
204
+ };
205
+ if (dna.workflow)
206
+ checkWorkflow(dna.workflow, "workflow");
207
+ if (dna.workflows) {
208
+ for (const [wfName, wfDef] of Object.entries(dna.workflows)) {
209
+ checkWorkflow(wfDef, `workflows.${wfName}`);
210
+ }
211
+ }
212
+ }
74
213
  function validateCodon(codon, path) {
75
214
  const errors = [];
76
215
  switch (codon.type) {
@@ -149,6 +288,9 @@ function validateContext(name, ctx, geneNames) {
149
288
  function validateRole(name, role, geneNames) {
150
289
  const errors = [];
151
290
  const path = `roles.${name}`;
291
+ if (!isSafeGeneratedIdentifier(name)) {
292
+ errors.push({ path, message: "role name must contain only letters, digits, underscores, and hyphens" });
293
+ }
152
294
  // description required
153
295
  if (!role.description) {
154
296
  errors.push({ path: `${path}.description`, message: "role requires 'description'" });
@@ -244,8 +386,10 @@ function validateRole(name, role, geneNames) {
244
386
  if (os.converter !== undefined && (typeof os.converter !== "string" || !os.converter)) {
245
387
  errors.push({ path: `${osPath}.converter`, message: "must be a non-empty string" });
246
388
  }
247
- if (os.path !== undefined && (typeof os.path !== "string" || !os.path)) {
248
- errors.push({ path: `${osPath}.path`, message: "must be a non-empty string" });
389
+ if (os.path !== undefined) {
390
+ if (typeof os.path !== "string" || !isSafeProjectTemplatePath(os.path)) {
391
+ errors.push({ path: `${osPath}.path`, message: "must be a safe project-relative path template" });
392
+ }
249
393
  }
250
394
  }
251
395
  return errors;
@@ -299,142 +443,6 @@ function validateCompletionCheck(check, path) {
299
443
  }
300
444
  return errors;
301
445
  }
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 validateStringList(value, path, errors) {
312
- if (!Array.isArray(value)) {
313
- errors.push({ path, message: "must be an array of non-empty strings" });
314
- return;
315
- }
316
- value.forEach((item, index) => {
317
- if (typeof item !== "string" || item.trim() === "") {
318
- errors.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
319
- }
320
- });
321
- }
322
- function validateControllerPolicy(policy, path, errors) {
323
- if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
324
- errors.push({ path, message: "policy must be an object" });
325
- return;
326
- }
327
- const record = policy;
328
- const listFields = [
329
- "diagnosis_reuse_criteria",
330
- "progress_provenance_checks",
331
- "verdict_branches",
332
- "verdict_mapping",
333
- "stop_report",
334
- "constraints",
335
- ];
336
- const allowedFields = new Set([...listFields, "progress_provenance_failure"]);
337
- for (const field of Object.keys(record)) {
338
- if (!allowedFields.has(field)) {
339
- errors.push({ path: `${path}.${field}`, message: "unknown controller policy field" });
340
- }
341
- }
342
- for (const field of listFields) {
343
- if (record[field] !== undefined)
344
- validateStringList(record[field], `${path}.${field}`, errors);
345
- }
346
- if (record.progress_provenance_failure !== undefined &&
347
- (typeof record.progress_provenance_failure !== "string" || record.progress_provenance_failure.trim() === "")) {
348
- errors.push({ path: `${path}.progress_provenance_failure`, message: "must be a non-empty string" });
349
- }
350
- }
351
- function validateController(controller, roleNames, workflows, path) {
352
- const errors = [];
353
- if (!controller.name) {
354
- errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
355
- }
356
- if (controller.kind !== "fix_loop" && controller.kind !== "flutter_rewrite_fix_loop") {
357
- errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
358
- }
359
- if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
360
- errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
361
- }
362
- const requiredStringFields = [
363
- "progress_artifact_path",
364
- "diagnosis_workflow",
365
- "fix_workflow",
366
- "diagnosis_artifact_path",
367
- "diagnosis_review_artifact_path",
368
- "review_artifact_path",
369
- ];
370
- for (const field of requiredStringFields) {
371
- if (typeof controller[field] !== "string" || !controller[field]) {
372
- errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
373
- }
374
- }
375
- const artifactPathFields = [
376
- "progress_artifact_path",
377
- "diagnosis_artifact_path",
378
- "diagnosis_review_artifact_path",
379
- "review_artifact_path",
380
- ];
381
- for (const field of artifactPathFields) {
382
- if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
383
- errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
384
- }
385
- }
386
- const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
387
- const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
388
- if (controller.diagnosis_workflow && !diagnosisWorkflow) {
389
- errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
390
- }
391
- if (controller.fix_workflow && !fixWorkflow) {
392
- errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
393
- }
394
- const roleMap = controller.roles;
395
- const requiredRoles = ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
396
- if (!roleMap || typeof roleMap !== "object") {
397
- errors.push({ path: `${path}.roles`, message: "controller requires roles" });
398
- }
399
- else {
400
- for (const roleKey of requiredRoles) {
401
- const roleName = roleMap[roleKey];
402
- if (typeof roleName !== "string" || !roleName) {
403
- errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
404
- }
405
- else if (!roleNames.has(roleName)) {
406
- errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
407
- }
408
- }
409
- }
410
- if (roleMap && typeof roleMap === "object") {
411
- validateControllerWorkflowRoles(diagnosisWorkflow, ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
412
- validateControllerWorkflowRoles(fixWorkflow, ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
413
- }
414
- if (controller.stop_format !== "configured_stop_report" && controller.stop_format !== "zh_three_part") {
415
- errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'configured_stop_report' or legacy 'zh_three_part'" });
416
- }
417
- if (controller.policy !== undefined) {
418
- validateControllerPolicy(controller.policy, `${path}.policy`, errors);
419
- }
420
- return errors;
421
- }
422
- function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
423
- if (!workflow)
424
- return;
425
- const workflowRoles = new Set(workflow.steps.map((step) => step.role));
426
- for (const roleKey of requiredRoleKeys) {
427
- const roleName = roleMap[roleKey];
428
- if (typeof roleName !== "string" || !roleName)
429
- continue;
430
- if (!workflowRoles.has(roleName)) {
431
- errors.push({
432
- path: `${path}.${roleKey}`,
433
- message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
434
- });
435
- }
436
- }
437
- }
438
446
  function validateWorkflow(workflow, roleNames, pathPrefix) {
439
447
  const errors = [];
440
448
  const path = pathPrefix ?? "workflow";
@@ -442,6 +450,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
442
450
  if (!workflow.name) {
443
451
  errors.push({ path: `${path}.name`, message: "workflow requires 'name'" });
444
452
  }
453
+ else if (!isSafeGeneratedDisplayName(workflow.name)) {
454
+ errors.push({ path: `${path}.name`, message: "workflow name must contain only letters, digits, spaces, underscores, and hyphens" });
455
+ }
445
456
  // steps required and non-empty
446
457
  if (!workflow.steps || workflow.steps.length === 0) {
447
458
  errors.push({ path: `${path}.steps`, message: "workflow requires at least one step" });
@@ -457,6 +468,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
457
468
  if (!step.id) {
458
469
  errors.push({ path: `${stepPath}.id`, message: "step requires 'id'" });
459
470
  }
471
+ else if (!isSafeGeneratedIdentifier(step.id)) {
472
+ errors.push({ path: `${stepPath}.id`, message: "step id must contain only letters, digits, underscores, and hyphens" });
473
+ }
460
474
  else if (seenStepIds.has(step.id)) {
461
475
  errors.push({ path: `${stepPath}.id`, message: `duplicate step id '${step.id}'` });
462
476
  }
@@ -468,6 +482,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
468
482
  if (!step.role) {
469
483
  errors.push({ path: `${stepPath}.role`, message: "step requires 'role'" });
470
484
  }
485
+ else if (!isSafeGeneratedIdentifier(step.role)) {
486
+ errors.push({ path: `${stepPath}.role`, message: "step role must contain only letters, digits, underscores, and hyphens" });
487
+ }
471
488
  else if (roleNames.size > 0 && !roleNames.has(step.role)) {
472
489
  errors.push({
473
490
  path: `${stepPath}.role`,
@@ -718,6 +735,7 @@ export function validateDNA(dna) {
718
735
  errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
719
736
  }
720
737
  }
738
+ validateContextSourceHandoffOverlap(dna, errors);
721
739
  if (dna.controllers) {
722
740
  const workflowsForControllers = { ...(dna.workflows ?? {}) };
723
741
  if (dna.workflow)
@@ -756,6 +774,7 @@ export function validateDNA(dna) {
756
774
  }
757
775
  }
758
776
  validateLegibilityAssets(dna, roleNamesSet, errors, warnings);
777
+ validateContextSources(dna, errors);
759
778
  // Validate context_files
760
779
  if (dna.context_files) {
761
780
  const cf = dna.context_files;
@@ -0,0 +1,3 @@
1
+ import type { ControllerDef, WorkflowDef } from "../types.js";
2
+ import type { ValidationError } from "../validate.js";
3
+ export declare function validateController(controller: ControllerDef, roleNames: Set<string>, workflows: Record<string, WorkflowDef>, path: string): ValidationError[];
@@ -0,0 +1,156 @@
1
+ import { CONTROLLER_ROLE_KEYS, getControllerKindMetadata, isSupportedControllerKind } from "../controller-registry.js";
2
+ function isSafeControllerArtifactPath(path) {
3
+ return path.startsWith(".dna/") &&
4
+ !path.startsWith("/") &&
5
+ !path.includes("..") &&
6
+ !/[*?[\]{}]/.test(path) &&
7
+ !path.includes("{{") &&
8
+ !path.includes("}}") &&
9
+ path.includes("$ARGUMENTS");
10
+ }
11
+ function isSafeGeneratedIdentifier(value) {
12
+ return /^[A-Za-z0-9_-]+$/.test(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\");
13
+ }
14
+ function isSafeGeneratedDisplayName(value) {
15
+ return /^[A-Za-z0-9][A-Za-z0-9 _-]*$/.test(value) && !value.includes("..");
16
+ }
17
+ function validateStringList(value, path, errors) {
18
+ if (!Array.isArray(value)) {
19
+ errors.push({ path, message: "must be an array of non-empty strings" });
20
+ return;
21
+ }
22
+ value.forEach((item, index) => {
23
+ if (typeof item !== "string" || item.trim() === "") {
24
+ errors.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
25
+ }
26
+ });
27
+ }
28
+ function validateControllerPolicy(policy, path, errors) {
29
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
30
+ errors.push({ path, message: "policy must be an object" });
31
+ return;
32
+ }
33
+ const record = policy;
34
+ const listFields = [
35
+ "diagnosis_reuse_criteria",
36
+ "progress_provenance_checks",
37
+ "verdict_branches",
38
+ "verdict_mapping",
39
+ "stop_report",
40
+ "constraints",
41
+ ];
42
+ const allowedFields = new Set([...listFields, "progress_provenance_failure"]);
43
+ for (const field of Object.keys(record)) {
44
+ if (!allowedFields.has(field)) {
45
+ errors.push({ path: `${path}.${field}`, message: "unknown controller policy field" });
46
+ }
47
+ }
48
+ for (const field of listFields) {
49
+ if (record[field] !== undefined)
50
+ validateStringList(record[field], `${path}.${field}`, errors);
51
+ }
52
+ if (record.progress_provenance_failure !== undefined &&
53
+ (typeof record.progress_provenance_failure !== "string" || record.progress_provenance_failure.trim() === "")) {
54
+ errors.push({ path: `${path}.progress_provenance_failure`, message: "must be a non-empty string" });
55
+ }
56
+ }
57
+ function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
58
+ if (!workflow)
59
+ return;
60
+ const workflowRoles = new Set(workflow.steps.map((step) => step.role));
61
+ for (const roleKey of requiredRoleKeys) {
62
+ const roleName = roleMap[roleKey];
63
+ if (typeof roleName !== "string" || !roleName)
64
+ continue;
65
+ if (!workflowRoles.has(roleName)) {
66
+ errors.push({
67
+ path: `${path}.${roleKey}`,
68
+ message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
69
+ });
70
+ }
71
+ }
72
+ }
73
+ export function validateController(controller, roleNames, workflows, path) {
74
+ const errors = [];
75
+ if (!controller.name) {
76
+ errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
77
+ }
78
+ else if (!isSafeGeneratedDisplayName(controller.name)) {
79
+ errors.push({ path: `${path}.name`, message: "controller name must contain only letters, digits, spaces, underscores, and hyphens" });
80
+ }
81
+ const controllerKey = path.startsWith("controllers.") ? path.slice("controllers.".length) : "";
82
+ if (controllerKey && !isSafeGeneratedIdentifier(controllerKey)) {
83
+ errors.push({ path, message: "controller key must contain only letters, digits, underscores, and hyphens" });
84
+ }
85
+ const kindMetadata = isSupportedControllerKind(controller.kind)
86
+ ? getControllerKindMetadata(controller.kind)
87
+ : undefined;
88
+ if (!kindMetadata) {
89
+ errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
90
+ }
91
+ if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
92
+ errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
93
+ }
94
+ const requiredStringFields = [
95
+ "progress_artifact_path",
96
+ "diagnosis_workflow",
97
+ "fix_workflow",
98
+ "diagnosis_artifact_path",
99
+ "diagnosis_review_artifact_path",
100
+ "review_artifact_path",
101
+ ];
102
+ for (const field of requiredStringFields) {
103
+ if (typeof controller[field] !== "string" || !controller[field]) {
104
+ errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
105
+ }
106
+ }
107
+ const artifactPathFields = [
108
+ "progress_artifact_path",
109
+ "diagnosis_artifact_path",
110
+ "diagnosis_review_artifact_path",
111
+ "review_artifact_path",
112
+ ];
113
+ for (const field of artifactPathFields) {
114
+ if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
115
+ errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
116
+ }
117
+ }
118
+ const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
119
+ const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
120
+ if (controller.diagnosis_workflow && !diagnosisWorkflow) {
121
+ errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
122
+ }
123
+ if (controller.fix_workflow && !fixWorkflow) {
124
+ errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
125
+ }
126
+ const roleMap = controller.roles;
127
+ const requiredRoles = kindMetadata?.requiredRoleKeys ?? CONTROLLER_ROLE_KEYS;
128
+ if (!roleMap || typeof roleMap !== "object") {
129
+ errors.push({ path: `${path}.roles`, message: "controller requires roles" });
130
+ }
131
+ else {
132
+ for (const roleKey of requiredRoles) {
133
+ const roleName = roleMap[roleKey];
134
+ if (typeof roleName !== "string" || !roleName) {
135
+ errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
136
+ }
137
+ else if (!isSafeGeneratedIdentifier(roleName)) {
138
+ errors.push({ path: `${path}.roles.${roleKey}`, message: "role name must contain only letters, digits, underscores, and hyphens" });
139
+ }
140
+ else if (!roleNames.has(roleName)) {
141
+ errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
142
+ }
143
+ }
144
+ }
145
+ if (roleMap && typeof roleMap === "object") {
146
+ validateControllerWorkflowRoles(diagnosisWorkflow, kindMetadata?.diagnosisRoleKeys ?? ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
147
+ validateControllerWorkflowRoles(fixWorkflow, kindMetadata?.fixRoleKeys ?? ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
148
+ }
149
+ if (controller.stop_format !== "configured_stop_report" && controller.stop_format !== "zh_three_part") {
150
+ errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'configured_stop_report' or legacy 'zh_three_part'" });
151
+ }
152
+ if (controller.policy !== undefined) {
153
+ validateControllerPolicy(controller.policy, `${path}.policy`, errors);
154
+ }
155
+ return errors;
156
+ }
@@ -2,12 +2,14 @@ import type { TraceEntry, VerifierResultEntry } from "../hooks/state.js";
2
2
  import type { ExecutionOutcome } from "../evolution/types.js";
3
3
  export type SignalKind = "trace" | "verifier" | "external";
4
4
  export type SignalStatus = "positive" | "negative" | "neutral";
5
+ export type ExternalSignalClassification = "protective" | "friction" | "unknown";
5
6
  export interface SignalEvidence {
6
7
  target?: string;
7
8
  evidence?: string;
8
9
  exit_code?: number;
9
10
  artifact?: string;
10
11
  message?: string;
12
+ evidence_ref?: string;
11
13
  }
12
14
  export interface SignalEnvelope {
13
15
  id: string;
@@ -19,6 +21,10 @@ export interface SignalEnvelope {
19
21
  detail?: string;
20
22
  adapter?: string;
21
23
  evidence_payload?: SignalEvidence;
24
+ classification?: ExternalSignalClassification;
25
+ confidence?: number;
26
+ mapping_policy?: string;
27
+ evolution_eligible?: boolean;
22
28
  }
23
29
  export interface ExternalSignalInput {
24
30
  id?: string;
@@ -32,6 +38,10 @@ export interface ExternalSignalInput {
32
38
  exit_code?: number;
33
39
  artifact?: string;
34
40
  message?: string;
41
+ evidence_ref?: string;
42
+ classification?: ExternalSignalClassification;
43
+ confidence?: number;
44
+ mapping_policy?: string;
35
45
  }
36
46
  export interface SignalSourceAdapter<T = unknown> {
37
47
  name: string;