intentdna 1.6.5 → 1.7.4

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 (82) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +26 -15
  4. package/dist/cli/commands/compile.js +2 -47
  5. package/dist/cli/commands/context.d.ts +8 -0
  6. package/dist/cli/commands/context.js +63 -0
  7. package/dist/cli/commands/feedback.d.ts +3 -2
  8. package/dist/cli/commands/feedback.js +16 -5
  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 +191 -186
  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.d.ts +32 -0
  18. package/dist/cli/commands/verify.js +270 -31
  19. package/dist/cli/index.js +78 -11
  20. package/dist/compiler/activate.js +11 -6
  21. package/dist/compiler/cascade.d.ts +5 -1
  22. package/dist/compiler/cascade.js +74 -1
  23. package/dist/compiler/compile.js +38 -0
  24. package/dist/compiler/diagnostics.d.ts +17 -0
  25. package/dist/compiler/diagnostics.js +30 -0
  26. package/dist/compiler/index.d.ts +7 -0
  27. package/dist/compiler/index.js +13 -13
  28. package/dist/compiler/input-resolver.d.ts +32 -0
  29. package/dist/compiler/input-resolver.js +281 -0
  30. package/dist/compiler/provenance.d.ts +8 -0
  31. package/dist/compiler/provenance.js +127 -0
  32. package/dist/governance/index.d.ts +4 -3
  33. package/dist/governance/index.js +3 -4
  34. package/dist/governance/runtime-decision-event.d.ts +85 -0
  35. package/dist/governance/runtime-decision-event.js +231 -0
  36. package/dist/governance/types.d.ts +3 -3
  37. package/dist/governance/types.js +3 -3
  38. package/dist/hooks/cli.d.ts +10 -1
  39. package/dist/hooks/cli.js +199 -35
  40. package/dist/hooks/state.d.ts +5 -10
  41. package/dist/hooks/state.js +170 -2
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/mcp/index.js +2 -0
  45. package/dist/mcp/tools-compile.js +18 -49
  46. package/dist/mcp/tools-context.d.ts +2 -0
  47. package/dist/mcp/tools-context.js +85 -0
  48. package/dist/mcp/tools-enforce.d.ts +2 -2
  49. package/dist/mcp/tools-enforce.js +19 -49
  50. package/dist/mcp/tools-observability.js +26 -2
  51. package/dist/mcp/tools-state.d.ts +1 -1
  52. package/dist/mcp/tools-state.js +17 -7
  53. package/dist/report/kernel-report.d.ts +27 -0
  54. package/dist/report/kernel-report.js +60 -19
  55. package/dist/report/kernel-signals.d.ts +5 -2
  56. package/dist/report/kernel-signals.js +87 -2
  57. package/dist/report/report-package.d.ts +64 -0
  58. package/dist/report/report-package.js +90 -0
  59. package/dist/runtime/agent-md.d.ts +1 -0
  60. package/dist/runtime/agent-md.js +21 -3
  61. package/dist/runtime/claude-sdk.d.ts +9 -4
  62. package/dist/runtime/claude-sdk.js +9 -0
  63. package/dist/runtime/context-sources.d.ts +14 -0
  64. package/dist/runtime/context-sources.js +60 -0
  65. package/dist/runtime/plugin-adapter.d.ts +5 -1
  66. package/dist/runtime/plugin-adapter.js +2 -0
  67. package/dist/runtime/skill-adapter.d.ts +32 -4
  68. package/dist/runtime/skill-adapter.js +184 -9
  69. package/dist/runtime/workflow-runner.d.ts +1 -1
  70. package/dist/runtime/workflow-runner.js +1 -1
  71. package/dist/schema/types.d.ts +84 -0
  72. package/dist/schema/validate.js +156 -2
  73. package/dist/schema/validators/controllers.js +16 -0
  74. package/dist/signals/index.d.ts +10 -0
  75. package/dist/signals/index.js +90 -5
  76. package/dist/templates/catalog.d.ts +19 -0
  77. package/dist/templates/catalog.js +57 -0
  78. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  79. package/package.json +1 -1
  80. package/spec/README.md +1 -1
  81. package/spec/foundation-hardening.md +2 -1
  82. package/spec/schema-spec.md +78 -1
@@ -7,7 +7,25 @@
7
7
  * - Codon values in valid ranges
8
8
  * - No circular inheritance
9
9
  */
10
+ import { isAbsolute } from "node:path";
10
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
+ }
11
29
  function isLegibilityAssetArray(value) {
12
30
  return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
13
31
  typeof item.type === "string" &&
@@ -72,6 +90,126 @@ function validateLegibilityAssets(dna, roleNamesSet, errors, warnings) {
72
90
  }
73
91
  }
74
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
+ }
75
213
  function validateCodon(codon, path) {
76
214
  const errors = [];
77
215
  switch (codon.type) {
@@ -150,6 +288,9 @@ function validateContext(name, ctx, geneNames) {
150
288
  function validateRole(name, role, geneNames) {
151
289
  const errors = [];
152
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
+ }
153
294
  // description required
154
295
  if (!role.description) {
155
296
  errors.push({ path: `${path}.description`, message: "role requires 'description'" });
@@ -245,8 +386,10 @@ function validateRole(name, role, geneNames) {
245
386
  if (os.converter !== undefined && (typeof os.converter !== "string" || !os.converter)) {
246
387
  errors.push({ path: `${osPath}.converter`, message: "must be a non-empty string" });
247
388
  }
248
- if (os.path !== undefined && (typeof os.path !== "string" || !os.path)) {
249
- 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
+ }
250
393
  }
251
394
  }
252
395
  return errors;
@@ -307,6 +450,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
307
450
  if (!workflow.name) {
308
451
  errors.push({ path: `${path}.name`, message: "workflow requires 'name'" });
309
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
+ }
310
456
  // steps required and non-empty
311
457
  if (!workflow.steps || workflow.steps.length === 0) {
312
458
  errors.push({ path: `${path}.steps`, message: "workflow requires at least one step" });
@@ -322,6 +468,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
322
468
  if (!step.id) {
323
469
  errors.push({ path: `${stepPath}.id`, message: "step requires 'id'" });
324
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
+ }
325
474
  else if (seenStepIds.has(step.id)) {
326
475
  errors.push({ path: `${stepPath}.id`, message: `duplicate step id '${step.id}'` });
327
476
  }
@@ -333,6 +482,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
333
482
  if (!step.role) {
334
483
  errors.push({ path: `${stepPath}.role`, message: "step requires 'role'" });
335
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
+ }
336
488
  else if (roleNames.size > 0 && !roleNames.has(step.role)) {
337
489
  errors.push({
338
490
  path: `${stepPath}.role`,
@@ -583,6 +735,7 @@ export function validateDNA(dna) {
583
735
  errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
584
736
  }
585
737
  }
738
+ validateContextSourceHandoffOverlap(dna, errors);
586
739
  if (dna.controllers) {
587
740
  const workflowsForControllers = { ...(dna.workflows ?? {}) };
588
741
  if (dna.workflow)
@@ -621,6 +774,7 @@ export function validateDNA(dna) {
621
774
  }
622
775
  }
623
776
  validateLegibilityAssets(dna, roleNamesSet, errors, warnings);
777
+ validateContextSources(dna, errors);
624
778
  // Validate context_files
625
779
  if (dna.context_files) {
626
780
  const cf = dna.context_files;
@@ -8,6 +8,12 @@ function isSafeControllerArtifactPath(path) {
8
8
  !path.includes("}}") &&
9
9
  path.includes("$ARGUMENTS");
10
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
+ }
11
17
  function validateStringList(value, path, errors) {
12
18
  if (!Array.isArray(value)) {
13
19
  errors.push({ path, message: "must be an array of non-empty strings" });
@@ -69,6 +75,13 @@ export function validateController(controller, roleNames, workflows, path) {
69
75
  if (!controller.name) {
70
76
  errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
71
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
+ }
72
85
  const kindMetadata = isSupportedControllerKind(controller.kind)
73
86
  ? getControllerKindMetadata(controller.kind)
74
87
  : undefined;
@@ -121,6 +134,9 @@ export function validateController(controller, roleNames, workflows, path) {
121
134
  if (typeof roleName !== "string" || !roleName) {
122
135
  errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
123
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
+ }
124
140
  else if (!roleNames.has(roleName)) {
125
141
  errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
126
142
  }
@@ -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;
@@ -1,4 +1,35 @@
1
1
  import { extractGeneFromReason } from "../evolution/trace-bridge.js";
2
+ const SAFE_EXTERNAL_GENE = /^[A-Za-z0-9:_-]+$/;
3
+ function hasSafeExternalGene(gene) {
4
+ return typeof gene === "string" && SAFE_EXTERNAL_GENE.test(gene);
5
+ }
6
+ function hasNonBlankText(value) {
7
+ return typeof value === "string" && value.trim().length > 0;
8
+ }
9
+ function isSignalStatus(value) {
10
+ return value === "positive" || value === "negative" || value === "neutral";
11
+ }
12
+ function isExternalSignalClassification(value) {
13
+ return value === undefined || value === "protective" || value === "friction" || value === "unknown";
14
+ }
15
+ function hasFiniteConfidence(value) {
16
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
17
+ }
18
+ function hasExternalSignalShape(signal) {
19
+ return isSignalStatus(signal.status) &&
20
+ hasNonBlankText(signal.source) &&
21
+ isExternalSignalClassification(signal.classification) &&
22
+ (signal.confidence === undefined || hasFiniteConfidence(signal.confidence));
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function asString(value) {
28
+ return typeof value === "string" ? value : undefined;
29
+ }
30
+ function asNumber(value) {
31
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
32
+ }
2
33
  function toSignalEvidence(result) {
3
34
  const payload = {
4
35
  target: result.target,
@@ -6,23 +37,75 @@ function toSignalEvidence(result) {
6
37
  exit_code: result.exit_code,
7
38
  artifact: result.artifact,
8
39
  message: result.message,
40
+ evidence_ref: "evidence_ref" in result ? result.evidence_ref : undefined,
9
41
  };
10
42
  return Object.values(payload).some((value) => value !== undefined) ? payload : undefined;
11
43
  }
44
+ function hasExternalSignalInputContract(signal) {
45
+ return (hasExternalSignalShape(signal) &&
46
+ hasFiniteConfidence(signal.confidence) &&
47
+ hasNonBlankText(signal.mapping_policy) &&
48
+ hasNonBlankText(signal.evidence_ref) &&
49
+ hasSafeExternalGene(signal.gene));
50
+ }
51
+ function hasExternalSignalEnvelopeContract(signal) {
52
+ return (isSignalStatus(signal.status) &&
53
+ hasNonBlankText(signal.source) &&
54
+ isExternalSignalClassification(signal.classification) &&
55
+ hasFiniteConfidence(signal.confidence) &&
56
+ hasNonBlankText(signal.mapping_policy) &&
57
+ hasNonBlankText(signal.evidence_payload?.evidence_ref) &&
58
+ hasSafeExternalGene(signal.gene));
59
+ }
12
60
  function normalizeExternalSignals(adapterName, payload) {
13
61
  const items = Array.isArray(payload) ? payload : [payload];
14
- return items.map((item, index) => {
15
- const timestamp = item.timestamp ?? new Date().toISOString();
62
+ return items.map((raw, index) => {
63
+ const timestamp = isRecord(raw) ? asString(raw.timestamp) ?? new Date().toISOString() : new Date().toISOString();
64
+ if (!isRecord(raw)) {
65
+ return {
66
+ id: `${adapterName}:invalid:${timestamp}:${index}`,
67
+ kind: "external",
68
+ status: "neutral",
69
+ timestamp,
70
+ source: `${adapterName}.invalid`,
71
+ adapter: adapterName,
72
+ classification: "unknown",
73
+ evolution_eligible: false,
74
+ };
75
+ }
76
+ const item = {
77
+ id: asString(raw.id),
78
+ status: raw.status,
79
+ timestamp,
80
+ gene: asString(raw.gene),
81
+ source: asString(raw.source) ?? "",
82
+ detail: asString(raw.detail),
83
+ target: asString(raw.target),
84
+ evidence: asString(raw.evidence),
85
+ exit_code: asNumber(raw.exit_code),
86
+ artifact: asString(raw.artifact),
87
+ message: asString(raw.message),
88
+ evidence_ref: asString(raw.evidence_ref),
89
+ classification: raw.classification,
90
+ confidence: typeof raw.confidence === "number" ? raw.confidence : undefined,
91
+ mapping_policy: asString(raw.mapping_policy),
92
+ };
93
+ const shapeValid = hasExternalSignalShape(item);
94
+ const complete = hasExternalSignalInputContract(item);
16
95
  return {
17
- id: item.id ?? `${adapterName}:${item.source}:${timestamp}:${index}`,
96
+ id: item.id ?? `${adapterName}:${shapeValid ? item.source : "invalid"}:${timestamp}:${index}`,
18
97
  kind: "external",
19
- status: item.status,
98
+ status: shapeValid ? item.status : "neutral",
20
99
  timestamp,
21
100
  gene: item.gene,
22
- source: item.source,
101
+ source: hasNonBlankText(item.source) ? item.source : `${adapterName}.invalid`,
23
102
  detail: item.detail ?? item.message ?? item.evidence,
24
103
  adapter: adapterName,
25
104
  evidence_payload: toSignalEvidence(item),
105
+ classification: isExternalSignalClassification(item.classification) ? (item.classification ?? "unknown") : "unknown",
106
+ confidence: hasFiniteConfidence(item.confidence) ? item.confidence : undefined,
107
+ mapping_policy: item.mapping_policy,
108
+ evolution_eligible: complete,
26
109
  };
27
110
  });
28
111
  }
@@ -85,6 +168,8 @@ export function signalsToOutcomes(signals, dnaId = "default") {
85
168
  for (const signal of signals) {
86
169
  if (!signal.gene)
87
170
  continue;
171
+ if (signal.kind === "external" && !hasExternalSignalEnvelopeContract(signal))
172
+ continue;
88
173
  const effect = signal.status === "positive"
89
174
  ? "helped"
90
175
  : signal.status === "negative"
@@ -0,0 +1,19 @@
1
+ export declare const BUILTIN_TEMPLATES_DIR: string;
2
+ export declare const PROJECT_TEMPLATES_DIR = ".dna/templates";
3
+ export interface TemplateCatalogEntry {
4
+ name: string;
5
+ path: string;
6
+ namespace?: string;
7
+ displayName?: string;
8
+ version?: string;
9
+ contentHash: string;
10
+ source: "builtin" | "project";
11
+ keywords: readonly string[];
12
+ }
13
+ export declare function calculateTemplateContentHash(content: string): string;
14
+ export declare function listTemplateCatalog(options?: {
15
+ projectDir?: string;
16
+ }): Promise<TemplateCatalogEntry[]>;
17
+ export declare function findTemplateCatalogEntry(name: string, options?: {
18
+ projectDir?: string;
19
+ }): Promise<TemplateCatalogEntry | undefined>;
@@ -0,0 +1,57 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseYAML } from "../schema/yaml-parser.js";
6
+ import { getTemplateMatchMetadata } from "./metadata.js";
7
+ export const BUILTIN_TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates");
8
+ export const PROJECT_TEMPLATES_DIR = ".dna/templates";
9
+ function calculateContentHash(content) {
10
+ return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
11
+ }
12
+ async function readTemplateEntry(path, source) {
13
+ const content = await readFile(path, "utf-8");
14
+ const data = parseYAML(content);
15
+ const fileName = path.split("/").pop() ?? path;
16
+ const name = fileName.replace(/\.dna\.ya?ml$/, "");
17
+ const matchMetadata = getTemplateMatchMetadata(name);
18
+ return {
19
+ name,
20
+ path,
21
+ namespace: typeof data.namespace === "string" ? data.namespace : undefined,
22
+ displayName: typeof data.name === "string" ? data.name : undefined,
23
+ version: typeof data.version === "string" ? data.version : undefined,
24
+ contentHash: calculateContentHash(content),
25
+ source,
26
+ keywords: matchMetadata?.keywords ?? [],
27
+ };
28
+ }
29
+ async function collectTemplates(dir, source) {
30
+ try {
31
+ const files = await readdir(dir);
32
+ const templateFiles = files
33
+ .filter((file) => file.endsWith(".dna.yaml") || file.endsWith(".dna.yml"))
34
+ .sort();
35
+ const entries = [];
36
+ for (const file of templateFiles) {
37
+ entries.push(await readTemplateEntry(resolve(dir, file), source));
38
+ }
39
+ return entries;
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ }
45
+ export function calculateTemplateContentHash(content) {
46
+ return calculateContentHash(content);
47
+ }
48
+ export async function listTemplateCatalog(options) {
49
+ const projectDir = options?.projectDir ?? process.cwd();
50
+ const builtin = await collectTemplates(BUILTIN_TEMPLATES_DIR, "builtin");
51
+ const project = await collectTemplates(resolve(projectDir, PROJECT_TEMPLATES_DIR), "project");
52
+ return [...builtin, ...project];
53
+ }
54
+ export async function findTemplateCatalogEntry(name, options) {
55
+ const entries = await listTemplateCatalog(options);
56
+ return entries.find((entry) => entry.name === name || entry.displayName === name || entry.namespace === name);
57
+ }
@@ -335,8 +335,8 @@ controllers:
335
335
  - "- MAX_ROUNDS: stop, report max rounds reached and next focus"
336
336
  stop_report:
337
337
  - "On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, output exactly these Chinese sections:"
338
- - "结论:state the direct stop trigger, current round, and current commit if one exists."
339
- - "为什么停止:explain the workflow principle in plain language; if this is a current-round new red, explain that continuing would build on a bad baseline."
338
+ - "结论:state the direct stop trigger, current round, and current commit if one exists; cite verifier result_id, trace_id, or ArtifactManifest artifact id."
339
+ - "为什么停止:explain the workflow principle in plain language; if this is a current-round new red, explain that continuing would build on a bad baseline; prose-only claims are not sufficient."
340
340
  - "是否回滚:say whether rollback/revert is recommended. If the commit direction looks wrong or the blast radius is unclear, recommend revert; if the failure is narrow and fixable, recommend fix-forward."
341
341
  - "下一步:give one concrete next action. If recommending fix-forward, include a copy-paste single-fix prompt with current commit, exact failure, allowed scope, forbidden actions, verification command, commit expectation, and stop conditions."
342
342
  constraints:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.6.5",
3
+ "version": "1.7.4",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/spec/README.md CHANGED
@@ -15,7 +15,7 @@ This directory contains engineering specs. Use [ROADMAP.md](../ROADMAP.md) as th
15
15
 
16
16
  | Spec | Status | Purpose |
17
17
  |------|--------|---------|
18
- | [schema-spec.md](schema-spec.md) | Canonical | Intent DNA document structure, genes, roles, workflows, cascade, and IR contract. |
18
+ | [schema-spec.md](schema-spec.md) | Canonical | Intent DNA document structure, genes, roles, workflows, cascade, Enterprise bundle inheritance, RuntimeDecisionEvent, and IR contract. |
19
19
  | [foundation-hardening.md](foundation-hardening.md) | Active reference | Reliability, governance, MCP, and tool-experience hardening plan. |
20
20
  | [hooks-infra-harness-hardening.md](hooks-infra-harness-hardening.md) | Active reference | Hook boundary schema, enforcement result types, state manager, context/reflection/workflow gates. |
21
21
  | [workflow-pipeline.md](workflow-pipeline.md) | Active reference | Workflow pipeline design and state management unification. |
@@ -173,5 +173,6 @@ hook 报错时输出人类可读的提示而非 JSON。`dna verify` 输出友好
173
173
 
174
174
  ### 第三阶段
175
175
  - [x] dna sync 自动检测模板版本 + 提示升级
176
- - [x] MCP server 常驻进程解决冷启动
176
+ - [x] MCP server 常驻进程复用完整 hook runtime + IR cache
177
+ - [ ] Claude settings hook 零进程调度(后续性能优化)
177
178
  - [x] 错误信息人类可读
@@ -386,7 +386,7 @@ Or shorthand: `max_rounds: 3` (equivalent to max_retries: 2)
386
386
 
387
387
  ## Cascade (Inheritance)
388
388
 
389
- DNA configs inherit from parent layers using CSS-like cascade.
389
+ DNA configs inherit from parent layers using CSS-like cascade. The enterprise safety cascade order is Species → Enterprise → Project → Personal → Role → Context → Task.
390
390
 
391
391
  ```yaml
392
392
  cascade:
@@ -404,6 +404,83 @@ cascade:
404
404
  ### Threshold Protection
405
405
  Threshold codons from lower-priority layers **cannot be removed** by higher-priority layers. This ensures safety invariants propagate up the chain.
406
406
 
407
+ Species and Enterprise thresholds are organization red lines. Project, Personal, Role, Context, and Task layers may add stricter thresholds, but they cannot relax, remove, bypass, or downgrade Species/Enterprise thresholds.
408
+
409
+ ### Enterprise Bundle Inheritance (v1.7.x local/shared slice)
410
+
411
+ The v1.7.x Enterprise Safety Slice is scoped to local and shared policy bundle references only; it does not implement remote registry, signing, push/pull, or SaaS permissions. In v1.7.1 this section freezes the contract and resolver boundary; the concrete `dna sync` / `dna verify` resolver implementation is planned for v1.7.2.
412
+
413
+ Candidate bundle locations:
414
+
415
+ - `INTENTDNA_ENTERPRISE_POLICY_DIR` — admin/team-controlled shared root for authoritative local dogfood
416
+ - `.dna/policies/enterprise/*.dna.yaml` — project-local bundle for development and fixture use
417
+ - `~/.intentdna/policies/enterprise/*.dna.yaml` — user-local bundle for development and personal dogfood
418
+ - `test/fixtures/enterprise-policies/` — test-only fixture root
419
+
420
+ Project-local and user-local bundles are not organization-authoritative by themselves. They must be treated as local/dev provenance unless the future v1.7.2 resolver can tie them to an admin-controlled root or pinned fingerprint.
421
+
422
+ Supported reference forms:
423
+
424
+ ```yaml
425
+ cascade:
426
+ inherits:
427
+ - "enterprise:baseline" # resolves baseline.dna.yaml from candidate dirs
428
+ - "file:../shared/baseline.dna.yaml" # explicit local/shared file
429
+ ```
430
+
431
+ For v1.7.2, `dna sync` must materialize the resolved bundle id/version, source path, fingerprint, cascade layer fingerprints, and compiled IR hash into provenance. `dna verify` must reject unresolved references, inheritance cycles, missing bundle metadata, and attempts by lower layers to relax Species/Enterprise thresholds.
432
+
433
+ ---
434
+
435
+ ## Enterprise Provenance Contract (v1.7.1)
436
+
437
+ v1.7.1 freezes the enterprise evidence contract before the full implementation lands. `RuntimeDecisionEvent` is required for enterprise governance evidence; legacy trace/audit/state records without this provenance remain diagnostic only.
438
+
439
+ ### PolicyBundle
440
+
441
+ A policy bundle records the policy source that produced enforcement-relevant constraints:
442
+
443
+ | Field | Required | Description |
444
+ |-------|----------|-------------|
445
+ | `policy_bundle_id` | yes | Stable bundle id, e.g. `enterprise_baseline` |
446
+ | `policy_bundle_version` | yes | Bundle version used for the decision |
447
+ | `source_ref` | yes | `enterprise:baseline`, `file:...`, or a local source path |
448
+ | `fingerprint` | yes | Stable content/source fingerprint |
449
+
450
+ ### CascadeLayerFingerprint
451
+
452
+ `cascade_layers` must include every layer: `species`, `enterprise`, `project`, `personal`, `role`, `context`, and `task`.
453
+
454
+ | Field | Required | Description |
455
+ |-------|----------|-------------|
456
+ | `layer` | yes | One of the fixed cascade layer names |
457
+ | `source_id` | yes | DNA or placeholder source id |
458
+ | `source_ref` | yes | Resolved source path/reference or `unknown` / `unsupported` |
459
+ | `fingerprint` | yes | Layer fingerprint or `unknown` / `unsupported` |
460
+ | `status` | yes | `active`, `unknown`, or `unsupported` |
461
+
462
+ ### RuntimeDecisionEvent
463
+
464
+ Every enterprise runtime decision must include these required fields:
465
+
466
+ ```text
467
+ schema_version, event_id, timestamp,
468
+ org_id, team_id, user_id, project_id,
469
+ agent_id, agent_role, agent_type, session_agent,
470
+ harness_adapter, runtime,
471
+ policy_bundle_id, policy_bundle_version,
472
+ cascade_layers, winning_constraint, compiled_ir_hash,
473
+ decision, decision_reason,
474
+ enforcement_point, harness_runtime_context,
475
+ session_id, run_id, step_id,
476
+ tool_name, action_kind, resource_ref,
477
+ evidence_refs
478
+ ```
479
+
480
+ `decision` is one of `allow`, `warn`, `block`, `escalate`, or `validate`. `enforcement_point` is one of `hook`, `sdk`, `ci`, `plugin_runtime`, `mcp_external_write`, `unknown`, or `unsupported`.
481
+
482
+ The first implementation may fill unavailable dimensions with explicit `unknown` or `unsupported` values, but it must not omit required fields. Placeholder values make the event syntactically valid for phased rollout, not enterprise-authoritative: any required provenance field that remains `unknown` / `unsupported`, or any `legacy` / placeholder `evidence_refs` entry, keeps the event classified as `legacy_diagnostic`. Events missing required fields or classified as diagnostic cannot enter enterprise compliance statistics. MCP, reports, dashboards, and evolution consume this event as evidence; they do not create policy truth.
483
+
407
484
  ---
408
485
 
409
486
  ## Variables