intentdna 1.9.0 → 1.9.3
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +227 -87
- package/dist/cli/commands/run-lifecycle.d.ts +61 -11
- package/dist/cli/commands/run-lifecycle.js +184 -50
- package/dist/cli/commands/run-observer.d.ts +15 -0
- package/dist/cli/commands/run-observer.js +210 -0
- package/dist/cli/commands/run.d.ts +16 -20
- package/dist/cli/commands/run.js +461 -659
- package/dist/cli/commands/sync.js +16 -12
- package/dist/cli/index.js +14 -5
- package/dist/compiler/compile.d.ts +1 -0
- package/dist/compiler/compile.js +1 -1
- package/dist/compiler/controller.d.ts +16 -0
- package/dist/compiler/controller.js +693 -0
- package/dist/compiler/input-resolver.d.ts +2 -0
- package/dist/compiler/input-resolver.js +22 -6
- package/dist/hooks/cli.d.ts +45 -0
- package/dist/hooks/cli.js +263 -31
- package/dist/hooks/host-event.d.ts +57 -0
- package/dist/hooks/host-event.js +187 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/protocol.d.ts +7 -0
- package/dist/hooks/schema.d.ts +1 -0
- package/dist/hooks/schema.js +89 -1
- package/dist/hooks/state.d.ts +23 -1
- package/dist/hooks/state.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-run.d.ts +4 -0
- package/dist/mcp/tools-run.js +419 -0
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +13 -241
- package/dist/runtime/artifact-store.d.ts +146 -0
- package/dist/runtime/artifact-store.js +1436 -0
- package/dist/runtime/canonical-attempt-outcome.d.ts +12 -0
- package/dist/runtime/canonical-attempt-outcome.js +195 -0
- package/dist/runtime/canonical-json.d.ts +10 -0
- package/dist/runtime/canonical-json.js +105 -0
- package/dist/runtime/canonical-run-application.d.ts +93 -0
- package/dist/runtime/canonical-run-application.js +229 -0
- package/dist/runtime/canonical-run-service.d.ts +454 -0
- package/dist/runtime/canonical-run-service.js +4760 -0
- package/dist/runtime/canonical-runtime-composition.d.ts +22 -0
- package/dist/runtime/canonical-runtime-composition.js +34 -0
- package/dist/runtime/canonical-runtime-projection.d.ts +3 -0
- package/dist/runtime/canonical-runtime-projection.js +4 -0
- package/dist/runtime/canonical-target-compiler.d.ts +46 -0
- package/dist/runtime/canonical-target-compiler.js +514 -0
- package/dist/runtime/claude-agent-identity.d.ts +2 -0
- package/dist/runtime/claude-agent-identity.js +21 -0
- package/dist/runtime/executable-run-plan.d.ts +328 -0
- package/dist/runtime/executable-run-plan.js +1485 -0
- package/dist/runtime/execution-authority.d.ts +83 -0
- package/dist/runtime/execution-authority.js +98 -0
- package/dist/runtime/handoff-resolver.js +4 -1
- package/dist/runtime/harness-pull-adapter.d.ts +146 -0
- package/dist/runtime/harness-pull-adapter.js +315 -0
- package/dist/runtime/index.d.ts +39 -14
- package/dist/runtime/index.js +29 -7
- package/dist/runtime/local-execution-authority.d.ts +194 -0
- package/dist/runtime/local-execution-authority.js +2897 -0
- package/dist/runtime/local-execution-reconciliation.d.ts +20 -0
- package/dist/runtime/local-execution-reconciliation.js +107 -0
- package/dist/runtime/local-execution-supervisor-script.d.ts +7 -0
- package/dist/runtime/local-execution-supervisor-script.js +793 -0
- package/dist/runtime/local-provider-sandbox.d.ts +42 -0
- package/dist/runtime/local-provider-sandbox.js +154 -0
- package/dist/runtime/local-verifier-execution.d.ts +24 -0
- package/dist/runtime/local-verifier-execution.js +290 -0
- package/dist/runtime/local-windows-execution-supervisor-script.d.ts +6 -0
- package/dist/runtime/local-windows-execution-supervisor-script.js +788 -0
- package/dist/runtime/plan-store.d.ts +30 -0
- package/dist/runtime/plan-store.js +231 -0
- package/dist/runtime/process-tree.d.ts +9 -0
- package/dist/runtime/process-tree.js +97 -24
- package/dist/runtime/providers/codex.js +2 -2
- package/dist/runtime/push-driver.d.ts +184 -0
- package/dist/runtime/push-driver.js +1002 -0
- package/dist/runtime/result-store.d.ts +34 -3
- package/dist/runtime/result-store.js +1073 -2
- package/dist/runtime/run-binding.d.ts +68 -0
- package/dist/runtime/run-binding.js +278 -0
- package/dist/runtime/run-contracts.d.ts +575 -0
- package/dist/runtime/run-contracts.js +58 -7
- package/dist/runtime/run-controller.d.ts +1 -6
- package/dist/runtime/run-controller.js +0 -41
- package/dist/runtime/run-store.d.ts +85 -7
- package/dist/runtime/run-store.js +2528 -186
- package/dist/runtime/skill-adapter.d.ts +18 -7
- package/dist/runtime/skill-adapter.js +100 -742
- package/dist/runtime/structured-output-validator.d.ts +44 -0
- package/dist/runtime/structured-output-validator.js +171 -0
- package/dist/runtime/verifier-command-binding.d.ts +22 -0
- package/dist/runtime/verifier-command-binding.js +162 -0
- package/dist/runtime/verifier.d.ts +46 -6
- package/dist/runtime/verifier.js +355 -53
- package/dist/runtime/windows-job-keeper.d.ts +47 -0
- package/dist/runtime/windows-job-keeper.js +231 -0
- package/dist/runtime/worker-executor.d.ts +17 -1
- package/dist/runtime/worker-executor.js +26 -5
- package/dist/runtime/workflow-plan-adapter.d.ts +19 -0
- package/dist/runtime/workflow-plan-adapter.js +502 -6
- package/dist/runtime/workflow-runtime-manifest.d.ts +1 -0
- package/dist/runtime/workflow-runtime-manifest.js +5 -0
- package/dist/runtime/workspace-isolation.d.ts +29 -2
- package/dist/runtime/workspace-isolation.js +488 -11
- package/dist/runtime/workspace-observation.d.ts +44 -0
- package/dist/runtime/workspace-observation.js +216 -0
- package/dist/schema/controller-registry.d.ts +0 -7
- package/dist/schema/controller-registry.js +0 -8
- package/dist/schema/types.d.ts +84 -10
- package/dist/schema/types.js +2 -0
- package/dist/schema/validate.js +151 -1
- package/dist/schema/validators/controllers.d.ts +1 -1
- package/dist/schema/validators/controllers.js +126 -41
- package/dist/schema/workflow-authoring-contract.js +2 -0
- package/dist/schema/yaml-parser.js +1 -1
- package/dist/templates/flutter-rewrite-new.dna.yaml +1 -0
- package/dist/templates/flutter-rewrite.dna.yaml +87 -60
- package/dist/templates/safe-refactoring.dna.yaml +1 -0
- package/native/windows-job-keeper/README.md +80 -0
- package/native/windows-job-keeper/bin/aarch64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/bin/x86_64/intentdna-windows-job-keeper.exe +0 -0
- package/native/windows-job-keeper/keeper.c +979 -0
- package/native/windows-job-keeper/manifest.json +33 -0
- package/package.json +6 -2
- package/scripts/build-windows-job-keeper.mjs +172 -0
- package/scripts/verify-windows-job-keeper.mjs +184 -0
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { TextDecoder } from "node:util";
|
|
6
7
|
import { validateDNA } from "../schema/validate.js";
|
|
7
8
|
import { parseYAML } from "../schema/yaml-parser.js";
|
|
8
9
|
import { DNADiagnosticsError, hasDiagnosticErrors } from "./diagnostics.js";
|
|
@@ -59,8 +60,8 @@ export async function detectDNAConfigs(projectDir = process.cwd()) {
|
|
|
59
60
|
}
|
|
60
61
|
return [];
|
|
61
62
|
}
|
|
62
|
-
function fingerprint(
|
|
63
|
-
return `sha256:${createHash("sha256").update(
|
|
63
|
+
function fingerprint(snapshotBytes) {
|
|
64
|
+
return `sha256:${createHash("sha256").update(snapshotBytes).digest("hex")}`;
|
|
64
65
|
}
|
|
65
66
|
function enterprisePolicyCandidates(name, cwd, options) {
|
|
66
67
|
const configuredDirs = [
|
|
@@ -103,8 +104,13 @@ function sourceTrustForInput(file, ref, cwd) {
|
|
|
103
104
|
return "input";
|
|
104
105
|
}
|
|
105
106
|
async function readDNAWithRaw(path) {
|
|
106
|
-
const
|
|
107
|
-
|
|
107
|
+
const snapshotBytes = await readFile(path);
|
|
108
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(snapshotBytes);
|
|
109
|
+
return {
|
|
110
|
+
raw,
|
|
111
|
+
dna: parseDNAContent(path, raw),
|
|
112
|
+
snapshot_bytes: Uint8Array.from(snapshotBytes),
|
|
113
|
+
};
|
|
108
114
|
}
|
|
109
115
|
export async function expandDNAInputs(files, options) {
|
|
110
116
|
const cwd = options?.cwd ?? process.cwd();
|
|
@@ -112,6 +118,8 @@ export async function expandDNAInputs(files, options) {
|
|
|
112
118
|
const seen = new Set();
|
|
113
119
|
const diagnostics = [];
|
|
114
120
|
const provenance = new Map();
|
|
121
|
+
const snapshots = new Map();
|
|
122
|
+
const snapshotBytes = new Map();
|
|
115
123
|
const stack = [];
|
|
116
124
|
const visit = async (file, ref, inheritedBy, trust) => {
|
|
117
125
|
const abs = resolve(cwd, file);
|
|
@@ -169,11 +177,13 @@ export async function expandDNAInputs(files, options) {
|
|
|
169
177
|
stack.pop();
|
|
170
178
|
seen.add(abs);
|
|
171
179
|
ordered.push(abs);
|
|
180
|
+
snapshots.set(abs, loaded.dna);
|
|
181
|
+
snapshotBytes.set(abs, loaded.snapshot_bytes);
|
|
172
182
|
provenance.set(abs, {
|
|
173
183
|
source_id: loaded.dna.id,
|
|
174
184
|
source_ref: ref,
|
|
175
185
|
resolved_path: abs,
|
|
176
|
-
fingerprint: fingerprint(loaded.
|
|
186
|
+
fingerprint: fingerprint(loaded.snapshot_bytes),
|
|
177
187
|
type: loaded.dna.type,
|
|
178
188
|
cascade_priority: loaded.dna.cascade.priority,
|
|
179
189
|
version: loaded.dna.version,
|
|
@@ -188,7 +198,13 @@ export async function expandDNAInputs(files, options) {
|
|
|
188
198
|
if (hasDiagnosticErrors(diagnostics)) {
|
|
189
199
|
throw new DNADiagnosticsError("DNA inheritance resolution failed", diagnostics);
|
|
190
200
|
}
|
|
191
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
files: ordered,
|
|
203
|
+
dnas: ordered.map((file) => snapshots.get(file)).filter(Boolean),
|
|
204
|
+
snapshot_bytes: ordered.map((file) => snapshotBytes.get(file)).filter(Boolean),
|
|
205
|
+
provenance: ordered.map((file) => provenance.get(file)).filter(Boolean),
|
|
206
|
+
diagnostics,
|
|
207
|
+
};
|
|
192
208
|
}
|
|
193
209
|
export async function expandDNAInputFiles(files, options) {
|
|
194
210
|
return (await expandDNAInputs(files, options)).files;
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -18,11 +18,18 @@ import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schem
|
|
|
18
18
|
import type { HookEvent, HookOutput } from "./protocol.js";
|
|
19
19
|
import { blockOutput } from "./protocol.js";
|
|
20
20
|
import { readWorkflowState } from "./state.js";
|
|
21
|
+
import type { DNAWorkflowState } from "./state.js";
|
|
21
22
|
import type { VerifierResultEntry } from "./state.js";
|
|
23
|
+
import type { HeartbeatRequest } from "../runtime/canonical-run-service.js";
|
|
22
24
|
interface HookRuntimePaths {
|
|
23
25
|
projectDir: string;
|
|
24
26
|
irPath: string;
|
|
25
27
|
}
|
|
28
|
+
interface HookRuntimeIdentity {
|
|
29
|
+
readonly host_session_id: string | undefined;
|
|
30
|
+
readonly worker_session_id: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
declare function resolveHookRuntimeIdentity(event: HookEvent, input: Record<string, unknown>, rawInput: Record<string, unknown>): HookRuntimeIdentity;
|
|
26
33
|
declare function resolveHookRuntimePaths(cwd: string, irPath?: string, explicitIRPath?: boolean): Promise<HookRuntimePaths>;
|
|
27
34
|
export interface SessionSummary {
|
|
28
35
|
total: number;
|
|
@@ -57,6 +64,40 @@ export declare function runHookEvent(options: RunHookEventOptions): Promise<Hook
|
|
|
57
64
|
export declare function codexHookEventForInput(rawInput: Record<string, unknown>, args: string[]): unknown;
|
|
58
65
|
declare function inferWorkflowAssetFromIr(ir: ConstraintIR, workflowId?: string): string | undefined;
|
|
59
66
|
declare function runCodexHookWithLoadedIr(rawInput: Record<string, unknown>, projectDir: string, args: string[], ir: ConstraintIR): Promise<import("../runtime/codex-adapter.js").CodexHookRunnerResult>;
|
|
67
|
+
interface HookIRLoadOptions {
|
|
68
|
+
readonly projectDir: string;
|
|
69
|
+
readonly projectIRPath: string;
|
|
70
|
+
readonly sessionId?: string;
|
|
71
|
+
readonly trustedWorkerIdentity?: string;
|
|
72
|
+
}
|
|
73
|
+
interface HookIRLoadResult {
|
|
74
|
+
readonly ir: ConstraintIR | null;
|
|
75
|
+
readonly error?: string;
|
|
76
|
+
readonly source: "session" | "project" | "none";
|
|
77
|
+
}
|
|
78
|
+
interface CanonicalHeartbeatPort {
|
|
79
|
+
heartbeat(request: HeartbeatRequest): Promise<unknown>;
|
|
80
|
+
}
|
|
81
|
+
type CanonicalHeartbeatOutcome = {
|
|
82
|
+
readonly kind: "renewed";
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: "not_canonical";
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: "no_projection";
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: "projection_unreadable";
|
|
89
|
+
readonly message: string;
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: "mismatched_projection";
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: "legacy_projection";
|
|
94
|
+
} | {
|
|
95
|
+
readonly kind: "service_rejected";
|
|
96
|
+
readonly message: string;
|
|
97
|
+
};
|
|
98
|
+
declare function heartbeatCanonicalAttempt(projectDir: string, state: DNAWorkflowState | null, service?: CanonicalHeartbeatPort): Promise<CanonicalHeartbeatOutcome>;
|
|
99
|
+
declare function recordCanonicalHeartbeatOutcome(projectDir: string, outcome: CanonicalHeartbeatOutcome, event: string, state: DNAWorkflowState | null, sessionId?: string): Promise<void>;
|
|
100
|
+
declare function loadHookIR(options: HookIRLoadOptions): Promise<HookIRLoadResult>;
|
|
60
101
|
declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
|
|
61
102
|
workflow: string;
|
|
62
103
|
current_step: string;
|
|
@@ -97,6 +138,10 @@ export declare const resolveWorkflowArtifactFactsForTest: typeof resolveWorkflow
|
|
|
97
138
|
export declare const readWorkflowStateForHookTest: typeof readWorkflowStateForHook;
|
|
98
139
|
export declare const runCodexHookWithLoadedIrForTest: typeof runCodexHookWithLoadedIr;
|
|
99
140
|
export declare const resolveHookRuntimePathsForTest: typeof resolveHookRuntimePaths;
|
|
141
|
+
export declare const resolveHookRuntimeIdentityForTest: typeof resolveHookRuntimeIdentity;
|
|
142
|
+
export declare const loadHookIRForTest: typeof loadHookIR;
|
|
143
|
+
export declare const heartbeatCanonicalAttemptForTest: typeof heartbeatCanonicalAttempt;
|
|
144
|
+
export declare const recordCanonicalHeartbeatOutcomeForTest: typeof recordCanonicalHeartbeatOutcome;
|
|
100
145
|
export declare const inferWorkflowAssetFromIrForTest: typeof inferWorkflowAssetFromIr;
|
|
101
146
|
export declare const resolveWorkflowArtifactFactsForHookTest: typeof resolveWorkflowArtifactFactsForHook;
|
|
102
147
|
export declare const finalizeAndResolveArtifactsForHookTest: typeof finalizeAndResolveArtifactsForHook;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -21,18 +21,52 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
21
21
|
import { createHash, randomUUID } from "node:crypto";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { readStdinResult, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
24
|
-
import { validateHookInput } from "./schema.js";
|
|
24
|
+
import { isCurrentClaudeSubagentStopInput, validateHookInput } from "./schema.js";
|
|
25
25
|
import { hookFailureOutput } from "./enforcement-boundary.js";
|
|
26
26
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
27
|
-
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, updateSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, workflowTemplateVariables, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
|
|
27
|
+
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, appendEvidenceCaptureEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, updateSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, readVerifierResults, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, resolveSessionCompiledIRPath, safePathComponent, workflowTemplateVariables, writeArtifactManifest, EVIDENCE_CAPTURE_SCHEMA_VERSION } from "./state.js";
|
|
28
28
|
import { hookEventsForSurface } from "./event-registry.js";
|
|
29
29
|
import { writeAuditEvent } from "../audit/index.js";
|
|
30
30
|
import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION, trustedEvidenceCaptureAttribution } from "../governance/index.js";
|
|
31
31
|
import { runCodexHook } from "../runtime/codex-adapter.js";
|
|
32
|
+
import { createCanonicalRuntimeComposition } from "../runtime/canonical-runtime-composition.js";
|
|
33
|
+
import { buildClaudeSubagentStopHostEvent, encodeClaudeHostEventForStderr, } from "./host-event.js";
|
|
32
34
|
import { runCheckpointVerifier as runSharedCheckpointVerifier, runCompletionVerifier, toProjectRelativePath, } from "../runtime/verifier.js";
|
|
33
35
|
// ── Constants ──────────────────────────────────────────────
|
|
34
36
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
35
37
|
const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
|
|
38
|
+
function resolveHookRuntimeIdentity(event, input, rawInput) {
|
|
39
|
+
const hostSessionId = typeof rawInput.session_id === "string"
|
|
40
|
+
? rawInput.session_id
|
|
41
|
+
: typeof rawInput.sessionId === "string"
|
|
42
|
+
? rawInput.sessionId
|
|
43
|
+
: typeof input.sessionId === "string"
|
|
44
|
+
? input.sessionId
|
|
45
|
+
: undefined;
|
|
46
|
+
const agentId = typeof input.agent_id === "string" && input.agent_id.trim().length > 0
|
|
47
|
+
? input.agent_id
|
|
48
|
+
: undefined;
|
|
49
|
+
const suppliedWorkerIdentity = typeof rawInput.worker_session_id === "string"
|
|
50
|
+
&& rawInput.worker_session_id.trim().length > 0
|
|
51
|
+
? rawInput.worker_session_id
|
|
52
|
+
: undefined;
|
|
53
|
+
const useHostAgentIdentity = event === "SubagentStop"
|
|
54
|
+
&& agentId !== undefined
|
|
55
|
+
&& isCurrentClaudeSubagentStopInput(rawInput);
|
|
56
|
+
return {
|
|
57
|
+
host_session_id: hostSessionId,
|
|
58
|
+
worker_session_id: useHostAgentIdentity
|
|
59
|
+
? agentId
|
|
60
|
+
: suppliedWorkerIdentity ?? (typeof input.sessionId === "string" ? input.sessionId : hostSessionId),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isCanonicalWorkflowState(state) {
|
|
64
|
+
return Boolean(state?.active
|
|
65
|
+
&& state.worker_session_id
|
|
66
|
+
&& state.run_id
|
|
67
|
+
&& state.step_id
|
|
68
|
+
&& state.attempt_id);
|
|
69
|
+
}
|
|
36
70
|
function workflowRuntimeRevisionMismatch(ir, state) {
|
|
37
71
|
if (!state?.active || state.resolved_variables_source !== "compiled_manifest")
|
|
38
72
|
return null;
|
|
@@ -391,15 +425,34 @@ async function main() {
|
|
|
391
425
|
const cwd = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
|
|
392
426
|
const runtimePaths = await resolveHookRuntimePaths(cwd, irPath, explicitIRPath);
|
|
393
427
|
const { projectDir } = runtimePaths;
|
|
394
|
-
const
|
|
395
|
-
//
|
|
428
|
+
const identity = resolveHookRuntimeIdentity(event, rawInput, rawStdin);
|
|
429
|
+
// Canonical attempts are pinned to an attempt-local projection. Legacy hooks
|
|
430
|
+
// keep the project-level compiled IR compatibility path.
|
|
396
431
|
const resolvedIRPath = runtimePaths.irPath;
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
432
|
+
const hookStateIdentity = identity.worker_session_id;
|
|
433
|
+
const loaded = await loadHookIR({
|
|
434
|
+
projectDir,
|
|
435
|
+
projectIRPath: resolvedIRPath,
|
|
436
|
+
sessionId: hookStateIdentity,
|
|
437
|
+
trustedWorkerIdentity: hookStateIdentity,
|
|
438
|
+
});
|
|
439
|
+
if (!loaded.ir) {
|
|
440
|
+
writeOutput(hookFailureOutput(event, "missing_ir", loaded.error ?? `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${resolvedIRPath}.`));
|
|
400
441
|
return;
|
|
401
442
|
}
|
|
402
|
-
const output = await runHookEvent({
|
|
443
|
+
const output = await runHookEvent({
|
|
444
|
+
event,
|
|
445
|
+
ir: loaded.ir,
|
|
446
|
+
rawInput,
|
|
447
|
+
projectDir,
|
|
448
|
+
sessionId: hookStateIdentity,
|
|
449
|
+
});
|
|
450
|
+
if (event === "SubagentStop" && output.continue !== false && hookStateIdentity) {
|
|
451
|
+
const state = await readWorkflowState(projectDir, hookStateIdentity);
|
|
452
|
+
const hostEvent = buildClaudeSubagentStopHostEvent(rawStdin, state);
|
|
453
|
+
if (hostEvent)
|
|
454
|
+
process.stderr.write(encodeClaudeHostEventForStderr(hostEvent));
|
|
455
|
+
}
|
|
403
456
|
writeOutput(output);
|
|
404
457
|
}
|
|
405
458
|
export function normalizeHookOutputForEvent(output, event) {
|
|
@@ -485,6 +538,10 @@ export async function runHookEvent(options) {
|
|
|
485
538
|
&& wfStateRaw.attempt_id),
|
|
486
539
|
};
|
|
487
540
|
}
|
|
541
|
+
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
542
|
+
const heartbeatOutcome = await heartbeatCanonicalAttempt(projectDir, wfStateRaw);
|
|
543
|
+
await recordCanonicalHeartbeatOutcome(projectDir, heartbeatOutcome, event, wfStateRaw, sessionId);
|
|
544
|
+
}
|
|
488
545
|
}
|
|
489
546
|
if (event === "PreToolUse" && wfStateRaw?.active) {
|
|
490
547
|
try {
|
|
@@ -824,10 +881,11 @@ function inferWorkflowAssetFromIr(ir, workflowId) {
|
|
|
824
881
|
function agentTypeForWorkflowRole(role) {
|
|
825
882
|
if (!role)
|
|
826
883
|
return undefined;
|
|
827
|
-
return `dna-${role
|
|
884
|
+
return `dna-${toKebabCase(role)}`;
|
|
828
885
|
}
|
|
829
886
|
function subagentMatchesWorkflowRole(rawInput, wfState) {
|
|
830
|
-
return typeof rawInput.agent_type === "string"
|
|
887
|
+
return typeof rawInput.agent_type === "string"
|
|
888
|
+
&& rawInput.agent_type === agentTypeForWorkflowRole(wfState.current_role);
|
|
831
889
|
}
|
|
832
890
|
async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
|
|
833
891
|
const sessionId = typeof rawInput.worker_session_id === "string"
|
|
@@ -838,8 +896,13 @@ async function runCodexHookWithLoadedIr(rawInput, projectDir, args, ir) {
|
|
|
838
896
|
? rawInput.sessionId
|
|
839
897
|
: undefined;
|
|
840
898
|
const workflowState = await readWorkerWorkflowState(projectDir, sessionId, typeof rawInput.worker_session_id !== "string");
|
|
899
|
+
const hookEvent = codexHookEventForInput(rawInput, args);
|
|
900
|
+
if (hookEvent === "PreToolUse" || hookEvent === "PostToolUse") {
|
|
901
|
+
const heartbeatOutcome = await heartbeatCanonicalAttempt(projectDir, workflowState);
|
|
902
|
+
await recordCanonicalHeartbeatOutcome(projectDir, heartbeatOutcome, typeof hookEvent === "string" ? hookEvent : "CodexHook", workflowState, sessionId);
|
|
903
|
+
}
|
|
841
904
|
const workflowAsset = inferWorkflowAssetFromIr(ir, workflowState?.workflow);
|
|
842
|
-
return runCodexHook({ ...rawInput, event:
|
|
905
|
+
return runCodexHook({ ...rawInput, event: hookEvent }, {
|
|
843
906
|
ir,
|
|
844
907
|
projectDir,
|
|
845
908
|
hookInstallMode: codexInstallMode(argValue(args, "--codex-install-mode")),
|
|
@@ -868,12 +931,23 @@ async function runCodexHookCli(args) {
|
|
|
868
931
|
const explicitIRPath = argValue(args, "--ir");
|
|
869
932
|
const runtimePaths = await resolveHookRuntimePaths(cwd, explicitIRPath ?? DEFAULT_IR_PATH, explicitIRPath !== undefined);
|
|
870
933
|
const { projectDir, irPath } = runtimePaths;
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
934
|
+
const workerSessionId = typeof rawInput.worker_session_id === "string"
|
|
935
|
+
? rawInput.worker_session_id
|
|
936
|
+
: undefined;
|
|
937
|
+
const claimedSessionId = workerSessionId
|
|
938
|
+
?? (typeof rawInput.session_id === "string" ? rawInput.session_id : undefined)
|
|
939
|
+
?? (typeof rawInput.sessionId === "string" ? rawInput.sessionId : undefined);
|
|
940
|
+
const loaded = await loadHookIR({
|
|
941
|
+
projectDir,
|
|
942
|
+
projectIRPath: irPath,
|
|
943
|
+
sessionId: claimedSessionId,
|
|
944
|
+
trustedWorkerIdentity: workerSessionId,
|
|
945
|
+
});
|
|
946
|
+
if (!loaded.ir) {
|
|
947
|
+
writeOutput(hookFailureOutput(policyEvent, "missing_ir", loaded.error ?? `Compiled IR is missing, unreadable, invalid, or version-incompatible at ${irPath}.`));
|
|
874
948
|
return;
|
|
875
949
|
}
|
|
876
|
-
const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, ir);
|
|
950
|
+
const result = await runCodexHookWithLoadedIr(rawInput, projectDir, args, loaded.ir);
|
|
877
951
|
writeOutput(result.output);
|
|
878
952
|
}
|
|
879
953
|
catch (err) {
|
|
@@ -885,26 +959,180 @@ async function runCodexHookCli(args) {
|
|
|
885
959
|
// ── IR Loading ─────────────────────────────────────────────
|
|
886
960
|
/** Expected IR schema version — must match plugin-adapter.ts */
|
|
887
961
|
const EXPECTED_IR_VERSION = 1;
|
|
888
|
-
|
|
962
|
+
function canonicalProjectionMismatch(projection, state) {
|
|
963
|
+
if (projection.schema_version !== "intentdna.canonical_hook_projection.v1"
|
|
964
|
+
&& projection.schema_version !== "intentdna.canonical_hook_projection.v2") {
|
|
965
|
+
return "Canonical session IR has an unsupported schema version.";
|
|
966
|
+
}
|
|
967
|
+
for (const [label, actual, expected] of [
|
|
968
|
+
["run", projection.run_id, state.run_id],
|
|
969
|
+
["step", projection.step_id, state.step_id],
|
|
970
|
+
["attempt", projection.attempt_id, state.attempt_id],
|
|
971
|
+
["worker session", projection.worker_session_id, state.worker_session_id],
|
|
972
|
+
]) {
|
|
973
|
+
if (actual !== expected)
|
|
974
|
+
return `Canonical session IR ${label} identity does not match workflow state.`;
|
|
975
|
+
}
|
|
976
|
+
const compiled = projection.compiled_ir;
|
|
977
|
+
if (!projection.compiled_ir_hash
|
|
978
|
+
|| compiled?.compiled_ir_hash !== projection.compiled_ir_hash
|
|
979
|
+
|| compiled?.ir?.compiled_ir_hash !== projection.compiled_ir_hash
|
|
980
|
+
|| compiled?.workflow_runtime?.compiled_ir_hash !== projection.compiled_ir_hash) {
|
|
981
|
+
return "Canonical session IR hash does not match its compiled projection.";
|
|
982
|
+
}
|
|
983
|
+
if (state.workflow_runtime_compiled_ir_hash !== projection.compiled_ir_hash) {
|
|
984
|
+
return "Canonical session IR hash does not match workflow state.";
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
function canonicalHeartbeatService(projectDir) {
|
|
989
|
+
return createCanonicalRuntimeComposition({
|
|
990
|
+
project_directory: projectDir,
|
|
991
|
+
}).service;
|
|
992
|
+
}
|
|
993
|
+
async function heartbeatCanonicalAttempt(projectDir, state, service) {
|
|
994
|
+
if (!isCanonicalWorkflowState(state))
|
|
995
|
+
return { kind: "not_canonical" };
|
|
996
|
+
let projection;
|
|
889
997
|
try {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
998
|
+
const projectionPath = resolveSessionCompiledIRPath(projectDir, state.worker_session_id);
|
|
999
|
+
projection = JSON.parse(await readFile(projectionPath, "utf-8"));
|
|
1000
|
+
}
|
|
1001
|
+
catch (error) {
|
|
1002
|
+
const code = error instanceof Error && "code" in error
|
|
1003
|
+
? error.code
|
|
1004
|
+
: undefined;
|
|
1005
|
+
if (code === "ENOENT")
|
|
1006
|
+
return { kind: "no_projection" };
|
|
1007
|
+
return {
|
|
1008
|
+
kind: "projection_unreadable",
|
|
1009
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
if (canonicalProjectionMismatch(projection, state))
|
|
1013
|
+
return { kind: "mismatched_projection" };
|
|
1014
|
+
if (projection.schema_version !== "intentdna.canonical_hook_projection.v2") {
|
|
1015
|
+
return { kind: "legacy_projection" };
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
await (service ?? canonicalHeartbeatService(projectDir)).heartbeat({
|
|
1019
|
+
lease_token: projection.lease_token,
|
|
1020
|
+
owner_id: projection.owner_id,
|
|
1021
|
+
});
|
|
1022
|
+
return { kind: "renewed" };
|
|
1023
|
+
}
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
return {
|
|
1026
|
+
kind: "service_rejected",
|
|
1027
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async function recordCanonicalHeartbeatOutcome(projectDir, outcome, event, state, sessionId) {
|
|
1032
|
+
if (outcome.kind === "renewed" || outcome.kind === "not_canonical")
|
|
1033
|
+
return;
|
|
1034
|
+
const detail = outcome.kind === "service_rejected" || outcome.kind === "projection_unreadable"
|
|
1035
|
+
? `: ${outcome.message}`
|
|
1036
|
+
: "";
|
|
1037
|
+
await appendTrace(projectDir, {
|
|
1038
|
+
trace_id: randomUUID(),
|
|
1039
|
+
event,
|
|
1040
|
+
workflow: state?.workflow,
|
|
1041
|
+
step: state?.current_step,
|
|
1042
|
+
decision: "warn",
|
|
1043
|
+
reason: `canonical heartbeat ${outcome.kind}${detail}`,
|
|
1044
|
+
duration_ms: 0,
|
|
1045
|
+
timestamp: new Date().toISOString(),
|
|
1046
|
+
}, sessionId);
|
|
1047
|
+
}
|
|
1048
|
+
async function loadHookIR(options) {
|
|
1049
|
+
let projectionRaw = null;
|
|
1050
|
+
let projectionPath = null;
|
|
1051
|
+
if (options.sessionId) {
|
|
1052
|
+
try {
|
|
1053
|
+
projectionPath = resolveSessionCompiledIRPath(options.projectDir, options.sessionId);
|
|
1054
|
+
projectionRaw = await readFile(projectionPath, "utf-8");
|
|
900
1055
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
const code = error instanceof Error && "code" in error
|
|
1058
|
+
? error.code
|
|
1059
|
+
: undefined;
|
|
1060
|
+
if (code !== "ENOENT") {
|
|
1061
|
+
return {
|
|
1062
|
+
ir: null,
|
|
1063
|
+
source: "none",
|
|
1064
|
+
error: `Canonical session IR is unreadable at ${projectionPath ?? "unknown"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
904
1067
|
}
|
|
905
|
-
|
|
906
|
-
|
|
1068
|
+
}
|
|
1069
|
+
let state = null;
|
|
1070
|
+
try {
|
|
1071
|
+
state = options.sessionId
|
|
1072
|
+
? await readWorkflowState(options.projectDir, options.sessionId)
|
|
1073
|
+
: null;
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
return { ir: null, source: "none", error: `Hook session identity is invalid: ${error instanceof Error ? error.message : String(error)}` };
|
|
1077
|
+
}
|
|
1078
|
+
if (!isCanonicalWorkflowState(state) && projectionRaw === null) {
|
|
1079
|
+
return { ir: await loadIR(options.projectIRPath), source: "project" };
|
|
1080
|
+
}
|
|
1081
|
+
if (!isCanonicalWorkflowState(state)) {
|
|
1082
|
+
return {
|
|
1083
|
+
ir: null,
|
|
1084
|
+
source: "none",
|
|
1085
|
+
error: "Canonical Hook workflow state is missing, unreadable, or incomplete; project/root IR fallback is forbidden.",
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (!options.trustedWorkerIdentity || options.trustedWorkerIdentity !== state.worker_session_id) {
|
|
1089
|
+
return {
|
|
1090
|
+
ir: null,
|
|
1091
|
+
source: "none",
|
|
1092
|
+
error: "Canonical Hook execution requires a trusted worker_session_id; project/root IR fallback is forbidden.",
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
projectionPath ??= resolveSessionCompiledIRPath(options.projectDir, state.worker_session_id);
|
|
1096
|
+
try {
|
|
1097
|
+
const projection = JSON.parse(projectionRaw ?? await readFile(projectionPath, "utf-8"));
|
|
1098
|
+
const mismatch = canonicalProjectionMismatch(projection, state);
|
|
1099
|
+
if (mismatch)
|
|
1100
|
+
return { ir: null, source: "none", error: mismatch };
|
|
1101
|
+
const ir = await parseIR(projection.compiled_ir, projectionPath);
|
|
1102
|
+
return ir
|
|
1103
|
+
? { ir, source: "session" }
|
|
1104
|
+
: { ir: null, source: "none", error: `Canonical session IR is invalid at ${projectionPath}.` };
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
return {
|
|
1108
|
+
ir: null,
|
|
1109
|
+
source: "none",
|
|
1110
|
+
error: `Canonical session IR is missing, unreadable, or malformed at ${projectionPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async function parseIR(data, irPath) {
|
|
1115
|
+
if (!data || typeof data !== "object")
|
|
907
1116
|
return null;
|
|
1117
|
+
const record = data;
|
|
1118
|
+
if (record.ir_version && record.ir) {
|
|
1119
|
+
if (record.ir_version !== EXPECTED_IR_VERSION) {
|
|
1120
|
+
process.stderr.write(`\n Intent DNA: IR version mismatch (v${record.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
|
|
1121
|
+
` Fix: run \`dna sync\` to recompile\n\n`);
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
return record.ir;
|
|
1125
|
+
}
|
|
1126
|
+
if (record.compiled_at && record.source_dna_ids)
|
|
1127
|
+
return record;
|
|
1128
|
+
process.stderr.write(`\n Intent DNA: unrecognized IR format at ${irPath}\n` +
|
|
1129
|
+
` Fix: run \`dna sync\` to regenerate\n\n`);
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
async function loadIR(irPath) {
|
|
1133
|
+
try {
|
|
1134
|
+
const raw = await readFile(irPath, "utf-8");
|
|
1135
|
+
return parseIR(JSON.parse(raw), irPath);
|
|
908
1136
|
}
|
|
909
1137
|
catch (err) {
|
|
910
1138
|
// U3: Human-readable error instead of silent failure
|
|
@@ -1122,6 +1350,10 @@ export const resolveWorkflowArtifactFactsForTest = resolveWorkflowArtifactFacts;
|
|
|
1122
1350
|
export const readWorkflowStateForHookTest = readWorkflowStateForHook;
|
|
1123
1351
|
export const runCodexHookWithLoadedIrForTest = runCodexHookWithLoadedIr;
|
|
1124
1352
|
export const resolveHookRuntimePathsForTest = resolveHookRuntimePaths;
|
|
1353
|
+
export const resolveHookRuntimeIdentityForTest = resolveHookRuntimeIdentity;
|
|
1354
|
+
export const loadHookIRForTest = loadHookIR;
|
|
1355
|
+
export const heartbeatCanonicalAttemptForTest = heartbeatCanonicalAttempt;
|
|
1356
|
+
export const recordCanonicalHeartbeatOutcomeForTest = recordCanonicalHeartbeatOutcome;
|
|
1125
1357
|
export const inferWorkflowAssetFromIrForTest = inferWorkflowAssetFromIr;
|
|
1126
1358
|
export const resolveWorkflowArtifactFactsForHookTest = resolveWorkflowArtifactFactsForHook;
|
|
1127
1359
|
export const finalizeAndResolveArtifactsForHookTest = finalizeAndResolveArtifactsForHook;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DNAWorkflowState } from "./state.js";
|
|
2
|
+
export declare const CLAUDE_HOST_EVENT_SCHEMA_VERSION: "intentdna.claude_host_event.v1";
|
|
3
|
+
export declare const CLAUDE_HOST_EVENT_STDERR_PREFIX = "INTENTDNA_CLAUDE_HOST_EVENT_V1 ";
|
|
4
|
+
export interface ClaudeSubagentStopHostEvent {
|
|
5
|
+
readonly schema_version: typeof CLAUDE_HOST_EVENT_SCHEMA_VERSION;
|
|
6
|
+
readonly hook_event_name: "SubagentStop";
|
|
7
|
+
readonly session_id: string;
|
|
8
|
+
readonly transcript_path: string;
|
|
9
|
+
readonly agent_id: string;
|
|
10
|
+
readonly agent_type: string;
|
|
11
|
+
readonly agent_transcript_path: string;
|
|
12
|
+
readonly last_assistant_message_digest: `sha256:${string}`;
|
|
13
|
+
readonly run_id: string;
|
|
14
|
+
readonly step_id: string;
|
|
15
|
+
readonly attempt_id: string;
|
|
16
|
+
readonly worker_session_id: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ClaudeHookResponseFrame {
|
|
19
|
+
readonly type: "system";
|
|
20
|
+
readonly subtype: "hook_response";
|
|
21
|
+
readonly hook_id: string;
|
|
22
|
+
readonly hook_event: "SubagentStop";
|
|
23
|
+
readonly session_id: string;
|
|
24
|
+
readonly uuid: string;
|
|
25
|
+
readonly outcome: "success";
|
|
26
|
+
readonly exit_code: 0;
|
|
27
|
+
readonly stderr: string;
|
|
28
|
+
}
|
|
29
|
+
export interface VerifiedClaudeSubagentStopHostEvent {
|
|
30
|
+
readonly hook_id: string;
|
|
31
|
+
readonly frame_uuid: string;
|
|
32
|
+
readonly event: ClaudeSubagentStopHostEvent;
|
|
33
|
+
}
|
|
34
|
+
export interface ClaudeSubagentStopHostEventExpectation {
|
|
35
|
+
readonly hook_id: string;
|
|
36
|
+
readonly session_id: string;
|
|
37
|
+
readonly run_id: string;
|
|
38
|
+
readonly step_id: string;
|
|
39
|
+
readonly attempt_id: string;
|
|
40
|
+
readonly worker_session_id: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ClaudeHostEventConsumptionKey {
|
|
43
|
+
readonly session_id: string;
|
|
44
|
+
readonly frame_uuid: string;
|
|
45
|
+
readonly hook_id: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ClaudeHostEventConsumptionStore {
|
|
48
|
+
consume(key: ClaudeHostEventConsumptionKey): Promise<boolean>;
|
|
49
|
+
}
|
|
50
|
+
export type ClaudeHostEventErrorCode = "invalid_frame" | "missing_envelope" | "duplicate_envelope" | "malformed_envelope" | "identity_mismatch" | "context_mismatch" | "replayed_frame";
|
|
51
|
+
export declare class ClaudeHostEventError extends Error {
|
|
52
|
+
readonly code: ClaudeHostEventErrorCode;
|
|
53
|
+
constructor(code: ClaudeHostEventErrorCode, message: string);
|
|
54
|
+
}
|
|
55
|
+
export declare function buildClaudeSubagentStopHostEvent(input: Record<string, unknown>, state: DNAWorkflowState | null): ClaudeSubagentStopHostEvent | null;
|
|
56
|
+
export declare function encodeClaudeHostEventForStderr(event: ClaudeSubagentStopHostEvent): string;
|
|
57
|
+
export declare function parseClaudeSubagentStopHookResponse(frame: unknown, expected: ClaudeSubagentStopHostEventExpectation, consumptionStore: ClaudeHostEventConsumptionStore): Promise<VerifiedClaudeSubagentStopHostEvent>;
|