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
@@ -0,0 +1,231 @@
1
+ export const RUNTIME_DECISION_EVENT_SCHEMA_VERSION = "intentdna.runtime_decision_event.v1.7.1";
2
+ export const CASCADE_LAYER_NAMES = [
3
+ "species",
4
+ "enterprise",
5
+ "project",
6
+ "personal",
7
+ "role",
8
+ "context",
9
+ "task",
10
+ ];
11
+ const RUNTIME_DECISIONS = new Set(["allow", "warn", "block", "escalate", "validate"]);
12
+ const ENFORCEMENT_POINTS = new Set([
13
+ "hook",
14
+ "sdk",
15
+ "ci",
16
+ "plugin_runtime",
17
+ "mcp_external_write",
18
+ "unknown",
19
+ "unsupported",
20
+ ]);
21
+ const CASCADE_LAYER_VALUES = new Set([...CASCADE_LAYER_NAMES, "unknown", "unsupported"]);
22
+ const CODON_TYPES = new Set(["attract", "repel", "threshold", "weight", "sense"]);
23
+ const EVIDENCE_REF_TYPES = new Set([
24
+ "trace",
25
+ "audit",
26
+ "artifact",
27
+ "verifier",
28
+ "handoff",
29
+ "legacy",
30
+ "unknown",
31
+ "unsupported",
32
+ ]);
33
+ const REQUIRED_STRING_FIELDS = [
34
+ "schema_version",
35
+ "event_id",
36
+ "timestamp",
37
+ "org_id",
38
+ "team_id",
39
+ "user_id",
40
+ "project_id",
41
+ "agent_id",
42
+ "agent_role",
43
+ "agent_type",
44
+ "session_agent",
45
+ "harness_adapter",
46
+ "runtime",
47
+ "policy_bundle_id",
48
+ "policy_bundle_version",
49
+ "compiled_ir_hash",
50
+ "decision_reason",
51
+ "enforcement_point",
52
+ "session_id",
53
+ "run_id",
54
+ "step_id",
55
+ "tool_name",
56
+ "action_kind",
57
+ "resource_ref",
58
+ ];
59
+ const REQUIRED_WINNING_CONSTRAINT_FIELDS = [
60
+ "constraint_id",
61
+ "source_layer",
62
+ "source_dna_id",
63
+ "gene_id",
64
+ "codon_type",
65
+ "provenance_reason",
66
+ ];
67
+ const REQUIRED_HARNESS_CONTEXT_FIELDS = [
68
+ "agent_id",
69
+ "agent_role",
70
+ "agent_type",
71
+ "session_agent",
72
+ "harness_adapter",
73
+ "runtime",
74
+ ];
75
+ const ENTERPRISE_EVIDENCE_FIELDS = [
76
+ ...REQUIRED_STRING_FIELDS,
77
+ "decision",
78
+ ];
79
+ const ENTERPRISE_WINNING_CONSTRAINT_FIELDS = [
80
+ ...REQUIRED_WINNING_CONSTRAINT_FIELDS,
81
+ "action",
82
+ ];
83
+ function isRecord(value) {
84
+ return typeof value === "object" && value !== null && !Array.isArray(value);
85
+ }
86
+ function hasString(record, field) {
87
+ return typeof record[field] === "string" && record[field].trim().length > 0;
88
+ }
89
+ function isPlaceholder(value) {
90
+ return value === "unknown" || value === "unsupported";
91
+ }
92
+ function validateStringFields(record, fields, prefix, errors) {
93
+ for (const field of fields) {
94
+ if (!hasString(record, field))
95
+ errors.push(`${prefix}${field} must be a non-empty string`);
96
+ }
97
+ }
98
+ function validateCascadeLayers(value, errors) {
99
+ if (!isRecord(value)) {
100
+ errors.push("cascade_layers must be an object with all cascade layer keys");
101
+ return;
102
+ }
103
+ for (const layer of CASCADE_LAYER_NAMES) {
104
+ const entry = value[layer];
105
+ if (!isRecord(entry)) {
106
+ errors.push(`cascade_layers.${layer} must be an object`);
107
+ continue;
108
+ }
109
+ if (entry.layer !== layer)
110
+ errors.push(`cascade_layers.${layer}.layer must be ${layer}`);
111
+ validateStringFields(entry, ["source_id", "source_ref", "fingerprint", "status"], `cascade_layers.${layer}.`, errors);
112
+ if (hasString(entry, "status") && entry.status !== "active" && entry.status !== "unknown" && entry.status !== "unsupported") {
113
+ errors.push(`cascade_layers.${layer}.status must be active, unknown, or unsupported`);
114
+ }
115
+ }
116
+ }
117
+ function validateWinningConstraint(value, event, errors) {
118
+ if (!isRecord(value)) {
119
+ errors.push("winning_constraint must be an object");
120
+ return;
121
+ }
122
+ validateStringFields(value, REQUIRED_WINNING_CONSTRAINT_FIELDS, "winning_constraint.", errors);
123
+ if (hasString(value, "source_layer") && !CASCADE_LAYER_VALUES.has(value.source_layer)) {
124
+ errors.push("winning_constraint.source_layer must be a known cascade layer, unknown, or unsupported");
125
+ }
126
+ if (hasString(value, "codon_type") && !CODON_TYPES.has(value.codon_type)) {
127
+ errors.push("winning_constraint.codon_type must be a known codon type");
128
+ }
129
+ if (!RUNTIME_DECISIONS.has(value.action)) {
130
+ errors.push("winning_constraint.action must be allow, warn, block, escalate, or validate");
131
+ }
132
+ if (hasString(event, "decision") && value.action !== event.decision) {
133
+ errors.push("winning_constraint.action must match top-level decision");
134
+ }
135
+ }
136
+ function validateHarnessRuntimeContext(value, event, errors) {
137
+ if (!isRecord(value)) {
138
+ errors.push("harness_runtime_context must be an object");
139
+ return;
140
+ }
141
+ validateStringFields(value, REQUIRED_HARNESS_CONTEXT_FIELDS, "harness_runtime_context.", errors);
142
+ for (const field of REQUIRED_HARNESS_CONTEXT_FIELDS) {
143
+ if (hasString(event, field) && hasString(value, field) && event[field] !== value[field]) {
144
+ errors.push(`harness_runtime_context.${field} must match top-level ${field}`);
145
+ }
146
+ }
147
+ }
148
+ function validateEvidenceRefs(value, errors) {
149
+ if (!Array.isArray(value)) {
150
+ errors.push("evidence_refs must be an array");
151
+ return;
152
+ }
153
+ if (value.length === 0) {
154
+ errors.push("evidence_refs must contain at least one evidence reference");
155
+ return;
156
+ }
157
+ value.forEach((ref, index) => {
158
+ if (!isRecord(ref)) {
159
+ errors.push(`evidence_refs.${index} must be an object`);
160
+ return;
161
+ }
162
+ validateStringFields(ref, ["type", "ref"], `evidence_refs.${index}.`, errors);
163
+ if (hasString(ref, "type") && !EVIDENCE_REF_TYPES.has(ref.type)) {
164
+ errors.push(`evidence_refs.${index}.type must be a known evidence reference type`);
165
+ }
166
+ });
167
+ }
168
+ function hasCompleteEnterpriseProvenance(event) {
169
+ if (!ENTERPRISE_EVIDENCE_FIELDS.every((field) => hasString(event, field) && !isPlaceholder(event[field])))
170
+ return false;
171
+ if (!isRecord(event.harness_runtime_context))
172
+ return false;
173
+ if (!REQUIRED_HARNESS_CONTEXT_FIELDS.every((field) => hasString(event.harness_runtime_context, field) && !isPlaceholder(event.harness_runtime_context[field])))
174
+ return false;
175
+ if (!isRecord(event.winning_constraint))
176
+ return false;
177
+ if (!ENTERPRISE_WINNING_CONSTRAINT_FIELDS.every((field) => hasString(event.winning_constraint, field) && !isPlaceholder(event.winning_constraint[field])))
178
+ return false;
179
+ if (!isRecord(event.cascade_layers))
180
+ return false;
181
+ for (const layer of CASCADE_LAYER_NAMES) {
182
+ const entry = event.cascade_layers[layer];
183
+ if (!isRecord(entry))
184
+ return false;
185
+ if (entry.status !== "active")
186
+ return false;
187
+ if (["source_id", "source_ref", "fingerprint"].some((field) => !hasString(entry, field) || isPlaceholder(entry[field])))
188
+ return false;
189
+ }
190
+ if (!Array.isArray(event.evidence_refs) || event.evidence_refs.length === 0)
191
+ return false;
192
+ for (const ref of event.evidence_refs) {
193
+ if (!isRecord(ref))
194
+ return false;
195
+ if (!hasString(ref, "type") || isPlaceholder(ref.type) || ref.type === "legacy")
196
+ return false;
197
+ if (!hasString(ref, "ref") || isPlaceholder(ref.ref))
198
+ return false;
199
+ }
200
+ return true;
201
+ }
202
+ export function validateRuntimeDecisionEvent(event) {
203
+ const errors = [];
204
+ if (!isRecord(event)) {
205
+ return {
206
+ valid: false,
207
+ classification: "legacy_diagnostic",
208
+ errors: ["RuntimeDecisionEvent must be an object"],
209
+ };
210
+ }
211
+ validateStringFields(event, REQUIRED_STRING_FIELDS, "", errors);
212
+ if (event.schema_version !== RUNTIME_DECISION_EVENT_SCHEMA_VERSION) {
213
+ errors.push(`schema_version must be ${RUNTIME_DECISION_EVENT_SCHEMA_VERSION}`);
214
+ }
215
+ if (!RUNTIME_DECISIONS.has(event.decision)) {
216
+ errors.push("decision must be allow, warn, block, escalate, or validate");
217
+ }
218
+ if (!ENFORCEMENT_POINTS.has(event.enforcement_point)) {
219
+ errors.push("enforcement_point must identify hook, sdk, ci, plugin runtime, MCP external write, unknown, or unsupported");
220
+ }
221
+ validateCascadeLayers(event.cascade_layers, errors);
222
+ validateWinningConstraint(event.winning_constraint, event, errors);
223
+ validateHarnessRuntimeContext(event.harness_runtime_context, event, errors);
224
+ validateEvidenceRefs(event.evidence_refs, errors);
225
+ const valid = errors.length === 0;
226
+ return {
227
+ valid,
228
+ classification: valid && hasCompleteEnterpriseProvenance(event) ? "enterprise_evidence" : "legacy_diagnostic",
229
+ errors,
230
+ };
231
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Intent DNA — Remote Governance Types
3
3
  *
4
- * Architecture reservation for enterprise governance communication.
5
- * Phase 7 implementation current phase only defines interfaces.
4
+ * Architecture reservation for future remote enterprise governance communication.
5
+ * Current Enterprise Safety Slice uses local/shared policy bundles and RuntimeDecisionEvent evidence.
6
6
  *
7
- * Capabilities:
7
+ * Reserved capabilities:
8
8
  * - Audit data reporting to central server
9
9
  * - Enterprise DNA policy pull
10
10
  * - Policy update push notifications
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Intent DNA — Remote Governance Types
3
3
  *
4
- * Architecture reservation for enterprise governance communication.
5
- * Phase 7 implementation current phase only defines interfaces.
4
+ * Architecture reservation for future remote enterprise governance communication.
5
+ * Current Enterprise Safety Slice uses local/shared policy bundles and RuntimeDecisionEvent evidence.
6
6
  *
7
- * Capabilities:
7
+ * Reserved capabilities:
8
8
  * - Audit data reporting to central server
9
9
  * - Enterprise DNA policy pull
10
10
  * - Policy update push notifications
@@ -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,18 +15,141 @@
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
- import { randomUUID } from "node:crypto";
21
+ import { createHash, 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
- import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
26
+ import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
25
27
  import { hookEventsForSurface } from "./event-registry.js";
26
28
  import { writeAuditEvent } from "../audit/index.js";
29
+ import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION } from "../governance/index.js";
27
30
  // ── Constants ──────────────────────────────────────────────
28
31
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
29
32
  const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
33
+ // ── Runtime Decision Evidence ────────────────────────────
34
+ function normalizeDecision(output) {
35
+ if (output.continue === false)
36
+ return "block";
37
+ if (output.hookSpecificOutput?.permissionDecision === "ask")
38
+ return "escalate";
39
+ if (output.hookSpecificOutput?.additionalContext?.startsWith("WARN"))
40
+ return "warn";
41
+ return "allow";
42
+ }
43
+ function hashedRef(value) {
44
+ const raw = typeof value === "string" && value.length > 0 ? value : "unknown";
45
+ return `sha256:${createHash("sha256").update(raw, "utf-8").digest("hex")}`;
46
+ }
47
+ function resourceRef(rawInput) {
48
+ const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
49
+ ? rawInput.tool_input
50
+ : {};
51
+ return hashedRef(toolInput.file_path ?? toolInput.notebook_path ?? toolInput.command ?? rawInput.message ?? rawInput.prompt ?? "unknown");
52
+ }
53
+ function actionKind(rawInput) {
54
+ const toolName = typeof rawInput.tool_name === "string" ? rawInput.tool_name : "unknown";
55
+ if (["Edit", "Write", "NotebookEdit"].includes(toolName))
56
+ return "file_write";
57
+ if (toolName === "Bash")
58
+ return "shell_command";
59
+ return toolName === "unknown" ? "unknown" : "tool_use";
60
+ }
61
+ function cascadeLayers(ir) {
62
+ const layers = ir.provenance?.cascade_layers;
63
+ const result = {};
64
+ for (const layer of ["species", "enterprise", "project", "personal", "role", "context", "task"]) {
65
+ const entry = layers?.[layer];
66
+ result[layer] = {
67
+ layer,
68
+ source_id: entry?.source_id ?? "unknown",
69
+ source_ref: entry?.source_ref ?? "unknown",
70
+ fingerprint: entry?.fingerprint ?? "unknown",
71
+ status: entry?.status ?? "unknown",
72
+ };
73
+ }
74
+ return result;
75
+ }
76
+ function fallbackConstraint(decision, reason, ir) {
77
+ return {
78
+ constraint_id: decision === "allow" ? "allow:no_blocking_constraint" : "unknown",
79
+ source_layer: decision === "allow" && ir?.provenance?.policy_bundle_id ? "enterprise" : "unknown",
80
+ source_dna_id: decision === "allow" ? ir?.provenance?.policy_bundle_id ?? "unknown" : "unknown",
81
+ gene_id: decision === "allow" ? "no_blocking_constraint" : "unknown",
82
+ codon_type: "threshold",
83
+ action: decision,
84
+ provenance_reason: reason || "unknown",
85
+ };
86
+ }
87
+ function candidateConstraintMatches(candidate, decision, result) {
88
+ if (candidate.action !== decision)
89
+ return false;
90
+ const geneId = result?.trace?.gene_id;
91
+ return !geneId || candidate.gene_id === geneId;
92
+ }
93
+ function winningConstraint(ir, decision, reason, result) {
94
+ const candidates = ir.provenance?.constraint_winners ?? [];
95
+ if (decision === "allow")
96
+ return fallbackConstraint(decision, reason, ir);
97
+ const matched = candidates.find((candidate) => candidateConstraintMatches(candidate, decision, result))
98
+ ?? candidates.find((candidate) => candidate.action === decision);
99
+ if (!matched)
100
+ return fallbackConstraint(decision, reason, ir);
101
+ return { ...matched, action: decision };
102
+ }
103
+ function identityField(rawInput, inputKey, envKey) {
104
+ const value = rawInput[inputKey];
105
+ if (typeof value === "string" && value.trim().length > 0)
106
+ return value;
107
+ return process.env[envKey] ?? "unknown";
108
+ }
109
+ function buildRuntimeDecisionEvent(params) {
110
+ const agentType = typeof params.rawInput.agent_type === "string" ? params.rawInput.agent_type : "unknown";
111
+ const derivedRole = agentType.startsWith("dna-") ? agentType.slice("dna-".length) : agentType;
112
+ const agentRole = params.wfState?.current_role ?? params.result?.trace?.role ?? (derivedRole === "unknown" ? "unknown" : derivedRole);
113
+ const sessionId = params.sessionId ?? "unknown";
114
+ return {
115
+ schema_version: RUNTIME_DECISION_EVENT_SCHEMA_VERSION,
116
+ event_id: randomUUID(),
117
+ timestamp: new Date().toISOString(),
118
+ org_id: identityField(params.rawInput, "org_id", "INTENTDNA_ORG_ID"),
119
+ team_id: identityField(params.rawInput, "team_id", "INTENTDNA_TEAM_ID"),
120
+ user_id: identityField(params.rawInput, "user_id", "INTENTDNA_USER_ID"),
121
+ project_id: params.projectDir,
122
+ agent_id: agentType,
123
+ agent_role: agentRole,
124
+ agent_type: agentType,
125
+ session_agent: sessionId,
126
+ harness_adapter: "claude-code",
127
+ runtime: "hook",
128
+ policy_bundle_id: params.ir.provenance?.policy_bundle_id ?? "unknown",
129
+ policy_bundle_version: params.ir.provenance?.policy_bundle_version ?? "unknown",
130
+ cascade_layers: cascadeLayers(params.ir),
131
+ winning_constraint: winningConstraint(params.ir, params.decision, params.reason, params.result),
132
+ compiled_ir_hash: params.ir.compiled_ir_hash ?? params.ir.provenance?.compiled_ir_hash ?? "unknown",
133
+ decision: params.decision,
134
+ decision_reason: params.reason || "unknown",
135
+ enforcement_point: "hook",
136
+ harness_runtime_context: {
137
+ agent_id: agentType,
138
+ agent_role: agentRole,
139
+ agent_type: agentType,
140
+ session_agent: sessionId,
141
+ harness_adapter: "claude-code",
142
+ runtime: "hook",
143
+ },
144
+ session_id: sessionId,
145
+ run_id: params.wfState?.workflow ?? sessionId,
146
+ step_id: params.wfState?.current_step ?? params.result?.trace?.step_id ?? `${params.event}:direct`,
147
+ tool_name: typeof params.rawInput.tool_name === "string" ? params.rawInput.tool_name : "unknown",
148
+ action_kind: actionKind(params.rawInput),
149
+ resource_ref: resourceRef(params.rawInput),
150
+ evidence_refs: [{ type: "trace", ref: params.traceId }],
151
+ };
152
+ }
30
153
  export function computeSummary(traces) {
31
154
  let blocks = 0, warns = 0;
32
155
  const toolCounts = new Map();
@@ -101,14 +224,36 @@ async function main() {
101
224
  writeOutput(silentOutput());
102
225
  return;
103
226
  }
227
+ const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
228
+ writeOutput(output);
229
+ }
230
+ export async function runHookEvent(options) {
231
+ const { event, ir, rawInput, projectDir, sessionId, roles } = options;
104
232
  const state = {};
233
+ const writeRuntimeDecision = async (output, result, wfState, traceId = randomUUID()) => {
234
+ const decision = normalizeDecision(output);
235
+ const reason = decision !== "allow" ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown") : "allowed by active policy";
236
+ await appendRuntimeDecisionEvent(projectDir, buildRuntimeDecisionEvent({
237
+ ir,
238
+ rawInput,
239
+ output,
240
+ result,
241
+ event,
242
+ projectDir,
243
+ sessionId,
244
+ traceId,
245
+ decision,
246
+ reason,
247
+ wfState,
248
+ }), sessionId);
249
+ };
105
250
  let wfStateRaw = null;
106
251
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
107
252
  if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
108
253
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
109
254
  if ("output" in workflowState) {
110
- writeOutput(workflowState.output);
111
- return;
255
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
256
+ return workflowState.output;
112
257
  }
113
258
  wfStateRaw = workflowState.state;
114
259
  if (wfStateRaw && wfStateRaw.active) {
@@ -121,9 +266,9 @@ async function main() {
121
266
  };
122
267
  const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
123
268
  if ("output" in artifactFacts) {
124
- writeOutput(artifactFacts.output);
125
269
  appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
126
- return;
270
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
271
+ return artifactFacts.output;
127
272
  }
128
273
  state.artifactFacts = artifactFacts.facts;
129
274
  }
@@ -139,9 +284,9 @@ async function main() {
139
284
  if (event === "PreToolUse") {
140
285
  const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
141
286
  if (gateResult) {
142
- writeOutput(gateResult.output);
287
+ const traceId = randomUUID();
143
288
  appendTrace(projectDir, {
144
- trace_id: randomUUID(),
289
+ trace_id: traceId,
145
290
  event,
146
291
  tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
147
292
  agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
@@ -152,24 +297,25 @@ async function main() {
152
297
  duration_ms: 0,
153
298
  timestamp: new Date().toISOString(),
154
299
  }, sessionId).catch(() => { });
155
- return;
300
+ await writeRuntimeDecision(gateResult.output, { output: gateResult.output, trace: { matched_rule: gateResult.matched_rule, step_id: wfStateRaw?.current_step } }, wfStateRaw, traceId);
301
+ return gateResult.output;
156
302
  }
157
303
  }
158
304
  // Special handling for Stop — needs async workflow state read + session summary
159
305
  if (event === "Stop") {
160
306
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
161
307
  if ("output" in workflowState) {
162
- writeOutput(workflowState.output);
163
- return;
308
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
309
+ return workflowState.output;
164
310
  }
165
311
  const wfState = workflowState.state;
166
312
  let stopArtifactFacts = [];
167
313
  if (wfState?.active) {
168
314
  const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
169
315
  if ("output" in artifactFacts) {
170
- writeOutput(artifactFacts.output);
171
316
  appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
172
- return;
317
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfState.current_step } }, wfState);
318
+ return artifactFacts.output;
173
319
  }
174
320
  stopArtifactFacts = artifactFacts.facts;
175
321
  }
@@ -217,10 +363,10 @@ async function main() {
217
363
  }
218
364
  }
219
365
  catch { /* fail-open */ }
220
- writeOutput(stopOutput);
221
366
  // Trace for Stop
367
+ const traceId = randomUUID();
222
368
  appendTrace(projectDir, {
223
- trace_id: randomUUID(),
369
+ trace_id: traceId,
224
370
  event: "Stop",
225
371
  workflow: wfState?.workflow,
226
372
  step: wfState?.current_step,
@@ -228,11 +374,12 @@ async function main() {
228
374
  duration_ms: 0,
229
375
  timestamp: new Date().toISOString(),
230
376
  }, sessionId).catch(() => { });
231
- return;
377
+ await writeRuntimeDecision(stopOutput, { output: stopOutput, trace: { matched_rule: "validator", step_id: wfState?.current_step } }, wfState, traceId);
378
+ return stopOutput;
232
379
  }
233
380
  // Dispatch to enforcement engine with timing
234
381
  const start = Date.now();
235
- let result = dispatch(event, ir, rawInput, state);
382
+ let result = dispatch(event, ir, rawInput, state, roles);
236
383
  let output = result?.output ?? silentOutput();
237
384
  const durationMs = Date.now() - start;
238
385
  // PostToolUse side effects: session read tracking + surgeon reflection gate.
@@ -304,26 +451,28 @@ async function main() {
304
451
  }
305
452
  catch { /* fail-open: pattern detection never blocks */ }
306
453
  }
307
- writeOutput(output);
308
454
  // Trace logging (async, fail-open)
309
- const decision = output.continue === false ? "block"
310
- : output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
311
- : "allow";
455
+ const decision = normalizeDecision(output);
456
+ const traceDecision = decision === "block" ? "block" : decision === "allow" ? "allow" : "warn";
457
+ const traceId = randomUUID();
458
+ const decisionReason = decision !== "allow"
459
+ ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown")
460
+ : "allowed by active policy";
461
+ const traceTimestamp = new Date().toISOString();
312
462
  appendTrace(projectDir, {
313
- trace_id: randomUUID(),
463
+ trace_id: traceId,
314
464
  event,
315
465
  tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
316
466
  agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
317
467
  workflow: state.workflowState?.workflow,
318
468
  step: state.workflowState?.current_step,
319
- decision: decision,
320
- reason: decision !== "allow"
321
- ? (output.reason ?? output.hookSpecificOutput?.additionalContext)
322
- : undefined,
469
+ decision: traceDecision,
470
+ reason: decision !== "allow" ? decisionReason : undefined,
323
471
  target_path: decision !== "allow" ? targetPath : undefined,
324
472
  duration_ms: durationMs,
325
- timestamp: new Date().toISOString(),
473
+ timestamp: traceTimestamp,
326
474
  }, sessionId).catch(() => { }); // Fail-open
475
+ await writeRuntimeDecision(output, result, state.workflowState, traceId);
327
476
  // Side effect: rotate traces + clean stale state on SessionStart
328
477
  if (event === "SessionStart") {
329
478
  rotateTraces(projectDir).catch(() => { });
@@ -339,9 +488,10 @@ async function main() {
339
488
  session_id: sessionId,
340
489
  }).catch(() => { }); // Fail-open
341
490
  }
491
+ return output;
342
492
  }
343
493
  // ── Dispatch ───────────────────────────────────────────────
344
- function dispatch(event, ir, input, state) {
494
+ function dispatch(event, ir, input, state, roles) {
345
495
  switch (event) {
346
496
  case "PreToolUse":
347
497
  return enforcePreToolUse(ir, {
@@ -351,7 +501,7 @@ function dispatch(event, ir, input, state) {
351
501
  cwd: typeof input.cwd === "string" ? input.cwd : undefined,
352
502
  sessionId: typeof input.session_id === "string" ? input.session_id
353
503
  : typeof input.sessionId === "string" ? input.sessionId : undefined,
354
- }, state);
504
+ }, state, roles);
355
505
  case "PostToolUse":
356
506
  return enforcePostToolUse(ir, {
357
507
  tool_name: String(input.tool_name ?? ""),
@@ -574,7 +724,7 @@ async function finalizeAndResolveArtifacts(projectDir, ir, wfState, sessionId) {
574
724
  }
575
725
  function artifactResolverErrorOutput(event, error) {
576
726
  const detail = error instanceof Error ? error.message : String(error);
577
- return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}`);
727
+ return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}. Fix the workflow/session identifiers in DNA state, then rerun the hook or \`dna sync\`.`);
578
728
  }
579
729
  async function readWorkflowStateForHook(projectDir, event, sessionId) {
580
730
  try {
@@ -689,7 +839,8 @@ export function appendStopVerifierWarnings(output, verifierResults, currentStep)
689
839
  if (warningVerifierFailures.length === 0)
690
840
  return output;
691
841
  const warningText = `[Intent DNA] Verifier warnings at step '${currentStep ?? "unknown"}':\n` +
692
- warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n");
842
+ warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n") +
843
+ "\nFix the warning above before relying on this step, or rerun the verifier after repair.";
693
844
  return appendOutputText(output, warningText, "Stop");
694
845
  }
695
846
  function trimEvidence(raw) {
@@ -1521,7 +1672,20 @@ function toKebabCase(s) {
1521
1672
  .toLowerCase();
1522
1673
  }
1523
1674
  // ── Entry Point ────────────────────────────────────────────
1524
- main().catch(() => {
1525
- // Fail-open: never block Claude Code on unexpected errors
1526
- writeOutput(silentOutput());
1527
- });
1675
+ function isDirectEntryPoint() {
1676
+ const entry = process.argv[1];
1677
+ if (!entry)
1678
+ return false;
1679
+ try {
1680
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
1681
+ }
1682
+ catch {
1683
+ return resolve(entry) === resolve(fileURLToPath(import.meta.url));
1684
+ }
1685
+ }
1686
+ if (isDirectEntryPoint()) {
1687
+ main().catch(() => {
1688
+ // Fail-open: never block Claude Code on unexpected errors
1689
+ writeOutput(silentOutput());
1690
+ });
1691
+ }