@cairnkeep/cli 2.3.1 → 2.5.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.
- package/CHANGELOG.md +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -9,28 +9,609 @@ trap 'rm -rf "$tmp"' EXIT
|
|
|
9
9
|
|
|
10
10
|
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
11
11
|
|
|
12
|
+
write_recovery_runtime_fixture() {
|
|
13
|
+
local runtime_root="$1"
|
|
14
|
+
local name
|
|
15
|
+
mkdir -p "$runtime_root/bin" "$runtime_root/mcp-memory-server/dist"
|
|
16
|
+
for name in scripts claude opencode templates; do
|
|
17
|
+
ln -s "$ROOT/$name" "$runtime_root/$name"
|
|
18
|
+
done
|
|
19
|
+
for asset in "$ROOT"/mcp-memory-server/dist/*; do
|
|
20
|
+
name=${asset##*/}
|
|
21
|
+
[[ "$name" == capability-cli.js ]] || ln -s "$asset" "$runtime_root/mcp-memory-server/dist/$name"
|
|
22
|
+
done
|
|
23
|
+
cat > "$runtime_root/bin/cairn" <<'SH'
|
|
24
|
+
#!/usr/bin/env bash
|
|
25
|
+
exit 0
|
|
26
|
+
SH
|
|
27
|
+
chmod +x "$runtime_root/bin/cairn"
|
|
28
|
+
cat > "$runtime_root/mcp-memory-server/dist/capability-cli.js" <<NODE
|
|
29
|
+
#!/usr/bin/env node
|
|
30
|
+
const fs = require("node:fs");
|
|
31
|
+
const { spawnSync } = require("node:child_process");
|
|
32
|
+
const operation = process.argv[2] ?? "";
|
|
33
|
+
fs.appendFileSync(process.env.CAIRN_RECOVERY_TRACE, \`coordinator:\${operation}\\n\`);
|
|
34
|
+
if (operation === "harness-recover" && process.env.CAIRN_TEST_RECOVERY_FAIL === "1") process.exit(23);
|
|
35
|
+
const input = fs.readFileSync(0);
|
|
36
|
+
const result = spawnSync(process.execPath, ["$ROOT/mcp-memory-server/dist/capability-cli.js", ...process.argv.slice(2)], {
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
env: process.env,
|
|
39
|
+
input,
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
process.stdout.write(result.stdout ?? "");
|
|
43
|
+
process.stderr.write(result.stderr ?? "");
|
|
44
|
+
process.exit(result.status ?? 1);
|
|
45
|
+
NODE
|
|
46
|
+
chmod +x "$runtime_root/mcp-memory-server/dist/capability-cli.js"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
write_recovery_harness_fixtures() {
|
|
50
|
+
local bin_root="$1"
|
|
51
|
+
local name
|
|
52
|
+
mkdir -p "$bin_root"
|
|
53
|
+
for name in claude opencode; do
|
|
54
|
+
cat > "$bin_root/$name" <<'SH'
|
|
55
|
+
#!/usr/bin/env bash
|
|
56
|
+
name=${0##*/}
|
|
57
|
+
if [[ "$name" == claude ]]; then config=${CLAUDE_CONFIG_DIR:-}; else config=${OPENCODE_CONFIG_DIR:-}; fi
|
|
58
|
+
printf 'harness:%s|args:%s|config:%s\n' "$name" "$*" "$config" >> "$CAIRN_RECOVERY_TRACE"
|
|
59
|
+
exit "${FAKE_EXIT:-0}"
|
|
60
|
+
SH
|
|
61
|
+
chmod +x "$bin_root/$name"
|
|
62
|
+
done
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
write_recovery_config() {
|
|
66
|
+
local project="$1"
|
|
67
|
+
mkdir -p "$project/.ai"
|
|
68
|
+
printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":true},"logging":{"callbacks":true}}' \
|
|
69
|
+
> "$project/.ai/capabilities.json"
|
|
70
|
+
chmod 600 "$project/.ai/capabilities.json"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
create_recovery_lease() {
|
|
74
|
+
local project="$1"
|
|
75
|
+
local state_root="$2"
|
|
76
|
+
local harness="$3"
|
|
77
|
+
local session="$4"
|
|
78
|
+
local output
|
|
79
|
+
output=$(
|
|
80
|
+
cd "$project"
|
|
81
|
+
printf '%s' "{\"schema_version\":1,\"harness\":\"$harness\",\"command\":\"wiki-query\",\"session_id\":\"$session\",\"project_root\":\"$project\"}" | \
|
|
82
|
+
env CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 CAIRN_TRAJECTORY_CAPTURE=1 \
|
|
83
|
+
CAIRN_HARNESS_STATE_DIR="$state_root" \
|
|
84
|
+
node "$ROOT/mcp-memory-server/dist/capability-cli.js" harness-before
|
|
85
|
+
)
|
|
86
|
+
[[ "$output" == '{"schema_version":1,"decision":"allow"}' ]] || fail "could not create genuine recovery lease"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
inject_terminal_crash() {
|
|
90
|
+
local project="$1"
|
|
91
|
+
local state_root="$2"
|
|
92
|
+
local harness="$3"
|
|
93
|
+
local session="$4"
|
|
94
|
+
(
|
|
95
|
+
cd "$project"
|
|
96
|
+
env CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 CAIRN_TRAJECTORY_CAPTURE=1 \
|
|
97
|
+
CAIRN_HARNESS_STATE_DIR="$state_root" \
|
|
98
|
+
node --input-type=module - "$ROOT/mcp-memory-server/dist/capability-harness.js" "$harness" "$session" <<'NODE'
|
|
99
|
+
import assert from "node:assert/strict";
|
|
100
|
+
import { pathToFileURL } from "node:url";
|
|
101
|
+
const [modulePath, harness, sessionID] = process.argv.slice(2);
|
|
102
|
+
const coordinator = await import(pathToFileURL(modulePath).href);
|
|
103
|
+
await assert.rejects(
|
|
104
|
+
coordinator.finishHarnessCapability({
|
|
105
|
+
schema_version: 1,
|
|
106
|
+
harness,
|
|
107
|
+
session_id: sessionID,
|
|
108
|
+
outcome: "success",
|
|
109
|
+
}, { testCrashAt: "after-claim" }),
|
|
110
|
+
(error) => error?.name === "HarnessCrashInjection",
|
|
111
|
+
);
|
|
112
|
+
NODE
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expire_recovery_lease() {
|
|
117
|
+
local state_root="$1"
|
|
118
|
+
local lease
|
|
119
|
+
lease=$(find "$state_root/capability-leases-v1" -type f -name '*.json' -print -quit)
|
|
120
|
+
[[ -n "$lease" ]] || fail "active recovery lease is absent"
|
|
121
|
+
node - "$lease" <<'NODE'
|
|
122
|
+
const fs = require("node:fs");
|
|
123
|
+
const path = process.argv[2];
|
|
124
|
+
const before = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
125
|
+
const after = { ...before, expires_at: "2020-01-01T00:00:00.000Z" };
|
|
126
|
+
if (Object.keys(before).sort().join(",") !== Object.keys(after).sort().join(",")) process.exit(1);
|
|
127
|
+
fs.writeFileSync(path, `${JSON.stringify(after)}\n`, { mode: 0o600 });
|
|
128
|
+
fs.chmodSync(path, 0o600);
|
|
129
|
+
NODE
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
recovery_state() {
|
|
133
|
+
local project="$1"
|
|
134
|
+
local state_root="$2"
|
|
135
|
+
(
|
|
136
|
+
cd "$ROOT/mcp-memory-server"
|
|
137
|
+
NODE_NO_WARNINGS=1 node --input-type=module - "$project" "$state_root" <<'NODE'
|
|
138
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
139
|
+
import { join } from "node:path";
|
|
140
|
+
import { AgentFS } from "agentfs-sdk";
|
|
141
|
+
const [project, stateRoot] = process.argv.slice(2);
|
|
142
|
+
const database = join(project, ".agentfs", "trajectory.db");
|
|
143
|
+
let pending = [];
|
|
144
|
+
let finals = [];
|
|
145
|
+
if (existsSync(database)) {
|
|
146
|
+
const agent = await AgentFS.open({ id: "trajectory", path: database });
|
|
147
|
+
pending = await agent.kv.list("capability-callback/v1/pending/");
|
|
148
|
+
finals = await agent.kv.list("capability-callback/v1/record/");
|
|
149
|
+
await agent.close();
|
|
150
|
+
}
|
|
151
|
+
function files(root) {
|
|
152
|
+
if (!existsSync(root)) return [];
|
|
153
|
+
return readdirSync(root).flatMap((name) => {
|
|
154
|
+
const path = join(root, name);
|
|
155
|
+
return statSync(path).isDirectory() ? files(path) : [path];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const stateFiles = files(stateRoot);
|
|
159
|
+
const raw = Buffer.concat([
|
|
160
|
+
...stateFiles.map((path) => readFileSync(path)),
|
|
161
|
+
...(existsSync(join(project, ".agentfs")) ? readdirSync(join(project, ".agentfs"))
|
|
162
|
+
.filter((name) => /^trajectory\.db(?:-(?:wal|shm))?$/.test(name))
|
|
163
|
+
.map((name) => readFileSync(join(project, ".agentfs", name))) : []),
|
|
164
|
+
]).toString("utf8");
|
|
165
|
+
process.stdout.write(JSON.stringify({
|
|
166
|
+
pending: pending.length,
|
|
167
|
+
finals: finals.map(({ value }) => value),
|
|
168
|
+
leases: stateFiles.filter((path) => path.endsWith(".json")).length,
|
|
169
|
+
leaked: ["launcher-argument-sentinel-18-28", "launcher-result-sentinel-18-28"].some((value) => raw.includes(value)),
|
|
170
|
+
}));
|
|
171
|
+
NODE
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
assert_recovered_state() {
|
|
176
|
+
local state="$1"
|
|
177
|
+
local outcome="$2"
|
|
178
|
+
node - "$state" "$outcome" <<'NODE'
|
|
179
|
+
const state = JSON.parse(process.argv[2]);
|
|
180
|
+
const outcome = process.argv[3];
|
|
181
|
+
if (state.pending !== 0 || state.leases !== 0 || state.leaked) process.exit(1);
|
|
182
|
+
if (state.finals.length !== 1 || state.finals[0].outcome !== outcome) process.exit(1);
|
|
183
|
+
if (["launcher-argument-sentinel-18-28", "launcher-result-sentinel-18-28"]
|
|
184
|
+
.some((value) => JSON.stringify(state.finals[0]).includes(value))) process.exit(1);
|
|
185
|
+
NODE
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
capability_recovery() {
|
|
189
|
+
local recovery_root="$tmp/capability-recovery"
|
|
190
|
+
local runtime_root="$recovery_root/runtime"
|
|
191
|
+
local harness_bin="$recovery_root/harness-bin"
|
|
192
|
+
local trace="$recovery_root/trace"
|
|
193
|
+
local claude_repo="$recovery_root/claude-project"
|
|
194
|
+
local opencode_repo="$recovery_root/opencode-project"
|
|
195
|
+
local claude_state="$recovery_root/claude-state"
|
|
196
|
+
local opencode_state="$recovery_root/opencode-state"
|
|
197
|
+
local state before status launcher harness project state_root master_value
|
|
198
|
+
|
|
199
|
+
npm --prefix "$ROOT/mcp-memory-server" run build >/dev/null
|
|
200
|
+
mkdir -p "$claude_repo" "$opencode_repo"
|
|
201
|
+
git -C "$claude_repo" init -q
|
|
202
|
+
git -C "$opencode_repo" init -q
|
|
203
|
+
"$ROOT/scripts/bootstrap.sh" "$claude_repo" >/dev/null
|
|
204
|
+
"$ROOT/scripts/bootstrap.sh" "$opencode_repo" >/dev/null
|
|
205
|
+
write_recovery_config "$claude_repo"
|
|
206
|
+
write_recovery_config "$opencode_repo"
|
|
207
|
+
write_recovery_runtime_fixture "$runtime_root"
|
|
208
|
+
write_recovery_harness_fixtures "$harness_bin"
|
|
209
|
+
: > "$trace"
|
|
210
|
+
|
|
211
|
+
create_recovery_lease "$claude_repo" "$claude_state" claude-code launcher-crash-claude-18-28
|
|
212
|
+
inject_terminal_crash "$claude_repo" "$claude_state" claude-code launcher-crash-claude-18-28
|
|
213
|
+
create_recovery_lease "$opencode_repo" "$opencode_state" opencode launcher-expired-opencode-18-28
|
|
214
|
+
expire_recovery_lease "$opencode_state"
|
|
215
|
+
|
|
216
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 \
|
|
217
|
+
CAIRN_TRAJECTORY_CAPTURE=1 CAIRN_HARNESS_STATE_DIR="$claude_state" \
|
|
218
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" \
|
|
219
|
+
"$claude_repo/.ai/start-claude.sh" --launcher-argument-sentinel-18-28 >/dev/null 2>&1 || \
|
|
220
|
+
fail "Claude recovery fixture did not reach the harness"
|
|
221
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 \
|
|
222
|
+
CAIRN_TRAJECTORY_CAPTURE=1 CAIRN_HARNESS_STATE_DIR="$opencode_state" \
|
|
223
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" \
|
|
224
|
+
"$opencode_repo/.ai/start-opencode.sh" --launcher-argument-sentinel-18-28 >/dev/null 2>&1 || \
|
|
225
|
+
fail "OpenCode recovery fixture did not reach the harness"
|
|
226
|
+
|
|
227
|
+
if [[ $(grep -c '^coordinator:harness-recover$' "$trace" || true) -eq 0 ]]; then
|
|
228
|
+
[[ $(grep -c '^harness:' "$trace" || true) -eq 2 ]] || fail "known launcher recovery gap changed harness execution"
|
|
229
|
+
[[ $(node -e 'const s=JSON.parse(process.argv[1]);process.stdout.write(String(s.leases))' "$(recovery_state "$claude_repo" "$claude_state")") -eq 1 ]] || \
|
|
230
|
+
fail "known launcher recovery gap consumed the Claude crash lease"
|
|
231
|
+
[[ $(node -e 'const s=JSON.parse(process.argv[1]);process.stdout.write(String(s.leases))' "$(recovery_state "$opencode_repo" "$opencode_state")") -eq 1 ]] || \
|
|
232
|
+
fail "known launcher recovery gap consumed the OpenCode expiry lease"
|
|
233
|
+
echo "PHASE18_RED:PRODUCTION_STARTUP_RECOVERY"
|
|
234
|
+
return 86
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
node - "$trace" <<'NODE' || fail "recovery did not run once before each harness"
|
|
238
|
+
const fs = require("node:fs");
|
|
239
|
+
const rows = fs.readFileSync(process.argv[2], "utf8").trim().split(/\n/);
|
|
240
|
+
if (rows.length !== 4) process.exit(1);
|
|
241
|
+
if (rows[0] !== "coordinator:harness-recover" || !rows[1].startsWith("harness:claude|")) process.exit(1);
|
|
242
|
+
if (rows[2] !== "coordinator:harness-recover" || !rows[3].startsWith("harness:opencode|")) process.exit(1);
|
|
243
|
+
NODE
|
|
244
|
+
assert_recovered_state "$(recovery_state "$claude_repo" "$claude_state")" success || fail "Claude crash recovery state is incorrect"
|
|
245
|
+
assert_recovered_state "$(recovery_state "$opencode_repo" "$opencode_state")" timeout || fail "OpenCode expiry recovery state is incorrect"
|
|
246
|
+
|
|
247
|
+
before="$(recovery_state "$claude_repo" "$claude_state")|$(recovery_state "$opencode_repo" "$opencode_state")"
|
|
248
|
+
: > "$trace"
|
|
249
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_CAPABILITY_CONTRACT=1 CAIRN_HARNESS_STATE_DIR="$claude_state" \
|
|
250
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" "$claude_repo/.ai/start-claude.sh" --replay >/dev/null 2>&1
|
|
251
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_CAPABILITY_CONTRACT=1 CAIRN_HARNESS_STATE_DIR="$opencode_state" \
|
|
252
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" "$opencode_repo/.ai/start-opencode.sh" --replay >/dev/null 2>&1
|
|
253
|
+
[[ "$before" == "$(recovery_state "$claude_repo" "$claude_state")|$(recovery_state "$opencode_repo" "$opencode_state")" ]] || \
|
|
254
|
+
fail "replayed startup changed recovered finals"
|
|
255
|
+
|
|
256
|
+
for harness in claude opencode; do
|
|
257
|
+
if [[ "$harness" == claude ]]; then
|
|
258
|
+
project="$claude_repo"; state_root="$claude_state"; launcher="$project/.ai/start-claude.sh"
|
|
259
|
+
else
|
|
260
|
+
project="$opencode_repo"; state_root="$opencode_state"; launcher="$project/.ai/start-opencode.sh"
|
|
261
|
+
fi
|
|
262
|
+
: > "$trace"
|
|
263
|
+
status=0
|
|
264
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_TEST_RECOVERY_FAIL=1 FAKE_EXIT=7 \
|
|
265
|
+
CAIRN_CAPABILITY_CONTRACT=1 CAIRN_HARNESS_STATE_DIR="$state_root" \
|
|
266
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" "$launcher" --failure-argv >/dev/null 2>&1 || status=$?
|
|
267
|
+
[[ "$status" -eq 7 ]] || fail "$harness recovery failure changed the harness exit status"
|
|
268
|
+
node - "$trace" "$harness" "$project" <<'NODE' || fail "recovery failure changed harness ordering, argv, or config"
|
|
269
|
+
const fs = require("node:fs");
|
|
270
|
+
const [trace, harness, project] = process.argv.slice(2);
|
|
271
|
+
const rows = fs.readFileSync(trace, "utf8").trim().split(/\n/);
|
|
272
|
+
if (rows.length !== 2 || rows[0] !== "coordinator:harness-recover") process.exit(1);
|
|
273
|
+
if (!rows[1].startsWith(`harness:${harness}|args:--failure-argv|`)) process.exit(1);
|
|
274
|
+
if (!rows[1].includes(`config:${project}/.ai/capability-contract/${harness}`)) process.exit(1);
|
|
275
|
+
NODE
|
|
276
|
+
done
|
|
277
|
+
|
|
278
|
+
for master_value in 0 false FALSE no off invalid; do
|
|
279
|
+
for harness in claude opencode; do
|
|
280
|
+
if [[ "$harness" == claude ]]; then project="$claude_repo"; launcher="$project/.ai/start-claude.sh"
|
|
281
|
+
else project="$opencode_repo"; launcher="$project/.ai/start-opencode.sh"; fi
|
|
282
|
+
: > "$trace"
|
|
283
|
+
CAIRN_RECOVERY_TRACE="$trace" CAIRN_CAPABILITY_CONTRACT="$master_value" \
|
|
284
|
+
PATH="$runtime_root/bin:$harness_bin:$PATH" "$launcher" --legacy-argv >/dev/null 2>&1 || \
|
|
285
|
+
fail "$harness master-off spelling failed: $master_value"
|
|
286
|
+
[[ $(grep -c '^coordinator:' "$trace" || true) -eq 0 ]] || fail "$harness master-off invoked recovery: $master_value"
|
|
287
|
+
grep -q "^harness:$harness|args:--legacy-argv|config:$" "$trace" || \
|
|
288
|
+
fail "$harness master-off changed argv or config: $master_value"
|
|
289
|
+
done
|
|
290
|
+
done
|
|
291
|
+
|
|
292
|
+
echo "PASS: production launcher crash and expiry recovery contract"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
opencode_sync_tree_digest() {
|
|
296
|
+
local live_root="$1"
|
|
297
|
+
(
|
|
298
|
+
cd "$live_root"
|
|
299
|
+
find . -type f -print | LC_ALL=C sort | while IFS= read -r asset; do
|
|
300
|
+
sha256sum "$asset"
|
|
301
|
+
done
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
assert_opencode_legacy_plugins() {
|
|
306
|
+
local live_root="$1"
|
|
307
|
+
local plugin
|
|
308
|
+
for plugin in memory-wakeup.ts memory-capture.ts memory-recall.ts; do
|
|
309
|
+
[[ -f "$live_root/plugins/$plugin" ]] || fail "OpenCode sync omitted legacy plugin: $plugin"
|
|
310
|
+
cmp -s <(sed "s|@@INFRA_ROOT@@|$ROOT|g" "$ROOT/opencode/plugins/$plugin") \
|
|
311
|
+
"$live_root/plugins/$plugin" || fail "OpenCode sync changed legacy plugin bytes: $plugin"
|
|
312
|
+
done
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
assert_no_retired_opencode_owners() {
|
|
316
|
+
local live_root="$1"
|
|
317
|
+
[[ ! -e "$live_root/capability-contract" ]] || fail "OpenCode sync installed a retired capability owner tree"
|
|
318
|
+
[[ ! -e "$live_root/hooks/capability-command-start.sh" ]] || fail "OpenCode sync installed a Claude capability hook"
|
|
319
|
+
[[ ! -e "$live_root/hooks/capability-command-finish.sh" ]] || fail "OpenCode sync installed a Claude capability hook"
|
|
320
|
+
if grep -R -q -E 'cairn capabilities (guard|start|finish)|capability_handle' \
|
|
321
|
+
"$live_root/command" "$live_root/workflows" 2>/dev/null; then
|
|
322
|
+
fail "OpenCode sync installed retired model-visible lifecycle ownership"
|
|
323
|
+
fi
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
assert_opencode_sync_assets() {
|
|
327
|
+
local name="$1"
|
|
328
|
+
local live_root="$2"
|
|
329
|
+
case "$name" in
|
|
330
|
+
wiki)
|
|
331
|
+
cmp -s "$ROOT/opencode/command/wiki-ingest.md" "$live_root/command/wiki-ingest.md" || fail "wiki sync changed legacy command bytes"
|
|
332
|
+
cmp -s "$ROOT/opencode/workflows/wiki-ingest-workflow.md" "$live_root/workflows/wiki-ingest-workflow.md" || fail "wiki sync changed legacy workflow bytes"
|
|
333
|
+
;;
|
|
334
|
+
graph)
|
|
335
|
+
cmp -s "$ROOT/opencode/command/graphify.md" "$live_root/command/graphify.md" || fail "graph sync changed legacy command bytes"
|
|
336
|
+
;;
|
|
337
|
+
security)
|
|
338
|
+
cmp -s "$ROOT/opencode/command/security-audit.md" "$live_root/command/security-audit.md" || fail "security sync changed legacy command bytes"
|
|
339
|
+
cmp -s "$ROOT/opencode/workflows/security-audit-workflow.md" "$live_root/workflows/security-audit-workflow.md" || fail "security sync changed legacy workflow bytes"
|
|
340
|
+
;;
|
|
341
|
+
esac
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
opencode_all_sync_modes() {
|
|
345
|
+
local probe_bin="$tmp/sync-probe-bin"
|
|
346
|
+
local process_log="$tmp/sync-processes.log"
|
|
347
|
+
local entry name normal_root enabled_root before after output state_root plugin_count
|
|
348
|
+
mkdir -p "$probe_bin"
|
|
349
|
+
: > "$process_log"
|
|
350
|
+
for command_name in cairn node; do
|
|
351
|
+
cat > "$probe_bin/$command_name" <<EOF
|
|
352
|
+
#!/usr/bin/env bash
|
|
353
|
+
echo "$command_name:\$*" >> "$process_log"
|
|
354
|
+
exit 91
|
|
355
|
+
EOF
|
|
356
|
+
chmod +x "$probe_bin/$command_name"
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
for spec in \
|
|
360
|
+
"wiki:scripts/sync-opencode-wiki-assets.sh" \
|
|
361
|
+
"graph:scripts/sync-opencode-graphify-assets.sh" \
|
|
362
|
+
"security:scripts/sync-opencode-security-assets.sh"
|
|
363
|
+
do
|
|
364
|
+
name=${spec%%:*}
|
|
365
|
+
entry="$ROOT/${spec#*:}"
|
|
366
|
+
normal_root="$tmp/$name-normal"
|
|
367
|
+
enabled_root="$tmp/$name-enabled"
|
|
368
|
+
state_root="$tmp/$name-state"
|
|
369
|
+
|
|
370
|
+
output=$(env -u CAIRN_CAPABILITY_CONTRACT \
|
|
371
|
+
PATH="$probe_bin:$PATH" CAIRN_HARNESS_STATE_DIR="$state_root/normal" \
|
|
372
|
+
"$entry" --apply --live-root "$normal_root") || fail "$name normal sync failed"
|
|
373
|
+
assert_opencode_sync_assets "$name" "$normal_root"
|
|
374
|
+
assert_opencode_legacy_plugins "$normal_root"
|
|
375
|
+
assert_no_retired_opencode_owners "$normal_root"
|
|
376
|
+
[[ ! -e "$normal_root/plugins/capability-command.ts" ]] || fail "$name normal sync registered capability plugin"
|
|
377
|
+
before=$(opencode_sync_tree_digest "$normal_root")
|
|
378
|
+
|
|
379
|
+
output+=$(CAIRN_CAPABILITY_CONTRACT=0 \
|
|
380
|
+
PATH="$probe_bin:$PATH" CAIRN_HARNESS_STATE_DIR="$state_root/master-off" \
|
|
381
|
+
"$entry" --apply --capability-overlay --live-root "$normal_root") || fail "$name master-off overlay sync failed"
|
|
382
|
+
after=$(opencode_sync_tree_digest "$normal_root")
|
|
383
|
+
[[ "$after" == "$before" ]] || fail "$name master-off overlay differs from exact normal sync"
|
|
384
|
+
assert_no_retired_opencode_owners "$normal_root"
|
|
385
|
+
[[ ! -e "$normal_root/plugins/capability-command.ts" ]] || fail "$name master-off sync registered capability plugin"
|
|
386
|
+
|
|
387
|
+
output+=$(CAIRN_CAPABILITY_CONTRACT=1 \
|
|
388
|
+
PATH="$probe_bin:$PATH" CAIRN_HARNESS_STATE_DIR="$state_root/enabled" \
|
|
389
|
+
"$entry" --apply --capability-overlay --live-root "$enabled_root") || fail "$name enabled overlay sync failed"
|
|
390
|
+
assert_opencode_sync_assets "$name" "$enabled_root"
|
|
391
|
+
assert_opencode_legacy_plugins "$enabled_root"
|
|
392
|
+
assert_no_retired_opencode_owners "$enabled_root"
|
|
393
|
+
plugin_count=$(find "$enabled_root/plugins" -maxdepth 1 -type f -name 'capability-*.ts' | wc -l | tr -d ' ')
|
|
394
|
+
[[ "$plugin_count" -eq 1 ]] || fail "$name enabled overlay installed more than the native capability plugin"
|
|
395
|
+
cmp -s <(sed "s|@@INFRA_ROOT@@|$ROOT|g" "$ROOT/opencode/capability-contract/plugins/capability-command.ts") \
|
|
396
|
+
"$enabled_root/plugins/capability-command.ts" || fail "$name enabled overlay capability plugin bytes differ"
|
|
397
|
+
grep -qF 'export const CapabilityCommandPlugin' "$enabled_root/plugins/capability-command.ts" || \
|
|
398
|
+
fail "$name enabled overlay omitted native plugin registration"
|
|
399
|
+
[[ ! -e "$state_root" ]] || fail "$name sync created capability measurement state"
|
|
400
|
+
[[ ! -s "$process_log" ]] || fail "$name sync started a capability coordinator process"
|
|
401
|
+
if grep -q -E 'disabled|permissionDecision|block' <<<"$output"; then
|
|
402
|
+
fail "$name sync emitted capability blocking state"
|
|
403
|
+
fi
|
|
404
|
+
done
|
|
405
|
+
|
|
406
|
+
echo "PASS: all OpenCode sync entrypoints preserve inert identity and select only the native overlay plugin"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if [[ "${1:-}" == "opencode-all-sync-modes" ]]; then
|
|
410
|
+
opencode_all_sync_modes
|
|
411
|
+
exit 0
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
if [[ "${1:-}" == "capability-recovery" ]]; then
|
|
415
|
+
capability_recovery
|
|
416
|
+
exit $?
|
|
417
|
+
fi
|
|
418
|
+
|
|
12
419
|
# A git repo scaffolded with the real templates.
|
|
13
420
|
repo="$tmp/repo"; mkdir "$repo"; git -C "$repo" init -q
|
|
14
421
|
"$ROOT/scripts/bootstrap.sh" "$repo" >/dev/null
|
|
15
422
|
launcher="$repo/.ai/start-claude.sh"
|
|
16
423
|
[[ -x "$launcher" ]] || fail "launcher not scaffolded"
|
|
424
|
+
pi_launcher="$repo/.ai/start-pi.sh"
|
|
425
|
+
[[ -x "$pi_launcher" ]] || fail "Pi launcher not scaffolded"
|
|
426
|
+
kimi_launcher="$repo/.ai/start-kimi.sh"
|
|
427
|
+
[[ -x "$kimi_launcher" ]] || fail "Kimi launcher not scaffolded"
|
|
428
|
+
|
|
429
|
+
# OpenCode's run mode ends a turn after content-only output. Commands that need
|
|
430
|
+
# owner I/O must therefore make their first response action a tool call rather
|
|
431
|
+
# than requiring a standalone banner before the config gate.
|
|
432
|
+
! grep -qF '**Before ANY tool calls**, display this banner:' "$ROOT/opencode/command/graphify.md" \
|
|
433
|
+
|| fail "OpenCode graphify can stop after its pre-tool banner"
|
|
434
|
+
grep -qF '**First action:** invoke the Read tool' "$ROOT/opencode/command/graphify.md" \
|
|
435
|
+
|| fail "OpenCode graphify does not require the config-gate tool call first"
|
|
436
|
+
grep -qF '**Invoke the shell tool** with this exact command' "$ROOT/opencode/command/graphify.md" \
|
|
437
|
+
|| fail "OpenCode graphify does not require real shell delegation"
|
|
438
|
+
grep -qF '<argument>$ARGUMENTS</argument>' "$ROOT/opencode/command/graphify.md" \
|
|
439
|
+
|| fail "OpenCode graphify does not delimit its substituted argument"
|
|
17
440
|
|
|
18
|
-
# Fake
|
|
441
|
+
# Fake harnesses on PATH: record argv, selected config root, and a marker env var.
|
|
19
442
|
mkdir "$tmp/bin"
|
|
20
443
|
cat > "$tmp/bin/claude" <<'FAKE'
|
|
21
444
|
#!/usr/bin/env bash
|
|
22
|
-
{ echo "args:$*"; echo "prelaunch:${PRELAUNCH_RAN:-0}"; } > "$CLAUDE_LOG"
|
|
445
|
+
{ echo "args:$*"; echo "prelaunch:${PRELAUNCH_RAN:-0}"; echo "config:${CLAUDE_CONFIG_DIR:-}"; echo "contract:${CAIRN_CAPABILITY_CONTRACT:-}"; } > "$CLAUDE_LOG"
|
|
446
|
+
exit "${FAKE_EXIT:-0}"
|
|
447
|
+
FAKE
|
|
448
|
+
cat > "$tmp/bin/opencode" <<'FAKE'
|
|
449
|
+
#!/usr/bin/env bash
|
|
450
|
+
{
|
|
451
|
+
echo "launch-time:$(date +%s)"
|
|
452
|
+
echo "args:$*"
|
|
453
|
+
echo "prelaunch:${PRELAUNCH_RAN:-0}"
|
|
454
|
+
echo "config:${OPENCODE_CONFIG_DIR:-}"
|
|
455
|
+
echo "explicit-config:${OPENCODE_CONFIG:-}"
|
|
456
|
+
echo "contract:${CAIRN_CAPABILITY_CONTRACT:-}"
|
|
457
|
+
if [[ -n "${OPENCODE_CONFIG_DIR:-}" && -d "$OPENCODE_CONFIG_DIR/plugins" ]]; then
|
|
458
|
+
find "$OPENCODE_CONFIG_DIR/plugins" -maxdepth 1 -type f -name '*.ts' -printf 'plugin:%f\n' | LC_ALL=C sort
|
|
459
|
+
fi
|
|
460
|
+
if [[ -n "${OPENCODE_CONFIG_DIR:-}" && -f "$OPENCODE_CONFIG_DIR/plugins/capability-command.ts" ]] \
|
|
461
|
+
&& grep -qF 'export const CapabilityCommandPlugin' "$OPENCODE_CONFIG_DIR/plugins/capability-command.ts"; then
|
|
462
|
+
echo "capability-registration:1"
|
|
463
|
+
else
|
|
464
|
+
echo "capability-registration:0"
|
|
465
|
+
fi
|
|
466
|
+
} > "$OPENCODE_LOG"
|
|
467
|
+
exit "${FAKE_EXIT:-0}"
|
|
468
|
+
FAKE
|
|
469
|
+
cat > "$tmp/bin/kimi" <<'FAKE'
|
|
470
|
+
#!/usr/bin/env bash
|
|
471
|
+
{ echo "args:$*"; echo "prelaunch:${PRELAUNCH_RAN:-0}"; echo "extra:${CAIRN_EXTRA_SETTINGS:-}"; } > "$KIMI_LOG"
|
|
23
472
|
exit "${FAKE_EXIT:-0}"
|
|
24
473
|
FAKE
|
|
25
|
-
chmod +x "$tmp/bin/claude"
|
|
474
|
+
chmod +x "$tmp/bin/claude" "$tmp/bin/opencode" "$tmp/bin/kimi"
|
|
475
|
+
ln -s "$ROOT/bin/cairn" "$tmp/bin/cairn"
|
|
26
476
|
export PATH="$tmp/bin:$PATH"
|
|
27
477
|
export CLAUDE_LOG="$tmp/claude.log"
|
|
478
|
+
export OPENCODE_LOG="$tmp/opencode.log"
|
|
479
|
+
export KIMI_LOG="$tmp/kimi.log"
|
|
480
|
+
|
|
481
|
+
# Claude sync has three exact installation states. Normal sync and an explicit
|
|
482
|
+
# overlay request with the master off must contain no capability hook bytes or
|
|
483
|
+
# registrations; only master-on plus --capability-overlay may install them.
|
|
484
|
+
normal_sync="$tmp/claude-normal-sync"
|
|
485
|
+
normal_sync_before="$tmp/claude-normal-sync-before"
|
|
486
|
+
master_on_sync="$tmp/claude-master-on-sync"
|
|
487
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
488
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$normal_sync" >/dev/null
|
|
489
|
+
cp -a "$normal_sync" "$normal_sync_before"
|
|
490
|
+
CAIRN_CAPABILITY_CONTRACT=0 \
|
|
491
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$normal_sync" >/dev/null
|
|
492
|
+
for live in "$normal_sync" "$normal_sync_before"; do
|
|
493
|
+
[[ ! -e "$live/hooks/capability-command-start.sh" ]] || fail "inert Claude sync installed start-hook bytes"
|
|
494
|
+
[[ ! -e "$live/hooks/capability-command-finish.sh" ]] || fail "inert Claude sync installed finish-hook bytes"
|
|
495
|
+
! grep -R -qF 'capability-command-' "$live" || fail "inert Claude sync registered a capability hook"
|
|
496
|
+
[[ ! -e "$repo/.agentfs/trajectory.db" ]] || fail "inert Claude sync created measurement state"
|
|
497
|
+
done
|
|
498
|
+
diff -qr "$normal_sync_before" "$normal_sync" >/dev/null || fail "master-off overlay differs from exact normal sync"
|
|
499
|
+
|
|
500
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
501
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$master_on_sync" >/dev/null
|
|
502
|
+
for name in capability-command-start.sh capability-command-finish.sh; do
|
|
503
|
+
installed="$master_on_sync/hooks/$name"
|
|
504
|
+
[[ -x "$installed" ]] || fail "enabled Claude overlay omitted $name"
|
|
505
|
+
grep -qF "INFRA_ROOT=\"$ROOT\"" "$installed" || fail "enabled Claude hook was not rendered"
|
|
506
|
+
grep -qF 'mcp-memory-server/dist/capability-cli.js' "$installed" || fail "enabled Claude hook omitted coordinator delegation"
|
|
507
|
+
done
|
|
508
|
+
node - "$master_on_sync/settings.json" <<'NODE' || fail "enabled Claude overlay registrations are incomplete"
|
|
509
|
+
const fs = require("node:fs");
|
|
510
|
+
const settings = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
511
|
+
const registrations = [];
|
|
512
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
513
|
+
for (const entry of entries) for (const hook of entry.hooks ?? []) {
|
|
514
|
+
if (String(hook.command ?? "").includes("capability-command-")) {
|
|
515
|
+
registrations.push({ event, matcher: entry.matcher ?? "", command: hook.command });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (registrations.filter((row) => row.command.includes("capability-command-start.sh")).length !== 1) process.exit(1);
|
|
520
|
+
if (registrations.filter((row) => row.command.includes("capability-command-finish.sh")).length !== 4) process.exit(1);
|
|
521
|
+
if (!registrations.some((row) => row.event === "UserPromptExpansion" && row.matcher === "wiki-ingest|wiki-query|wiki-lint|graphify|security-audit")) process.exit(1);
|
|
522
|
+
if (["Stop", "StopFailure", "CwdChanged", "SessionEnd"].some((event) => !registrations.some((row) => row.event === event))) process.exit(1);
|
|
523
|
+
NODE
|
|
28
524
|
|
|
29
525
|
# 1. No hooks: launcher execs claude with the passed args (baseline unchanged).
|
|
30
526
|
rm -f "$CLAUDE_LOG"
|
|
31
527
|
"$launcher" --foo bar >/dev/null 2>&1 || fail "baseline launch failed"
|
|
32
528
|
grep -qx "args:--foo bar" "$CLAUDE_LOG" || fail "baseline did not pass args through"
|
|
33
529
|
grep -qx "prelaunch:0" "$CLAUDE_LOG" || fail "prelaunch marker leaked without a hook"
|
|
530
|
+
grep -qx "config:" "$CLAUDE_LOG" || fail "baseline selected a Claude config root"
|
|
531
|
+
|
|
532
|
+
opencode_launcher="$repo/.ai/start-opencode.sh"
|
|
533
|
+
[[ -x "$opencode_launcher" ]] || fail "OpenCode launcher not scaffolded"
|
|
534
|
+
"$opencode_launcher" --foo bar >/dev/null 2>&1 || fail "OpenCode baseline launch failed"
|
|
535
|
+
grep -qx "args:--foo bar" "$OPENCODE_LOG" || fail "OpenCode baseline did not pass args through"
|
|
536
|
+
grep -qx "config:" "$OPENCODE_LOG" || fail "OpenCode baseline selected a config root"
|
|
537
|
+
grep -qx "capability-registration:0" "$OPENCODE_LOG" || fail "OpenCode baseline registered a capability plugin"
|
|
538
|
+
|
|
539
|
+
"$kimi_launcher" --foo bar >/dev/null 2>&1 || fail "Kimi baseline launch failed"
|
|
540
|
+
grep -qx "args:--foo bar" "$KIMI_LOG" || fail "Kimi baseline did not pass args through"
|
|
541
|
+
grep -qx "prelaunch:0" "$KIMI_LOG" || fail "Kimi prelaunch marker leaked without a hook"
|
|
542
|
+
|
|
543
|
+
# Invalid/off master values stay on the legacy direct-exec path: one harness
|
|
544
|
+
# process, unchanged argv/config environment, and no isolated assets or store.
|
|
545
|
+
legacy_claude="$tmp/legacy-claude"
|
|
546
|
+
legacy_opencode="$tmp/legacy-opencode"
|
|
547
|
+
mkdir -p "$legacy_claude/commands" "$legacy_opencode/command"
|
|
548
|
+
printf 'legacy-claude\n' > "$legacy_claude/commands/sentinel.md"
|
|
549
|
+
printf 'legacy-opencode\n' > "$legacy_opencode/command/sentinel.md"
|
|
550
|
+
sha256sum "$legacy_claude/commands/sentinel.md" > "$tmp/legacy-claude.sha"
|
|
551
|
+
sha256sum "$legacy_opencode/command/sentinel.md" > "$tmp/legacy-opencode.sha"
|
|
552
|
+
for master_value in 0 false FALSE no off invalid; do
|
|
553
|
+
rm -rf "$repo/.ai/capability-contract" "$repo/.agentfs/trajectory.db" "$CLAUDE_LOG" "$OPENCODE_LOG"
|
|
554
|
+
CAIRN_CAPABILITY_CONTRACT="$master_value" CLAUDE_CONFIG_DIR="$legacy_claude" \
|
|
555
|
+
"$launcher" --identity >/dev/null 2>&1 || fail "Claude invalid-master launch failed: $master_value"
|
|
556
|
+
CAIRN_CAPABILITY_CONTRACT="$master_value" OPENCODE_CONFIG_DIR="$legacy_opencode" \
|
|
557
|
+
"$opencode_launcher" --identity >/dev/null 2>&1 || fail "OpenCode invalid-master launch failed: $master_value"
|
|
558
|
+
grep -qx "args:--identity" "$CLAUDE_LOG" || fail "Claude invalid-master argv changed: $master_value"
|
|
559
|
+
grep -qx "config:$legacy_claude" "$CLAUDE_LOG" || fail "Claude invalid-master config changed: $master_value"
|
|
560
|
+
grep -qx "contract:$master_value" "$CLAUDE_LOG" || fail "Claude invalid-master environment changed: $master_value"
|
|
561
|
+
grep -qx "args:--identity" "$OPENCODE_LOG" || fail "OpenCode invalid-master argv changed: $master_value"
|
|
562
|
+
grep -qx "config:$legacy_opencode" "$OPENCODE_LOG" || fail "OpenCode invalid-master config changed: $master_value"
|
|
563
|
+
grep -qx "contract:$master_value" "$OPENCODE_LOG" || fail "OpenCode invalid-master environment changed: $master_value"
|
|
564
|
+
grep -qx "capability-registration:0" "$OPENCODE_LOG" || fail "OpenCode invalid-master launch registered a capability plugin: $master_value"
|
|
565
|
+
[[ ! -e "$repo/.ai/capability-contract" ]] || fail "invalid master created an isolated root: $master_value"
|
|
566
|
+
[[ ! -e "$repo/.agentfs/trajectory.db" ]] || fail "invalid master created a callback store: $master_value"
|
|
567
|
+
sha256sum -c "$tmp/legacy-claude.sha" >/dev/null || fail "invalid master changed legacy Claude assets"
|
|
568
|
+
sha256sum -c "$tmp/legacy-opencode.sha" >/dev/null || fail "invalid master changed legacy OpenCode assets"
|
|
569
|
+
done
|
|
570
|
+
|
|
571
|
+
# A truthy master selects project-local isolated roots and installs every guarded
|
|
572
|
+
# family through the explicit overlay modes. Legacy live roots remain untouched.
|
|
573
|
+
rm -rf "$repo/.ai/capability-contract" "$CLAUDE_LOG" "$OPENCODE_LOG"
|
|
574
|
+
CAIRN_CAPABILITY_CONTRACT=' TrUe ' CLAUDE_CONFIG_DIR="$legacy_claude" \
|
|
575
|
+
"$launcher" --overlay >/dev/null 2>&1 || fail "Claude capability-overlay launch failed"
|
|
576
|
+
claude_overlay="$repo/.ai/capability-contract/claude"
|
|
577
|
+
grep -qx "args:--overlay" "$CLAUDE_LOG" || fail "Claude overlay launch changed argv"
|
|
578
|
+
grep -qx "config:$claude_overlay" "$CLAUDE_LOG" || fail "Claude overlay root was not selected"
|
|
579
|
+
for rel in commands/wiki-ingest.md commands/wiki-query.md commands/wiki-lint.md commands/graphify.md commands/security-audit.md; do
|
|
580
|
+
cmp -s "$ROOT/claude/$rel" "$claude_overlay/$rel" || fail "Claude overlay changed legacy owner bytes: $rel"
|
|
581
|
+
done
|
|
582
|
+
CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/scripts/sync-claude-assets.sh" --check --capability-overlay --live-root "$claude_overlay" >/dev/null || fail "Claude overlay family is incomplete"
|
|
583
|
+
for name in capability-command-start.sh capability-command-finish.sh; do
|
|
584
|
+
cmp -s "$master_on_sync/hooks/$name" "$claude_overlay/hooks/$name" || fail "launcher overlay hook bytes differ: $name"
|
|
585
|
+
done
|
|
586
|
+
|
|
587
|
+
CAIRN_CAPABILITY_CONTRACT=1 OPENCODE_CONFIG_DIR="$legacy_opencode" \
|
|
588
|
+
"$opencode_launcher" --overlay >/dev/null 2>&1 || fail "OpenCode capability-overlay launch failed"
|
|
589
|
+
opencode_overlay="$repo/.ai/capability-contract/opencode"
|
|
590
|
+
grep -qx "args:--overlay" "$OPENCODE_LOG" || fail "OpenCode overlay launch changed argv"
|
|
591
|
+
grep -qx "config:$opencode_overlay" "$OPENCODE_LOG" || fail "OpenCode overlay root was not selected"
|
|
592
|
+
grep -Eq '^launch-time:[0-9]+$' "$OPENCODE_LOG" || fail "OpenCode stub did not record launch time"
|
|
593
|
+
grep -qx 'capability-registration:1' "$OPENCODE_LOG" || fail "OpenCode capability plugin was not registered before process start"
|
|
594
|
+
for plugin in memory-wakeup.ts memory-capture.ts memory-recall.ts capability-command.ts; do
|
|
595
|
+
grep -qx "plugin:$plugin" "$OPENCODE_LOG" || fail "OpenCode plugin was not present before process start: $plugin"
|
|
596
|
+
done
|
|
597
|
+
cmp -s <(sed "s|@@INFRA_ROOT@@|$ROOT|g" "$ROOT/opencode/capability-contract/plugins/capability-command.ts") \
|
|
598
|
+
"$opencode_overlay/plugins/capability-command.ts" || fail "launcher capability plugin bytes differ from rendered source"
|
|
599
|
+
for rel in \
|
|
600
|
+
command/wiki-ingest.md command/wiki-query.md command/wiki-lint.md \
|
|
601
|
+
workflows/wiki-ingest-workflow.md workflows/wiki-query-workflow.md workflows/wiki-lint-workflow.md \
|
|
602
|
+
command/graphify.md command/security-audit.md workflows/security-audit-workflow.md
|
|
603
|
+
do
|
|
604
|
+
cmp -s "$ROOT/opencode/$rel" "$opencode_overlay/$rel" || fail "OpenCode overlay changed legacy owner bytes: $rel"
|
|
605
|
+
done
|
|
606
|
+
"$ROOT/scripts/sync-opencode-wiki-assets.sh" --check --capability-overlay --live-root "$opencode_overlay" >/dev/null || fail "OpenCode wiki overlay family is incomplete"
|
|
607
|
+
"$ROOT/scripts/sync-opencode-graphify-assets.sh" --check --capability-overlay --live-root "$opencode_overlay" >/dev/null || fail "OpenCode graph overlay family is incomplete"
|
|
608
|
+
"$ROOT/scripts/sync-opencode-security-assets.sh" --check --capability-overlay --live-root "$opencode_overlay" >/dev/null || fail "OpenCode security overlay family is incomplete"
|
|
609
|
+
CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/scripts/sync-opencode-plugin-assets.sh" --check --capability-overlay --live-root "$opencode_overlay" >/dev/null || fail "OpenCode plugin overlay family is incomplete"
|
|
610
|
+
[[ ! -e "$repo/.agentfs/trajectory.db" ]] || fail "OpenCode launcher sync created measurement state before command admission"
|
|
611
|
+
"$ROOT/scripts/test-phase18-capability-lifecycle.sh" operating --harness opencode-workflow --capability wiki >/dev/null
|
|
612
|
+
"$ROOT/scripts/test-phase18-capability-lifecycle.sh" operating --harness opencode-workflow --capability security.audit >/dev/null
|
|
613
|
+
sha256sum -c "$tmp/legacy-claude.sha" >/dev/null || fail "overlay launch changed legacy Claude assets"
|
|
614
|
+
sha256sum -c "$tmp/legacy-opencode.sha" >/dev/null || fail "overlay launch changed legacy OpenCode assets"
|
|
34
615
|
|
|
35
616
|
# 2. pre-launch.sh is sourced (can export env) + CAIRN_EXTRA_SETTINGS layers in.
|
|
36
617
|
rm -f "$CLAUDE_LOG"
|
|
@@ -43,6 +624,18 @@ echo '{}' > "$tmp/settings.json"
|
|
|
43
624
|
grep -qx "prelaunch:1" "$CLAUDE_LOG" || fail "pre-launch env not visible to harness"
|
|
44
625
|
grep -q -- "--settings $tmp/settings.json" "$CLAUDE_LOG" || fail "CAIRN_EXTRA_SETTINGS not passed as --settings"
|
|
45
626
|
|
|
627
|
+
rm -f "$OPENCODE_LOG"
|
|
628
|
+
"$opencode_launcher" --settings-probe >/dev/null 2>&1 || fail "OpenCode launch with extra config failed"
|
|
629
|
+
grep -qx "prelaunch:1" "$OPENCODE_LOG" || fail "OpenCode pre-launch env not visible to harness"
|
|
630
|
+
grep -qx "args:--settings-probe" "$OPENCODE_LOG" || fail "OpenCode extra config changed harness argv"
|
|
631
|
+
grep -qx "explicit-config:$tmp/settings.json" "$OPENCODE_LOG" || fail "CAIRN_EXTRA_SETTINGS not exported as OPENCODE_CONFIG"
|
|
632
|
+
|
|
633
|
+
rm -f "$KIMI_LOG"
|
|
634
|
+
"$kimi_launcher" --plan >/dev/null 2>&1 || fail "Kimi launch with pre-launch failed"
|
|
635
|
+
grep -qx "args:--plan" "$KIMI_LOG" || fail "Kimi pre-launch changed harness argv"
|
|
636
|
+
grep -qx "prelaunch:1" "$KIMI_LOG" || fail "Kimi pre-launch env not visible to harness"
|
|
637
|
+
grep -qx "extra:$tmp/settings.json" "$KIMI_LOG" || fail "Kimi pre-launch environment was not preserved"
|
|
638
|
+
|
|
46
639
|
# 3. pre-launch.sh non-zero aborts the launch before the harness runs.
|
|
47
640
|
rm -f "$CLAUDE_LOG"
|
|
48
641
|
cat > "$repo/.ai/pre-launch.sh" <<'EOF'
|
|
@@ -51,6 +644,9 @@ return 1
|
|
|
51
644
|
EOF
|
|
52
645
|
if "$launcher" >/dev/null 2>&1; then fail "non-zero pre-launch should abort the launch"; fi
|
|
53
646
|
[[ ! -f "$CLAUDE_LOG" ]] || fail "harness ran despite pre-launch abort"
|
|
647
|
+
rm -f "$KIMI_LOG"
|
|
648
|
+
if "$kimi_launcher" >/dev/null 2>&1; then fail "non-zero pre-launch should abort Kimi"; fi
|
|
649
|
+
[[ ! -f "$KIMI_LOG" ]] || fail "Kimi ran despite pre-launch abort"
|
|
54
650
|
rm -f "$repo/.ai/pre-launch.sh"
|
|
55
651
|
|
|
56
652
|
# 4. post-exit.sh runs after the harness with $CAIRN_EXIT_STATUS.
|
|
@@ -63,4 +659,35 @@ status=$?
|
|
|
63
659
|
[[ "$status" -eq 7 ]] || fail "expected exit 7 from launcher, got $status"
|
|
64
660
|
grep -qx "post:7" "$tmp/post.log" || fail "post-exit hook did not see CAIRN_EXIT_STATUS"
|
|
65
661
|
|
|
662
|
+
rm -f "$tmp/post.log" "$KIMI_LOG"
|
|
663
|
+
FAKE_EXIT=8 "$kimi_launcher" --auto >/dev/null 2>&1 && fail "Kimi launcher should propagate harness exit code"
|
|
664
|
+
status=$?
|
|
665
|
+
[[ "$status" -eq 8 ]] || fail "expected exit 8 from Kimi launcher, got $status"
|
|
666
|
+
grep -qx "args:--auto" "$KIMI_LOG" || fail "Kimi launcher changed arguments"
|
|
667
|
+
grep -qx "post:8" "$tmp/post.log" || fail "Kimi launcher post-exit status missing"
|
|
668
|
+
|
|
669
|
+
# 5. Pi launcher preserves the same env/pre/post seams and passes arguments
|
|
670
|
+
# unchanged; Pi has no generic settings-file flag so CAIRN_EXTRA_SETTINGS is ignored.
|
|
671
|
+
rm -f "$repo/.ai/pre-launch.sh" "$repo/.ai/post-exit.sh"
|
|
672
|
+
cat > "$tmp/bin/pi" <<'FAKE'
|
|
673
|
+
#!/usr/bin/env bash
|
|
674
|
+
{ echo "args:$*"; echo "prelaunch:${PRELAUNCH_RAN:-0}"; echo "extra:${CAIRN_EXTRA_SETTINGS:-}"; } > "$PI_LOG"
|
|
675
|
+
exit "${FAKE_EXIT:-0}"
|
|
676
|
+
FAKE
|
|
677
|
+
chmod +x "$tmp/bin/pi"
|
|
678
|
+
export PI_LOG="$tmp/pi.log"
|
|
679
|
+
cat > "$repo/.ai/pre-launch.sh" <<EOF
|
|
680
|
+
export PRELAUNCH_RAN=1
|
|
681
|
+
export CAIRN_EXTRA_SETTINGS="$tmp/pi-settings.json"
|
|
682
|
+
EOF
|
|
683
|
+
cat > "$repo/.ai/post-exit.sh" <<EOF
|
|
684
|
+
echo "pi-post:\${CAIRN_EXIT_STATUS}" > "$tmp/pi-post.log"
|
|
685
|
+
EOF
|
|
686
|
+
FAKE_EXIT=9 "$pi_launcher" --model fixture/model >/dev/null 2>&1 && fail "Pi launcher should propagate harness exit code"
|
|
687
|
+
status=$?
|
|
688
|
+
[[ "$status" -eq 9 ]] || fail "expected exit 9 from Pi launcher, got $status"
|
|
689
|
+
grep -qx "args:--model fixture/model" "$PI_LOG" || fail "Pi launcher changed arguments"
|
|
690
|
+
grep -qx "prelaunch:1" "$PI_LOG" || fail "Pi launcher did not source pre-launch"
|
|
691
|
+
grep -qx "pi-post:9" "$tmp/pi-post.log" || fail "Pi launcher post-exit status missing"
|
|
692
|
+
|
|
66
693
|
echo "PASS: launcher hook seams (pre-launch source/abort, settings layering, post-exit)"
|