intentdna 1.7.0 → 1.7.5

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 (48) 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/feedback.d.ts +2 -2
  5. package/dist/cli/commands/feedback.js +5 -5
  6. package/dist/cli/commands/sync.js +8 -5
  7. package/dist/cli/commands/verify.d.ts +32 -0
  8. package/dist/cli/commands/verify.js +173 -6
  9. package/dist/cli/index.js +21 -9
  10. package/dist/compiler/activate.js +11 -6
  11. package/dist/compiler/cascade.d.ts +3 -1
  12. package/dist/compiler/cascade.js +23 -1
  13. package/dist/compiler/compile.js +1 -0
  14. package/dist/compiler/index.d.ts +5 -1
  15. package/dist/compiler/index.js +7 -4
  16. package/dist/compiler/input-resolver.d.ts +8 -1
  17. package/dist/compiler/input-resolver.js +128 -22
  18. package/dist/compiler/provenance.d.ts +8 -0
  19. package/dist/compiler/provenance.js +127 -0
  20. package/dist/governance/index.d.ts +4 -3
  21. package/dist/governance/index.js +3 -4
  22. package/dist/governance/runtime-decision-event.d.ts +85 -0
  23. package/dist/governance/runtime-decision-event.js +231 -0
  24. package/dist/governance/types.d.ts +3 -3
  25. package/dist/governance/types.js +3 -3
  26. package/dist/hooks/cli.d.ts +1 -0
  27. package/dist/hooks/cli.js +181 -20
  28. package/dist/hooks/state.d.ts +3 -10
  29. package/dist/hooks/state.js +147 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +1 -0
  32. package/dist/mcp/tools-observability.js +3 -3
  33. package/dist/mcp/tools-state.d.ts +1 -1
  34. package/dist/mcp/tools-state.js +17 -7
  35. package/dist/report/kernel-report.d.ts +27 -0
  36. package/dist/report/kernel-report.js +60 -19
  37. package/dist/report/kernel-signals.d.ts +5 -2
  38. package/dist/report/kernel-signals.js +84 -2
  39. package/dist/report/report-package.d.ts +9 -1
  40. package/dist/report/report-package.js +32 -27
  41. package/dist/runtime/claude-sdk.d.ts +9 -4
  42. package/dist/runtime/claude-sdk.js +9 -0
  43. package/dist/runtime/plugin-adapter.d.ts +5 -1
  44. package/dist/runtime/plugin-adapter.js +2 -0
  45. package/dist/schema/types.d.ts +51 -0
  46. package/package.json +1 -1
  47. package/spec/README.md +1 -1
  48. 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
@@ -47,6 +47,7 @@ export interface RunHookEventOptions {
47
47
  sessionId?: string;
48
48
  roles?: Record<string, RoleDef>;
49
49
  }
50
+ export declare function normalizeHookOutputForEvent(output: HookOutput, event: HookEvent): HookOutput;
50
51
  export declare function runHookEvent(options: RunHookEventOptions): Promise<HookOutput>;
51
52
  declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
52
53
  workflow: string;
package/dist/hooks/cli.js CHANGED
@@ -18,17 +18,138 @@ import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/prom
18
18
  import { realpathSync } from "node:fs";
19
19
  import { spawn } from "node:child_process";
20
20
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
21
- import { randomUUID } from "node:crypto";
21
+ import { createHash, randomUUID } from "node:crypto";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
24
24
  import { validateHookInput } from "./schema.js";
25
25
  import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
26
- 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";
27
27
  import { hookEventsForSurface } from "./event-registry.js";
28
28
  import { writeAuditEvent } from "../audit/index.js";
29
+ import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION } from "../governance/index.js";
29
30
  // ── Constants ──────────────────────────────────────────────
30
31
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
31
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
+ }
32
153
  export function computeSummary(traces) {
33
154
  let blocks = 0, warns = 0;
34
155
  const toolCounts = new Map();
@@ -106,15 +227,45 @@ async function main() {
106
227
  const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
107
228
  writeOutput(output);
108
229
  }
230
+ export function normalizeHookOutputForEvent(output, event) {
231
+ if (!output.hookSpecificOutput)
232
+ return output;
233
+ return {
234
+ ...output,
235
+ hookSpecificOutput: {
236
+ ...output.hookSpecificOutput,
237
+ hookEventName: output.hookSpecificOutput.hookEventName ?? event,
238
+ },
239
+ };
240
+ }
109
241
  export async function runHookEvent(options) {
110
242
  const { event, ir, rawInput, projectDir, sessionId, roles } = options;
243
+ const finish = (output) => normalizeHookOutputForEvent(output, event);
111
244
  const state = {};
245
+ const writeRuntimeDecision = async (output, result, wfState, traceId = randomUUID()) => {
246
+ const decision = normalizeDecision(output);
247
+ const reason = decision !== "allow" ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown") : "allowed by active policy";
248
+ await appendRuntimeDecisionEvent(projectDir, buildRuntimeDecisionEvent({
249
+ ir,
250
+ rawInput,
251
+ output,
252
+ result,
253
+ event,
254
+ projectDir,
255
+ sessionId,
256
+ traceId,
257
+ decision,
258
+ reason,
259
+ wfState,
260
+ }), sessionId);
261
+ };
112
262
  let wfStateRaw = null;
113
263
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
114
264
  if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
115
265
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
116
266
  if ("output" in workflowState) {
117
- return workflowState.output;
267
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
268
+ return finish(workflowState.output);
118
269
  }
119
270
  wfStateRaw = workflowState.state;
120
271
  if (wfStateRaw && wfStateRaw.active) {
@@ -128,7 +279,8 @@ export async function runHookEvent(options) {
128
279
  const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
129
280
  if ("output" in artifactFacts) {
130
281
  appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
131
- return artifactFacts.output;
282
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
283
+ return finish(artifactFacts.output);
132
284
  }
133
285
  state.artifactFacts = artifactFacts.facts;
134
286
  }
@@ -144,8 +296,9 @@ export async function runHookEvent(options) {
144
296
  if (event === "PreToolUse") {
145
297
  const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
146
298
  if (gateResult) {
299
+ const traceId = randomUUID();
147
300
  appendTrace(projectDir, {
148
- trace_id: randomUUID(),
301
+ trace_id: traceId,
149
302
  event,
150
303
  tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
151
304
  agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
@@ -156,14 +309,16 @@ export async function runHookEvent(options) {
156
309
  duration_ms: 0,
157
310
  timestamp: new Date().toISOString(),
158
311
  }, sessionId).catch(() => { });
159
- return gateResult.output;
312
+ await writeRuntimeDecision(gateResult.output, { output: gateResult.output, trace: { matched_rule: gateResult.matched_rule, step_id: wfStateRaw?.current_step } }, wfStateRaw, traceId);
313
+ return finish(gateResult.output);
160
314
  }
161
315
  }
162
316
  // Special handling for Stop — needs async workflow state read + session summary
163
317
  if (event === "Stop") {
164
318
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
165
319
  if ("output" in workflowState) {
166
- return workflowState.output;
320
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
321
+ return finish(workflowState.output);
167
322
  }
168
323
  const wfState = workflowState.state;
169
324
  let stopArtifactFacts = [];
@@ -171,7 +326,8 @@ export async function runHookEvent(options) {
171
326
  const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
172
327
  if ("output" in artifactFacts) {
173
328
  appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
174
- return artifactFacts.output;
329
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfState.current_step } }, wfState);
330
+ return finish(artifactFacts.output);
175
331
  }
176
332
  stopArtifactFacts = artifactFacts.facts;
177
333
  }
@@ -220,8 +376,9 @@ export async function runHookEvent(options) {
220
376
  }
221
377
  catch { /* fail-open */ }
222
378
  // Trace for Stop
379
+ const traceId = randomUUID();
223
380
  appendTrace(projectDir, {
224
- trace_id: randomUUID(),
381
+ trace_id: traceId,
225
382
  event: "Stop",
226
383
  workflow: wfState?.workflow,
227
384
  step: wfState?.current_step,
@@ -229,7 +386,8 @@ export async function runHookEvent(options) {
229
386
  duration_ms: 0,
230
387
  timestamp: new Date().toISOString(),
231
388
  }, sessionId).catch(() => { });
232
- return stopOutput;
389
+ await writeRuntimeDecision(stopOutput, { output: stopOutput, trace: { matched_rule: "validator", step_id: wfState?.current_step } }, wfState, traceId);
390
+ return finish(stopOutput);
233
391
  }
234
392
  // Dispatch to enforcement engine with timing
235
393
  const start = Date.now();
@@ -306,24 +464,27 @@ export async function runHookEvent(options) {
306
464
  catch { /* fail-open: pattern detection never blocks */ }
307
465
  }
308
466
  // Trace logging (async, fail-open)
309
- const decision = output.continue === false ? "block"
310
- : output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
311
- : "allow";
467
+ const decision = normalizeDecision(output);
468
+ const traceDecision = decision === "block" ? "block" : decision === "allow" ? "allow" : "warn";
469
+ const traceId = randomUUID();
470
+ const decisionReason = decision !== "allow"
471
+ ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown")
472
+ : "allowed by active policy";
473
+ const traceTimestamp = new Date().toISOString();
312
474
  appendTrace(projectDir, {
313
- trace_id: randomUUID(),
475
+ trace_id: traceId,
314
476
  event,
315
477
  tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
316
478
  agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
317
479
  workflow: state.workflowState?.workflow,
318
480
  step: state.workflowState?.current_step,
319
- decision: decision,
320
- reason: decision !== "allow"
321
- ? (output.reason ?? output.hookSpecificOutput?.additionalContext)
322
- : undefined,
481
+ decision: traceDecision,
482
+ reason: decision !== "allow" ? decisionReason : undefined,
323
483
  target_path: decision !== "allow" ? targetPath : undefined,
324
484
  duration_ms: durationMs,
325
- timestamp: new Date().toISOString(),
485
+ timestamp: traceTimestamp,
326
486
  }, sessionId).catch(() => { }); // Fail-open
487
+ await writeRuntimeDecision(output, result, state.workflowState, traceId);
327
488
  // Side effect: rotate traces + clean stale state on SessionStart
328
489
  if (event === "SessionStart") {
329
490
  rotateTraces(projectDir).catch(() => { });
@@ -339,7 +500,7 @@ export async function runHookEvent(options) {
339
500
  session_id: sessionId,
340
501
  }).catch(() => { }); // Fail-open
341
502
  }
342
- return output;
503
+ return finish(output);
343
504
  }
344
505
  // ── Dispatch ───────────────────────────────────────────────
345
506
  function dispatch(event, ir, input, state, roles) {
@@ -10,6 +10,7 @@
10
10
  * All write operations use atomic temp+rename pattern.
11
11
  */
12
12
  import type { ArtifactFact, ArtifactKey, ArtifactManifest, CompletedArtifactEntry, HandoffArtifact, HandoffType, VerifierKind, VerifierSeverity, VerifierWhen } from "../schema/types.js";
13
+ import { type RuntimeDecisionEvent } from "../governance/index.js";
13
14
  /** Workflow state written by runtime and read by hooks */
14
15
  export interface DNAWorkflowState {
15
16
  active: boolean;
@@ -180,16 +181,8 @@ export interface TraceEntry {
180
181
  * Fail-open: never throws.
181
182
  */
182
183
  export declare function appendTrace(projectDir: string, entry: TraceEntry, sessionId?: string): Promise<void>;
183
- /**
184
- * Read trace entries.
185
- *
186
- * G1 session isolation:
187
- * - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
188
- * (falls back to legacy global files with sessionId in name for backward compat)
189
- * - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
190
- *
191
- * Returns parsed entries sorted by timestamp.
192
- */
184
+ export declare function appendRuntimeDecisionEvent(projectDir: string, event: RuntimeDecisionEvent, sessionId?: string): Promise<void>;
185
+ export declare function readRuntimeDecisionEvents(projectDir: string, days?: number, sessionId?: string): Promise<RuntimeDecisionEvent[]>;
193
186
  export declare function readTraces(projectDir: string, days?: number, sessionId?: string): Promise<TraceEntry[]>;
194
187
  /**
195
188
  * List active session directories under `.dna/state/sessions/`.