intentdna 1.7.0 → 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 (47) 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 +3 -1
  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.js +162 -13
  27. package/dist/hooks/state.d.ts +3 -10
  28. package/dist/hooks/state.js +147 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mcp/tools-observability.js +3 -3
  32. package/dist/mcp/tools-state.d.ts +1 -1
  33. package/dist/mcp/tools-state.js +17 -7
  34. package/dist/report/kernel-report.d.ts +27 -0
  35. package/dist/report/kernel-report.js +60 -19
  36. package/dist/report/kernel-signals.d.ts +5 -2
  37. package/dist/report/kernel-signals.js +84 -2
  38. package/dist/report/report-package.d.ts +9 -1
  39. package/dist/report/report-package.js +32 -27
  40. package/dist/runtime/claude-sdk.d.ts +9 -4
  41. package/dist/runtime/claude-sdk.js +9 -0
  42. package/dist/runtime/plugin-adapter.d.ts +5 -1
  43. package/dist/runtime/plugin-adapter.js +2 -0
  44. package/dist/schema/types.d.ts +51 -0
  45. package/package.json +1 -1
  46. package/spec/README.md +1 -1
  47. 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
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();
@@ -109,11 +230,29 @@ async function main() {
109
230
  export async function runHookEvent(options) {
110
231
  const { event, ir, rawInput, projectDir, sessionId, roles } = options;
111
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
+ };
112
250
  let wfStateRaw = null;
113
251
  // Load workflow state for events that need it (handoff context + PreCompact preservation)
114
252
  if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
115
253
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
116
254
  if ("output" in workflowState) {
255
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
117
256
  return workflowState.output;
118
257
  }
119
258
  wfStateRaw = workflowState.state;
@@ -128,6 +267,7 @@ export async function runHookEvent(options) {
128
267
  const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
129
268
  if ("output" in artifactFacts) {
130
269
  appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
270
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
131
271
  return artifactFacts.output;
132
272
  }
133
273
  state.artifactFacts = artifactFacts.facts;
@@ -144,8 +284,9 @@ export async function runHookEvent(options) {
144
284
  if (event === "PreToolUse") {
145
285
  const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
146
286
  if (gateResult) {
287
+ const traceId = randomUUID();
147
288
  appendTrace(projectDir, {
148
- trace_id: randomUUID(),
289
+ trace_id: traceId,
149
290
  event,
150
291
  tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
151
292
  agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
@@ -156,6 +297,7 @@ export async function runHookEvent(options) {
156
297
  duration_ms: 0,
157
298
  timestamp: new Date().toISOString(),
158
299
  }, sessionId).catch(() => { });
300
+ await writeRuntimeDecision(gateResult.output, { output: gateResult.output, trace: { matched_rule: gateResult.matched_rule, step_id: wfStateRaw?.current_step } }, wfStateRaw, traceId);
159
301
  return gateResult.output;
160
302
  }
161
303
  }
@@ -163,6 +305,7 @@ export async function runHookEvent(options) {
163
305
  if (event === "Stop") {
164
306
  const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
165
307
  if ("output" in workflowState) {
308
+ await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
166
309
  return workflowState.output;
167
310
  }
168
311
  const wfState = workflowState.state;
@@ -171,6 +314,7 @@ export async function runHookEvent(options) {
171
314
  const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
172
315
  if ("output" in artifactFacts) {
173
316
  appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
317
+ await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfState.current_step } }, wfState);
174
318
  return artifactFacts.output;
175
319
  }
176
320
  stopArtifactFacts = artifactFacts.facts;
@@ -220,8 +364,9 @@ export async function runHookEvent(options) {
220
364
  }
221
365
  catch { /* fail-open */ }
222
366
  // Trace for Stop
367
+ const traceId = randomUUID();
223
368
  appendTrace(projectDir, {
224
- trace_id: randomUUID(),
369
+ trace_id: traceId,
225
370
  event: "Stop",
226
371
  workflow: wfState?.workflow,
227
372
  step: wfState?.current_step,
@@ -229,6 +374,7 @@ export async function runHookEvent(options) {
229
374
  duration_ms: 0,
230
375
  timestamp: new Date().toISOString(),
231
376
  }, sessionId).catch(() => { });
377
+ await writeRuntimeDecision(stopOutput, { output: stopOutput, trace: { matched_rule: "validator", step_id: wfState?.current_step } }, wfState, traceId);
232
378
  return stopOutput;
233
379
  }
234
380
  // Dispatch to enforcement engine with timing
@@ -306,24 +452,27 @@ export async function runHookEvent(options) {
306
452
  catch { /* fail-open: pattern detection never blocks */ }
307
453
  }
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(() => { });
@@ -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/`.