@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.
- package/CHANGELOG.md +78 -0
- package/README.md +221 -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 +1 -1
- package/docs/design/meta-agent.md +439 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/operating.md +531 -4
- 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 +9 -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 +4 -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 +48 -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 +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -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 +37 -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-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Phase 18 capability CLI, lifecycle, operating-overlay, and ablation contract.
|
|
3
|
+
# No-argument execution is the complete GREEN contract. Explicit expected-RED
|
|
4
|
+
# modes remain available only to classify historical missing-owner simulations.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
8
|
+
EXPECTED_RED_EXIT=86
|
|
9
|
+
RED_MARKER="PHASE18_RED:CAPABILITY_LIFECYCLE_MISSING"
|
|
10
|
+
MODE="${1:-full}"
|
|
11
|
+
[[ $# -gt 0 ]] && shift
|
|
12
|
+
RUNTIME=0
|
|
13
|
+
HARNESS=""
|
|
14
|
+
CAPABILITY=""
|
|
15
|
+
|
|
16
|
+
usage() {
|
|
17
|
+
cat <<'USAGE'
|
|
18
|
+
Usage: test-phase18-capability-lifecycle.sh [baseline|--expect-red-contract|contract|matrix|bootstrap|uninstall|lifecycle [--runtime]|claude-native|native-boundary|owner-retirement|full]
|
|
19
|
+
test-phase18-capability-lifecycle.sh operating --harness claude|opencode-command|opencode-workflow --capability wiki|graph|security.audit
|
|
20
|
+
USAGE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fail() {
|
|
24
|
+
echo "FAIL: $1" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case "$MODE" in
|
|
29
|
+
baseline|--expect-red-contract|contract|matrix|bootstrap|uninstall|claude-native|native-boundary|owner-retirement|full)
|
|
30
|
+
[[ $# -eq 0 ]] || { usage >&2; exit 2; }
|
|
31
|
+
;;
|
|
32
|
+
lifecycle)
|
|
33
|
+
if [[ "${1:-}" == "--runtime" ]]; then RUNTIME=1; shift; fi
|
|
34
|
+
[[ $# -eq 0 ]] || { usage >&2; exit 2; }
|
|
35
|
+
;;
|
|
36
|
+
operating)
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--harness) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; HARNESS="$2"; shift 2 ;;
|
|
40
|
+
--capability) [[ $# -ge 2 ]] || { usage >&2; exit 2; }; CAPABILITY="$2"; shift 2 ;;
|
|
41
|
+
*) usage >&2; exit 2 ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
case "$HARNESS" in claude|opencode-command|opencode-workflow) ;; *) usage >&2; exit 2 ;; esac
|
|
45
|
+
case "$CAPABILITY" in wiki|graph|security.audit) ;; *) usage >&2; exit 2 ;; esac
|
|
46
|
+
if [[ "$HARNESS" == "opencode-workflow" && "$CAPABILITY" == "graph" ]]; then
|
|
47
|
+
echo "graph has no directly invokable OpenCode workflow" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
;;
|
|
51
|
+
*) usage >&2; exit 2 ;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
# Selector validation above intentionally precedes all fixture mutation.
|
|
55
|
+
tmp=$(mktemp -d)
|
|
56
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
57
|
+
baseline_number=0
|
|
58
|
+
|
|
59
|
+
run_baseline_check() {
|
|
60
|
+
baseline_number=$((baseline_number + 1))
|
|
61
|
+
local output="$tmp/baseline-$baseline_number.out"
|
|
62
|
+
if ! env -u CAIRN_CAPABILITY_CONTRACT "$@" >"$output" 2>&1; then
|
|
63
|
+
cat "$output" >&2
|
|
64
|
+
fail "baseline failed: $*"
|
|
65
|
+
fi
|
|
66
|
+
if grep -qF 'PHASE18_RED:' "$output"; then
|
|
67
|
+
cat "$output" >&2
|
|
68
|
+
fail "baseline emitted a Phase 18 RED marker: $*"
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
assert_same_file() {
|
|
73
|
+
cmp -s "$1" "$2" || fail "installed bytes differ: $2"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
run_master_off_identity() {
|
|
77
|
+
local project="$tmp/identity-project"
|
|
78
|
+
local fake_bin="$tmp/identity-bin"
|
|
79
|
+
local trace="$tmp/identity.trace"
|
|
80
|
+
local claude_live="$tmp/claude-live"
|
|
81
|
+
local opencode_live="$tmp/opencode-live"
|
|
82
|
+
mkdir -p "$project" "$fake_bin"
|
|
83
|
+
git -C "$project" init -q
|
|
84
|
+
"$ROOT/scripts/bootstrap.sh" "$project" >"$tmp/bootstrap.out"
|
|
85
|
+
printf '%s' '{"schema_version":1,"capabilities":' >"$project/.ai/capabilities.json"
|
|
86
|
+
chmod 600 "$project/.ai/capabilities.json"
|
|
87
|
+
sha256sum "$project/.ai/capabilities.json" >"$tmp/config.before"
|
|
88
|
+
|
|
89
|
+
for executable in claude opencode; do
|
|
90
|
+
cat >"$fake_bin/$executable" <<'STUB'
|
|
91
|
+
#!/usr/bin/env bash
|
|
92
|
+
printf '%s|%s' "$(basename "$0")" "$PWD" >>"$TRACE_FILE"
|
|
93
|
+
printf '|%s' "$@" >>"$TRACE_FILE"
|
|
94
|
+
printf '\n' >>"$TRACE_FILE"
|
|
95
|
+
exit "${HARNESS_EXIT:-0}"
|
|
96
|
+
STUB
|
|
97
|
+
chmod 755 "$fake_bin/$executable"
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
: >"$trace"
|
|
101
|
+
if ! env -u CAIRN_CAPABILITY_CONTRACT PATH="$fake_bin:$PATH" TRACE_FILE="$trace" \
|
|
102
|
+
"$project/.ai/start-claude.sh" --identity >"$tmp/claude.out" 2>"$tmp/claude.err"; then
|
|
103
|
+
fail "master-off Claude launcher changed exit behavior"
|
|
104
|
+
fi
|
|
105
|
+
if ! env -u CAIRN_CAPABILITY_CONTRACT PATH="$fake_bin:$PATH" TRACE_FILE="$trace" \
|
|
106
|
+
"$project/.ai/start-opencode.sh" --identity >"$tmp/opencode.out" 2>"$tmp/opencode.err"; then
|
|
107
|
+
fail "master-off OpenCode launcher changed exit behavior"
|
|
108
|
+
fi
|
|
109
|
+
[[ ! -s "$tmp/claude.out" && ! -s "$tmp/claude.err" && ! -s "$tmp/opencode.out" && ! -s "$tmp/opencode.err" ]] || \
|
|
110
|
+
fail "master-off launcher added output"
|
|
111
|
+
printf 'claude|%s|--identity\nopencode|%s|--identity\n' "$project" "$project" >"$tmp/identity.expected"
|
|
112
|
+
cmp -s "$tmp/identity.expected" "$trace" || fail "master-off launcher process/argv trace changed"
|
|
113
|
+
|
|
114
|
+
env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
|
|
115
|
+
for rel in commands/wiki-ingest.md commands/wiki-query.md commands/wiki-lint.md commands/graphify.md commands/security-audit.md; do
|
|
116
|
+
assert_same_file "$ROOT/claude/$rel" "$claude_live/$rel"
|
|
117
|
+
done
|
|
118
|
+
env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-wiki-assets.sh" --apply --live-root "$opencode_live" >/dev/null
|
|
119
|
+
env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-graphify-assets.sh" --apply --live-root "$opencode_live" >/dev/null
|
|
120
|
+
env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/scripts/sync-opencode-security-assets.sh" --apply --live-root "$opencode_live" >/dev/null
|
|
121
|
+
for rel in \
|
|
122
|
+
command/wiki-ingest.md command/wiki-query.md command/wiki-lint.md \
|
|
123
|
+
workflows/wiki-ingest-workflow.md workflows/wiki-query-workflow.md workflows/wiki-lint-workflow.md \
|
|
124
|
+
command/graphify.md command/security-audit.md workflows/security-audit-workflow.md
|
|
125
|
+
do
|
|
126
|
+
assert_same_file "$ROOT/opencode/$rel" "$opencode_live/$rel"
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
sha256sum -c "$tmp/config.before" >/dev/null || fail "master-off path changed malformed capability config"
|
|
130
|
+
if find "$project/.agentfs" -type f \( -name '*.db' -o -name '*.db-wal' -o -name '*.db-shm' \) | grep -q .; then
|
|
131
|
+
fail "master-off path created a capability/logging store"
|
|
132
|
+
fi
|
|
133
|
+
if grep -R -qF '{"schema_version":1,"capabilities":' "$tmp/claude.out" "$tmp/claude.err" "$tmp/opencode.out" "$tmp/opencode.err"; then
|
|
134
|
+
fail "master-off path exposed configuration contents"
|
|
135
|
+
fi
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
run_baseline() {
|
|
139
|
+
run_baseline_check "$ROOT/scripts/test-cli-dispatch.sh"
|
|
140
|
+
run_baseline_check "$ROOT/scripts/test-bootstrap-untracked.sh"
|
|
141
|
+
run_baseline_check "$ROOT/scripts/test-doctor.sh"
|
|
142
|
+
run_baseline_check "$ROOT/scripts/test-completion.sh"
|
|
143
|
+
run_baseline_check "$ROOT/scripts/test-package-install.sh"
|
|
144
|
+
run_baseline_check "$ROOT/scripts/test-uninstall.sh"
|
|
145
|
+
run_baseline_check bash -n "$ROOT/bin/cairn"
|
|
146
|
+
run_master_off_identity
|
|
147
|
+
echo "PASS: Phase 18 master-off CLI/package/lifecycle identity baseline"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
new_project() {
|
|
151
|
+
local project="$1"
|
|
152
|
+
mkdir -p "$project"
|
|
153
|
+
git -C "$project" init -q
|
|
154
|
+
"$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
run_capability() {
|
|
158
|
+
local project="$1"
|
|
159
|
+
shift
|
|
160
|
+
(cd "$project" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities "$@")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
enable_all_capabilities() {
|
|
164
|
+
local project="$1" id
|
|
165
|
+
for id in memory.write memory.search notes.distill wiki graph security.audit route.check context.explore; do
|
|
166
|
+
run_capability "$project" enable "$id" >/dev/null
|
|
167
|
+
done
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
run_contract() {
|
|
171
|
+
local project="$tmp/contract-project"
|
|
172
|
+
local config_path="$project/.ai/capabilities.json"
|
|
173
|
+
local status="$tmp/status.json"
|
|
174
|
+
local before after
|
|
175
|
+
new_project "$project"
|
|
176
|
+
|
|
177
|
+
run_capability "$project" list >"$tmp/list.out" 2>"$tmp/list.err" || fail "capabilities list failed"
|
|
178
|
+
run_capability "$project" status >"$tmp/status.out" 2>"$tmp/status.err" || fail "human status failed"
|
|
179
|
+
run_capability "$project" status --json >"$status" 2>"$tmp/status-json.err" || fail "JSON status failed"
|
|
180
|
+
node - "$status" <<'NODE'
|
|
181
|
+
const fs = require("fs");
|
|
182
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
183
|
+
const ids = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
|
|
184
|
+
if (value.schema_version !== 1 || value.contract_enabled !== true) process.exit(1);
|
|
185
|
+
if (JSON.stringify(value.capabilities.map((row) => row.id)) !== JSON.stringify(ids)) process.exit(1);
|
|
186
|
+
if (!/^[0-9a-f]{64}$/.test(value.configuration_digest)) process.exit(1);
|
|
187
|
+
for (const row of value.capabilities) {
|
|
188
|
+
if (row.restart_required !== (row.kind === "mcp-tool")) process.exit(1);
|
|
189
|
+
if (!["environment", "project", "compatibility"].includes(row.source)) process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
NODE
|
|
192
|
+
|
|
193
|
+
before=$(sha256sum "$config_path" | cut -d' ' -f1)
|
|
194
|
+
if run_capability "$project" disable unknown.owner >"$tmp/unknown.out" 2>"$tmp/unknown.err"; then
|
|
195
|
+
fail "unknown capability ID was accepted"
|
|
196
|
+
fi
|
|
197
|
+
after=$(sha256sum "$config_path" | cut -d' ' -f1)
|
|
198
|
+
[[ "$before" == "$after" ]] || fail "invalid ID mutated configuration"
|
|
199
|
+
|
|
200
|
+
run_capability "$project" disable memory.write >/dev/null || fail "disable failed"
|
|
201
|
+
run_capability "$project" enable wiki >/dev/null || fail "enable failed"
|
|
202
|
+
run_capability "$project" reset memory.write >/dev/null || fail "reset failed"
|
|
203
|
+
run_capability "$project" logging enable >/dev/null || fail "logging enable failed"
|
|
204
|
+
run_capability "$project" logging disable >/dev/null || fail "logging disable failed"
|
|
205
|
+
run_capability "$project" logging reset >/dev/null || fail "logging reset failed"
|
|
206
|
+
node - "$config_path" <<'NODE'
|
|
207
|
+
const fs = require("fs");
|
|
208
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
209
|
+
if (Object.hasOwn(value.capabilities, "memory.write")) process.exit(1);
|
|
210
|
+
if (value.capabilities.wiki !== true) process.exit(1);
|
|
211
|
+
if (Object.hasOwn(value.logging, "callbacks")) process.exit(1);
|
|
212
|
+
NODE
|
|
213
|
+
|
|
214
|
+
run_capability "$project" disable memory.search >"$tmp/concurrent-one.out" 2>&1 &
|
|
215
|
+
local first_pid=$!
|
|
216
|
+
run_capability "$project" disable security.audit >"$tmp/concurrent-two.out" 2>&1 &
|
|
217
|
+
local second_pid=$!
|
|
218
|
+
wait "$first_pid" || fail "first concurrent mutation failed"
|
|
219
|
+
wait "$second_pid" || fail "second concurrent mutation failed"
|
|
220
|
+
node - "$config_path" <<'NODE'
|
|
221
|
+
const fs = require("fs");
|
|
222
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
223
|
+
if (value.capabilities["memory.search"] !== false || value.capabilities["security.audit"] !== false) process.exit(1);
|
|
224
|
+
NODE
|
|
225
|
+
|
|
226
|
+
printf '%s\n' '{"schema_version":1,"capabilities":{"memory.write":"PHASE18_RAW_SENTINEL","wiki":false},"logging":{"callbacks":false}}' >"$config_path"
|
|
227
|
+
chmod 600 "$config_path"
|
|
228
|
+
run_capability "$project" status --json >"$tmp/invalid-status.out" 2>"$tmp/invalid-status.err" || fail "row-local invalid status failed"
|
|
229
|
+
(cd "$project" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" doctor) >"$tmp/invalid-doctor.out" 2>"$tmp/invalid-doctor.err" || true
|
|
230
|
+
if grep -R -qF 'PHASE18_RAW_SENTINEL' "$tmp/invalid-status.out" "$tmp/invalid-status.err" "$tmp/invalid-doctor.out" "$tmp/invalid-doctor.err"; then
|
|
231
|
+
fail "status or doctor exposed raw configuration contents"
|
|
232
|
+
fi
|
|
233
|
+
grep -qF 'invalid-capability-value' "$tmp/invalid-status.out" || fail "status omitted the fixed invalid-row issue"
|
|
234
|
+
grep -q '"id":"wiki".*"enabled":false\|"enabled":false.*"id":"wiki"' "$tmp/invalid-status.out" || fail "one invalid row poisoned an unrelated valid row"
|
|
235
|
+
|
|
236
|
+
cp "$config_path" "$tmp/config-before-rerun"
|
|
237
|
+
"$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
|
|
238
|
+
cmp -s "$tmp/config-before-rerun" "$config_path" || fail "bootstrap rerun overwrote capability state"
|
|
239
|
+
[[ $(stat -c '%a' "$config_path" 2>/dev/null || stat -f '%Lp' "$config_path") == 600 ]] || fail "capability config is not mode 0600"
|
|
240
|
+
echo "PASS: Phase 18 capability CLI/configuration contract"
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
run_bootstrap() {
|
|
244
|
+
local project="$tmp/bootstrap-project"
|
|
245
|
+
local config_path="$project/.ai/capabilities.json"
|
|
246
|
+
new_project "$project"
|
|
247
|
+
[[ -f "$config_path" ]] || fail "fresh bootstrap omitted capabilities.json"
|
|
248
|
+
cmp -s "$ROOT/templates/capabilities.json.template" "$config_path" || fail "fresh capability config differs from template"
|
|
249
|
+
[[ $(stat -c '%a' "$config_path" 2>/dev/null || stat -f '%Lp' "$config_path") == 600 ]] || fail "fresh capability config is not mode 0600"
|
|
250
|
+
printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$config_path"
|
|
251
|
+
chmod 600 "$config_path"
|
|
252
|
+
cp "$config_path" "$tmp/bootstrap-operator-state"
|
|
253
|
+
"$ROOT/scripts/bootstrap.sh" "$project" >/dev/null
|
|
254
|
+
cmp -s "$tmp/bootstrap-operator-state" "$config_path" || fail "bootstrap rerun replaced operator state"
|
|
255
|
+
|
|
256
|
+
local untracked="$tmp/bootstrap-untracked"
|
|
257
|
+
mkdir -p "$untracked"
|
|
258
|
+
git -C "$untracked" init -q
|
|
259
|
+
"$ROOT/scripts/bootstrap.sh" --untracked "$untracked" >/dev/null
|
|
260
|
+
[[ -f "$untracked/.ai/capabilities.json" ]] || fail "untracked bootstrap omitted capability config"
|
|
261
|
+
[[ -z $(git -C "$untracked" status --porcelain) ]] || fail "untracked capability config became visible to git"
|
|
262
|
+
echo "PASS: Phase 18 capability bootstrap fresh/rerun contract"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
run_uninstall() {
|
|
266
|
+
local project="$tmp/uninstall-project"
|
|
267
|
+
local fixture_home="$tmp/uninstall-home"
|
|
268
|
+
local fixture_bin="$tmp/uninstall-bin"
|
|
269
|
+
local live="$tmp/uninstall-live"
|
|
270
|
+
local config_before="$tmp/capabilities.before"
|
|
271
|
+
local db_before="$tmp/callbacks.before"
|
|
272
|
+
local unrelated_before="$tmp/unrelated-ai.before"
|
|
273
|
+
local project_before="$tmp/uninstall-project.before"
|
|
274
|
+
local bundle
|
|
275
|
+
new_project "$project"
|
|
276
|
+
mkdir -p "$fixture_home" "$fixture_bin" "$project/.agentfs"
|
|
277
|
+
printf '#!/usr/bin/env bash\nexit 0\n' >"$fixture_bin/claude"
|
|
278
|
+
printf '#!/usr/bin/env bash\nexit 0\n' >"$fixture_bin/systemctl"
|
|
279
|
+
chmod 755 "$fixture_bin/claude" "$fixture_bin/systemctl"
|
|
280
|
+
printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$project/.ai/capabilities.json"
|
|
281
|
+
chmod 600 "$project/.ai/capabilities.json"
|
|
282
|
+
printf 'operator-owned ai bytes\000must-survive\377\n' >"$project/.ai/operator-state.bin"
|
|
283
|
+
printf 'capability-callback-v1\000durable\377bytes\n' >"$project/.agentfs/trajectory.db"
|
|
284
|
+
printf 'capability-wal-v1\000durable\377bytes\n' >"$project/.agentfs/trajectory.db-wal"
|
|
285
|
+
cp "$project/.ai/capabilities.json" "$config_before"
|
|
286
|
+
cp "$project/.ai/operator-state.bin" "$unrelated_before"
|
|
287
|
+
cp "$project/.agentfs/trajectory.db" "$db_before"
|
|
288
|
+
cp -a "$project" "$project_before"
|
|
289
|
+
|
|
290
|
+
HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
|
|
291
|
+
"$ROOT/scripts/uninstall.sh" --dry-run --live-root "$live" "$project" >/dev/null 2>&1 || fail "capability uninstall dry-run failed"
|
|
292
|
+
diff -qr "$project_before" "$project" >/dev/null || fail "capability uninstall dry-run changed project bytes"
|
|
293
|
+
|
|
294
|
+
HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
|
|
295
|
+
"$ROOT/scripts/uninstall.sh" --yes --live-root "$live" "$project" >/dev/null 2>&1 || fail "default capability uninstall failed"
|
|
296
|
+
[[ ! -e "$project/.ai/capabilities.json" ]] || fail "default uninstall retained project capability config"
|
|
297
|
+
cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "default uninstall changed unrelated .ai bytes"
|
|
298
|
+
cmp -s "$db_before" "$project/.agentfs/trajectory.db" || fail "default uninstall changed callback store bytes"
|
|
299
|
+
bundle=$(find "$fixture_home" -maxdepth 1 -type d -name '.cairnkeep-uninstall-*' | sort | tail -1)
|
|
300
|
+
[[ -n "$bundle" && -x "$bundle/revert.sh" ]] || fail "default uninstall created no reversible backup"
|
|
301
|
+
cmp -s "$config_before" "$bundle/files/${project#/}/.ai/capabilities.json" || fail "capability config backup is not exact"
|
|
302
|
+
HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" bash "$bundle/revert.sh" >/dev/null 2>&1 || fail "capability revert failed"
|
|
303
|
+
cmp -s "$config_before" "$project/.ai/capabilities.json" || fail "capability revert did not restore config bytes"
|
|
304
|
+
[[ $(stat -c '%a' "$project/.ai/capabilities.json" 2>/dev/null || stat -f '%Lp' "$project/.ai/capabilities.json") == 600 ]] || fail "capability revert did not restore config mode"
|
|
305
|
+
cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "capability revert changed unrelated .ai bytes"
|
|
306
|
+
|
|
307
|
+
HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" PATH="$fixture_bin:$PATH" \
|
|
308
|
+
"$ROOT/scripts/uninstall.sh" --yes --purge-memory --live-root "$live" "$project" >/dev/null 2>&1 || fail "capability purge failed"
|
|
309
|
+
[[ ! -e "$project/.agentfs" ]] || fail "purge retained callback store"
|
|
310
|
+
bundle=$(find "$fixture_home" -maxdepth 1 -type d -name '.cairnkeep-uninstall-*' | sort | tail -1)
|
|
311
|
+
cmp -s "$db_before" "$bundle/files/${project#/}/.agentfs/trajectory.db" || fail "callback store purge backup is not exact"
|
|
312
|
+
HOME="$fixture_home" XDG_CONFIG_HOME="$fixture_home/.config" bash "$bundle/revert.sh" >/dev/null 2>&1 || fail "capability purge revert failed"
|
|
313
|
+
cmp -s "$db_before" "$project/.agentfs/trajectory.db" || fail "capability purge revert did not restore callback bytes"
|
|
314
|
+
cmp -s "$unrelated_before" "$project/.ai/operator-state.bin" || fail "capability purge revert changed unrelated .ai bytes"
|
|
315
|
+
echo "PASS: Phase 18 capability uninstall keep/purge/revert contract"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
run_operating() {
|
|
319
|
+
if [[ "$HARNESS" == "claude" ]]; then
|
|
320
|
+
"$ROOT/scripts/test-phase18-harness-boundary.sh" claude-delegate-calls >/dev/null || \
|
|
321
|
+
fail "Claude native delegate-call boundary failed"
|
|
322
|
+
else
|
|
323
|
+
"$ROOT/scripts/test-phase18-harness-boundary.sh" opencode-delegate-calls >/dev/null || \
|
|
324
|
+
fail "OpenCode native delegate-call boundary failed"
|
|
325
|
+
fi
|
|
326
|
+
echo "PASS: Phase 18 $HARNESS $CAPABILITY deterministic policy/boundary coverage (not live owner execution)"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
run_claude_native() {
|
|
330
|
+
local start="$ROOT/claude/capability-contract/hooks/capability-command-start.sh"
|
|
331
|
+
local finish="$ROOT/claude/capability-contract/hooks/capability-command-finish.sh"
|
|
332
|
+
[[ -x "$start" && -x "$finish" ]] || fail "Claude native capability hooks are absent"
|
|
333
|
+
grep -qF 'harness-before' "$start" || fail "Claude admission hook does not delegate to the coordinator"
|
|
334
|
+
grep -qF 'harness-terminal' "$finish" || fail "Claude terminal hook does not delegate to the coordinator"
|
|
335
|
+
grep -qF 'harness-cwd' "$finish" || fail "Claude cwd hook does not delegate to the coordinator"
|
|
336
|
+
"$ROOT/scripts/test-phase18-harness-boundary.sh" claude-hooks >/dev/null || fail "Claude native lifecycle outcomes failed"
|
|
337
|
+
echo "PASS: Phase 18 Claude native hook delegation and durable outcomes"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
run_owner_retirement() {
|
|
341
|
+
if find "$ROOT/claude/capability-contract/commands" -type f -print -quit 2>/dev/null | grep -q .; then
|
|
342
|
+
fail "obsolete Claude Markdown capability owner remains"
|
|
343
|
+
fi
|
|
344
|
+
echo "PASS: Phase 18 Claude Markdown owner retirement"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
run_matrix() {
|
|
348
|
+
local fixture="$ROOT/mcp-memory-server/scripts/fixtures/capabilities/ablation-snapshots.json"
|
|
349
|
+
local project="$tmp/matrix-project"
|
|
350
|
+
[[ -f "$fixture" ]] || fail "missing ablation snapshot fixture"
|
|
351
|
+
new_project "$project"
|
|
352
|
+
node - "$fixture" <<'NODE'
|
|
353
|
+
const { createHash } = require("crypto");
|
|
354
|
+
const fs = require("fs");
|
|
355
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
356
|
+
const ids = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
|
|
357
|
+
const tools = { "memory.write": "memory_write", "memory.search": "memory_search", "route.check": "route_check", "context.explore": "context_explore" };
|
|
358
|
+
const digest = (snapshot) => createHash("sha256").update(JSON.stringify({
|
|
359
|
+
schema_version: 1,
|
|
360
|
+
contract_enabled: true,
|
|
361
|
+
capabilities: snapshot.capabilities.map(({ id, enabled }) => ({ id, enabled })),
|
|
362
|
+
logging: { callbacks: snapshot.logging.enabled },
|
|
363
|
+
})).digest("hex");
|
|
364
|
+
if (value.schema_version !== 1 || !value.all_enabled || JSON.stringify(Object.keys(value.one_disabled)) !== JSON.stringify(ids)) process.exit(1);
|
|
365
|
+
if (value.all_enabled.configuration_digest !== digest(value.all_enabled)) process.exit(1);
|
|
366
|
+
if (JSON.stringify(value.all_enabled.capabilities.map((row) => row.id)) !== JSON.stringify(ids)) process.exit(1);
|
|
367
|
+
if (JSON.stringify(value.all_enabled.registered_mcp_tools) !== JSON.stringify(Object.values(tools))) process.exit(1);
|
|
368
|
+
for (const id of ids) {
|
|
369
|
+
const snapshot = value.one_disabled[id];
|
|
370
|
+
if (snapshot.configuration_digest !== digest(snapshot) || snapshot.configuration_digest === value.all_enabled.configuration_digest) process.exit(1);
|
|
371
|
+
if (snapshot.logging.enabled !== value.all_enabled.logging.enabled || snapshot.logging.source !== value.all_enabled.logging.source) process.exit(1);
|
|
372
|
+
if (snapshot.capabilities.filter((row) => !row.enabled).map((row) => row.id).join() !== id) process.exit(1);
|
|
373
|
+
for (const row of snapshot.capabilities) {
|
|
374
|
+
const baseline = value.all_enabled.capabilities.find((candidate) => candidate.id === row.id);
|
|
375
|
+
if (!baseline || row.source !== baseline.source || row.restart_required !== baseline.restart_required || row.kind !== baseline.kind) process.exit(1);
|
|
376
|
+
if (row.id !== id && row.enabled !== baseline.enabled) process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
const expectedTools = Object.entries(tools).filter(([capability]) => capability !== id).map(([, tool]) => tool);
|
|
379
|
+
if (JSON.stringify(snapshot.registered_mcp_tools) !== JSON.stringify(expectedTools)) process.exit(1);
|
|
380
|
+
if (tools[id]) {
|
|
381
|
+
if (snapshot.omission_evidence?.tool !== tools[id] || snapshot.omission_evidence?.configuration_digest !== snapshot.configuration_digest) process.exit(1);
|
|
382
|
+
} else if (snapshot.omission_evidence !== null) process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
NODE
|
|
385
|
+
CAIRN_CAPABILITY_CONTRACT=1 node "$ROOT/mcp-memory-server/scripts/smoke-capability-contract.mjs" --baseline >/dev/null
|
|
386
|
+
run_capability "$project" reset --all >/dev/null
|
|
387
|
+
enable_all_capabilities "$project"
|
|
388
|
+
local id output="$tmp/matrix-status.json" matrix_states=1
|
|
389
|
+
run_capability "$project" status --json >"$output"
|
|
390
|
+
node - "$output" "$fixture" <<'NODE'
|
|
391
|
+
const fs = require("fs");
|
|
392
|
+
const [statusPath, fixturePath] = process.argv.slice(2);
|
|
393
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
394
|
+
const expected = JSON.parse(fs.readFileSync(fixturePath, "utf8")).all_enabled;
|
|
395
|
+
if (status.configuration_digest !== expected.configuration_digest) process.exit(1);
|
|
396
|
+
if (JSON.stringify(status.logging) !== JSON.stringify(expected.logging)) process.exit(1);
|
|
397
|
+
if (JSON.stringify(status.capabilities) !== JSON.stringify(expected.capabilities)) process.exit(1);
|
|
398
|
+
NODE
|
|
399
|
+
for id in memory.write memory.search notes.distill wiki graph security.audit route.check context.explore; do
|
|
400
|
+
run_capability "$project" reset --all >/dev/null
|
|
401
|
+
enable_all_capabilities "$project"
|
|
402
|
+
run_capability "$project" disable "$id" >/dev/null
|
|
403
|
+
run_capability "$project" status --json >"$output"
|
|
404
|
+
node - "$output" "$fixture" "$id" <<'NODE'
|
|
405
|
+
const fs = require("fs");
|
|
406
|
+
const [statusPath, fixturePath, disabled] = process.argv.slice(2);
|
|
407
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
408
|
+
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
|
|
409
|
+
const expected = fixture.one_disabled[disabled];
|
|
410
|
+
if (!expected || status.configuration_digest !== expected.configuration_digest) process.exit(1);
|
|
411
|
+
if (status.capabilities.filter((row) => !row.enabled).map((row) => row.id).join() !== disabled) process.exit(1);
|
|
412
|
+
for (const row of status.capabilities) {
|
|
413
|
+
const wanted = expected.capabilities.find((item) => item.id === row.id);
|
|
414
|
+
if (!wanted || wanted.enabled !== row.enabled || wanted.source !== row.source || wanted.restart_required !== row.restart_required) process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
NODE
|
|
417
|
+
matrix_states=$((matrix_states + 1))
|
|
418
|
+
done
|
|
419
|
+
[[ "$matrix_states" -eq 9 ]] || fail "matrix did not execute exactly one all-enabled and eight one-disabled states"
|
|
420
|
+
echo "PASS: Phase 18 all-enabled and eight one-disabled policy/status matrix (not live owner execution)"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
run_runtime() {
|
|
424
|
+
node --version | grep -qE '^v(22|24|26)\.' || fail "lifecycle runtime is outside the supported Node matrix"
|
|
425
|
+
bash --version | head -1
|
|
426
|
+
bash -n "$ROOT/scripts/test-phase18-capability-lifecycle.sh"
|
|
427
|
+
echo "PASS: Phase 18 local runtime contract"
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
run_lifecycle() {
|
|
431
|
+
run_contract
|
|
432
|
+
run_matrix
|
|
433
|
+
run_bootstrap
|
|
434
|
+
run_uninstall
|
|
435
|
+
[[ "$RUNTIME" -eq 0 ]] || run_runtime
|
|
436
|
+
echo "PASS: Phase 18 complete capability lifecycle"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
run_native_boundary() {
|
|
440
|
+
local evidence="$tmp/native-boundary-evidence.log"
|
|
441
|
+
run_matrix
|
|
442
|
+
"$ROOT/scripts/test-phase18-harness-boundary.sh" evidence-scope >"$evidence" || {
|
|
443
|
+
cat "$evidence" >&2
|
|
444
|
+
fail "native delegate-call evidence failed"
|
|
445
|
+
}
|
|
446
|
+
node - "$evidence" <<'NODE' || fail "native boundary evidence overstated its acceptance scope"
|
|
447
|
+
const fs = require("node:fs");
|
|
448
|
+
const lines = fs.readFileSync(process.argv[2], "utf8").trim().split(/\n/);
|
|
449
|
+
const parse = (prefix) => {
|
|
450
|
+
const line = lines.find((candidate) => candidate.startsWith(`${prefix}:`));
|
|
451
|
+
if (!line) process.exit(1);
|
|
452
|
+
return JSON.parse(line.slice(prefix.length + 1));
|
|
453
|
+
};
|
|
454
|
+
const boundary = parse("DETERMINISTIC_BOUNDARY_EVIDENCE");
|
|
455
|
+
const live = parse("PHASE18_REQUIRED_LIVE_MATRIX");
|
|
456
|
+
if (boundary.schema_version !== 1 || boundary.status !== "pass") process.exit(1);
|
|
457
|
+
if (boundary.scope !== "native-delegate-order" || boundary.owner_execution !== "simulated-after-admission") process.exit(1);
|
|
458
|
+
if (live.schema_version !== 1 || live.required_cells !== 56 || live.passing_cells !== 0) process.exit(1);
|
|
459
|
+
if (live.status !== "blocking" || live.acceptance !== false || live.replacement_plan !== "18-27") process.exit(1);
|
|
460
|
+
NODE
|
|
461
|
+
cat "$evidence"
|
|
462
|
+
echo "PASS: deterministic native-boundary coverage; live real-owner acceptance remains blocked"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case "$MODE" in
|
|
466
|
+
baseline)
|
|
467
|
+
run_baseline
|
|
468
|
+
;;
|
|
469
|
+
--expect-red-contract)
|
|
470
|
+
run_baseline
|
|
471
|
+
set +e
|
|
472
|
+
(cd "$tmp" && env -u CAIRN_CAPABILITY_CONTRACT "$ROOT/bin/cairn" capabilities status --json) >"$tmp/red.out" 2>"$tmp/red.err"
|
|
473
|
+
red_status=$?
|
|
474
|
+
set -e
|
|
475
|
+
if [[ "$red_status" -eq 2 ]] && grep -qxF 'Unknown command: capabilities' "$tmp/red.err"; then
|
|
476
|
+
printf '%s\n' "$RED_MARKER" >&2
|
|
477
|
+
exit "$EXPECTED_RED_EXIT"
|
|
478
|
+
fi
|
|
479
|
+
cat "$tmp/red.out" "$tmp/red.err" >&2
|
|
480
|
+
[[ "$red_status" -ne 0 ]] || fail "expected missing capability CLI, but the command succeeded"
|
|
481
|
+
fail "capability CLI failed for a reason other than the absent dispatch surface"
|
|
482
|
+
;;
|
|
483
|
+
contract) run_contract ;;
|
|
484
|
+
matrix) run_matrix ;;
|
|
485
|
+
operating) run_operating ;;
|
|
486
|
+
bootstrap) run_bootstrap ;;
|
|
487
|
+
uninstall) run_uninstall ;;
|
|
488
|
+
claude-native) run_claude_native ;;
|
|
489
|
+
native-boundary) run_native_boundary ;;
|
|
490
|
+
owner-retirement) run_owner_retirement ;;
|
|
491
|
+
lifecycle) run_lifecycle ;;
|
|
492
|
+
full)
|
|
493
|
+
run_baseline
|
|
494
|
+
run_lifecycle
|
|
495
|
+
HARNESS=claude CAPABILITY=wiki run_operating
|
|
496
|
+
HARNESS=claude CAPABILITY=graph run_operating
|
|
497
|
+
HARNESS=claude CAPABILITY=security.audit run_operating
|
|
498
|
+
HARNESS=opencode-command CAPABILITY=wiki run_operating
|
|
499
|
+
HARNESS=opencode-command CAPABILITY=graph run_operating
|
|
500
|
+
HARNESS=opencode-command CAPABILITY=security.audit run_operating
|
|
501
|
+
HARNESS=opencode-workflow CAPABILITY=wiki run_operating
|
|
502
|
+
HARNESS=opencode-workflow CAPABILITY=security.audit run_operating
|
|
503
|
+
;;
|
|
504
|
+
esac
|