@cairnkeep/cli 2.3.1 → 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 +69 -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 +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 +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,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Public CLI contract for `cairn trajectory list|show|prune`.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
6
|
+
tmp=$(mktemp -d)
|
|
7
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
8
|
+
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
9
|
+
|
|
10
|
+
cairn="$ROOT/bin/cairn"
|
|
11
|
+
fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-opencode.json"
|
|
12
|
+
cli="$ROOT/mcp-memory-server/dist/trajectory-cli.js"
|
|
13
|
+
repo="$tmp/project"
|
|
14
|
+
mkdir -p "$repo/.agentfs"
|
|
15
|
+
|
|
16
|
+
"$cairn" help | grep -q "cairn trajectory" || fail "help missing trajectory"
|
|
17
|
+
|
|
18
|
+
for shell in bash zsh fish; do
|
|
19
|
+
"$cairn" completion "$shell" > "$tmp/completion-$shell"
|
|
20
|
+
grep -q "trajectory" "$tmp/completion-$shell" || fail "$shell completion missing trajectory"
|
|
21
|
+
grep -q "list.*show.*prune\|list show prune" "$tmp/completion-$shell" \
|
|
22
|
+
|| fail "$shell completion missing trajectory subcommands"
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
bootstrap_repo="$tmp/bootstrap"
|
|
26
|
+
mkdir -p "$bootstrap_repo"
|
|
27
|
+
"$cairn" bootstrap "$bootstrap_repo" >/dev/null
|
|
28
|
+
[[ -f "$bootstrap_repo/.ai/trajectory-redaction.json" ]] \
|
|
29
|
+
|| fail "bootstrap missing optional trajectory redaction template"
|
|
30
|
+
if grep -R -E -q '^[[:space:]]*CAIRN_TRAJECTORY_CAPTURE[[:space:]]*=[[:space:]]*1' "$bootstrap_repo"; then
|
|
31
|
+
fail "bootstrap enabled trajectory capture"
|
|
32
|
+
fi
|
|
33
|
+
printf '{"version":1,"patterns":[]}\n' > "$bootstrap_repo/.ai/trajectory-redaction.json"
|
|
34
|
+
"$cairn" bootstrap "$bootstrap_repo" >/dev/null
|
|
35
|
+
grep -q '"patterns":\[\]' "$bootstrap_repo/.ai/trajectory-redaction.json" \
|
|
36
|
+
|| fail "bootstrap overwrote an existing redaction configuration"
|
|
37
|
+
|
|
38
|
+
printf '%s' "$(cat "$fixture")" | node "$cli" capture-opencode "$repo" >/dev/null
|
|
39
|
+
|
|
40
|
+
(cd "$repo" && "$cairn" trajectory list --json) > "$tmp/list.json"
|
|
41
|
+
node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(j.sessions?.[0]?.session_id!=="opencode-session-001")process.exit(1)' "$tmp/list.json" \
|
|
42
|
+
|| fail "trajectory list JSON missing captured session"
|
|
43
|
+
|
|
44
|
+
(cd "$repo" && "$cairn" trajectory show opencode-session-001 --json) > "$tmp/show.json"
|
|
45
|
+
node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(j.schema_version!==1||!Array.isArray(j.events))process.exit(1)' "$tmp/show.json" \
|
|
46
|
+
|| fail "trajectory show JSON is not a v1 session"
|
|
47
|
+
|
|
48
|
+
if ! (set -o pipefail; cd "$repo" && "$cairn" trajectory show opencode-session-001 --json | head -c 0 >/dev/null); then
|
|
49
|
+
fail "trajectory CLI did not treat a closed output pipe as a normal consumer exit"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
(cd "$repo" && "$cairn" trajectory prune --dry-run --json) > "$tmp/prune.json"
|
|
53
|
+
node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));if(!Array.isArray(j.removed)||j.dry_run!==true)process.exit(1)' "$tmp/prune.json" \
|
|
54
|
+
|| fail "trajectory prune dry-run JSON invalid"
|
|
55
|
+
|
|
56
|
+
if (cd "$repo" && "$cairn" trajectory show does-not-exist --json) >"$tmp/missing.out" 2>"$tmp/missing.err"; then
|
|
57
|
+
fail "show of missing session should fail"
|
|
58
|
+
fi
|
|
59
|
+
grep -q "not found" "$tmp/missing.err" || fail "missing-session error is not actionable"
|
|
60
|
+
|
|
61
|
+
# Doctor detects missing trajectory metadata and repairs indexes explicitly,
|
|
62
|
+
# preserving the valid full session record.
|
|
63
|
+
(cd "$ROOT/mcp-memory-server" && node --input-type=module -e '
|
|
64
|
+
import { AgentFS } from "agentfs-sdk"
|
|
65
|
+
const agent = await AgentFS.open({ id: "trajectory", path: process.argv[1] })
|
|
66
|
+
for (const row of await agent.kv.list("trajectory/index/")) await agent.kv.delete(row.key)
|
|
67
|
+
await agent.kv.delete("trajectory/meta/schema-version")
|
|
68
|
+
await agent.close()
|
|
69
|
+
' "$repo/.agentfs/trajectory.db")
|
|
70
|
+
|
|
71
|
+
mkdir -p "$tmp/home"
|
|
72
|
+
if (cd "$repo" && env -u CAIRN_LLM_API_URL -u CAIRN_MEMORY_EMBEDDING_URL \
|
|
73
|
+
-u CAIRN_GIT_PROVIDER HOME="$tmp/home" "$cairn" doctor) >"$tmp/doctor-bad.out" 2>&1; then
|
|
74
|
+
fail "doctor accepted missing trajectory metadata"
|
|
75
|
+
fi
|
|
76
|
+
grep -q 'cairn doctor --repair' "$tmp/doctor-bad.out" \
|
|
77
|
+
|| fail "doctor did not offer an actionable trajectory repair"
|
|
78
|
+
|
|
79
|
+
(cd "$repo" && env -u CAIRN_LLM_API_URL -u CAIRN_MEMORY_EMBEDDING_URL \
|
|
80
|
+
-u CAIRN_GIT_PROVIDER HOME="$tmp/home" "$cairn" doctor --repair) >"$tmp/doctor-repair.out" 2>&1 \
|
|
81
|
+
|| fail "doctor failed to repair trajectory metadata/indexes"
|
|
82
|
+
grep -q '\[PASS\] trajectory store repaired' "$tmp/doctor-repair.out" \
|
|
83
|
+
|| fail "doctor did not report trajectory repair"
|
|
84
|
+
(cd "$repo" && "$cairn" trajectory show opencode-session-001 --json) >/dev/null \
|
|
85
|
+
|| fail "trajectory record was lost during doctor repair"
|
|
86
|
+
|
|
87
|
+
echo "PASS: cairn trajectory CLI dispatch and JSON contract"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cross-harness trajectory capture and disabled-path regression test.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
6
|
+
tmp=$(mktemp -d)
|
|
7
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
8
|
+
fail() { echo "FAIL: $1" >&2; exit 1; }
|
|
9
|
+
|
|
10
|
+
claude_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-claude.jsonl"
|
|
11
|
+
opencode_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-opencode.json"
|
|
12
|
+
pi_fixture="$ROOT/mcp-memory-server/scripts/fixtures/trajectory-pi.json"
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
sed "s|@@INFRA_ROOT@@|$ROOT|g" "$1" > "$2"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Claude: flag off must not read stdin, create a store, or perturb the existing
|
|
19
|
+
# no-model/no-project-db path.
|
|
20
|
+
claude_repo="$tmp/claude-off"
|
|
21
|
+
mkdir -p "$claude_repo/.agentfs"
|
|
22
|
+
render "$ROOT/claude/hooks/memory-capture.sh" "$tmp/memory-capture.sh"
|
|
23
|
+
chmod +x "$tmp/memory-capture.sh"
|
|
24
|
+
before=$(find "$claude_repo" -type f -print | sort)
|
|
25
|
+
(cd "$claude_repo" && printf '%s\n' '{"transcript_path":"/does/not/exist"}' | "$tmp/memory-capture.sh") >"$tmp/claude-off.out" 2>"$tmp/claude-off.err"
|
|
26
|
+
after=$(find "$claude_repo" -type f -print | sort)
|
|
27
|
+
[[ "$before" == "$after" ]] || fail "Claude flag-off path changed files"
|
|
28
|
+
[[ ! -s "$tmp/claude-off.out" && ! -s "$tmp/claude-off.err" ]] || fail "Claude flag-off path emitted output"
|
|
29
|
+
|
|
30
|
+
# In a bootstrapped project, the original no-model path also exits before
|
|
31
|
+
# parsing stdin. A python spy makes that no-process guarantee observable.
|
|
32
|
+
claude_bootstrapped_off="$tmp/claude-bootstrapped-off"
|
|
33
|
+
mkdir -p "$claude_bootstrapped_off/.agentfs" "$tmp/spy-bin"
|
|
34
|
+
touch "$claude_bootstrapped_off/.agentfs/project.db"
|
|
35
|
+
cat > "$tmp/spy-bin/python3" <<EOF
|
|
36
|
+
#!/usr/bin/env bash
|
|
37
|
+
touch "$tmp/python-was-called"
|
|
38
|
+
exit 1
|
|
39
|
+
EOF
|
|
40
|
+
chmod +x "$tmp/spy-bin/python3"
|
|
41
|
+
(cd "$claude_bootstrapped_off" && printf '%s\n' '{"transcript_path":"/does/not/exist"}' \
|
|
42
|
+
| PATH="$tmp/spy-bin:$PATH" "$tmp/memory-capture.sh") >/dev/null 2>&1
|
|
43
|
+
[[ ! -e "$tmp/python-was-called" ]] || fail "Claude flag-off no-model path parsed stdin"
|
|
44
|
+
|
|
45
|
+
# Claude: enabled capture works without project.db, API key, extraction model,
|
|
46
|
+
# or network service.
|
|
47
|
+
claude_repo="$tmp/claude-on"
|
|
48
|
+
mkdir -p "$claude_repo/.agentfs"
|
|
49
|
+
hook_input=$(node -e 'console.log(JSON.stringify({transcript_path:process.argv[1],hook_event_name:"SessionEnd"}))' "$claude_fixture")
|
|
50
|
+
(cd "$claude_repo" && CAIRN_TRAJECTORY_CAPTURE=1 printf '%s\n' "$hook_input" | CAIRN_TRAJECTORY_CAPTURE=1 "$tmp/memory-capture.sh") >"$tmp/claude-on.out" 2>"$tmp/claude-on.err"
|
|
51
|
+
[[ -f "$claude_repo/.agentfs/trajectory.db" ]] || fail "Claude enabled hook did not create trajectory store"
|
|
52
|
+
(cd "$claude_repo" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show claude-session-001 --json) >/dev/null \
|
|
53
|
+
|| fail "Claude hook trajectory not readable"
|
|
54
|
+
|
|
55
|
+
# OpenCode: render the installed plugin shape and drive a real session.idle
|
|
56
|
+
# callback with the SDK fixture. Type-only imports are stripped by Node 22+.
|
|
57
|
+
render "$ROOT/opencode/plugins/memory-capture.ts" "$tmp/memory-capture.ts"
|
|
58
|
+
opencode_off="$tmp/opencode-off"
|
|
59
|
+
mkdir -p "$opencode_off/.agentfs"
|
|
60
|
+
node --experimental-strip-types "$ROOT/scripts/lib/trajectory-opencode-plugin-harness.mjs" \
|
|
61
|
+
"$tmp/memory-capture.ts" "$opencode_off" "$opencode_fixture" >/dev/null 2>"$tmp/opencode-off.err"
|
|
62
|
+
[[ ! -f "$opencode_off/.agentfs/trajectory.db" ]] || fail "OpenCode flag-off path created trajectory store"
|
|
63
|
+
|
|
64
|
+
opencode_on="$tmp/opencode-on"
|
|
65
|
+
mkdir -p "$opencode_on/.agentfs"
|
|
66
|
+
CAIRN_TRAJECTORY_CAPTURE=1 node --experimental-strip-types \
|
|
67
|
+
"$ROOT/scripts/lib/trajectory-opencode-plugin-harness.mjs" \
|
|
68
|
+
"$tmp/memory-capture.ts" "$opencode_on" "$opencode_fixture" >/dev/null 2>"$tmp/opencode-on.err"
|
|
69
|
+
[[ -f "$opencode_on/.agentfs/trajectory.db" ]] || fail "OpenCode enabled plugin did not create trajectory store"
|
|
70
|
+
(cd "$opencode_on" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show opencode-session-001 --json) >/dev/null \
|
|
71
|
+
|| fail "OpenCode plugin trajectory not readable"
|
|
72
|
+
|
|
73
|
+
# Exercise the real backup-first sync paths twice and compare their rendered
|
|
74
|
+
# installed assets, rather than relying only on the source-level render above.
|
|
75
|
+
claude_live="$tmp/claude-live"
|
|
76
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
|
|
77
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_live" >/dev/null
|
|
78
|
+
render "$ROOT/claude/hooks/memory-capture.sh" "$tmp/expected-claude-capture.sh"
|
|
79
|
+
cmp -s "$tmp/expected-claude-capture.sh" "$claude_live/hooks/memory-capture.sh" \
|
|
80
|
+
|| fail "installed Claude trajectory hook differs from rendered source"
|
|
81
|
+
claude_capture_registrations=$(node -e '
|
|
82
|
+
const fs = require("fs")
|
|
83
|
+
const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
|
|
84
|
+
let count = 0
|
|
85
|
+
for (const entry of settings.hooks?.SessionEnd ?? []) {
|
|
86
|
+
for (const hook of entry.hooks ?? []) {
|
|
87
|
+
if (String(hook.command ?? "").includes("memory-capture.sh")) count += 1
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(String(count))
|
|
91
|
+
' "$claude_live/settings.json")
|
|
92
|
+
[[ "$claude_capture_registrations" -eq 1 ]] \
|
|
93
|
+
|| fail "Claude SessionEnd memory-capture registration is missing or duplicated"
|
|
94
|
+
|
|
95
|
+
opencode_live="$tmp/opencode-live"
|
|
96
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$opencode_live" >/dev/null
|
|
97
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$opencode_live" >/dev/null
|
|
98
|
+
render "$ROOT/opencode/plugins/memory-capture.ts" "$tmp/expected-opencode-capture.ts"
|
|
99
|
+
cmp -s "$tmp/expected-opencode-capture.ts" "$opencode_live/plugins/memory-capture.ts" \
|
|
100
|
+
|| fail "installed OpenCode trajectory plugin differs from rendered source"
|
|
101
|
+
[[ $(find "$opencode_live" -type f -name 'memory-capture.ts' | wc -l) -eq 1 ]] \
|
|
102
|
+
|| fail "OpenCode memory-capture plugin is duplicated"
|
|
103
|
+
|
|
104
|
+
# Pi: the native extension must not touch SessionManager when disabled and
|
|
105
|
+
# must send the active branch to the same local capture CLI when enabled.
|
|
106
|
+
render "$ROOT/pi/extensions/cairnkeep-trajectory.ts" "$tmp/cairnkeep-trajectory.ts"
|
|
107
|
+
pi_off="$tmp/pi-off"
|
|
108
|
+
mkdir -p "$pi_off/.agentfs"
|
|
109
|
+
node --experimental-strip-types "$ROOT/scripts/lib/trajectory-pi-extension-harness.mjs" \
|
|
110
|
+
"$tmp/cairnkeep-trajectory.ts" "$pi_off" "$pi_fixture" >"$tmp/pi-off.out" 2>"$tmp/pi-off.err"
|
|
111
|
+
[[ ! -f "$pi_off/.agentfs/trajectory.db" ]] || fail "Pi flag-off path created trajectory store"
|
|
112
|
+
[[ ! -s "$tmp/pi-off.out" && ! -s "$tmp/pi-off.err" ]] || fail "Pi flag-off path emitted output"
|
|
113
|
+
|
|
114
|
+
pi_on="$tmp/pi-on"
|
|
115
|
+
mkdir -p "$pi_on/.agentfs"
|
|
116
|
+
CAIRN_TRAJECTORY_CAPTURE=1 node --experimental-strip-types \
|
|
117
|
+
"$ROOT/scripts/lib/trajectory-pi-extension-harness.mjs" \
|
|
118
|
+
"$tmp/cairnkeep-trajectory.ts" "$pi_on" "$pi_fixture" >"$tmp/pi-on.out" 2>"$tmp/pi-on.err"
|
|
119
|
+
[[ -f "$pi_on/.agentfs/trajectory.db" ]] || fail "Pi enabled extension did not create trajectory store"
|
|
120
|
+
pi_json=$(cd "$pi_on" && node "$ROOT/mcp-memory-server/dist/trajectory-cli.js" show pi-session-001 --json)
|
|
121
|
+
[[ "$pi_json" != *"sk-live-pi-141414"* ]] || fail "Pi sentinel survived redaction"
|
|
122
|
+
[[ "$pi_json" != *"private pi reasoning"* ]] || fail "Pi reasoning survived omission"
|
|
123
|
+
|
|
124
|
+
pi_live="$tmp/pi-live"
|
|
125
|
+
"$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$pi_live" >/dev/null
|
|
126
|
+
"$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$pi_live" >/dev/null
|
|
127
|
+
render "$ROOT/pi/extensions/cairnkeep-trajectory.ts" "$tmp/expected-pi-capture.ts"
|
|
128
|
+
cmp -s "$tmp/expected-pi-capture.ts" "$pi_live/extensions/cairnkeep-trajectory.ts" \
|
|
129
|
+
|| fail "installed Pi trajectory extension differs from rendered source"
|
|
130
|
+
[[ $(find "$pi_live" -type f -name 'cairnkeep-trajectory.ts' | wc -l) -eq 1 ]] \
|
|
131
|
+
|| fail "Pi trajectory extension is duplicated"
|
|
132
|
+
|
|
133
|
+
# Integration must reuse the existing capture registration rather than add a
|
|
134
|
+
# second SessionEnd hook or plugin asset.
|
|
135
|
+
[[ $(grep -c 'memory-capture.sh)' "$ROOT/scripts/sync-claude-assets.sh") -eq 1 ]] \
|
|
136
|
+
|| fail "Claude memory-capture hook registration is missing or duplicated"
|
|
137
|
+
[[ $(grep -c 'plugins/memory-capture.ts' "$ROOT/scripts/sync-opencode-plugin-assets.sh") -eq 1 ]] \
|
|
138
|
+
|| fail "OpenCode memory-capture plugin asset is missing or duplicated"
|
|
139
|
+
[[ $(grep -c 'extensions/cairnkeep-trajectory.ts' "$ROOT/scripts/sync-pi-assets.sh") -eq 1 ]] \
|
|
140
|
+
|| fail "Pi trajectory extension asset is missing or duplicated"
|
|
141
|
+
|
|
142
|
+
echo "PASS: Claude/OpenCode/Pi trajectory hooks and flag-off regression"
|
|
@@ -16,6 +16,63 @@ fails=0
|
|
|
16
16
|
ok() { printf ' [PASS] %s\n' "$1"; }
|
|
17
17
|
bad() { printf ' [FAIL] %s\n' "$1"; fails=$((fails + 1)); }
|
|
18
18
|
check() { if [[ "$2" == "$3" ]]; then ok "$1 ($2)"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
19
|
+
tree_hash() { find "$1" -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1; }
|
|
20
|
+
tree_identity() {
|
|
21
|
+
node - "$1" <<'NODE'
|
|
22
|
+
const { createHash } = require("crypto")
|
|
23
|
+
const { lstatSync, readdirSync, readFileSync } = require("fs")
|
|
24
|
+
const { join, relative } = require("path")
|
|
25
|
+
|
|
26
|
+
const root = process.argv[2]
|
|
27
|
+
const entries = []
|
|
28
|
+
const visit = (path) => {
|
|
29
|
+
const stat = lstatSync(path)
|
|
30
|
+
const label = relative(root, path) || "."
|
|
31
|
+
const mode = (stat.mode & 0o777).toString(8).padStart(3, "0")
|
|
32
|
+
if (stat.isDirectory()) {
|
|
33
|
+
entries.push(`d\t${mode}\t${label}`)
|
|
34
|
+
for (const name of readdirSync(path).sort()) visit(join(path, name))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const digest = createHash("sha256").update(readFileSync(path)).digest("hex")
|
|
38
|
+
entries.push(`f\t${mode}\t${label}\t${digest}`)
|
|
39
|
+
}
|
|
40
|
+
visit(root)
|
|
41
|
+
process.stdout.write(createHash("sha256").update(`${entries.join("\n")}\n`).digest("hex"))
|
|
42
|
+
NODE
|
|
43
|
+
}
|
|
44
|
+
managed_hook_count() {
|
|
45
|
+
node -e '
|
|
46
|
+
const fs = require("fs")
|
|
47
|
+
const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
|
|
48
|
+
const names = process.argv.slice(2)
|
|
49
|
+
let count = 0
|
|
50
|
+
for (const entries of Object.values(settings.hooks ?? {})) {
|
|
51
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
52
|
+
for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
|
|
53
|
+
if (typeof hook.command === "string" && names.some((name) => hook.command.includes(name))) count += 1
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(String(count))
|
|
58
|
+
' "$1" memory-wakeup.sh memory-capture.sh compaction-capture.sh memory-recall.sh context-explore-pretask.sh
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
capability_hook_count() {
|
|
62
|
+
node -e '
|
|
63
|
+
const fs = require("fs")
|
|
64
|
+
const settings = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
|
|
65
|
+
let count = 0
|
|
66
|
+
for (const entries of Object.values(settings.hooks ?? {})) {
|
|
67
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
68
|
+
for (const hook of Array.isArray(entry.hooks) ? entry.hooks : []) {
|
|
69
|
+
if (typeof hook.command === "string" && /capability-command-(start|finish)\.sh/.test(hook.command)) count += 1
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
process.stdout.write(String(count))
|
|
74
|
+
' "$1"
|
|
75
|
+
}
|
|
19
76
|
|
|
20
77
|
# Isolated environment: fake HOME + stubbed system commands.
|
|
21
78
|
mkdir -p "$SB/home" "$SB/bin"
|
|
@@ -24,38 +81,205 @@ printf '#!/usr/bin/env bash\ntrue\n' >"$SB/bin/systemctl"
|
|
|
24
81
|
chmod +x "$SB/bin/"*
|
|
25
82
|
export HOME="$SB/home" XDG_CONFIG_HOME="$SB/home/.config" PATH="$SB/bin:$PATH"
|
|
26
83
|
LIVE="$SB/live"
|
|
84
|
+
PI_LIVE="$SB/pi-live"
|
|
27
85
|
|
|
28
86
|
echo "test-uninstall"
|
|
29
87
|
|
|
30
88
|
# --- install the operating layer -------------------------------------------
|
|
31
89
|
"$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --live-root "$LIVE" >/dev/null 2>&1
|
|
90
|
+
"$ROOT_DIR/scripts/sync-pi-assets.sh" --apply --live-root "$PI_LIVE" >/dev/null 2>&1
|
|
32
91
|
md_installed=$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')
|
|
33
92
|
check "assets installed" "$([[ $md_installed -gt 0 ]] && echo yes || echo no)" "yes"
|
|
34
|
-
check "hooks registered" "$(
|
|
93
|
+
check "managed hooks registered" "$(managed_hook_count "$LIVE/settings.json")" "5"
|
|
94
|
+
node - "$LIVE/settings.json" <<'NODE'
|
|
95
|
+
const fs = require("fs")
|
|
96
|
+
const path = process.argv[2]
|
|
97
|
+
const settings = JSON.parse(fs.readFileSync(path, "utf8"))
|
|
98
|
+
settings.hooks ??= {}
|
|
99
|
+
settings.hooks.PostToolUse ??= []
|
|
100
|
+
settings.hooks.PostToolUse.push({
|
|
101
|
+
matcher: "Read",
|
|
102
|
+
hooks: [{ type: "command", command: "bash /external/hooks/keep-me.sh" }],
|
|
103
|
+
})
|
|
104
|
+
fs.writeFileSync(path, JSON.stringify(settings, null, 2) + "\n")
|
|
105
|
+
NODE
|
|
35
106
|
cp "$LIVE/settings.json" "$SB/settings.before.json"
|
|
107
|
+
cp "$PI_LIVE/extensions/cairnkeep-trajectory.ts" "$SB/pi.before.ts"
|
|
108
|
+
mkdir -p "$SB/home/.cairnkeep/notes/projects/example/hindsight"
|
|
109
|
+
printf 'durable note bytes\n' >"$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md"
|
|
110
|
+
mkdir -p "$SB/home/.cairnkeep/notes/.cairnkeep/history/project-notes/knowledge/example" \
|
|
111
|
+
"$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/backups"
|
|
112
|
+
printf 'typed sqlite bytes\n' >"$SB/home/.cairnkeep/identity.db"
|
|
113
|
+
printf 'typed history bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/history/project-notes/knowledge/example/1.json"
|
|
114
|
+
printf 'prepared journal bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/journal-v1.json"
|
|
115
|
+
printf 'backup bytes\n' >"$SB/home/.cairnkeep/notes/.cairnkeep/transactions/prepared/backups/0.bin"
|
|
116
|
+
note_before=$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)
|
|
117
|
+
store_before=$(tree_hash "$SB/home/.cairnkeep")
|
|
118
|
+
PROJECT_DATA="$SB/project-data"
|
|
119
|
+
ADJACENT_PROJECT="$SB/adjacent-project"
|
|
120
|
+
mkdir -p "$PROJECT_DATA/.ai" \
|
|
121
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
|
|
122
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/partial" \
|
|
123
|
+
"$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent"
|
|
124
|
+
printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$PROJECT_DATA/.ai/capabilities.json"
|
|
125
|
+
chmod 600 "$PROJECT_DATA/.ai/capabilities.json"
|
|
126
|
+
printf 'operator-owned ai bytes\000must-survive\377\n' >"$PROJECT_DATA/.ai/operator-state.bin"
|
|
127
|
+
printf 'artifact-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/artifacts.db"
|
|
128
|
+
printf 'session-and-callback-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db"
|
|
129
|
+
printf 'callback-wal-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db-wal"
|
|
130
|
+
printf 'unrelated-memory-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/project.db"
|
|
131
|
+
printf 'complete-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json"
|
|
132
|
+
printf 'partial-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
|
|
133
|
+
printf 'note-snapshot-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
|
|
134
|
+
printf 'adjacent-eval-v1\000must-survive\377bytes\n' >"$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent/report.json"
|
|
135
|
+
chmod 700 "$PROJECT_DATA/.agentfs" "$PROJECT_DATA/.agentfs/eval" \
|
|
136
|
+
"$PROJECT_DATA/.agentfs/eval/experiments" \
|
|
137
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/complete" \
|
|
138
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots" \
|
|
139
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
|
|
140
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/partial"
|
|
141
|
+
chmod 600 "$PROJECT_DATA/.agentfs/project.db" \
|
|
142
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json" \
|
|
143
|
+
"$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
|
|
144
|
+
chmod 400 "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
|
|
145
|
+
chmod 700 "$ADJACENT_PROJECT/.agentfs" "$ADJACENT_PROJECT/.agentfs/eval" \
|
|
146
|
+
"$ADJACENT_PROJECT/.agentfs/eval/experiments" \
|
|
147
|
+
"$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent"
|
|
148
|
+
chmod 600 "$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent/report.json"
|
|
149
|
+
cp "$PROJECT_DATA/.ai/capabilities.json" "$SB/capabilities.before.json"
|
|
150
|
+
cp "$PROJECT_DATA/.ai/operator-state.bin" "$SB/operator-state.before.bin"
|
|
151
|
+
cp "$PROJECT_DATA/.agentfs/artifacts.db" "$SB/artifacts.before.db"
|
|
152
|
+
cp "$PROJECT_DATA/.agentfs/trajectory.db" "$SB/trajectory.before.db"
|
|
153
|
+
cp "$PROJECT_DATA/.agentfs/trajectory.db-wal" "$SB/trajectory.before.db-wal"
|
|
154
|
+
agentfs_before=$(tree_identity "$PROJECT_DATA/.agentfs")
|
|
155
|
+
eval_before=$(tree_identity "$PROJECT_DATA/.agentfs/eval")
|
|
156
|
+
adjacent_before=$(tree_identity "$ADJACENT_PROJECT/.agentfs")
|
|
157
|
+
|
|
158
|
+
# Normal and master-off sync are the same inert installation. Replaying an
|
|
159
|
+
# explicit overlay request with the master off must not change any legacy byte,
|
|
160
|
+
# registration, process state, block state, or measurement state.
|
|
161
|
+
CLAUDE_INERT="$SB/claude-inert"
|
|
162
|
+
OPENCODE_INERT="$SB/opencode-inert"
|
|
163
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
164
|
+
"$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --live-root "$CLAUDE_INERT" >/dev/null
|
|
165
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
166
|
+
"$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
|
|
167
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
168
|
+
"$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
|
|
169
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
170
|
+
"$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --live-root "$OPENCODE_INERT" >/dev/null
|
|
171
|
+
cp -a "$CLAUDE_INERT" "$SB/claude-inert.before"
|
|
172
|
+
cp -a "$OPENCODE_INERT" "$SB/opencode-inert.before"
|
|
173
|
+
CAIRN_CAPABILITY_CONTRACT=0 \
|
|
174
|
+
"$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$CLAUDE_INERT" >/dev/null
|
|
175
|
+
CAIRN_CAPABILITY_CONTRACT=0 \
|
|
176
|
+
"$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
|
|
177
|
+
CAIRN_CAPABILITY_CONTRACT=0 \
|
|
178
|
+
"$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
|
|
179
|
+
CAIRN_CAPABILITY_CONTRACT=0 \
|
|
180
|
+
"$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_INERT" >/dev/null
|
|
181
|
+
check "master-off Claude sync is exact normal identity" "$(diff -qr "$SB/claude-inert.before" "$CLAUDE_INERT" >/dev/null && echo yes || echo no)" "yes"
|
|
182
|
+
check "master-off OpenCode sync is exact normal identity" "$(diff -qr "$SB/opencode-inert.before" "$OPENCODE_INERT" >/dev/null && echo yes || echo no)" "yes"
|
|
183
|
+
check "inert Claude sync has no native hooks" "$([[ -e "$CLAUDE_INERT/hooks/capability-command-start.sh" || -e "$CLAUDE_INERT/hooks/capability-command-finish.sh" ]] && echo no || echo yes)" "yes"
|
|
184
|
+
check "inert OpenCode sync has no native plugin" "$([[ -e "$OPENCODE_INERT/plugins/capability-command.ts" ]] && echo no || echo yes)" "yes"
|
|
185
|
+
check "inert sync creates no measurement state" "$([[ -e "$SB/inert-state" ]] && echo no || echo yes)" "yes"
|
|
186
|
+
|
|
187
|
+
# Enabled sync owns one isolated project overlay containing legacy operating
|
|
188
|
+
# assets plus the native hook/plugin registrations. The whole overlay is a
|
|
189
|
+
# reversible managed unit; normal roots above are never cleanup targets.
|
|
190
|
+
CLAUDE_OVERLAY="$PROJECT_DATA/.ai/capability-contract/claude"
|
|
191
|
+
OPENCODE_OVERLAY="$PROJECT_DATA/.ai/capability-contract/opencode"
|
|
192
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
193
|
+
"$ROOT_DIR/scripts/sync-claude-assets.sh" --apply --capability-overlay --live-root "$CLAUDE_OVERLAY" >/dev/null
|
|
194
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
195
|
+
"$ROOT_DIR/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
|
|
196
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
197
|
+
"$ROOT_DIR/scripts/sync-opencode-graphify-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
|
|
198
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
199
|
+
"$ROOT_DIR/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$OPENCODE_OVERLAY" >/dev/null
|
|
200
|
+
check "enabled overlay registers five Claude hook events" "$(capability_hook_count "$CLAUDE_OVERLAY/settings.json")" "5"
|
|
201
|
+
check "enabled overlay registers one OpenCode plugin" "$([[ -f "$OPENCODE_OVERLAY/plugins/capability-command.ts" ]] && echo yes || echo no)" "yes"
|
|
202
|
+
check "enabled overlay contains no retired prompt owner" "$([[ -d "$CLAUDE_OVERLAY/capability-contract/commands" || -d "$OPENCODE_OVERLAY/capability-contract/command" || -d "$OPENCODE_OVERLAY/capability-contract/workflows" ]] && echo no || echo yes)" "yes"
|
|
203
|
+
overlay_before=$(tree_hash "$PROJECT_DATA/.ai/capability-contract")
|
|
204
|
+
project_before=$(tree_hash "$PROJECT_DATA")
|
|
36
205
|
|
|
37
206
|
# --- dry-run must change nothing -------------------------------------------
|
|
38
|
-
"$ROOT_DIR/scripts/uninstall.sh" --dry-run --live-root "$LIVE" >/dev/null 2>&1
|
|
207
|
+
"$ROOT_DIR/scripts/uninstall.sh" --dry-run --live-root "$LIVE" --pi-live-root "$PI_LIVE" "$PROJECT_DATA" >/dev/null 2>&1
|
|
39
208
|
check "dry-run leaves assets" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "$md_installed"
|
|
40
209
|
check "dry-run makes no bundle" "$(ls -d "$SB/home/.cairnkeep-uninstall-"* 2>/dev/null | wc -l | tr -d ' ')" "0"
|
|
210
|
+
check "dry-run leaves Pi extension" "$([[ -f "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] && echo yes || echo no)" "yes"
|
|
211
|
+
check "dry-run leaves settings identical" "$(cmp -s "$SB/settings.before.json" "$LIVE/settings.json" && echo yes || echo no)" "yes"
|
|
212
|
+
check "dry-run leaves artifact bytes exact" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
|
|
213
|
+
check "dry-run leaves eval tree, bytes, and modes exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
|
|
214
|
+
check "dry-run leaves unrelated project memory exact" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
|
|
215
|
+
check "dry-run leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
|
|
216
|
+
check "dry-run leaves project state byte-identical" "$(tree_hash "$PROJECT_DATA")" "$project_before"
|
|
41
217
|
|
|
42
218
|
# --- real uninstall ---------------------------------------------------------
|
|
43
|
-
"$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$LIVE" >/dev/null 2>&1
|
|
219
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$LIVE" --pi-live-root "$PI_LIVE" "$PROJECT_DATA" >/dev/null 2>&1
|
|
44
220
|
check "assets removed" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "0"
|
|
45
|
-
check "hooks de-registered" "$(
|
|
221
|
+
check "managed hooks de-registered" "$(managed_hook_count "$LIVE/settings.json")" "0"
|
|
222
|
+
check "unrelated hook preserved" "$(grep -cF 'keep-me.sh' "$LIVE/settings.json" 2>/dev/null || true)" "1"
|
|
223
|
+
check "Pi extension removed" "$([[ -e "$PI_LIVE/extensions/cairnkeep-trajectory.ts" ]] && echo no || echo yes)" "yes"
|
|
224
|
+
check "default uninstall keeps notes" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$note_before"
|
|
225
|
+
check "default uninstall keeps all typed and journal bytes" "$(tree_hash "$SB/home/.cairnkeep")" "$store_before"
|
|
226
|
+
check "default uninstall keeps artifact bytes exact" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
|
|
227
|
+
check "default uninstall removes managed capability config" "$([[ -e "$PROJECT_DATA/.ai/capabilities.json" ]] && echo no || echo yes)" "yes"
|
|
228
|
+
check "default uninstall removes managed capability overlay" "$([[ -e "$PROJECT_DATA/.ai/capability-contract" ]] && echo no || echo yes)" "yes"
|
|
229
|
+
check "default uninstall leaves normal Claude installation exact" "$(diff -qr "$SB/claude-inert.before" "$CLAUDE_INERT" >/dev/null && echo yes || echo no)" "yes"
|
|
230
|
+
check "default uninstall leaves normal OpenCode installation exact" "$(diff -qr "$SB/opencode-inert.before" "$OPENCODE_INERT" >/dev/null && echo yes || echo no)" "yes"
|
|
231
|
+
check "default uninstall keeps unrelated .ai bytes exact" "$(cmp -s "$SB/operator-state.before.bin" "$PROJECT_DATA/.ai/operator-state.bin" && echo yes || echo no)" "yes"
|
|
232
|
+
check "default uninstall keeps callback DB exact" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
|
|
233
|
+
check "default uninstall keeps callback WAL exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
|
|
234
|
+
check "default uninstall keeps complete and partial eval reports, snapshots, modes, and layout exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
|
|
235
|
+
check "default uninstall keeps unrelated project memory exact" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
|
|
236
|
+
check "default uninstall leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
|
|
46
237
|
BK=$(ls -d "$SB/home/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
|
|
47
238
|
check "revert.sh generated" "$([[ -n "$BK" && -f "$BK/revert.sh" ]] && echo yes || echo no)" "yes"
|
|
239
|
+
check "capability config backup is exact" "$(cmp -s "$SB/capabilities.before.json" "$BK/files/${PROJECT_DATA#/}/.ai/capabilities.json" && echo yes || echo no)" "yes"
|
|
48
240
|
|
|
49
241
|
# --- revert restores everything --------------------------------------------
|
|
50
242
|
bash "$BK/revert.sh" >/dev/null 2>&1
|
|
51
243
|
check "assets restored" "$(find "$LIVE" -type f -name '*.md' | wc -l | tr -d ' ')" "$md_installed"
|
|
52
244
|
check "settings.json identical" "$(cmp -s "$SB/settings.before.json" "$LIVE/settings.json" && echo yes || echo no)" "yes"
|
|
245
|
+
check "Pi extension restored" "$(cmp -s "$SB/pi.before.ts" "$PI_LIVE/extensions/cairnkeep-trajectory.ts" && echo yes || echo no)" "yes"
|
|
246
|
+
check "capability config bytes restored" "$(cmp -s "$SB/capabilities.before.json" "$PROJECT_DATA/.ai/capabilities.json" && echo yes || echo no)" "yes"
|
|
247
|
+
check "capability overlay bytes restored" "$(tree_hash "$PROJECT_DATA/.ai/capability-contract")" "$overlay_before"
|
|
248
|
+
check "capability hook registrations restored" "$(capability_hook_count "$CLAUDE_OVERLAY/settings.json")" "5"
|
|
249
|
+
check "capability plugin restored" "$([[ -f "$OPENCODE_OVERLAY/plugins/capability-command.ts" ]] && echo yes || echo no)" "yes"
|
|
250
|
+
check "capability config mode restored" "$(stat -c '%a' "$PROJECT_DATA/.ai/capabilities.json" 2>/dev/null || stat -f '%Lp' "$PROJECT_DATA/.ai/capabilities.json")" "600"
|
|
251
|
+
check "unrelated .ai bytes remain exact after revert" "$(cmp -s "$SB/operator-state.before.bin" "$PROJECT_DATA/.ai/operator-state.bin" && echo yes || echo no)" "yes"
|
|
252
|
+
check "default-uninstall revert leaves retained eval exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
|
|
253
|
+
|
|
254
|
+
# --- project artifact purge is backup-first and exactly reversible ---------
|
|
255
|
+
ARTIFACT_HOME="$SB/artifact-home"
|
|
256
|
+
mkdir -p "$ARTIFACT_HOME"
|
|
257
|
+
HOME="$ARTIFACT_HOME" XDG_CONFIG_HOME="$ARTIFACT_HOME/.config" \
|
|
258
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/unused-live" --pi-live-root "$SB/unused-pi" "$PROJECT_DATA" >/dev/null 2>&1
|
|
259
|
+
check "artifact purge removes project store" "$([[ -e "$PROJECT_DATA/.agentfs" ]] && echo no || echo yes)" "yes"
|
|
260
|
+
ARTIFACT_BK=$(ls -dt "$ARTIFACT_HOME/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
|
|
261
|
+
check "artifact purge backup is exact" "$(cmp -s "$SB/artifacts.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
|
|
262
|
+
check "callback purge backup is exact" "$(cmp -s "$SB/trajectory.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
|
|
263
|
+
check "callback WAL purge backup is exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
|
|
264
|
+
check "purge backup contains exact eval tree, bytes, and modes" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/eval")" "$eval_before"
|
|
265
|
+
check "purge backup contains exact unrelated project memory" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs")" "$agentfs_before"
|
|
266
|
+
check "selected-project purge leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
|
|
267
|
+
HOME="$ARTIFACT_HOME" XDG_CONFIG_HOME="$ARTIFACT_HOME/.config" bash "$ARTIFACT_BK/revert.sh" >/dev/null 2>&1
|
|
268
|
+
check "artifact revert restores exact bytes" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
|
|
269
|
+
check "callback revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
|
|
270
|
+
check "callback WAL revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
|
|
271
|
+
check "purge revert restores exact eval tree, bytes, and modes" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
|
|
272
|
+
check "purge revert restores exact unrelated project memory" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
|
|
273
|
+
check "purge revert leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
|
|
53
274
|
|
|
54
275
|
# --- project scaffold + memory purge round-trip ----------------------------
|
|
55
276
|
PROJ="$SB/proj"; mkdir -p "$PROJ"; git -C "$PROJ" init -q
|
|
56
277
|
"$ROOT_DIR/scripts/bootstrap.sh" --untracked "$PROJ" >/dev/null 2>&1
|
|
57
278
|
EXCL="$PROJ/.git/info/exclude"
|
|
58
|
-
mkdir -p "$SB/home/.cairnkeep"; echo data >"$SB/home/.cairnkeep/db"
|
|
279
|
+
mkdir -p "$SB/home/.cairnkeep/notes/projects/example/hindsight"; echo data >"$SB/home/.cairnkeep/db"
|
|
280
|
+
printf 'purge and restore exact note bytes\n' >"$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md"
|
|
281
|
+
purge_note_before=$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)
|
|
282
|
+
purge_store_before=$(tree_hash "$SB/home/.cairnkeep")
|
|
59
283
|
"$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/home/.claude" "$PROJ" >/dev/null 2>&1
|
|
60
284
|
check "project .ai removed" "$([[ -e "$PROJ/.ai" ]] && echo no || echo yes)" "yes"
|
|
61
285
|
check "project .agentfs removed" "$([[ -e "$PROJ/.agentfs" ]] && echo no || echo yes)" "yes"
|
|
@@ -66,6 +290,8 @@ bash "$BK2/revert.sh" >/dev/null 2>&1
|
|
|
66
290
|
check "project scaffold restored" "$([[ -d "$PROJ/.ai" ]] && echo yes || echo no)" "yes"
|
|
67
291
|
check "project ignore restored" "$([[ -f "$PROJ/.agentfs/.gitignore" ]] && echo yes || echo no)" "yes"
|
|
68
292
|
check "memory store restored" "$(cat "$SB/home/.cairnkeep/db" 2>/dev/null)" "data"
|
|
293
|
+
check "note store restored exactly" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$purge_note_before"
|
|
294
|
+
check "typed and journal state restored exactly" "$(tree_hash "$SB/home/.cairnkeep")" "$purge_store_before"
|
|
69
295
|
|
|
70
296
|
echo
|
|
71
297
|
if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails check(s) failed."; exit 1; fi
|
package/scripts/uninstall.sh
CHANGED
|
@@ -21,6 +21,7 @@ CLAUDE_SOURCE="$CAIRN_ROOT/claude"
|
|
|
21
21
|
TPL="$CAIRN_ROOT/templates"
|
|
22
22
|
|
|
23
23
|
LIVE_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
24
|
+
PI_LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
24
25
|
STORE_DIR="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
|
|
25
26
|
STORE_DIR="${STORE_DIR/#\~/$HOME}"
|
|
26
27
|
DRY_RUN=0
|
|
@@ -29,20 +30,34 @@ PURGE_MEMORY=0
|
|
|
29
30
|
PROJECTS=()
|
|
30
31
|
|
|
31
32
|
# The hooks cairnkeep registers (basename is the settings.json match token).
|
|
32
|
-
HOOK_NAMES=(memory-wakeup.sh memory-capture.sh memory-recall.sh context-explore-pretask.sh)
|
|
33
|
+
HOOK_NAMES=(memory-wakeup.sh memory-capture.sh compaction-capture.sh memory-recall.sh context-explore-pretask.sh)
|
|
34
|
+
|
|
35
|
+
# Exact project-local files installed by bootstrap. Remove these individually so
|
|
36
|
+
# operator-owned files under .ai (notably .env and local state) are never swept
|
|
37
|
+
# up with Cairnkeep's managed scaffold.
|
|
38
|
+
PROJECT_AI_FILES=(
|
|
39
|
+
start-claude.sh
|
|
40
|
+
start-opencode.sh
|
|
41
|
+
start-pi.sh
|
|
42
|
+
env.example
|
|
43
|
+
trajectory-redaction.json
|
|
44
|
+
capabilities.json
|
|
45
|
+
)
|
|
33
46
|
|
|
34
47
|
usage() {
|
|
35
48
|
cat <<'EOF'
|
|
36
|
-
Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH]
|
|
49
|
+
Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH]
|
|
50
|
+
[--pi-live-root PATH] [PROJECT ...]
|
|
37
51
|
|
|
38
52
|
Reverse cairnkeep's install. Everything removed/edited is backed up into
|
|
39
53
|
$HOME/.cairnkeep-uninstall-<ts>/ with a revert.sh before any change.
|
|
40
54
|
|
|
41
55
|
--dry-run Print what would happen; touch nothing.
|
|
42
56
|
--yes Do not prompt for confirmation.
|
|
43
|
-
--purge-memory Also remove the global memory store and .agentfs/ memory
|
|
44
|
-
each PROJECT. Backed up first; off by default.
|
|
57
|
+
--purge-memory Also remove the global memory store and .agentfs/ memory and
|
|
58
|
+
trajectories in each PROJECT. Backed up first; off by default.
|
|
45
59
|
--live-root PATH Claude root to clean (default: $CLAUDE_CONFIG_DIR or ~/.claude).
|
|
60
|
+
--pi-live-root PATH Pi agent root to clean (default: $PI_CODING_AGENT_DIR or ~/.pi/agent).
|
|
46
61
|
PROJECT ... Also revert `cairn bootstrap` in these project dirs
|
|
47
62
|
(.ai/, .planning/, and any .git/info/exclude entries).
|
|
48
63
|
-h, --help Show this help.
|
|
@@ -55,6 +70,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
55
70
|
--yes) ASSUME_YES=1; shift ;;
|
|
56
71
|
--purge-memory) PURGE_MEMORY=1; shift ;;
|
|
57
72
|
--live-root) LIVE_ROOT="$2"; shift 2 ;;
|
|
73
|
+
--pi-live-root) PI_LIVE_ROOT="$2"; shift 2 ;;
|
|
58
74
|
-h|--help) usage; exit 0 ;;
|
|
59
75
|
-*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
|
60
76
|
*) PROJECTS+=("$1"); shift ;;
|
|
@@ -118,6 +134,7 @@ asset_dests() {
|
|
|
118
134
|
echo "cairn uninstall"
|
|
119
135
|
[[ $DRY_RUN -eq 1 ]] && echo "(dry run — nothing will change)"
|
|
120
136
|
echo " live root: $LIVE_ROOT"
|
|
137
|
+
echo " Pi live root: $PI_LIVE_ROOT"
|
|
121
138
|
echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
|
|
122
139
|
[[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
|
|
123
140
|
|
|
@@ -131,6 +148,11 @@ fi
|
|
|
131
148
|
echo "Operating layer (assets):"
|
|
132
149
|
while IFS= read -r dst; do remove_path "$dst"; done < <(asset_dests)
|
|
133
150
|
|
|
151
|
+
# The Pi adapter is one precisely owned extension; do not touch any other Pi
|
|
152
|
+
# configuration or extensions in the same user root.
|
|
153
|
+
echo "Pi extension:"
|
|
154
|
+
remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
|
|
155
|
+
|
|
134
156
|
# 2. settings.json hook registrations (back up whole file, then de-register).
|
|
135
157
|
SETTINGS="$LIVE_ROOT/settings.json"
|
|
136
158
|
if [[ -f "$SETTINGS" ]]; then
|
|
@@ -207,7 +229,17 @@ for proj in "${PROJECTS[@]:-}"; do
|
|
|
207
229
|
[[ -n "$proj" && -d "$proj" ]] || continue
|
|
208
230
|
proj=$(cd "$proj" && pwd)
|
|
209
231
|
echo "Project scaffold: $proj"
|
|
210
|
-
|
|
232
|
+
# Truthy launchers create this project-local root only for the explicit
|
|
233
|
+
# capability overlay. Back up the complete managed unit so its native Claude
|
|
234
|
+
# hook registrations and OpenCode plugin bytes restore together exactly.
|
|
235
|
+
# Normal and master-off installations never create it, so absence is inert.
|
|
236
|
+
remove_path "$proj/.ai/capability-contract"
|
|
237
|
+
for rel in "${PROJECT_AI_FILES[@]}"; do
|
|
238
|
+
remove_path "$proj/.ai/$rel"
|
|
239
|
+
done
|
|
240
|
+
if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
|
|
241
|
+
rmdir "$proj/.ai" 2>/dev/null || true
|
|
242
|
+
fi
|
|
211
243
|
remove_path "$proj/.planning"
|
|
212
244
|
if [[ $PURGE_MEMORY -eq 1 ]]; then
|
|
213
245
|
remove_path "$proj/.agentfs"
|