intentdna 1.6.5 → 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 (58) 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/init.js +11 -63
  9. package/dist/cli/commands/run.js +5 -4
  10. package/dist/cli/commands/show.js +2 -38
  11. package/dist/cli/commands/sync.d.ts +9 -6
  12. package/dist/cli/commands/sync.js +186 -184
  13. package/dist/cli/commands/templates.d.ts +10 -1
  14. package/dist/cli/commands/templates.js +50 -1
  15. package/dist/cli/commands/validate.js +15 -9
  16. package/dist/cli/commands/verify.js +97 -25
  17. package/dist/cli/index.js +76 -11
  18. package/dist/compiler/cascade.d.ts +3 -1
  19. package/dist/compiler/cascade.js +51 -0
  20. package/dist/compiler/compile.js +37 -0
  21. package/dist/compiler/diagnostics.d.ts +17 -0
  22. package/dist/compiler/diagnostics.js +30 -0
  23. package/dist/compiler/index.d.ts +3 -0
  24. package/dist/compiler/index.js +8 -11
  25. package/dist/compiler/input-resolver.d.ts +25 -0
  26. package/dist/compiler/input-resolver.js +175 -0
  27. package/dist/hooks/cli.d.ts +10 -1
  28. package/dist/hooks/cli.js +37 -22
  29. package/dist/hooks/state.d.ts +2 -0
  30. package/dist/hooks/state.js +23 -2
  31. package/dist/mcp/index.js +2 -0
  32. package/dist/mcp/tools-compile.js +18 -49
  33. package/dist/mcp/tools-context.d.ts +2 -0
  34. package/dist/mcp/tools-context.js +85 -0
  35. package/dist/mcp/tools-enforce.d.ts +2 -2
  36. package/dist/mcp/tools-enforce.js +19 -49
  37. package/dist/mcp/tools-observability.js +24 -0
  38. package/dist/report/kernel-signals.js +3 -0
  39. package/dist/report/report-package.d.ts +56 -0
  40. package/dist/report/report-package.js +85 -0
  41. package/dist/runtime/agent-md.d.ts +1 -0
  42. package/dist/runtime/agent-md.js +21 -3
  43. package/dist/runtime/context-sources.d.ts +14 -0
  44. package/dist/runtime/context-sources.js +60 -0
  45. package/dist/runtime/skill-adapter.d.ts +32 -4
  46. package/dist/runtime/skill-adapter.js +184 -9
  47. package/dist/runtime/workflow-runner.d.ts +1 -1
  48. package/dist/runtime/workflow-runner.js +1 -1
  49. package/dist/schema/types.d.ts +33 -0
  50. package/dist/schema/validate.js +156 -2
  51. package/dist/schema/validators/controllers.js +16 -0
  52. package/dist/signals/index.d.ts +10 -0
  53. package/dist/signals/index.js +90 -5
  54. package/dist/templates/catalog.d.ts +19 -0
  55. package/dist/templates/catalog.js +57 -0
  56. package/dist/templates/flutter-rewrite.dna.yaml +2 -2
  57. package/package.json +1 -1
  58. package/spec/foundation-hardening.md +2 -1
@@ -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.0",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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] 错误信息人类可读