@cairnkeep/cli 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +221 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +1 -1
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/operating.md +531 -4
  16. package/docs/privacy-and-data-flow.md +417 -0
  17. package/docs/storage.md +252 -0
  18. package/examples/eval/README.md +42 -0
  19. package/examples/eval/adapter.json +12 -0
  20. package/examples/eval/bundled-fake.json +1 -0
  21. package/examples/eval/task-set.json +1 -0
  22. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  23. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  24. package/mcp-memory-server/dist/artifact-store.js +812 -0
  25. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  26. package/mcp-memory-server/dist/capability-cli.js +361 -0
  27. package/mcp-memory-server/dist/capability-config.js +310 -0
  28. package/mcp-memory-server/dist/capability-harness.js +496 -0
  29. package/mcp-memory-server/dist/capability-registry.js +123 -0
  30. package/mcp-memory-server/dist/capability-schema.js +65 -0
  31. package/mcp-memory-server/dist/capability-store.js +431 -0
  32. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  33. package/mcp-memory-server/dist/eval-cli.js +523 -0
  34. package/mcp-memory-server/dist/eval-plan.js +346 -0
  35. package/mcp-memory-server/dist/eval-process.js +273 -0
  36. package/mcp-memory-server/dist/eval-report.js +609 -0
  37. package/mcp-memory-server/dist/eval-runner.js +827 -0
  38. package/mcp-memory-server/dist/eval-schema.js +368 -0
  39. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  40. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  41. package/mcp-memory-server/dist/failure-signature.js +141 -0
  42. package/mcp-memory-server/dist/index.js +600 -55
  43. package/mcp-memory-server/dist/node-cli.js +84 -0
  44. package/mcp-memory-server/dist/node-schema.js +170 -0
  45. package/mcp-memory-server/dist/node-store.js +455 -0
  46. package/mcp-memory-server/dist/note-cli.js +235 -0
  47. package/mcp-memory-server/dist/note-distiller.js +177 -0
  48. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  49. package/mcp-memory-server/dist/note-schema.js +4 -0
  50. package/mcp-memory-server/dist/note-store.js +985 -0
  51. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  52. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  53. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  54. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  55. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  56. package/mcp-memory-server/package.json +24 -2
  57. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  58. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  59. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  60. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  61. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  62. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  63. package/opencode/command/graphify.md +25 -14
  64. package/opencode/plugins/memory-capture.ts +76 -10
  65. package/opencode/plugins/memory-wakeup.ts +58 -6
  66. package/package.json +9 -4
  67. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  68. package/schemas/artifact.schema.json +133 -0
  69. package/schemas/capability-callback.schema.json +68 -0
  70. package/schemas/capability-contract.schema.json +126 -0
  71. package/schemas/eval-adapter.schema.json +43 -0
  72. package/schemas/eval-protocol.schema.json +114 -0
  73. package/schemas/eval-report.schema.json +423 -0
  74. package/schemas/eval-task-set.schema.json +114 -0
  75. package/schemas/memory-node.schema.json +90 -0
  76. package/schemas/note.schema.json +112 -0
  77. package/schemas/trajectory-redaction.schema.json +25 -0
  78. package/schemas/trajectory.schema.json +82 -0
  79. package/scripts/bootstrap.sh +4 -0
  80. package/scripts/completion.sh +119 -5
  81. package/scripts/doctor.sh +284 -0
  82. package/scripts/fake-eval-adapter.mjs +120 -0
  83. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  84. package/scripts/install-audit-timer.sh +3 -1
  85. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  86. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  87. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  88. package/scripts/memory-wiki-audit.sh +49 -0
  89. package/scripts/smoke-bash32.sh +9 -0
  90. package/scripts/sync-claude-assets.sh +99 -6
  91. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  92. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  93. package/scripts/sync-opencode-security-assets.sh +24 -3
  94. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  95. package/scripts/sync-pi-assets.sh +72 -0
  96. package/scripts/test-audit-timer.sh +98 -0
  97. package/scripts/test-bootstrap-untracked.sh +48 -0
  98. package/scripts/test-cli-dispatch.sh +41 -2
  99. package/scripts/test-compaction-hooks.sh +495 -0
  100. package/scripts/test-completion.sh +77 -0
  101. package/scripts/test-doctor.sh +348 -1
  102. package/scripts/test-eval-cli.sh +175 -0
  103. package/scripts/test-launcher-hooks.sh +602 -3
  104. package/scripts/test-notes-cli.sh +114 -0
  105. package/scripts/test-package-install.sh +251 -0
  106. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  107. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  108. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  109. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  110. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  111. package/scripts/test-trajectory-cli.sh +87 -0
  112. package/scripts/test-trajectory-hooks.sh +142 -0
  113. package/scripts/test-uninstall.sh +231 -5
  114. package/scripts/uninstall.sh +37 -5
  115. package/scripts/verify-dependency-security.mjs +121 -0
  116. package/scripts/verify-docs-parity.sh +527 -1
  117. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  118. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  119. package/templates/capabilities.json.template +7 -0
  120. package/templates/env.example.template +51 -0
  121. package/templates/memory-wiki-audit.service.template +1 -1
  122. package/templates/start-claude.sh.template +36 -0
  123. package/templates/start-opencode.sh.template +47 -5
  124. package/templates/start-pi.sh.template +30 -0
  125. package/templates/trajectory-redaction.json.template +4 -0
@@ -0,0 +1,895 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ renameSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { basename, dirname, join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const SCRIPT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
19
+ const EVIDENCE_RELATIVE = join(
20
+ ".planning",
21
+ "phases",
22
+ "18-typed-capability-contract",
23
+ "runtime-evidence",
24
+ );
25
+ const EXPECTED_LOGS = ["node-22.log", "node-24.log", "node-26.log", "bash-3.2.log"];
26
+ const COMMIT_PATTERN = /^[0-9a-f]{40}$/;
27
+ const NODE_IMAGES = new Map([
28
+ ["node-22", "node:22-bookworm"],
29
+ ["node-24", "node:24-bookworm"],
30
+ ["node-26", "node:26-bookworm"],
31
+ ]);
32
+ const BASH_IMAGE = "docker.io/library/bash:3.2";
33
+ const CANONICAL_CAPABILITIES = [
34
+ "memory.write",
35
+ "memory.search",
36
+ "notes.distill",
37
+ "wiki",
38
+ "graph",
39
+ "security.audit",
40
+ "route.check",
41
+ "context.explore",
42
+ ];
43
+ const PUBLIC_OPERATIONS = new Map([
44
+ ["memory.write", "memory_write"],
45
+ ["memory.search", "memory_search"],
46
+ ["notes.distill", "cairn note distill"],
47
+ ["wiki", "wiki-query"],
48
+ ["graph", "graphify"],
49
+ ["security.audit", "security-audit"],
50
+ ["route.check", "route_check"],
51
+ ["context.explore", "context_explore"],
52
+ ]);
53
+ const DUAL_HARNESS_OWNERS = new Set(["wiki", "graph", "security.audit"]);
54
+ const NODE_COMMANDS = [
55
+ ["node-version", "node --version", "runtime-identity"],
56
+ [
57
+ "fixture-git-init",
58
+ "rm -f .git && git init -q && git fetch -q .cairn-runtime-evidence.bundle HEAD && git reset -q --mixed FETCH_HEAD && rm -f .cairn-runtime-evidence.bundle && git config user.name 'Runtime Evidence' && git config user.email 'runtime-evidence@example.invalid'",
59
+ "runtime-setup",
60
+ ],
61
+ ["root-install", "npm ci --offline", "runtime-setup"],
62
+ ["server-install", "npm --prefix mcp-memory-server ci --offline", "runtime-setup"],
63
+ ["server-build", "npm --prefix mcp-memory-server run build", "core-lifecycle"],
64
+ ["server-test", "npm --prefix mcp-memory-server test", "core-lifecycle"],
65
+ [
66
+ "operating-sequential",
67
+ "node mcp-memory-server/scripts/smoke-capability-logging.mjs --operating-sequential-only",
68
+ "invocation-isolation",
69
+ ],
70
+ [
71
+ "session-isolation",
72
+ "node mcp-memory-server/scripts/smoke-capability-harness.mjs --session-isolation",
73
+ "policy-isolation",
74
+ ],
75
+ ["capability-lifecycle", "bash scripts/test-phase18-capability-lifecycle.sh full", "core-lifecycle"],
76
+ ["native-boundary", "bash scripts/test-phase18-capability-lifecycle.sh native-boundary", "native-boundary"],
77
+ ["claude-native", "bash scripts/test-phase18-harness-boundary.sh claude-hooks", "claude-native"],
78
+ ["opencode-native", "bash scripts/test-phase18-harness-boundary.sh opencode-plugin", "opencode-native"],
79
+ ["opencode-disposal", "bash scripts/test-phase18-harness-boundary.sh opencode-disposal", "opencode-native"],
80
+ ["opencode-overlay", "bash scripts/test-phase18-harness-boundary.sh opencode-sync-modes", "install-lifecycle"],
81
+ ["launcher-overlay", "bash scripts/test-launcher-hooks.sh opencode-all-sync-modes", "install-lifecycle"],
82
+ ["launcher-recovery", "bash scripts/test-launcher-hooks.sh capability-recovery", "production-lifecycle"],
83
+ ["package-install", "bash scripts/test-package-install.sh", "install-lifecycle"],
84
+ ["uninstall", "bash scripts/test-uninstall.sh", "install-lifecycle"],
85
+ ["docs-parity", "bash scripts/verify-docs-parity.sh", "documentation-parity"],
86
+ ["root-test", "npm test", "full-regression"],
87
+ ["repository-hygiene", "bash scripts/verify-no-private-references.sh", "repository-hygiene"],
88
+ ];
89
+ const BASH_COMMANDS = [
90
+ ["bash-version", "bash --version", "runtime-identity"],
91
+ [
92
+ "bash-syntax",
93
+ "bash -n scripts/test-phase18-capability-lifecycle.sh scripts/test-phase18-harness-boundary.sh templates/start-claude.sh.template templates/start-opencode.sh.template",
94
+ "runtime-portability",
95
+ ],
96
+ ["portable-shell", "bash scripts/test-portable-sh.sh", "runtime-portability"],
97
+ ];
98
+ const SUPPORTED_EVIDENCE_SCOPES = new Set([
99
+ "runtime-identity",
100
+ "runtime-setup",
101
+ "runtime-portability",
102
+ "core-lifecycle",
103
+ "invocation-isolation",
104
+ "policy-isolation",
105
+ "native-boundary",
106
+ "claude-native",
107
+ "opencode-native",
108
+ "install-lifecycle",
109
+ "production-lifecycle",
110
+ "documentation-parity",
111
+ "full-regression",
112
+ "repository-hygiene",
113
+ ]);
114
+
115
+ function usage() {
116
+ return `Usage:
117
+ node scripts/verify-phase18-runtime-evidence.mjs
118
+ node scripts/verify-phase18-runtime-evidence.mjs --self-test
119
+ node scripts/verify-phase18-runtime-evidence.mjs --capture <evidence-dir> <source-commit>
120
+ node scripts/verify-phase18-runtime-evidence.mjs --verify-only <evidence-dir> <source-commit>
121
+ node scripts/verify-phase18-runtime-evidence.mjs --live-matrix <matrix-path>\n`;
122
+ }
123
+
124
+ function run(command, args, options = {}) {
125
+ return spawnSync(command, args, {
126
+ cwd: options.cwd ?? SCRIPT_ROOT,
127
+ encoding: "utf8",
128
+ env: options.env ?? process.env,
129
+ timeout: options.timeout ?? 120_000,
130
+ maxBuffer: 64 * 1024 * 1024,
131
+ });
132
+ }
133
+
134
+ function combined(result) {
135
+ return `${result.stdout ?? ""}${result.stderr ?? ""}`;
136
+ }
137
+
138
+ function requireSuccess(result, label) {
139
+ if (result.status !== 0) {
140
+ throw new Error(`${label} failed (exit ${String(result.status)}):\n${combined(result)}`);
141
+ }
142
+ return String(result.stdout ?? "").trim();
143
+ }
144
+
145
+ function requireCommit(value, label = "source commit") {
146
+ if (!COMMIT_PATTERN.test(value)) throw new Error(`${label} must be one lowercase 40-hex commit.`);
147
+ return value;
148
+ }
149
+
150
+ function git(repo, args, options = {}) {
151
+ return run("git", ["-C", repo, ...args], options);
152
+ }
153
+
154
+ function resolveCommit(repo, value) {
155
+ requireCommit(value);
156
+ const resolved = requireSuccess(git(repo, ["rev-parse", `${value}^{commit}`]), "resolve source commit");
157
+ if (resolved !== value) throw new Error("The supplied source commit does not resolve exactly.");
158
+ return resolved;
159
+ }
160
+
161
+ function assertCallerSourceClean(repo, sourceCommit) {
162
+ const head = requireSuccess(git(repo, ["rev-parse", "HEAD"]), "read caller HEAD");
163
+ if (head !== sourceCommit) throw new Error("Caller HEAD does not match the supplied source commit.");
164
+ for (const [label, args] of [
165
+ ["worktree", ["diff", "--quiet", "HEAD", "--", ".", ":(exclude).planning/**"]],
166
+ ["index", ["diff", "--cached", "--quiet", "--", ".", ":(exclude).planning/**"]],
167
+ ]) {
168
+ const result = git(repo, args);
169
+ if (result.status !== 0) throw new Error(`Caller tracked source ${label} differs from HEAD.`);
170
+ }
171
+ }
172
+
173
+ function assertDetachedSourceClean(worktree, sourceCommit) {
174
+ const head = requireSuccess(git(worktree, ["rev-parse", "HEAD"]), "read detached HEAD");
175
+ if (head !== sourceCommit) throw new Error("Detached source commit does not match the requested commit.");
176
+ const status = requireSuccess(
177
+ git(worktree, ["status", "--porcelain", "--untracked-files=no"]),
178
+ "inspect detached tracked source",
179
+ );
180
+ if (status !== "") throw new Error("Detached source has tracked differences.");
181
+ if (git(worktree, ["diff", "--quiet", "HEAD", "--", "."]).status !== 0
182
+ || git(worktree, ["diff", "--cached", "--quiet", "--", "."]).status !== 0) {
183
+ throw new Error("Detached source worktree or index differs from HEAD.");
184
+ }
185
+ }
186
+
187
+ function withDetachedWorktree(repo, sourceCommit, operation) {
188
+ const parent = mkdtempSync(join(tmpdir(), "cairn-phase18-detached-"));
189
+ const worktree = join(parent, "source");
190
+ let added = false;
191
+ try {
192
+ requireSuccess(
193
+ git(repo, ["worktree", "add", "--detach", worktree, sourceCommit], { timeout: 120_000 }),
194
+ "create detached source worktree",
195
+ );
196
+ added = true;
197
+ assertDetachedSourceClean(worktree, sourceCommit);
198
+ return operation(worktree);
199
+ } finally {
200
+ if (added) git(repo, ["worktree", "remove", "--force", worktree], { timeout: 120_000 });
201
+ rmSync(parent, { recursive: true, force: true });
202
+ }
203
+ }
204
+
205
+ function findContainerEngine() {
206
+ for (const engine of ["docker", "podman"]) {
207
+ if (run(engine, ["--version"]).status === 0) return engine;
208
+ }
209
+ throw new Error("Docker or Podman is required for Phase 18 runtime evidence.");
210
+ }
211
+
212
+ function ensureImage(engine, image) {
213
+ if (run(engine, ["image", "inspect", image], { timeout: 120_000 }).status === 0) return;
214
+ requireSuccess(run(engine, ["pull", image], { timeout: 600_000 }), `pull standard image ${image}`);
215
+ }
216
+
217
+ function renderCommand(command, args) {
218
+ return [command, ...args].map((value) => JSON.stringify(String(value))).join(" ");
219
+ }
220
+
221
+ function shellEvidenceRunner() {
222
+ return `run_evidence() {
223
+ evidence_id="$1"
224
+ evidence_scope="$2"
225
+ evidence_command="$3"
226
+ printf 'EVIDENCE_COMMAND_BEGIN|%s|%s|%s\\n' "$evidence_id" "$evidence_scope" "$evidence_command"
227
+ set +e
228
+ bash -c "$evidence_command"
229
+ evidence_status=$?
230
+ set -e
231
+ if [ "$evidence_status" -eq 0 ]; then evidence_result=pass; else evidence_result=fail; fi
232
+ printf 'EVIDENCE_COMMAND_END|%s|%s|%s|%s\\n' "$evidence_id" "$evidence_scope" "$evidence_status" "$evidence_result"
233
+ return "$evidence_status"
234
+ }
235
+ `;
236
+ }
237
+
238
+ function shellCommandRows(commands) {
239
+ return commands.map(([id, command, scope]) => (
240
+ `run_evidence ${JSON.stringify(id)} ${JSON.stringify(scope)} ${JSON.stringify(command)}`
241
+ )).join("\n");
242
+ }
243
+
244
+ function suiteHeader(runtime, versionCommand) {
245
+ return `printf 'generated_at=%s\\n' "$CAIRN_EVIDENCE_TIMESTAMP"
246
+ printf 'runtime_version=%s\\n' "$(${versionCommand})"
247
+ printf 'fixture_claude_version=%s\\n' "$CAIRN_CLAUDE_FIXTURE_VERSION"
248
+ printf 'fixture_opencode_version=%s\\n' "$CAIRN_OPENCODE_FIXTURE_VERSION"
249
+ printf 'acceptance=blocked-pending-live-matrix\\n'
250
+ printf 'evidence_scope=deterministic-native-lifecycle\\n'
251
+ `;
252
+ }
253
+
254
+ function nodeSuite(runtime) {
255
+ return `set -eu
256
+ exec 2>&1
257
+ mkdir -p /work/source /tmp/npm-logs
258
+ cp -a /source/. /work/source/
259
+ cd /work/source
260
+ export NPM_CONFIG_CACHE=/npm-cache
261
+ export NPM_CONFIG_LOGS_DIR=/tmp/npm-logs
262
+ export NPM_CONFIG_AUDIT=false
263
+ export NPM_CONFIG_FUND=false
264
+ ${suiteHeader(runtime, "node --version")}${shellEvidenceRunner()}${shellCommandRows(NODE_COMMANDS)}
265
+ printf 'PASS: Phase 18 runtime ${runtime}\\n'`;
266
+ }
267
+
268
+ function bashSuite() {
269
+ return `set -eu
270
+ exec 2>&1
271
+ mkdir -p /work/source
272
+ cp -a /source/. /work/source/
273
+ cd /work/source
274
+ ${suiteHeader("bash-3.2", "bash --version | head -n 1")}${shellEvidenceRunner()}${shellCommandRows(BASH_COMMANDS)}
275
+ printf 'PASS: Phase 18 runtime bash-3.2\\n'`;
276
+ }
277
+
278
+ function runContainer(engine, image, runtime, worktree, npmCache, generatedAt, fixtureVersions) {
279
+ const script = runtime.startsWith("node-") ? nodeSuite(runtime) : bashSuite();
280
+ const args = ["run", "--rm"];
281
+ if (!runtime.startsWith("node-")) args.push("--network", "none");
282
+ args.push("--mount",
283
+ `type=bind,src=${worktree},dst=/source,readonly`,
284
+ "--env", `CAIRN_EVIDENCE_TIMESTAMP=${generatedAt}`,
285
+ "--env", `CAIRN_CLAUDE_FIXTURE_VERSION=${fixtureVersions.claude}`,
286
+ "--env", `CAIRN_OPENCODE_FIXTURE_VERSION=${fixtureVersions.opencode}`);
287
+ if (runtime.startsWith("node-")) {
288
+ args.push("--mount", `type=bind,src=${npmCache},dst=/npm-cache`);
289
+ }
290
+ args.push(image, "bash", "-c", script);
291
+ const command = renderCommand(engine, args);
292
+ const result = run(engine, args, { timeout: 900_000 });
293
+ return { command, output: combined(result), status: result.status };
294
+ }
295
+
296
+ function logHeader(sourceCommit, runtime, image, command) {
297
+ return [
298
+ "evidence_schema_version=2",
299
+ `source_commit=${sourceCommit}`,
300
+ `runtime=${runtime}`,
301
+ `container_image=${image}`,
302
+ `container_command=${command}`,
303
+ "source_tree_clean=true",
304
+ ].join("\n");
305
+ }
306
+
307
+ function requireLog(text, expected) {
308
+ const lines = text.split(/\r?\n/);
309
+ if (lines[0] !== "evidence_schema_version=2") throw new Error(`${expected.file}: wrong log schema version.`);
310
+ if (lines[1] !== `source_commit=${expected.commit}`) throw new Error(`${expected.file}: wrong source commit header.`);
311
+ if (lines[2] !== `runtime=${expected.runtime}`) throw new Error(`${expected.file}: wrong runtime label.`);
312
+ if (lines[3] !== `container_image=${expected.image}`) throw new Error(`${expected.file}: wrong container image.`);
313
+ if (lines[4] !== `container_command=${expected.command}` || !lines[4].includes(expected.image)) {
314
+ throw new Error(`${expected.file}: missing exact container command.`);
315
+ }
316
+ if (lines[5] !== "source_tree_clean=true") throw new Error(`${expected.file}: detached source was not recorded clean.`);
317
+ const expectedMajor = expected.runtime.startsWith("node-") ? `v${expected.runtime.slice(5)}.` : "GNU bash, version 3.2";
318
+ for (const marker of [
319
+ `generated_at=${expected.generatedAt}`,
320
+ `runtime_version=${expectedMajor}`,
321
+ "fixture_claude_version=2.1.220",
322
+ "fixture_opencode_version=1.17.20",
323
+ "acceptance=blocked-pending-live-matrix",
324
+ "evidence_scope=deterministic-native-lifecycle",
325
+ `PASS: Phase 18 runtime ${expected.runtime}`,
326
+ ]) {
327
+ if (!text.includes(marker)) throw new Error(`${expected.file}: missing ${marker}.`);
328
+ }
329
+ }
330
+
331
+ function sha256(text) {
332
+ return createHash("sha256").update(text).digest("hex");
333
+ }
334
+
335
+ function parseCommandRecords(text, file) {
336
+ const records = [];
337
+ let pending = null;
338
+ for (const line of text.split(/\r?\n/)) {
339
+ if (line.startsWith("EVIDENCE_COMMAND_BEGIN|")) {
340
+ if (pending !== null) throw new Error(`${file}: nested command evidence.`);
341
+ const [marker, id, evidenceScope, ...commandParts] = line.split("|");
342
+ const command = commandParts.join("|");
343
+ if (marker !== "EVIDENCE_COMMAND_BEGIN" || !id || !evidenceScope || !command) {
344
+ throw new Error(`${file}: malformed command start record.`);
345
+ }
346
+ pending = { id, command, evidence_scope: evidenceScope };
347
+ } else if (line.startsWith("EVIDENCE_COMMAND_END|")) {
348
+ const [marker, id, evidenceScope, statusText, result] = line.split("|");
349
+ if (marker !== "EVIDENCE_COMMAND_END" || pending === null) {
350
+ throw new Error(`${file}: command end has no matching start.`);
351
+ }
352
+ if (id !== pending.id || evidenceScope !== pending.evidence_scope) {
353
+ throw new Error(`${file}: command evidence identity changed during execution.`);
354
+ }
355
+ const exitStatus = Number(statusText);
356
+ if (!Number.isSafeInteger(exitStatus) || exitStatus < 0 || !["pass", "fail"].includes(result)) {
357
+ throw new Error(`${file}: malformed command result record.`);
358
+ }
359
+ records.push({ ...pending, exit_status: exitStatus, result });
360
+ pending = null;
361
+ }
362
+ }
363
+ if (pending !== null) throw new Error(`${file}: command evidence is missing its exit record.`);
364
+ return records;
365
+ }
366
+
367
+ function expectedCommands(runtime) {
368
+ return (runtime.startsWith("node-") ? NODE_COMMANDS : BASH_COMMANDS).map(
369
+ ([id, command, evidence_scope]) => ({ id, command, evidence_scope, exit_status: 0, result: "pass" }),
370
+ );
371
+ }
372
+
373
+ function requireIsoTimestamp(value, label) {
374
+ assert.equal(typeof value, "string", `${label}: timestamp missing`);
375
+ assert.equal(new Date(value).toISOString(), value, `${label}: timestamp is not canonical ISO-8601`);
376
+ }
377
+
378
+ function verifyEvidence(evidenceDirectory, sourceCommit) {
379
+ requireCommit(sourceCommit);
380
+ const manifestPath = join(evidenceDirectory, "manifest.json");
381
+ if (!existsSync(manifestPath)) throw new Error("Evidence manifest is missing.");
382
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
383
+ assert.equal(manifest.schema_version, 2, "manifest schema version mismatch");
384
+ assert.equal(manifest.source_commit, sourceCommit, "manifest source commit mismatch");
385
+ assert.equal(manifest.source_tree_clean, true, "manifest detached source is not clean");
386
+ assert.equal(manifest.detached_source, true, "manifest did not record detached source");
387
+ requireIsoTimestamp(manifest.generated_at, "manifest");
388
+ assert.equal(manifest.evidence_scope, "deterministic-native-lifecycle", "manifest evidence scope mismatch");
389
+ assert.deepEqual(manifest.acceptance, {
390
+ status: "blocked-pending-live-matrix",
391
+ accepted: false,
392
+ required_cells: 56,
393
+ }, "deterministic evidence cannot claim Phase 18 acceptance");
394
+ assert.deepEqual(manifest.logs?.map(({ file }) => file), EXPECTED_LOGS, "manifest log names mismatch");
395
+ assert.equal(manifest.logs.length, EXPECTED_LOGS.length, "manifest log count mismatch");
396
+
397
+ for (const row of manifest.logs) {
398
+ const expectedRuntime = row.file === "bash-3.2.log" ? "bash-3.2" : row.file.replace(/\.log$/, "");
399
+ assert.equal(row.runtime, expectedRuntime, `${row.file}: manifest runtime mismatch`);
400
+ assert.equal(row.source_commit, sourceCommit, `${row.file}: manifest mixed source commit`);
401
+ assert.equal(row.source_tree_clean, true, `${row.file}: manifest source not clean`);
402
+ assert.equal(typeof row.container_command, "string", `${row.file}: manifest command missing`);
403
+ const expectedImage = row.runtime === "bash-3.2" ? BASH_IMAGE : NODE_IMAGES.get(row.runtime);
404
+ assert.equal(row.image, expectedImage, `${row.file}: manifest image mismatch`);
405
+ assert.equal(row.result, "pass", `${row.file}: runtime result is not passing`);
406
+ requireIsoTimestamp(row.generated_at, row.file);
407
+ assert.equal(row.generated_at, manifest.generated_at, `${row.file}: timestamp differs from manifest`);
408
+ const logPath = join(evidenceDirectory, row.file);
409
+ if (!existsSync(logPath)) throw new Error(`${row.file}: runtime log is missing.`);
410
+ const text = readFileSync(logPath, "utf8");
411
+ assert.match(row.sha256, /^[0-9a-f]{64}$/, `${row.file}: SHA-256 is missing`);
412
+ assert.equal(sha256(text), row.sha256, `${row.file}: stale log hash`);
413
+ requireLog(text, {
414
+ file: row.file,
415
+ runtime: row.runtime,
416
+ image: expectedImage,
417
+ commit: sourceCommit,
418
+ generatedAt: row.generated_at,
419
+ command: row.container_command,
420
+ });
421
+ const records = parseCommandRecords(text, row.file);
422
+ assert.deepEqual(records, expectedCommands(row.runtime), `${row.file}: required command inventory mismatch`);
423
+ assert.deepEqual(row.commands, records, `${row.file}: manifest command records differ from log`);
424
+ for (const command of row.commands) {
425
+ assert.equal(SUPPORTED_EVIDENCE_SCOPES.has(command.evidence_scope), true,
426
+ `${row.file}: unsupported evidence scope ${command.evidence_scope}`);
427
+ assert.equal(command.exit_status, 0, `${row.file}: ${command.id} failed`);
428
+ assert.equal(command.result, "pass", `${row.file}: ${command.id} did not pass`);
429
+ }
430
+ }
431
+ return manifest;
432
+ }
433
+
434
+ function requireObservation(value, label) {
435
+ assert.equal(value !== null && typeof value === "object" && !Array.isArray(value), true,
436
+ `${label}: observation must be an object`);
437
+ assert.equal(typeof value.result, "string", `${label}: result is missing`);
438
+ assert.equal(value.result.length > 0, true, `${label}: result is empty`);
439
+ assert.equal(value.error === null || typeof value.error === "string", true, `${label}: error is invalid`);
440
+ assert.equal(typeof value.timeout, "boolean", `${label}: timeout is missing`);
441
+ assert.equal(typeof value.trace, "string", `${label}: trace is missing`);
442
+ assert.equal(value.trace.length > 0, true, `${label}: trace is empty`);
443
+ assert.equal(typeof value.delegate_identity, "string", `${label}: delegate identity is missing`);
444
+ assert.equal(value.delegate_identity.length > 0, true, `${label}: delegate identity is empty`);
445
+ }
446
+
447
+ function validateLiveMatrix(matrixPath) {
448
+ if (!existsSync(matrixPath)) throw new Error("Live matrix file is missing.");
449
+ const matrix = JSON.parse(readFileSync(matrixPath, "utf8"));
450
+ assert.equal(matrix.schema_version, 1, "live matrix schema version mismatch");
451
+ assert.equal(matrix.status, "pass", "live matrix status must be pass");
452
+ assert.equal(Array.isArray(matrix.cells), true, "live matrix cells are missing");
453
+ assert.equal(matrix.cells.length, 56, "live matrix must contain exactly 56 cells");
454
+ const expectedPairs = new Set();
455
+ for (const disabledCapability of CANONICAL_CAPABILITIES) {
456
+ for (const survivingOwner of CANONICAL_CAPABILITIES) {
457
+ if (disabledCapability !== survivingOwner) expectedPairs.add(`${disabledCapability}\0${survivingOwner}`);
458
+ }
459
+ }
460
+ const seen = new Set();
461
+ for (const [index, cell] of matrix.cells.entries()) {
462
+ const label = `live matrix cell ${index + 1}`;
463
+ assert.equal(cell !== null && typeof cell === "object" && !Array.isArray(cell), true, `${label}: invalid cell`);
464
+ assert.equal(CANONICAL_CAPABILITIES.includes(cell.disabled_capability), true,
465
+ `${label}: disabled capability is not canonical`);
466
+ assert.equal(CANONICAL_CAPABILITIES.includes(cell.surviving_owner), true,
467
+ `${label}: surviving owner is not canonical`);
468
+ assert.notEqual(cell.disabled_capability, cell.surviving_owner, `${label}: owner cannot survive its own ablation`);
469
+ const pair = `${cell.disabled_capability}\0${cell.surviving_owner}`;
470
+ assert.equal(expectedPairs.has(pair), true, `${label}: unexpected matrix pair`);
471
+ assert.equal(seen.has(pair), false, `${label}: duplicate matrix pair`);
472
+ seen.add(pair);
473
+ assert.equal(cell.operation, PUBLIC_OPERATIONS.get(cell.surviving_owner), `${label}: wrong public owner operation`);
474
+ assert.equal(cell.real_operation, true, `${label}: spies or simulations cannot satisfy a cell`);
475
+ assert.equal(cell.supported_seam, "public-installed-owner", `${label}: supported public seam is missing`);
476
+ assert.equal(cell.availability, "available", `${label}: unavailable providers, credentials, runtimes, or seams fail`);
477
+ requireObservation(cell.baseline, `${label} baseline`);
478
+ requireObservation(cell.observed, `${label} observed`);
479
+ assert.deepEqual(cell.observed, cell.baseline, `${label}: observed owner behavior differs from baseline`);
480
+ assert.equal(cell.equality, "pass", `${label}: baseline comparison did not pass`);
481
+ assert.equal(typeof cell.evidence_ref, "string", `${label}: evidence reference is missing`);
482
+ assert.equal(cell.evidence_ref.length > 0, true, `${label}: evidence reference is empty`);
483
+ if (DUAL_HARNESS_OWNERS.has(cell.surviving_owner)) {
484
+ for (const harness of ["claude", "opencode"]) {
485
+ const proof = cell.installed_harnesses?.[harness];
486
+ assert.equal(proof?.result, "success", `${label}: ${harness} installed-owner observation did not succeed`);
487
+ assert.equal(typeof proof?.evidence_ref, "string", `${label}: ${harness} evidence reference is missing`);
488
+ assert.equal(proof.evidence_ref.length > 0, true, `${label}: ${harness} evidence reference is empty`);
489
+ }
490
+ }
491
+ }
492
+ assert.deepEqual(seen, expectedPairs, "live matrix does not cover the exact eight-by-seven cross product");
493
+ return matrix;
494
+ }
495
+
496
+ function captureEvidence(evidenceDirectory, sourceCommit) {
497
+ const expectedDirectory = resolve(SCRIPT_ROOT, EVIDENCE_RELATIVE);
498
+ if (resolve(evidenceDirectory) !== expectedDirectory) {
499
+ throw new Error(`Capture output must be ${EVIDENCE_RELATIVE}.`);
500
+ }
501
+ resolveCommit(SCRIPT_ROOT, sourceCommit);
502
+ assertCallerSourceClean(SCRIPT_ROOT, sourceCommit);
503
+ const npmCache = requireSuccess(run("npm", ["config", "get", "cache"]), "resolve npm cache");
504
+ if (!existsSync(npmCache)) throw new Error("The local npm cache is unavailable for offline container installs.");
505
+ const engine = findContainerEngine();
506
+ for (const image of [...NODE_IMAGES.values(), BASH_IMAGE]) ensureImage(engine, image);
507
+
508
+ mkdirSync(dirname(expectedDirectory), { recursive: true, mode: 0o700 });
509
+ const staging = mkdtempSync(join(dirname(expectedDirectory), ".runtime-evidence-staging-"));
510
+ const generatedAt = new Date().toISOString();
511
+ try {
512
+ const logs = withDetachedWorktree(SCRIPT_ROOT, sourceCommit, (worktree) => {
513
+ const fixture = JSON.parse(readFileSync(join(worktree, "scripts", "fixtures", "capability-harness-contracts.json"), "utf8"));
514
+ const fixtureVersions = {
515
+ claude: fixture?.claude?.version,
516
+ opencode: fixture?.opencode?.version,
517
+ };
518
+ assert.equal(fixtureVersions.claude, "2.1.220", "Claude fixture version changed");
519
+ assert.equal(fixtureVersions.opencode, "1.17.20", "OpenCode fixture version changed");
520
+ const historyBundle = join(worktree, ".cairn-runtime-evidence.bundle");
521
+ requireSuccess(
522
+ git(worktree, ["bundle", "create", historyBundle, "HEAD"], { timeout: 120_000 }),
523
+ "create portable runtime evidence history",
524
+ );
525
+ const rows = [];
526
+ for (const [runtime, image] of [...NODE_IMAGES, ["bash-3.2", BASH_IMAGE]]) {
527
+ assertDetachedSourceClean(worktree, sourceCommit);
528
+ const result = runContainer(engine, image, runtime, worktree, npmCache, generatedAt, fixtureVersions);
529
+ assertDetachedSourceClean(worktree, sourceCommit);
530
+ const file = `${runtime}.log`;
531
+ const text = `${logHeader(sourceCommit, runtime, image, result.command)}\n${result.output.trimEnd()}\n`;
532
+ writeFileSync(join(staging, file), text, { encoding: "utf8", mode: 0o600 });
533
+ const commands = parseCommandRecords(text, file);
534
+ rows.push({
535
+ file,
536
+ runtime,
537
+ image,
538
+ container_command: result.command,
539
+ source_commit: sourceCommit,
540
+ source_tree_clean: true,
541
+ generated_at: generatedAt,
542
+ sha256: sha256(text),
543
+ result: result.status === 0 ? "pass" : "fail",
544
+ commands,
545
+ });
546
+ if (result.status !== 0) {
547
+ throw new Error(`${runtime} container failed (exit ${String(result.status)}):\n${result.output}`);
548
+ }
549
+ }
550
+ return rows;
551
+ });
552
+ const manifest = {
553
+ schema_version: 2,
554
+ source_commit: sourceCommit,
555
+ source_tree_clean: true,
556
+ detached_source: true,
557
+ generated_at: generatedAt,
558
+ evidence_scope: "deterministic-native-lifecycle",
559
+ acceptance: {
560
+ status: "blocked-pending-live-matrix",
561
+ accepted: false,
562
+ required_cells: 56,
563
+ },
564
+ logs,
565
+ };
566
+ writeFileSync(join(staging, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
567
+ verifyEvidence(staging, sourceCommit);
568
+
569
+ mkdirSync(expectedDirectory, { recursive: true, mode: 0o700 });
570
+ for (const file of [...EXPECTED_LOGS, "manifest.json"]) {
571
+ renameSync(join(staging, file), join(expectedDirectory, file));
572
+ }
573
+ verifyEvidence(expectedDirectory, sourceCommit);
574
+ process.stdout.write(`PASS: Phase 18 runtime evidence captured for ${sourceCommit}\n`);
575
+ } finally {
576
+ rmSync(staging, { recursive: true, force: true });
577
+ }
578
+ }
579
+
580
+ function fixtureLog(commit, runtime, image) {
581
+ const command = `docker run --rm ${image}`;
582
+ const commands = runtime.startsWith("node-") ? NODE_COMMANDS : BASH_COMMANDS;
583
+ const version = runtime.startsWith("node-")
584
+ ? `runtime_version=v${runtime.slice(5)}.0.0`
585
+ : "runtime_version=GNU bash, version 3.2.57(1)-release";
586
+ const records = commands.flatMap(([id, exact, scope]) => [
587
+ `EVIDENCE_COMMAND_BEGIN|${id}|${scope}|${exact}`,
588
+ id === "node-version" || id === "bash-version" ? version : `PASS: ${id}`,
589
+ `EVIDENCE_COMMAND_END|${id}|${scope}|0|pass`,
590
+ ]);
591
+ return [
592
+ logHeader(commit, runtime, image, command),
593
+ "generated_at=2026-07-27T00:00:00.000Z",
594
+ "fixture_claude_version=2.1.220",
595
+ "fixture_opencode_version=1.17.20",
596
+ "acceptance=blocked-pending-live-matrix",
597
+ "evidence_scope=deterministic-native-lifecycle",
598
+ ...records,
599
+ `PASS: Phase 18 runtime ${runtime}`,
600
+ "",
601
+ ].join("\n");
602
+ }
603
+
604
+ function writeFixture(directory, commit) {
605
+ mkdirSync(directory, { recursive: true });
606
+ const logs = [];
607
+ for (const [runtime, image] of [...NODE_IMAGES, ["bash-3.2", BASH_IMAGE]]) {
608
+ const file = `${runtime}.log`;
609
+ const text = fixtureLog(commit, runtime, image);
610
+ writeFileSync(join(directory, file), text);
611
+ const commands = (runtime.startsWith("node-") ? NODE_COMMANDS : BASH_COMMANDS).map(
612
+ ([id, command, evidence_scope]) => ({ id, command, evidence_scope, exit_status: 0, result: "pass" }),
613
+ );
614
+ logs.push({
615
+ file,
616
+ runtime,
617
+ image,
618
+ container_command: `docker run --rm ${image}`,
619
+ source_commit: commit,
620
+ source_tree_clean: true,
621
+ generated_at: "2026-07-27T00:00:00.000Z",
622
+ sha256: createHash("sha256").update(text).digest("hex"),
623
+ result: "pass",
624
+ commands,
625
+ });
626
+ }
627
+ writeFileSync(join(directory, "manifest.json"), `${JSON.stringify({
628
+ schema_version: 2,
629
+ source_commit: commit,
630
+ source_tree_clean: true,
631
+ detached_source: true,
632
+ generated_at: "2026-07-27T00:00:00.000Z",
633
+ evidence_scope: "deterministic-native-lifecycle",
634
+ acceptance: {
635
+ status: "blocked-pending-live-matrix",
636
+ accepted: false,
637
+ required_cells: 56,
638
+ },
639
+ logs,
640
+ }, null, 2)}\n`);
641
+ }
642
+
643
+ function writeLiveMatrixFixture(path) {
644
+ const cells = [];
645
+ for (const disabledCapability of CANONICAL_CAPABILITIES) {
646
+ for (const survivingOwner of CANONICAL_CAPABILITIES) {
647
+ if (disabledCapability === survivingOwner) continue;
648
+ const observation = {
649
+ result: "success",
650
+ error: null,
651
+ timeout: false,
652
+ trace: `trace:${disabledCapability}:${survivingOwner}`,
653
+ delegate_identity: `owner:${survivingOwner}`,
654
+ };
655
+ const cell = {
656
+ disabled_capability: disabledCapability,
657
+ surviving_owner: survivingOwner,
658
+ operation: PUBLIC_OPERATIONS.get(survivingOwner),
659
+ real_operation: true,
660
+ supported_seam: "public-installed-owner",
661
+ availability: "available",
662
+ baseline: observation,
663
+ observed: { ...observation },
664
+ equality: "pass",
665
+ evidence_ref: `evidence/${disabledCapability}/${survivingOwner}.json`,
666
+ };
667
+ if (DUAL_HARNESS_OWNERS.has(survivingOwner)) {
668
+ cell.installed_harnesses = {
669
+ claude: { result: "success", evidence_ref: `${cell.evidence_ref}#claude` },
670
+ opencode: { result: "success", evidence_ref: `${cell.evidence_ref}#opencode` },
671
+ };
672
+ }
673
+ cells.push(cell);
674
+ }
675
+ }
676
+ writeFileSync(path, `${JSON.stringify({ schema_version: 1, status: "pass", cells }, null, 2)}\n`);
677
+ }
678
+
679
+ function expectFailure(label, operation) {
680
+ assert.throws(operation, undefined, label);
681
+ }
682
+
683
+ function selfTest() {
684
+ const root = mkdtempSync(join(tmpdir(), "cairn-phase18-verifier-test-"));
685
+ const commit = "a".repeat(40);
686
+ try {
687
+ const valid = join(root, "valid");
688
+ writeFixture(valid, commit);
689
+ verifyEvidence(valid, commit);
690
+ expectFailure("short commit", () => verifyEvidence(valid, commit.slice(0, 39)));
691
+
692
+ const mixed = join(root, "mixed");
693
+ writeFixture(mixed, commit);
694
+ const mixedManifest = JSON.parse(readFileSync(join(mixed, "manifest.json"), "utf8"));
695
+ mixedManifest.logs[1].source_commit = "b".repeat(40);
696
+ writeFileSync(join(mixed, "manifest.json"), JSON.stringify(mixedManifest));
697
+ expectFailure("mixed commit", () => verifyEvidence(mixed, commit));
698
+
699
+ const wrongRuntime = join(root, "wrong-runtime");
700
+ writeFixture(wrongRuntime, commit);
701
+ writeFileSync(
702
+ join(wrongRuntime, "node-22.log"),
703
+ readFileSync(join(wrongRuntime, "node-22.log"), "utf8").replace("runtime=node-22", "runtime=node-24"),
704
+ );
705
+ expectFailure("wrong runtime label", () => verifyEvidence(wrongRuntime, commit));
706
+
707
+ const wrongVersion = join(root, "wrong-version");
708
+ writeFixture(wrongVersion, commit);
709
+ writeFileSync(
710
+ join(wrongVersion, "node-22.log"),
711
+ readFileSync(join(wrongVersion, "node-22.log"), "utf8").replace("runtime_version=v22.", "runtime_version=v24."),
712
+ );
713
+ expectFailure("wrong runtime version", () => verifyEvidence(wrongVersion, commit));
714
+
715
+ const missingCommand = join(root, "missing-command");
716
+ writeFixture(missingCommand, commit);
717
+ writeFileSync(
718
+ join(missingCommand, "node-24.log"),
719
+ readFileSync(join(missingCommand, "node-24.log"), "utf8").replace(
720
+ "npm --prefix mcp-memory-server test",
721
+ "omitted-server-test-command",
722
+ ),
723
+ );
724
+ expectFailure("missing command", () => verifyEvidence(missingCommand, commit));
725
+
726
+ const missingPass = join(root, "missing-pass");
727
+ writeFixture(missingPass, commit);
728
+ writeFileSync(
729
+ join(missingPass, "bash-3.2.log"),
730
+ readFileSync(join(missingPass, "bash-3.2.log"), "utf8").replace("PASS: Phase 18 runtime bash-3.2", "FAIL"),
731
+ );
732
+ expectFailure("missing PASS", () => verifyEvidence(missingPass, commit));
733
+
734
+ const manifestMismatch = join(root, "manifest-mismatch");
735
+ writeFixture(manifestMismatch, commit);
736
+ const manifest = JSON.parse(readFileSync(join(manifestMismatch, "manifest.json"), "utf8"));
737
+ manifest.logs[0].file = "unexpected.log";
738
+ writeFileSync(join(manifestMismatch, "manifest.json"), JSON.stringify(manifest));
739
+ expectFailure("manifest names", () => verifyEvidence(manifestMismatch, commit));
740
+
741
+ const missingLog = join(root, "missing-log");
742
+ writeFixture(missingLog, commit);
743
+ rmSync(join(missingLog, "node-26.log"));
744
+ expectFailure("missing runtime log", () => verifyEvidence(missingLog, commit));
745
+
746
+ const overstatedAcceptance = join(root, "overstated-acceptance");
747
+ writeFixture(overstatedAcceptance, commit);
748
+ const acceptanceManifest = JSON.parse(readFileSync(join(overstatedAcceptance, "manifest.json"), "utf8"));
749
+ acceptanceManifest.acceptance = { status: "pass", accepted: true, required_cells: 56 };
750
+ writeFileSync(join(overstatedAcceptance, "manifest.json"), JSON.stringify(acceptanceManifest));
751
+ expectFailure("deterministic evidence acceptance overstatement", () => verifyEvidence(overstatedAcceptance, commit));
752
+
753
+ const staleHash = join(root, "stale-hash");
754
+ writeFixture(staleHash, commit);
755
+ writeFileSync(join(staleHash, "node-22.log"), `${readFileSync(join(staleHash, "node-22.log"), "utf8")}tampered\n`);
756
+ expectFailure("stale log hash", () => verifyEvidence(staleHash, commit));
757
+
758
+ const unsupportedScope = join(root, "unsupported-scope");
759
+ writeFixture(unsupportedScope, commit);
760
+ const unsupportedManifest = JSON.parse(readFileSync(join(unsupportedScope, "manifest.json"), "utf8"));
761
+ unsupportedManifest.logs[0].commands[0].evidence_scope = "synthetic-owner-acceptance";
762
+ writeFileSync(join(unsupportedScope, "manifest.json"), JSON.stringify(unsupportedManifest));
763
+ expectFailure("unsupported evidence scope", () => verifyEvidence(unsupportedScope, commit));
764
+
765
+ const omittedCommand = join(root, "omitted-command");
766
+ writeFixture(omittedCommand, commit);
767
+ const omittedManifest = JSON.parse(readFileSync(join(omittedCommand, "manifest.json"), "utf8"));
768
+ omittedManifest.logs[1].commands.pop();
769
+ writeFileSync(join(omittedCommand, "manifest.json"), JSON.stringify(omittedManifest));
770
+ expectFailure("omitted required command", () => verifyEvidence(omittedCommand, commit));
771
+
772
+ for (const requiredId of [
773
+ "operating-sequential",
774
+ "session-isolation",
775
+ "opencode-disposal",
776
+ "launcher-recovery",
777
+ "root-test",
778
+ "repository-hygiene",
779
+ ]) {
780
+ const missingNewCommand = join(root, `missing-${requiredId}`);
781
+ writeFixture(missingNewCommand, commit);
782
+ const missingManifest = JSON.parse(readFileSync(join(missingNewCommand, "manifest.json"), "utf8"));
783
+ for (const log of missingManifest.logs.filter(({ runtime }) => runtime.startsWith("node-"))) {
784
+ log.commands = log.commands.filter(({ id }) => id !== requiredId);
785
+ }
786
+ writeFileSync(join(missingNewCommand, "manifest.json"), JSON.stringify(missingManifest));
787
+ expectFailure(`missing new command ${requiredId}`, () => verifyEvidence(missingNewCommand, commit));
788
+ }
789
+
790
+ const failedNewCommand = join(root, "failed-new-command");
791
+ writeFixture(failedNewCommand, commit);
792
+ const failedManifest = JSON.parse(readFileSync(join(failedNewCommand, "manifest.json"), "utf8"));
793
+ const failedRecord = failedManifest.logs[0].commands.find(({ id }) => id === "launcher-recovery");
794
+ failedRecord.exit_status = 23;
795
+ failedRecord.result = "fail";
796
+ writeFileSync(join(failedNewCommand, "manifest.json"), JSON.stringify(failedManifest));
797
+ expectFailure("failed new command", () => verifyEvidence(failedNewCommand, commit));
798
+
799
+ const liveMatrix = join(root, "live-matrix.json");
800
+ writeLiveMatrixFixture(liveMatrix);
801
+ validateLiveMatrix(liveMatrix);
802
+ const duplicateMatrix = JSON.parse(readFileSync(liveMatrix, "utf8"));
803
+ duplicateMatrix.cells[55] = structuredClone(duplicateMatrix.cells[0]);
804
+ writeFileSync(liveMatrix, JSON.stringify(duplicateMatrix));
805
+ expectFailure("duplicate live matrix cell", () => validateLiveMatrix(liveMatrix));
806
+ writeLiveMatrixFixture(liveMatrix);
807
+ const unavailableMatrix = JSON.parse(readFileSync(liveMatrix, "utf8"));
808
+ unavailableMatrix.cells[0].availability = "credentials-unavailable";
809
+ writeFileSync(liveMatrix, JSON.stringify(unavailableMatrix));
810
+ expectFailure("unavailable live matrix cell", () => validateLiveMatrix(liveMatrix));
811
+ writeLiveMatrixFixture(liveMatrix);
812
+ const spyOnlyMatrix = JSON.parse(readFileSync(liveMatrix, "utf8"));
813
+ spyOnlyMatrix.cells[0].real_operation = false;
814
+ writeFileSync(liveMatrix, JSON.stringify(spyOnlyMatrix));
815
+ expectFailure("spy-only live matrix cell", () => validateLiveMatrix(liveMatrix));
816
+ writeLiveMatrixFixture(liveMatrix);
817
+ const missingHarnessMatrix = JSON.parse(readFileSync(liveMatrix, "utf8"));
818
+ const dualCell = missingHarnessMatrix.cells.find((cell) => cell.surviving_owner === "wiki");
819
+ delete dualCell.installed_harnesses.opencode;
820
+ writeFileSync(liveMatrix, JSON.stringify(missingHarnessMatrix));
821
+ expectFailure("missing installed harness proof", () => validateLiveMatrix(liveMatrix));
822
+
823
+ const failureProbe = run(process.execPath, ["-e", "process.stdout.write('saved failure evidence\\n'); process.exit(19)"]);
824
+ const failureEvidence = join(root, "intentional-failure.log");
825
+ writeFileSync(failureEvidence, combined(failureProbe));
826
+ assert.equal(failureProbe.status, 19, "intentional command exit status was not propagated");
827
+ assert.equal(readFileSync(failureEvidence, "utf8"), "saved failure evidence\n",
828
+ "intentional command evidence was not saved");
829
+
830
+ const repo = join(root, "repo");
831
+ mkdirSync(repo);
832
+ requireSuccess(git(repo, ["init", "-q"]), "initialize self-test repository");
833
+ requireSuccess(git(repo, ["config", "user.name", "Runtime Evidence Test"]), "configure test name");
834
+ requireSuccess(git(repo, ["config", "user.email", "runtime-evidence@example.invalid"]), "configure test email");
835
+ writeFileSync(join(repo, "source.txt"), "clean\n");
836
+ requireSuccess(git(repo, ["add", "source.txt"]), "stage self-test source");
837
+ requireSuccess(git(repo, ["commit", "-qm", "fixture"]), "commit self-test source");
838
+ const repoCommit = requireSuccess(git(repo, ["rev-parse", "HEAD"]), "read self-test commit");
839
+ assertCallerSourceClean(repo, repoCommit);
840
+ let detachedPath;
841
+ expectFailure("detached cleanup callback", () => withDetachedWorktree(repo, repoCommit, (path) => {
842
+ detachedPath = path;
843
+ throw new Error("intentional cleanup probe");
844
+ }));
845
+ assert.equal(existsSync(detachedPath), false, "detached source was not removed in finally");
846
+ assert.equal(requireSuccess(git(repo, ["worktree", "list", "--porcelain"]), "list self-test worktrees").includes(detachedPath), false);
847
+ writeFileSync(join(repo, "source.txt"), "dirty\n");
848
+ expectFailure("tracked source difference", () => assertCallerSourceClean(repo, repoCommit));
849
+
850
+ process.stdout.write("PASS: Phase 18 runtime-evidence verifier self-test\n");
851
+ } finally {
852
+ rmSync(root, { recursive: true, force: true });
853
+ }
854
+ }
855
+
856
+ function main() {
857
+ const [mode, evidenceDirectory, sourceCommit, ...extra] = process.argv.slice(2);
858
+ if (mode === undefined && evidenceDirectory === undefined && sourceCommit === undefined && extra.length === 0) {
859
+ const evidencePath = resolve(SCRIPT_ROOT, EVIDENCE_RELATIVE);
860
+ const manifestPath = join(evidencePath, "manifest.json");
861
+ if (!existsSync(manifestPath)) throw new Error("Evidence manifest is missing.");
862
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
863
+ verifyEvidence(evidencePath, manifest.source_commit);
864
+ process.stdout.write(`PASS: Phase 18 deterministic runtime evidence verified for ${manifest.source_commit}\n`);
865
+ process.stdout.write("BLOCKED: Phase 18 acceptance still requires --live-matrix with 56 genuine real-owner cells\n");
866
+ return;
867
+ }
868
+ if (mode === "--self-test" && evidenceDirectory === undefined && sourceCommit === undefined && extra.length === 0) {
869
+ selfTest();
870
+ return;
871
+ }
872
+ if (mode === "--live-matrix" && evidenceDirectory !== undefined && sourceCommit === undefined && extra.length === 0) {
873
+ validateLiveMatrix(resolve(evidenceDirectory));
874
+ process.stdout.write("PASS: Phase 18 live matrix contains 56 genuine passing real-owner cells\n");
875
+ return;
876
+ }
877
+ if ((mode === "--capture" || mode === "--verify-only")
878
+ && evidenceDirectory !== undefined && sourceCommit !== undefined && extra.length === 0) {
879
+ if (mode === "--capture") captureEvidence(evidenceDirectory, sourceCommit);
880
+ else {
881
+ verifyEvidence(resolve(evidenceDirectory), sourceCommit);
882
+ process.stdout.write(`PASS: Phase 18 runtime evidence verified for ${sourceCommit}\n`);
883
+ }
884
+ return;
885
+ }
886
+ process.stderr.write(usage());
887
+ process.exitCode = 2;
888
+ }
889
+
890
+ try {
891
+ main();
892
+ } catch (error) {
893
+ process.stderr.write(`FATAL: ${error instanceof Error ? error.message : String(error)}\n`);
894
+ process.exitCode = 1;
895
+ }