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,30 @@
1
+ export class DNADiagnosticsError extends Error {
2
+ diagnostics;
3
+ constructor(message, diagnostics) {
4
+ super(message);
5
+ this.name = "DNADiagnosticsError";
6
+ this.diagnostics = diagnostics;
7
+ }
8
+ }
9
+ export function hasDiagnosticErrors(diagnostics) {
10
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
11
+ }
12
+ export function formatDiagnostic(diagnostic) {
13
+ const location = [diagnostic.file, diagnostic.path].filter(Boolean).join(":");
14
+ const prefix = location ? `${location}: ` : "";
15
+ const hint = diagnostic.hint ? ` (${diagnostic.hint})` : "";
16
+ return `${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${prefix}${diagnostic.message}${hint}`;
17
+ }
18
+ export function formatDiagnostics(diagnostics) {
19
+ return diagnostics.map(formatDiagnostic).join("\n");
20
+ }
21
+ export function diagnosticsFromError(error, file) {
22
+ if (error instanceof DNADiagnosticsError)
23
+ return error.diagnostics;
24
+ return [{
25
+ severity: "error",
26
+ code: "unexpected_error",
27
+ message: error instanceof Error ? error.message : String(error),
28
+ file,
29
+ }];
30
+ }
@@ -10,6 +10,9 @@ export { activateDNA } from "./activate.js";
10
10
  export { compileDNA } from "./compile.js";
11
11
  export { compileWorkflow } from "./workflow.js";
12
12
  export type { CompileWorkflowOptions, CompileWorkflowResult, CompileWorkflowError, } from "./workflow.js";
13
+ export type { Diagnostic, DiagnosticSeverity } from "./diagnostics.js";
14
+ export { DNADiagnosticsError, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
15
+ export { assertNoNamespaceCollisions, checkNamespaceCollisions, detectDNAConfigs, expandDNAInputFiles, loadDNAWithDiagnostics, resolveDNAInputs, resolveSpeciesReference, } from "./input-resolver.js";
13
16
  /**
14
17
  * Load and parse a DNA file from disk.
15
18
  */
@@ -4,29 +4,26 @@
4
4
  * Full compilation pipeline:
5
5
  * DNA files → parse → cascade → activate (+ epigenetic) → compile → IR
6
6
  */
7
- import { readFile } from "node:fs/promises";
8
- import { validateDNA } from "../schema/validate.js";
9
- import { parseYAML } from "../schema/yaml-parser.js";
10
7
  import { cascadeDNA } from "./cascade.js";
11
8
  import { activateDNA } from "./activate.js";
12
9
  import { compileDNA } from "./compile.js";
10
+ import { DNADiagnosticsError, formatDiagnostics } from "./diagnostics.js";
11
+ import { loadDNAWithDiagnostics } from "./input-resolver.js";
13
12
  export { cascadeDNA } from "./cascade.js";
14
13
  export { activateDNA } from "./activate.js";
15
14
  export { compileDNA } from "./compile.js";
16
15
  export { compileWorkflow } from "./workflow.js";
16
+ export { DNADiagnosticsError, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
17
+ export { assertNoNamespaceCollisions, checkNamespaceCollisions, detectDNAConfigs, expandDNAInputFiles, loadDNAWithDiagnostics, resolveDNAInputs, resolveSpeciesReference, } from "./input-resolver.js";
17
18
  /**
18
19
  * Load and parse a DNA file from disk.
19
20
  */
20
21
  export async function loadDNA(filePath) {
21
- const raw = await readFile(filePath, "utf-8");
22
- const isYAML = filePath.endsWith(".yaml") || filePath.endsWith(".yml");
23
- const dna = (isYAML ? parseYAML(raw) : JSON.parse(raw));
24
- const result = validateDNA(dna);
25
- if (!result.valid) {
26
- const messages = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
27
- throw new Error(`Invalid DNA file ${filePath}:\n${messages}`);
22
+ const result = await loadDNAWithDiagnostics(filePath);
23
+ if (!result.dna) {
24
+ throw new DNADiagnosticsError(`Invalid DNA file ${filePath}:\n${formatDiagnostics(result.diagnostics)}`, result.diagnostics);
28
25
  }
29
- return dna;
26
+ return result.dna;
30
27
  }
31
28
  /**
32
29
  * Full compilation pipeline: load DNA files, cascade, activate context, compile to IR.
@@ -0,0 +1,25 @@
1
+ import type { IntentDNA } from "../schema/types.js";
2
+ import type { Diagnostic } from "./diagnostics.js";
3
+ export interface ResolveDNAInputOptions {
4
+ cwd?: string;
5
+ includeDiagnostics?: boolean;
6
+ }
7
+ export interface ResolvedDNAInputs {
8
+ files: string[];
9
+ dnas: IntentDNA[];
10
+ diagnostics: Diagnostic[];
11
+ }
12
+ export declare function parseDNAFile(filePath: string): Promise<IntentDNA>;
13
+ export declare function resolveSpeciesReference(ref: string): string | null;
14
+ export declare function detectDNAConfigs(projectDir?: string): Promise<string[]>;
15
+ export interface ExpandDNAInputOptions {
16
+ cwd?: string;
17
+ }
18
+ export declare function expandDNAInputFiles(files: string[], options?: ExpandDNAInputOptions): Promise<string[]>;
19
+ export declare function checkNamespaceCollisions(configPaths: string[]): Promise<Diagnostic[]>;
20
+ export declare function assertNoNamespaceCollisions(configPaths: string[]): Promise<void>;
21
+ export declare function loadDNAWithDiagnostics(filePath: string): Promise<{
22
+ dna?: IntentDNA;
23
+ diagnostics: Diagnostic[];
24
+ }>;
25
+ export declare function resolveDNAInputs(files: string[], options?: ResolveDNAInputOptions): Promise<ResolvedDNAInputs>;
@@ -0,0 +1,175 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { validateDNA } from "../schema/validate.js";
5
+ import { parseYAML } from "../schema/yaml-parser.js";
6
+ import { DNADiagnosticsError, hasDiagnosticErrors } from "./diagnostics.js";
7
+ const DNA_CONFIG_CANDIDATES = [
8
+ ".dna/config.yaml",
9
+ ".dna/config.yml",
10
+ ".dna/config.json",
11
+ ".dna.yaml",
12
+ ".dna.yml",
13
+ ".dna.json",
14
+ ];
15
+ async function fileExists(path) {
16
+ try {
17
+ await stat(path);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function parseDNAContent(filePath, raw) {
25
+ const isYAML = filePath.endsWith(".yaml") || filePath.endsWith(".yml");
26
+ return (isYAML ? parseYAML(raw) : JSON.parse(raw));
27
+ }
28
+ export async function parseDNAFile(filePath) {
29
+ const raw = await readFile(filePath, "utf-8");
30
+ return parseDNAContent(filePath, raw);
31
+ }
32
+ export function resolveSpeciesReference(ref) {
33
+ if (!ref.startsWith("species:"))
34
+ return null;
35
+ const name = ref.slice("species:".length);
36
+ const thisDir = dirname(fileURLToPath(import.meta.url));
37
+ return resolve(thisDir, "..", "species", `${name}.dna.json`);
38
+ }
39
+ export async function detectDNAConfigs(projectDir = process.cwd()) {
40
+ const configsDir = resolve(projectDir, ".dna", "configs");
41
+ try {
42
+ const files = await readdir(configsDir);
43
+ const configs = files
44
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
45
+ .sort()
46
+ .map((file) => resolve(configsDir, file));
47
+ if (configs.length > 0)
48
+ return configs;
49
+ }
50
+ catch {
51
+ // Directory does not exist.
52
+ }
53
+ for (const candidate of DNA_CONFIG_CANDIDATES) {
54
+ const path = resolve(projectDir, candidate);
55
+ if (await fileExists(path))
56
+ return [path];
57
+ }
58
+ return [];
59
+ }
60
+ export async function expandDNAInputFiles(files, options) {
61
+ const cwd = options?.cwd ?? process.cwd();
62
+ const expanded = [];
63
+ const seen = new Set();
64
+ const add = (path, position) => {
65
+ if (seen.has(path))
66
+ return;
67
+ seen.add(path);
68
+ if (position === "front")
69
+ expanded.unshift(path);
70
+ else
71
+ expanded.push(path);
72
+ };
73
+ for (const file of files) {
74
+ const abs = resolve(cwd, file);
75
+ add(abs, "back");
76
+ try {
77
+ const dna = await parseDNAFile(abs);
78
+ for (const ref of dna.cascade?.inherits ?? []) {
79
+ const speciesPath = resolveSpeciesReference(ref);
80
+ if (speciesPath)
81
+ add(speciesPath, "front");
82
+ }
83
+ }
84
+ catch {
85
+ // Validation/loading reports the concrete parse error later.
86
+ }
87
+ }
88
+ return expanded;
89
+ }
90
+ export async function checkNamespaceCollisions(configPaths) {
91
+ const diagnostics = [];
92
+ const namespaces = new Map();
93
+ for (const configPath of configPaths) {
94
+ try {
95
+ const data = await parseDNAFile(configPath);
96
+ const namespace = typeof data.namespace === "string" ? data.namespace : undefined;
97
+ if (!namespace)
98
+ continue;
99
+ const previous = namespaces.get(namespace);
100
+ if (previous) {
101
+ diagnostics.push({
102
+ severity: "error",
103
+ code: "namespace_conflict",
104
+ message: `Namespace "${namespace}" conflict: ${previous} and ${configPath}. Each template must have a unique namespace.`,
105
+ file: configPath,
106
+ });
107
+ }
108
+ else {
109
+ namespaces.set(namespace, configPath);
110
+ }
111
+ }
112
+ catch {
113
+ // Parse/validation errors are reported by the load step.
114
+ }
115
+ }
116
+ return diagnostics;
117
+ }
118
+ export async function assertNoNamespaceCollisions(configPaths) {
119
+ const diagnostics = await checkNamespaceCollisions(configPaths);
120
+ if (diagnostics.length > 0) {
121
+ throw new DNADiagnosticsError(diagnostics[0].message, diagnostics);
122
+ }
123
+ }
124
+ export async function loadDNAWithDiagnostics(filePath) {
125
+ const diagnostics = [];
126
+ try {
127
+ const dna = await parseDNAFile(filePath);
128
+ const result = validateDNA(dna);
129
+ for (const warning of result.warnings ?? []) {
130
+ diagnostics.push({
131
+ severity: "warning",
132
+ code: "schema_warning",
133
+ message: warning.message,
134
+ file: filePath,
135
+ path: warning.path,
136
+ });
137
+ }
138
+ for (const error of result.errors) {
139
+ diagnostics.push({
140
+ severity: "error",
141
+ code: "schema_error",
142
+ message: error.message,
143
+ file: filePath,
144
+ path: error.path,
145
+ });
146
+ }
147
+ return { dna: result.valid ? dna : undefined, diagnostics };
148
+ }
149
+ catch (error) {
150
+ diagnostics.push({
151
+ severity: "error",
152
+ code: "parse_error",
153
+ message: error instanceof Error ? error.message : String(error),
154
+ file: filePath,
155
+ });
156
+ return { diagnostics };
157
+ }
158
+ }
159
+ export async function resolveDNAInputs(files, options) {
160
+ const cwd = options?.cwd ?? process.cwd();
161
+ const inputFiles = files.length > 0 ? files : await detectDNAConfigs(cwd);
162
+ const expandedFiles = await expandDNAInputFiles(inputFiles, { cwd });
163
+ const diagnostics = await checkNamespaceCollisions(expandedFiles);
164
+ const dnas = [];
165
+ for (const file of expandedFiles) {
166
+ const result = await loadDNAWithDiagnostics(file);
167
+ diagnostics.push(...result.diagnostics);
168
+ if (result.dna)
169
+ dnas.push(result.dna);
170
+ }
171
+ if (hasDiagnosticErrors(diagnostics)) {
172
+ throw new DNADiagnosticsError("DNA input resolution failed", diagnostics);
173
+ }
174
+ return { files: expandedFiles, dnas, diagnostics };
175
+ }
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Fail-open: all errors → { continue: true, suppressOutput: true }
16
16
  */
17
- import type { ArtifactFact, ConstraintIR, VerifierSpec } from "../schema/types.js";
17
+ import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schema/types.js";
18
18
  import type { HookEvent, HookOutput } from "./protocol.js";
19
19
  import { blockOutput } from "./protocol.js";
20
20
  import { readWorkflowState } from "./state.js";
@@ -39,6 +39,15 @@ export declare function computeSummary(traces: Array<{
39
39
  reason?: string;
40
40
  }>): SessionSummary;
41
41
  export declare function formatSummary(s: SessionSummary): string | null;
42
+ export interface RunHookEventOptions {
43
+ event: HookEvent;
44
+ ir: ConstraintIR;
45
+ rawInput: Record<string, unknown>;
46
+ projectDir: string;
47
+ sessionId?: string;
48
+ roles?: Record<string, RoleDef>;
49
+ }
50
+ export declare function runHookEvent(options: RunHookEventOptions): Promise<HookOutput>;
42
51
  declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
43
52
  workflow: string;
44
53
  current_step: string;
package/dist/hooks/cli.js CHANGED
@@ -15,20 +15,20 @@
15
15
  * Fail-open: all errors → { continue: true, suppressOutput: true }
16
16
  */
17
17
  import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
18
+ import { realpathSync } from "node:fs";
18
19
  import { spawn } from "node:child_process";
19
20
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
20
21
  import { randomUUID } from "node:crypto";
22
+ import { fileURLToPath } from "node:url";
21
23
  import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
22
24
  import { validateHookInput } from "./schema.js";
23
25
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
24
26
  import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
27
+ import { hookEventsForSurface } from "./event-registry.js";
25
28
  import { writeAuditEvent } from "../audit/index.js";
26
29
  // ── Constants ──────────────────────────────────────────────
27
30
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
28
- const VALID_EVENTS = new Set([
29
- "PreToolUse", "PostToolUse", "UserPromptSubmit",
30
- "SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart",
31
- ]);
31
+ const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
32
32
  export function computeSummary(traces) {
33
33
  let blocks = 0, warns = 0;
34
34
  const toolCounts = new Map();
@@ -103,14 +103,18 @@ async function main() {
103
103
  writeOutput(silentOutput());
104
104
  return;
105
105
  }
106
+ const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
107
+ writeOutput(output);
108
+ }
109
+ export async function runHookEvent(options) {
110
+ const { event, ir, rawInput, projectDir, sessionId, roles } = options;
106
111
  const state = {};
107
112
  let wfStateRaw = null;
108
113
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
109
114
  if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
110
115
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
111
116
  if ("output" in workflowState) {
112
- writeOutput(workflowState.output);
113
- return;
117
+ return workflowState.output;
114
118
  }
115
119
  wfStateRaw = workflowState.state;
116
120
  if (wfStateRaw && wfStateRaw.active) {
@@ -123,9 +127,8 @@ async function main() {
123
127
  };
124
128
  const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
125
129
  if ("output" in artifactFacts) {
126
- writeOutput(artifactFacts.output);
127
130
  appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
128
- return;
131
+ return artifactFacts.output;
129
132
  }
130
133
  state.artifactFacts = artifactFacts.facts;
131
134
  }
@@ -141,7 +144,6 @@ async function main() {
141
144
  if (event === "PreToolUse") {
142
145
  const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
143
146
  if (gateResult) {
144
- writeOutput(gateResult.output);
145
147
  appendTrace(projectDir, {
146
148
  trace_id: randomUUID(),
147
149
  event,
@@ -154,24 +156,22 @@ async function main() {
154
156
  duration_ms: 0,
155
157
  timestamp: new Date().toISOString(),
156
158
  }, sessionId).catch(() => { });
157
- return;
159
+ return gateResult.output;
158
160
  }
159
161
  }
160
162
  // Special handling for Stop — needs async workflow state read + session summary
161
163
  if (event === "Stop") {
162
164
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
163
165
  if ("output" in workflowState) {
164
- writeOutput(workflowState.output);
165
- return;
166
+ return workflowState.output;
166
167
  }
167
168
  const wfState = workflowState.state;
168
169
  let stopArtifactFacts = [];
169
170
  if (wfState?.active) {
170
171
  const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
171
172
  if ("output" in artifactFacts) {
172
- writeOutput(artifactFacts.output);
173
173
  appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
174
- return;
174
+ return artifactFacts.output;
175
175
  }
176
176
  stopArtifactFacts = artifactFacts.facts;
177
177
  }
@@ -219,7 +219,6 @@ async function main() {
219
219
  }
220
220
  }
221
221
  catch { /* fail-open */ }
222
- writeOutput(stopOutput);
223
222
  // Trace for Stop
224
223
  appendTrace(projectDir, {
225
224
  trace_id: randomUUID(),
@@ -230,11 +229,11 @@ async function main() {
230
229
  duration_ms: 0,
231
230
  timestamp: new Date().toISOString(),
232
231
  }, sessionId).catch(() => { });
233
- return;
232
+ return stopOutput;
234
233
  }
235
234
  // Dispatch to enforcement engine with timing
236
235
  const start = Date.now();
237
- let result = dispatch(event, ir, rawInput, state);
236
+ let result = dispatch(event, ir, rawInput, state, roles);
238
237
  let output = result?.output ?? silentOutput();
239
238
  const durationMs = Date.now() - start;
240
239
  // PostToolUse side effects: session read tracking + surgeon reflection gate.
@@ -306,7 +305,6 @@ async function main() {
306
305
  }
307
306
  catch { /* fail-open: pattern detection never blocks */ }
308
307
  }
309
- writeOutput(output);
310
308
  // Trace logging (async, fail-open)
311
309
  const decision = output.continue === false ? "block"
312
310
  : output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
@@ -341,9 +339,10 @@ async function main() {
341
339
  session_id: sessionId,
342
340
  }).catch(() => { }); // Fail-open
343
341
  }
342
+ return output;
344
343
  }
345
344
  // ── Dispatch ───────────────────────────────────────────────
346
- function dispatch(event, ir, input, state) {
345
+ function dispatch(event, ir, input, state, roles) {
347
346
  switch (event) {
348
347
  case "PreToolUse":
349
348
  return enforcePreToolUse(ir, {
@@ -353,7 +352,7 @@ function dispatch(event, ir, input, state) {
353
352
  cwd: typeof input.cwd === "string" ? input.cwd : undefined,
354
353
  sessionId: typeof input.session_id === "string" ? input.session_id
355
354
  : typeof input.sessionId === "string" ? input.sessionId : undefined,
356
- }, state);
355
+ }, state, roles);
357
356
  case "PostToolUse":
358
357
  return enforcePostToolUse(ir, {
359
358
  tool_name: String(input.tool_name ?? ""),
@@ -576,7 +575,7 @@ async function finalizeAndResolveArtifacts(projectDir, ir, wfState, sessionId) {
576
575
  }
577
576
  function artifactResolverErrorOutput(event, error) {
578
577
  const detail = error instanceof Error ? error.message : String(error);
579
- return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}`);
578
+ return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}. Fix the workflow/session identifiers in DNA state, then rerun the hook or \`dna sync\`.`);
580
579
  }
581
580
  async function readWorkflowStateForHook(projectDir, event, sessionId) {
582
581
  try {
@@ -691,7 +690,8 @@ export function appendStopVerifierWarnings(output, verifierResults, currentStep)
691
690
  if (warningVerifierFailures.length === 0)
692
691
  return output;
693
692
  const warningText = `[Intent DNA] Verifier warnings at step '${currentStep ?? "unknown"}':\n` +
694
- warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n");
693
+ warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n") +
694
+ "\nFix the warning above before relying on this step, or rerun the verifier after repair.";
695
695
  return appendOutputText(output, warningText, "Stop");
696
696
  }
697
697
  function trimEvidence(raw) {
@@ -1523,7 +1523,20 @@ function toKebabCase(s) {
1523
1523
  .toLowerCase();
1524
1524
  }
1525
1525
  // ── Entry Point ────────────────────────────────────────────
1526
- main().catch(() => {
1527
- // Fail-open: never block Claude Code on unexpected errors
1528
- writeOutput(silentOutput());
1529
- });
1526
+ function isDirectEntryPoint() {
1527
+ const entry = process.argv[1];
1528
+ if (!entry)
1529
+ return false;
1530
+ try {
1531
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
1532
+ }
1533
+ catch {
1534
+ return resolve(entry) === resolve(fileURLToPath(import.meta.url));
1535
+ }
1536
+ }
1537
+ if (isDirectEntryPoint()) {
1538
+ main().catch(() => {
1539
+ // Fail-open: never block Claude Code on unexpected errors
1540
+ writeOutput(silentOutput());
1541
+ });
1542
+ }
@@ -0,0 +1,17 @@
1
+ export declare const HOOK_EVENT_NAMES: readonly ["PreToolUse", "PostToolUse", "UserPromptSubmit", "SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart"];
2
+ export type HookEvent = typeof HOOK_EVENT_NAMES[number];
3
+ export declare const SETTINGS_HOOK_EVENT_KEYS: readonly ["preToolUse", "postToolUse", "userPromptSubmit", "subagentStop", "preCompact", "notification", "stop"];
4
+ export type SettingsHookEventKey = typeof SETTINGS_HOOK_EVENT_KEYS[number];
5
+ export type HookSurface = "protocol" | "cli" | "settings" | "manifest" | "mcp";
6
+ export interface HookEventMetadata {
7
+ event: HookEvent;
8
+ settingsKey?: SettingsHookEventKey;
9
+ manifestTimeout: number;
10
+ surfaces: Record<HookSurface, boolean>;
11
+ }
12
+ export declare const HOOK_EVENT_REGISTRY: readonly HookEventMetadata[];
13
+ export declare function getHookEventMetadata(event: unknown): HookEventMetadata | undefined;
14
+ export declare function getHookEventBySettingsKey(key: unknown): HookEventMetadata | undefined;
15
+ export declare function hookEventsForSurface(surface: HookSurface): HookEvent[];
16
+ export declare function settingsHookEventKeys(): SettingsHookEventKey[];
17
+ export declare function isHookEventSupportedBySurface(event: unknown, surface: HookSurface): event is HookEvent;
@@ -0,0 +1,89 @@
1
+ export const HOOK_EVENT_NAMES = [
2
+ "PreToolUse",
3
+ "PostToolUse",
4
+ "UserPromptSubmit",
5
+ "SubagentStop",
6
+ "PreCompact",
7
+ "Notification",
8
+ "Stop",
9
+ "SessionStart",
10
+ ];
11
+ export const SETTINGS_HOOK_EVENT_KEYS = [
12
+ "preToolUse",
13
+ "postToolUse",
14
+ "userPromptSubmit",
15
+ "subagentStop",
16
+ "preCompact",
17
+ "notification",
18
+ "stop",
19
+ ];
20
+ export const HOOK_EVENT_REGISTRY = [
21
+ {
22
+ event: "PreToolUse",
23
+ settingsKey: "preToolUse",
24
+ manifestTimeout: 5,
25
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
26
+ },
27
+ {
28
+ event: "PostToolUse",
29
+ settingsKey: "postToolUse",
30
+ manifestTimeout: 3,
31
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
32
+ },
33
+ {
34
+ event: "UserPromptSubmit",
35
+ settingsKey: "userPromptSubmit",
36
+ manifestTimeout: 5,
37
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
38
+ },
39
+ {
40
+ event: "SubagentStop",
41
+ settingsKey: "subagentStop",
42
+ manifestTimeout: 3,
43
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
44
+ },
45
+ {
46
+ event: "PreCompact",
47
+ settingsKey: "preCompact",
48
+ manifestTimeout: 3,
49
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
50
+ },
51
+ {
52
+ event: "Notification",
53
+ settingsKey: "notification",
54
+ manifestTimeout: 3,
55
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
56
+ },
57
+ {
58
+ event: "Stop",
59
+ settingsKey: "stop",
60
+ manifestTimeout: 45,
61
+ surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: false },
62
+ },
63
+ {
64
+ event: "SessionStart",
65
+ manifestTimeout: 5,
66
+ surfaces: { protocol: true, cli: true, settings: false, manifest: true, mcp: true },
67
+ },
68
+ ];
69
+ const EVENT_BY_NAME = new Map(HOOK_EVENT_REGISTRY.map((entry) => [entry.event, entry]));
70
+ const EVENT_BY_SETTINGS_KEY = new Map(HOOK_EVENT_REGISTRY.flatMap((entry) => entry.settingsKey ? [[entry.settingsKey, entry]] : []));
71
+ export function getHookEventMetadata(event) {
72
+ return typeof event === "string" ? EVENT_BY_NAME.get(event) : undefined;
73
+ }
74
+ export function getHookEventBySettingsKey(key) {
75
+ return typeof key === "string" ? EVENT_BY_SETTINGS_KEY.get(key) : undefined;
76
+ }
77
+ export function hookEventsForSurface(surface) {
78
+ return HOOK_EVENT_REGISTRY
79
+ .filter((entry) => entry.surfaces[surface])
80
+ .map((entry) => entry.event);
81
+ }
82
+ export function settingsHookEventKeys() {
83
+ return HOOK_EVENT_REGISTRY
84
+ .filter((entry) => entry.surfaces.settings && entry.settingsKey)
85
+ .map((entry) => entry.settingsKey);
86
+ }
87
+ export function isHookEventSupportedBySurface(event, surface) {
88
+ return getHookEventMetadata(event)?.surfaces[surface] === true;
89
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Protocol reference: docs/references/cc-source-analysis.md §2
8
8
  */
9
- export type HookEvent = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "SubagentStop" | "PreCompact" | "Notification" | "Stop" | "SessionStart";
9
+ export type { HookEvent } from "./event-registry.js";
10
10
  /** Base fields present in all hook inputs */
11
11
  export interface HookInputBase {
12
12
  cwd?: string;
@@ -3,6 +3,7 @@
3
3
  * Zero external dependencies. Hand-written validators.
4
4
  * Fail-open: invalid input → log warning + return { valid: false, ... }
5
5
  */
6
+ import { isHookEventSupportedBySurface } from "./event-registry.js";
6
7
  /**
7
8
  * Validate hook input against expected schema for each event type.
8
9
  * Returns normalized input (camelCase) on success.
@@ -20,6 +21,9 @@ export function validateHookInput(event, raw) {
20
21
  normalized.sessionId = sessionId;
21
22
  delete normalized.session_id;
22
23
  }
24
+ if (!isHookEventSupportedBySurface(event, "protocol")) {
25
+ return { valid: false, errors: [`Unknown event type: ${event}`] };
26
+ }
23
27
  switch (event) {
24
28
  case "PreToolUse":
25
29
  if (typeof input.tool_name !== "string") {
@@ -58,7 +62,7 @@ export function validateHookInput(event, raw) {
58
62
  // Minimal validation — these events have no required fields beyond base
59
63
  break;
60
64
  default:
61
- errors.push(`Unknown event type: ${event}`);
65
+ break;
62
66
  }
63
67
  return {
64
68
  valid: errors.length === 0,
@@ -136,6 +136,7 @@ export declare function writeSessionReads(projectDir: string, state: SessionRead
136
136
  */
137
137
  export declare function appendSessionRead(projectDir: string, filePath: string, sessionId?: string): Promise<void>;
138
138
  export interface VerifierResultEntry {
139
+ result_id?: string;
139
140
  verifier_id: string;
140
141
  when: VerifierWhen;
141
142
  severity: VerifierSeverity;
@@ -151,6 +152,7 @@ export interface VerifierResultEntry {
151
152
  message?: string;
152
153
  timestamp: string;
153
154
  }
155
+ export declare function verifierResultId(result: Omit<VerifierResultEntry, "result_id"> | VerifierResultEntry): string;
154
156
  export declare function readVerifierResults(projectDir: string, sessionId?: string): Promise<VerifierResultEntry[]>;
155
157
  export declare function writeVerifierResults(projectDir: string, results: VerifierResultEntry[], sessionId?: string): Promise<void>;
156
158
  export declare function appendVerifierResult(projectDir: string, result: VerifierResultEntry, sessionId?: string): Promise<void>;