@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
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cross-harness compaction capture/recovery contract. The default invocation
|
|
3
|
+
# is the complete green integration path and remains safe for root test globs.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
7
|
+
MODE="${1:-}"
|
|
8
|
+
EXPECTED_RED_EXIT=86
|
|
9
|
+
MISSING_CAPABILITY=85
|
|
10
|
+
RED_MARKER="PHASE17_RED:COMPACTION_HOOK_RECOVERY_MISSING"
|
|
11
|
+
|
|
12
|
+
case "$MODE" in
|
|
13
|
+
""|--expect-red|--claude-only|--opencode-only|--full) ;;
|
|
14
|
+
*) echo "usage: $0 [--expect-red|--claude-only|--opencode-only|--full]" >&2; exit 2 ;;
|
|
15
|
+
esac
|
|
16
|
+
|
|
17
|
+
tmp=$(mktemp -d)
|
|
18
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
19
|
+
fail() { echo "FAIL: $1" >&2; return 1; }
|
|
20
|
+
|
|
21
|
+
claude_fixture="$ROOT/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json"
|
|
22
|
+
claude_fixture_220="$ROOT/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json"
|
|
23
|
+
opencode_event_fixture="$ROOT/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json"
|
|
24
|
+
opencode_messages_fixture="$ROOT/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json"
|
|
25
|
+
PARITY_RECOVERY=$'## Compaction recovery\nSource: project_fallback\nSession: canonical:prior\nRevision: 2\nCaptured: 2026-07-25T00:00:00.000Z\nAge: 172800 seconds\nHarness: canonical\nCompleteness: goals=missing, decisions=missing, todos=missing, errors=missing\nWarning: this state is stale; validate it against the current repository before relying on it.\n\n### Task Goals\n(none captured)\n\n### Decisions Made\n(none captured)\n\n### Open TODOs\n(none captured)\n\n### Critical Error Traces\n(none captured)\n'
|
|
26
|
+
|
|
27
|
+
render() {
|
|
28
|
+
local source="$1"
|
|
29
|
+
local destination="$2"
|
|
30
|
+
local infra_root="${3:-$ROOT}"
|
|
31
|
+
sed "s|@@INFRA_ROOT@@|$infra_root|g" "$source" > "$destination"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
assert_marker_free() {
|
|
35
|
+
local path="$1"
|
|
36
|
+
! grep -qF "$RED_MARKER" "$path" || fail "default baseline emitted the compaction RED marker"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
run_existing_baseline() {
|
|
40
|
+
local output="$tmp/existing-baseline.out"
|
|
41
|
+
"$ROOT/scripts/test-trajectory-hooks.sh" >"$output" 2>&1
|
|
42
|
+
grep -qF "PASS: Claude/OpenCode/Pi trajectory hooks and flag-off regression" "$output" \
|
|
43
|
+
|| fail "existing trajectory hook baseline did not pass"
|
|
44
|
+
assert_marker_free "$output"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate_fixtures() {
|
|
48
|
+
node - "$claude_fixture" "$claude_fixture_220" "$opencode_event_fixture" "$opencode_messages_fixture" <<'NODE'
|
|
49
|
+
const fs = require("fs")
|
|
50
|
+
const [claudePath, claude220Path, eventPath, messagesPath] = process.argv.slice(2)
|
|
51
|
+
const claude = JSON.parse(fs.readFileSync(claudePath, "utf8"))
|
|
52
|
+
const claude220 = JSON.parse(fs.readFileSync(claude220Path, "utf8"))
|
|
53
|
+
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"))
|
|
54
|
+
const envelope = JSON.parse(fs.readFileSync(messagesPath, "utf8"))
|
|
55
|
+
if (claude.hook_event_name !== "PostCompact" || typeof claude.compact_summary !== "string") process.exit(1)
|
|
56
|
+
if (claude220.hook_event_name !== "PostCompact" || typeof claude220.prompt_id !== "string") process.exit(1)
|
|
57
|
+
if (Object.keys(claude220).sort().join(",") !== ["compact_summary", "cwd", "hook_event_name", "prompt_id", "session_id", "transcript_path", "trigger"].sort().join(",")) process.exit(1)
|
|
58
|
+
if (event.type !== "session.compacted" || Object.keys(event.properties ?? {}).join(",") !== "sessionID") process.exit(1)
|
|
59
|
+
if (envelope.session?.id !== event.properties.sessionID || !Array.isArray(envelope.messages)) process.exit(1)
|
|
60
|
+
NODE
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
require_managed_sources() {
|
|
64
|
+
local missing=0
|
|
65
|
+
[[ -f "$ROOT/claude/hooks/compaction-capture.sh" ]] || missing=1
|
|
66
|
+
grep -qF "compaction-capture.sh)" "$ROOT/scripts/sync-claude-assets.sh" || missing=1
|
|
67
|
+
grep -qF 'session.compacted' "$ROOT/opencode/plugins/memory-capture.ts" || missing=1
|
|
68
|
+
grep -qF 'CAIRN_COMPACTION_CAPTURE' "$ROOT/opencode/plugins/memory-wakeup.ts" || missing=1
|
|
69
|
+
[[ "$missing" -eq 0 ]] || return "$MISSING_CAPABILITY"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
make_fake_infra() {
|
|
73
|
+
local fake_root="$1"
|
|
74
|
+
mkdir -p "$fake_root/mcp-memory-server/dist"
|
|
75
|
+
cat > "$fake_root/mcp-memory-server/dist/artifact-cli.js" <<'NODE'
|
|
76
|
+
import fs from "node:fs"
|
|
77
|
+
const args = process.argv.slice(2)
|
|
78
|
+
const command = args[0] ?? ""
|
|
79
|
+
const input = command.startsWith("capture-") ? await new Promise((resolve) => {
|
|
80
|
+
let value = ""
|
|
81
|
+
process.stdin.setEncoding("utf8")
|
|
82
|
+
process.stdin.on("data", (chunk) => { value += chunk })
|
|
83
|
+
process.stdin.on("end", () => resolve(value))
|
|
84
|
+
}) : ""
|
|
85
|
+
if (process.env.CAIRN_TEST_CALLS) {
|
|
86
|
+
fs.appendFileSync(process.env.CAIRN_TEST_CALLS, `${JSON.stringify({ args, input })}\n`)
|
|
87
|
+
}
|
|
88
|
+
if (command === "recover" && process.env.CAIRN_TEST_RECOVERY) {
|
|
89
|
+
process.stdout.write(process.env.CAIRN_TEST_RECOVERY)
|
|
90
|
+
}
|
|
91
|
+
NODE
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
write_fake_claude() {
|
|
95
|
+
local fake_bin="$1"
|
|
96
|
+
local version="$2"
|
|
97
|
+
mkdir -p "$fake_bin"
|
|
98
|
+
cat > "$fake_bin/claude" <<EOF
|
|
99
|
+
#!/usr/bin/env bash
|
|
100
|
+
printf '%s (Claude Code)\n' '$version'
|
|
101
|
+
EOF
|
|
102
|
+
chmod +x "$fake_bin/claude"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
assert_no_spy_calls() {
|
|
106
|
+
local prefix="$1"
|
|
107
|
+
[[ ! -e "$prefix.node" ]] || fail "$prefix started Node while compaction capture was disabled"
|
|
108
|
+
[[ ! -e "$prefix.python" ]] || fail "$prefix parsed JSON while compaction capture was disabled"
|
|
109
|
+
[[ ! -e "$prefix.curl" ]] || fail "$prefix attempted network access while compaction capture was disabled"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
make_path_spies() {
|
|
113
|
+
local spy_dir="$1"
|
|
114
|
+
local prefix="$2"
|
|
115
|
+
mkdir -p "$spy_dir"
|
|
116
|
+
for executable in node python3 curl; do
|
|
117
|
+
cat > "$spy_dir/$executable" <<EOF
|
|
118
|
+
#!/usr/bin/env bash
|
|
119
|
+
touch "$prefix.${executable%%3}"
|
|
120
|
+
exit 97
|
|
121
|
+
EOF
|
|
122
|
+
chmod +x "$spy_dir/$executable"
|
|
123
|
+
done
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
claude_disabled_contract() {
|
|
127
|
+
local hook="$1"
|
|
128
|
+
local wakeup="$2"
|
|
129
|
+
local repo="$tmp/claude-disabled"
|
|
130
|
+
local spies="$tmp/claude-disabled-spies"
|
|
131
|
+
local prefix="$tmp/claude-disabled-call"
|
|
132
|
+
mkdir -p "$repo"
|
|
133
|
+
make_path_spies "$spies" "$prefix"
|
|
134
|
+
local before after
|
|
135
|
+
before=$(find "$repo" -type f -print | sort)
|
|
136
|
+
(cd "$repo" && printf '%s\n' '{not-json' | \
|
|
137
|
+
PATH="$spies:$PATH" CAIRN_ARTIFACT_STORE=1 "$hook") \
|
|
138
|
+
>"$tmp/claude-disabled.out" 2>"$tmp/claude-disabled.err"
|
|
139
|
+
after=$(find "$repo" -type f -print | sort)
|
|
140
|
+
[[ "$before" == "$after" ]] || fail "disabled Claude capture changed project files"
|
|
141
|
+
[[ ! -s "$tmp/claude-disabled.out" && ! -s "$tmp/claude-disabled.err" ]] \
|
|
142
|
+
|| fail "disabled Claude capture emitted output"
|
|
143
|
+
assert_no_spy_calls "$prefix"
|
|
144
|
+
|
|
145
|
+
(cd "$repo" && printf '%s\n' '{not-json' | \
|
|
146
|
+
PATH="$spies:$PATH" CAIRN_ARTIFACT_STORE=1 "$wakeup") \
|
|
147
|
+
>"$tmp/claude-wakeup-disabled.out" 2>"$tmp/claude-wakeup-disabled.err"
|
|
148
|
+
[[ ! -s "$tmp/claude-wakeup-disabled.out" && ! -s "$tmp/claude-wakeup-disabled.err" ]] \
|
|
149
|
+
|| fail "disabled Claude recovery changed the empty-project baseline"
|
|
150
|
+
assert_no_spy_calls "$prefix"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
claude_enabled_contract() {
|
|
154
|
+
local fake_root="$tmp/fake-infra-claude"
|
|
155
|
+
local calls="$tmp/claude-calls.jsonl"
|
|
156
|
+
local hook="$tmp/compaction-capture.sh"
|
|
157
|
+
local wakeup="$tmp/memory-wakeup.sh"
|
|
158
|
+
local repo="$tmp/claude-enabled"
|
|
159
|
+
local fake_bin="$tmp/claude-enabled-bin"
|
|
160
|
+
make_fake_infra "$fake_root"
|
|
161
|
+
write_fake_claude "$fake_bin" "2.1.219"
|
|
162
|
+
render "$ROOT/claude/hooks/compaction-capture.sh" "$hook" "$fake_root"
|
|
163
|
+
render "$ROOT/claude/hooks/memory-wakeup.sh" "$wakeup" "$fake_root"
|
|
164
|
+
chmod +x "$hook" "$wakeup"
|
|
165
|
+
claude_disabled_contract "$hook" "$wakeup"
|
|
166
|
+
mkdir -p "$repo/.agentfs"
|
|
167
|
+
touch "$repo/.agentfs/artifacts.db"
|
|
168
|
+
|
|
169
|
+
(cd "$repo" && PATH="$fake_bin:$PATH" CAIRN_COMPACTION_CAPTURE=1 CAIRN_TEST_CALLS="$calls" \
|
|
170
|
+
timeout 3 "$hook" < "$claude_fixture") >"$tmp/claude-enabled.out" 2>"$tmp/claude-enabled.err"
|
|
171
|
+
[[ ! -s "$tmp/claude-enabled.out" ]] || fail "enabled Claude capture emitted stdout"
|
|
172
|
+
node - "$calls" "$claude_fixture" <<'NODE'
|
|
173
|
+
const fs = require("fs")
|
|
174
|
+
const calls = fs.readFileSync(process.argv[2], "utf8").trim().split("\n").map(JSON.parse)
|
|
175
|
+
const fixture = JSON.parse(fs.readFileSync(process.argv[3], "utf8"))
|
|
176
|
+
if (calls.length !== 1 || calls[0].args[0] !== "capture-claude") process.exit(1)
|
|
177
|
+
if (calls[0].args.slice(-2).join("=") !== "--harness-version=2.1.219") process.exit(1)
|
|
178
|
+
if (JSON.stringify(JSON.parse(calls[0].input)) !== JSON.stringify(fixture)) process.exit(1)
|
|
179
|
+
NODE
|
|
180
|
+
|
|
181
|
+
rm -f "$calls"
|
|
182
|
+
write_fake_claude "$fake_bin" "2.1.220"
|
|
183
|
+
(cd "$repo" && PATH="$fake_bin:$PATH" CAIRN_COMPACTION_CAPTURE=1 CAIRN_TEST_CALLS="$calls" \
|
|
184
|
+
timeout 3 "$hook" < "$claude_fixture_220") >"$tmp/claude-220.out" 2>"$tmp/claude-220.err"
|
|
185
|
+
node - "$calls" "$claude_fixture_220" <<'NODE'
|
|
186
|
+
const fs = require("fs")
|
|
187
|
+
const calls = fs.readFileSync(process.argv[2], "utf8").trim().split("\n").map(JSON.parse)
|
|
188
|
+
const fixture = JSON.parse(fs.readFileSync(process.argv[3], "utf8"))
|
|
189
|
+
if (calls.length !== 1 || calls[0].args.slice(-2).join("=") !== "--harness-version=2.1.220") process.exit(1)
|
|
190
|
+
if (JSON.stringify(JSON.parse(calls[0].input)) !== JSON.stringify(fixture)) process.exit(1)
|
|
191
|
+
NODE
|
|
192
|
+
|
|
193
|
+
rm -f "$calls"
|
|
194
|
+
write_fake_claude "$fake_bin" "2.1.219"
|
|
195
|
+
(cd "$repo" && PATH="$fake_bin:$PATH" CAIRN_COMPACTION_CAPTURE=1 CAIRN_ARTIFACT_STORE=1 \
|
|
196
|
+
CAIRN_ARTIFACT_HTTP=1 CAIRN_TEST_CALLS="$calls" timeout 3 "$hook" < "$claude_fixture") \
|
|
197
|
+
>"$tmp/claude-both-http.out" 2>"$tmp/claude-both-http.err"
|
|
198
|
+
[[ $(wc -l < "$calls") -eq 1 ]] || fail "Claude both-flags/HTTP capture changed local call count"
|
|
199
|
+
|
|
200
|
+
local current_recovery fallback_recovery stale_recovery
|
|
201
|
+
current_recovery=$'Source: current_session\nSession: claude-code:claude-compaction-session-001\nRevision: 2\nTask Goals\n- current goal\nDecisions Made\n- current decision\nOpen TODOs\n- current todo\nCritical Error Traces\n- current error\n'
|
|
202
|
+
fallback_recovery=$'Source: project_fallback\nSession: claude-code:prior\nRevision: 1\nTask Goals\n- fallback goal\nDecisions Made\n- fallback decision\nOpen TODOs\n- fallback todo\nCritical Error Traces\n- fallback error\n'
|
|
203
|
+
stale_recovery="$PARITY_RECOVERY"
|
|
204
|
+
for case_name in current fallback stale; do
|
|
205
|
+
case "$case_name" in
|
|
206
|
+
current) recovery="$current_recovery"; session_id="claude-compaction-session-001" ;;
|
|
207
|
+
fallback) recovery="$fallback_recovery"; session_id="fresh-session" ;;
|
|
208
|
+
stale) recovery="$stale_recovery"; session_id="fresh-stale-session" ;;
|
|
209
|
+
esac
|
|
210
|
+
(cd "$repo" && CAIRN_COMPACTION_CAPTURE=1 CAIRN_TEST_CALLS="$calls" CAIRN_TEST_RECOVERY="$recovery" \
|
|
211
|
+
"$wakeup" <<<"{\"session_id\":\"$session_id\",\"source\":\"startup\"}") \
|
|
212
|
+
>"$tmp/claude-$case_name-recovery.out" 2>"$tmp/claude-$case_name-recovery.err"
|
|
213
|
+
grep -qF "## Compaction recovery" "$tmp/claude-$case_name-recovery.out" \
|
|
214
|
+
|| fail "Claude $case_name recovery heading is missing"
|
|
215
|
+
grep -qF "Task Goals" "$tmp/claude-$case_name-recovery.out" \
|
|
216
|
+
|| fail "Claude $case_name recovery body is missing"
|
|
217
|
+
! grep -qiE 'raw_summary|compact_summary|Bearer|sk-' "$tmp/claude-$case_name-recovery.out" \
|
|
218
|
+
|| fail "Claude $case_name recovery injected raw summary data"
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
(cd "$repo" && CAIRN_COMPACTION_CAPTURE=1 CAIRN_TEST_CALLS="$calls" "$wakeup" <<<'{not-json') \
|
|
222
|
+
>"$tmp/claude-malformed.out" 2>"$tmp/claude-malformed.err"
|
|
223
|
+
[[ ! -s "$tmp/claude-malformed.err" ]] || fail "malformed Claude recovery did not fail open"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
claude_sync_contract() {
|
|
227
|
+
local live="$tmp/claude-live"
|
|
228
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$live" >/dev/null
|
|
229
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$live" >/dev/null
|
|
230
|
+
[[ -x "$live/hooks/compaction-capture.sh" ]] || fail "installed PostCompact hook is not executable"
|
|
231
|
+
local registrations
|
|
232
|
+
registrations=$(node - "$live/settings.json" <<'NODE'
|
|
233
|
+
const fs = require("fs")
|
|
234
|
+
const settings = JSON.parse(fs.readFileSync(process.argv[2], "utf8"))
|
|
235
|
+
let count = 0
|
|
236
|
+
for (const entry of settings.hooks?.PostCompact ?? []) {
|
|
237
|
+
if (entry.matcher !== undefined && entry.matcher !== "") process.exit(2)
|
|
238
|
+
for (const hook of entry.hooks ?? []) {
|
|
239
|
+
if (String(hook.command ?? "").includes("compaction-capture.sh")) count += 1
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
process.stdout.write(String(count))
|
|
243
|
+
NODE
|
|
244
|
+
)
|
|
245
|
+
[[ "$registrations" -eq 1 ]] || fail "PostCompact registration is missing, matched, or duplicated"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
write_opencode_harness() {
|
|
249
|
+
cat > "$tmp/opencode-compaction-harness.mjs" <<'NODE'
|
|
250
|
+
import assert from "node:assert/strict"
|
|
251
|
+
import fs from "node:fs"
|
|
252
|
+
import { pathToFileURL } from "node:url"
|
|
253
|
+
const [capturePath, wakeupPath, eventPath, messagesPath, repo, mode] = process.argv.slice(2)
|
|
254
|
+
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"))
|
|
255
|
+
const envelope = JSON.parse(fs.readFileSync(messagesPath, "utf8"))
|
|
256
|
+
const calls = []
|
|
257
|
+
const client = { session: {
|
|
258
|
+
get: async (input) => { calls.push(["get", input]); return { data: envelope.session } },
|
|
259
|
+
messages: async (input) => { calls.push(["messages", input]); return { data: envelope.messages } },
|
|
260
|
+
} }
|
|
261
|
+
const capture = await import(`${pathToFileURL(capturePath).href}?capture=${Date.now()}`)
|
|
262
|
+
const plugin = await capture.MemoryCapturePlugin({ client, directory: repo })
|
|
263
|
+
await plugin.event({ event })
|
|
264
|
+
if (mode === "disabled") {
|
|
265
|
+
assert.deepEqual(calls, [])
|
|
266
|
+
assert.equal(fs.existsSync(process.env.CAIRN_TEST_CALLS), false)
|
|
267
|
+
} else {
|
|
268
|
+
assert.deepEqual(calls.map(([name]) => name), ["get", "messages"])
|
|
269
|
+
const subprocess = fs.readFileSync(process.env.CAIRN_TEST_CALLS, "utf8").trim().split("\n").map(JSON.parse)
|
|
270
|
+
assert.equal(subprocess.length, 1)
|
|
271
|
+
assert.equal(subprocess[0].args[0], "capture-opencode")
|
|
272
|
+
const payload = JSON.parse(subprocess[0].input)
|
|
273
|
+
assert.equal(payload.event.type, "session.compacted")
|
|
274
|
+
assert.equal(payload.session.id, envelope.session.id)
|
|
275
|
+
assert.equal(payload.harness_version, "1.17.20")
|
|
276
|
+
assert.deepEqual(payload.messages, envelope.messages)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const shellCalls = []
|
|
280
|
+
const dollar = (...args) => {
|
|
281
|
+
shellCalls.push(args)
|
|
282
|
+
return { quiet() { return this }, nothrow: async () => ({ stdout: "" }) }
|
|
283
|
+
}
|
|
284
|
+
const wakeup = await import(`${pathToFileURL(wakeupPath).href}?wakeup=${Date.now()}`)
|
|
285
|
+
const wakeupPlugin = await wakeup.MemoryWakeupPlugin({ $: dollar, directory: repo })
|
|
286
|
+
const output = { system: [] }
|
|
287
|
+
await wakeupPlugin["experimental.chat.system.transform"]({ sessionID: envelope.session.id }, output)
|
|
288
|
+
if (mode === "disabled") {
|
|
289
|
+
assert.deepEqual(shellCalls, [])
|
|
290
|
+
assert.deepEqual(output.system, [])
|
|
291
|
+
} else {
|
|
292
|
+
assert.equal(output.system.some((value) => String(value).includes("Task Goals")), true)
|
|
293
|
+
assert.equal(output.system.some((value) => /raw_summary|compact_summary|Bearer|sk-/i.test(String(value))), false)
|
|
294
|
+
if (process.env.CAIRN_TEST_SYSTEM_OUTPUT) {
|
|
295
|
+
fs.writeFileSync(process.env.CAIRN_TEST_SYSTEM_OUTPUT, JSON.stringify(output.system))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
NODE
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
opencode_contract() {
|
|
302
|
+
local fake_root="$tmp/fake-infra-opencode"
|
|
303
|
+
local calls="$tmp/opencode-calls.jsonl"
|
|
304
|
+
local capture="$tmp/opencode-memory-capture.ts"
|
|
305
|
+
local wakeup="$tmp/opencode-memory-wakeup.ts"
|
|
306
|
+
local repo="$tmp/opencode-project"
|
|
307
|
+
make_fake_infra "$fake_root"
|
|
308
|
+
render "$ROOT/opencode/plugins/memory-capture.ts" "$capture" "$fake_root"
|
|
309
|
+
render "$ROOT/opencode/plugins/memory-wakeup.ts" "$wakeup" "$fake_root"
|
|
310
|
+
mkdir -p "$repo"
|
|
311
|
+
write_opencode_harness
|
|
312
|
+
|
|
313
|
+
CAIRN_ARTIFACT_STORE=1 CAIRN_TEST_CALLS="$calls" node --experimental-strip-types \
|
|
314
|
+
"$tmp/opencode-compaction-harness.mjs" "$capture" "$wakeup" \
|
|
315
|
+
"$opencode_event_fixture" "$opencode_messages_fixture" "$repo" disabled \
|
|
316
|
+
>"$tmp/opencode-disabled.out" 2>"$tmp/opencode-disabled.err"
|
|
317
|
+
[[ ! -s "$tmp/opencode-disabled.out" && ! -s "$tmp/opencode-disabled.err" ]] \
|
|
318
|
+
|| fail "disabled OpenCode compaction path emitted output"
|
|
319
|
+
|
|
320
|
+
local recovery
|
|
321
|
+
recovery="$PARITY_RECOVERY"
|
|
322
|
+
rm -f "$calls"
|
|
323
|
+
CAIRN_COMPACTION_CAPTURE=1 CAIRN_TEST_CALLS="$calls" CAIRN_TEST_RECOVERY="$recovery" \
|
|
324
|
+
CAIRN_TEST_SYSTEM_OUTPUT="$tmp/opencode-system.json" \
|
|
325
|
+
timeout 3 node --experimental-strip-types "$tmp/opencode-compaction-harness.mjs" \
|
|
326
|
+
"$capture" "$wakeup" "$opencode_event_fixture" "$opencode_messages_fixture" "$repo" enabled \
|
|
327
|
+
>"$tmp/opencode-enabled.out" 2>"$tmp/opencode-enabled.err"
|
|
328
|
+
[[ ! -s "$tmp/opencode-enabled.out" && ! -s "$tmp/opencode-enabled.err" ]] \
|
|
329
|
+
|| fail "enabled OpenCode compaction path emitted output"
|
|
330
|
+
|
|
331
|
+
rm -f "$calls"
|
|
332
|
+
CAIRN_COMPACTION_CAPTURE=1 CAIRN_ARTIFACT_STORE=1 CAIRN_ARTIFACT_HTTP=1 \
|
|
333
|
+
CAIRN_TEST_CALLS="$calls" CAIRN_TEST_RECOVERY="$recovery" \
|
|
334
|
+
timeout 3 node --experimental-strip-types "$tmp/opencode-compaction-harness.mjs" \
|
|
335
|
+
"$capture" "$wakeup" "$opencode_event_fixture" "$opencode_messages_fixture" "$repo" enabled \
|
|
336
|
+
>"$tmp/opencode-both.out" 2>"$tmp/opencode-both.err"
|
|
337
|
+
[[ -s "$calls" ]] || fail "OpenCode capture did not remain active with both flags"
|
|
338
|
+
|
|
339
|
+
rm -f "$calls"
|
|
340
|
+
CAIRN_ARTIFACT_HTTP=1 CAIRN_TEST_CALLS="$calls" node --experimental-strip-types \
|
|
341
|
+
"$tmp/opencode-compaction-harness.mjs" "$capture" "$wakeup" \
|
|
342
|
+
"$opencode_event_fixture" "$opencode_messages_fixture" "$repo" disabled \
|
|
343
|
+
>"$tmp/opencode-http-only.out" 2>"$tmp/opencode-http-only.err"
|
|
344
|
+
[[ ! -e "$calls" ]] || fail "HTTP consent alone activated OpenCode compaction work"
|
|
345
|
+
|
|
346
|
+
local live="$tmp/opencode-live"
|
|
347
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$live" >/dev/null
|
|
348
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$live" >/dev/null
|
|
349
|
+
[[ $(find "$live" -type f -name 'memory-capture.ts' | wc -l) -eq 1 ]] \
|
|
350
|
+
|| fail "OpenCode compaction capture plugin is duplicated"
|
|
351
|
+
[[ $(find "$live" -type f -name 'memory-wakeup.ts' | wc -l) -eq 1 ]] \
|
|
352
|
+
|| fail "OpenCode compaction recovery plugin is duplicated"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
assert_semantic_parity() {
|
|
356
|
+
node - "$tmp/claude-stale-recovery.out" "$tmp/opencode-system.json" <<'NODE'
|
|
357
|
+
const fs = require("fs")
|
|
358
|
+
const claude = fs.readFileSync(process.argv[2], "utf8")
|
|
359
|
+
const opencode = JSON.parse(fs.readFileSync(process.argv[3], "utf8"))
|
|
360
|
+
const heading = "## Compaction recovery"
|
|
361
|
+
const claudeSection = claude.slice(claude.indexOf(heading)).trim()
|
|
362
|
+
const opencodeSection = opencode.find((value) => String(value).includes(heading))?.trim()
|
|
363
|
+
if (!claudeSection || claudeSection !== opencodeSection) process.exit(1)
|
|
364
|
+
const ordered = [
|
|
365
|
+
"Source:", "Session:", "Revision:", "Captured:", "Age:", "Harness:", "Completeness:",
|
|
366
|
+
"Warning:", "### Task Goals", "### Decisions Made", "### Open TODOs", "### Critical Error Traces",
|
|
367
|
+
]
|
|
368
|
+
let position = -1
|
|
369
|
+
for (const marker of ordered) {
|
|
370
|
+
const next = claudeSection.indexOf(marker)
|
|
371
|
+
if (next <= position) process.exit(2)
|
|
372
|
+
position = next
|
|
373
|
+
}
|
|
374
|
+
if ((claudeSection.match(/\(none captured\)/g) ?? []).length !== 4) process.exit(3)
|
|
375
|
+
if (!/validate it against the current repository/i.test(claudeSection)) process.exit(4)
|
|
376
|
+
NODE
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
artifact_revision_contract() {
|
|
380
|
+
local cli="$ROOT/mcp-memory-server/dist/artifact-cli.js"
|
|
381
|
+
local claude_repo="$tmp/claude-revisions"
|
|
382
|
+
local opencode_repo="$tmp/opencode-revisions"
|
|
383
|
+
local real_hook="$tmp/real-compaction-capture.sh"
|
|
384
|
+
local fake_bin="$tmp/claude-revisions-bin"
|
|
385
|
+
mkdir -p "$claude_repo" "$opencode_repo"
|
|
386
|
+
write_fake_claude "$fake_bin" "2.1.219"
|
|
387
|
+
render "$ROOT/claude/hooks/compaction-capture.sh" "$real_hook"
|
|
388
|
+
chmod +x "$real_hook"
|
|
389
|
+
|
|
390
|
+
node - "$claude_fixture" "$tmp/claude-second.json" <<'NODE'
|
|
391
|
+
const fs = require("fs")
|
|
392
|
+
const fixture = JSON.parse(fs.readFileSync(process.argv[2], "utf8"))
|
|
393
|
+
fixture.compact_summary += "\n- TODO: Validate parity after the second revision."
|
|
394
|
+
fs.writeFileSync(process.argv[3], JSON.stringify(fixture))
|
|
395
|
+
NODE
|
|
396
|
+
(cd "$claude_repo" && PATH="$fake_bin:$PATH" CAIRN_COMPACTION_CAPTURE=1 "$real_hook" < "$claude_fixture")
|
|
397
|
+
(cd "$claude_repo" && PATH="$fake_bin:$PATH" CAIRN_COMPACTION_CAPTURE=1 "$real_hook" < "$tmp/claude-second.json")
|
|
398
|
+
|
|
399
|
+
node - "$opencode_event_fixture" "$opencode_messages_fixture" \
|
|
400
|
+
"$tmp/opencode-first.json" "$tmp/opencode-second.json" <<'NODE'
|
|
401
|
+
const fs = require("fs")
|
|
402
|
+
const event = JSON.parse(fs.readFileSync(process.argv[2], "utf8"))
|
|
403
|
+
const envelope = JSON.parse(fs.readFileSync(process.argv[3], "utf8"))
|
|
404
|
+
const first = { event, session: envelope.session, messages: envelope.messages, harness_version: "1.17.20" }
|
|
405
|
+
const second = structuredClone(first)
|
|
406
|
+
const summary = second.messages.find((message) => message.info?.id === "oc-summary-valid-new")
|
|
407
|
+
const text = summary?.parts?.find((part) => part.type === "text" && part.ignored !== true)
|
|
408
|
+
if (!text) process.exit(1)
|
|
409
|
+
text.text += "\n- TODO: Validate parity after the second revision."
|
|
410
|
+
fs.writeFileSync(process.argv[4], JSON.stringify(first))
|
|
411
|
+
fs.writeFileSync(process.argv[5], JSON.stringify(second))
|
|
412
|
+
NODE
|
|
413
|
+
node "$cli" capture-opencode "$opencode_repo" < "$tmp/opencode-first.json"
|
|
414
|
+
node "$cli" capture-opencode "$opencode_repo" < "$tmp/opencode-second.json"
|
|
415
|
+
|
|
416
|
+
node - "$cli" "$claude_repo" "$opencode_repo" <<'NODE'
|
|
417
|
+
const fs = require("fs")
|
|
418
|
+
const { spawnSync } = require("child_process")
|
|
419
|
+
const [cli, claudeRepo, opencodeRepo] = process.argv.slice(2)
|
|
420
|
+
const cases = [
|
|
421
|
+
{ repo: claudeRepo, session: "claude-code:claude-compaction-session-001", secrets: ["claude-compaction-secret-001", "sk-claude-compaction-secret-002"] },
|
|
422
|
+
{ repo: opencodeRepo, session: "opencode:opencode-compaction-session-001", secrets: ["opencode-compaction-secret-001", "sk-opencode-compaction-secret-002"] },
|
|
423
|
+
]
|
|
424
|
+
const run = (repo, args) => {
|
|
425
|
+
const result = spawnSync(process.execPath, [cli, ...args], { cwd: repo, encoding: "utf8" })
|
|
426
|
+
if (result.status !== 0 || result.stderr) process.exit(1)
|
|
427
|
+
return result.stdout
|
|
428
|
+
}
|
|
429
|
+
for (const value of cases) {
|
|
430
|
+
const listedOutput = run(value.repo, ["list", "--kind", "compaction_summary", "--json"])
|
|
431
|
+
const listed = JSON.parse(listedOutput)
|
|
432
|
+
if (listed.artifacts.length !== 2) process.exit(2)
|
|
433
|
+
const shown = listed.artifacts.map((artifact) => JSON.parse(run(value.repo, ["show", artifact.artifact_id, "--json"])))
|
|
434
|
+
if (shown.some((artifact) => artifact.session_ref !== value.session)) process.exit(3)
|
|
435
|
+
if (shown.map((artifact) => artifact.content.revision).sort().join(",") !== "1,2") process.exit(4)
|
|
436
|
+
if (shown.some((artifact) => typeof artifact.content.raw_summary !== "string")) process.exit(5)
|
|
437
|
+
const current = run(value.repo, ["recover", value.repo, "--session-ref", value.session])
|
|
438
|
+
const fallback = run(value.repo, ["recover", value.repo, "--session-ref", `${value.session}-fresh`])
|
|
439
|
+
if (!current.includes("Source: current_session") || !current.includes("Revision: 2")) process.exit(6)
|
|
440
|
+
if (!fallback.includes("Source: project_fallback") || !fallback.includes("Revision: 2")) process.exit(7)
|
|
441
|
+
const automatic = `${listedOutput}\n${current}\n${fallback}`
|
|
442
|
+
if (/raw_summary|compact_summary/i.test(automatic)) process.exit(8)
|
|
443
|
+
const storeBytes = ["artifacts.db", "artifacts.db-wal", "artifacts.db-shm"]
|
|
444
|
+
.flatMap((name) => {
|
|
445
|
+
const path = `${value.repo}/.agentfs/${name}`
|
|
446
|
+
return fs.existsSync(path) ? [fs.readFileSync(path).toString("latin1")] : []
|
|
447
|
+
}).join("\n")
|
|
448
|
+
for (const secret of value.secrets) {
|
|
449
|
+
if (storeBytes.includes(secret) || automatic.includes(secret) || JSON.stringify(shown).includes(secret)) process.exit(9)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
NODE
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
run_claude_contract() {
|
|
456
|
+
require_managed_sources || return $?
|
|
457
|
+
claude_enabled_contract
|
|
458
|
+
claude_sync_contract
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
run_opencode_contract() {
|
|
462
|
+
require_managed_sources || return $?
|
|
463
|
+
opencode_contract
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
run_existing_baseline
|
|
467
|
+
validate_fixtures
|
|
468
|
+
|
|
469
|
+
if [[ "$MODE" == "--expect-red" ]]; then
|
|
470
|
+
set +e
|
|
471
|
+
require_managed_sources
|
|
472
|
+
status=$?
|
|
473
|
+
set -e
|
|
474
|
+
if [[ "$status" -eq "$MISSING_CAPABILITY" ]]; then
|
|
475
|
+
echo "$RED_MARKER"
|
|
476
|
+
exit "$EXPECTED_RED_EXIT"
|
|
477
|
+
fi
|
|
478
|
+
[[ "$status" -eq 0 ]] || exit "$status"
|
|
479
|
+
echo "FAIL: compaction hook/recovery integration unexpectedly exists; run a GREEN mode" >&2
|
|
480
|
+
exit 1
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
case "$MODE" in
|
|
484
|
+
--claude-only) run_claude_contract ;;
|
|
485
|
+
--opencode-only) run_opencode_contract ;;
|
|
486
|
+
""|--full)
|
|
487
|
+
node "$ROOT/mcp-memory-server/scripts/smoke-compaction-capture.mjs" >/dev/null
|
|
488
|
+
run_claude_contract
|
|
489
|
+
run_opencode_contract
|
|
490
|
+
assert_semantic_parity
|
|
491
|
+
artifact_revision_contract
|
|
492
|
+
;;
|
|
493
|
+
esac
|
|
494
|
+
|
|
495
|
+
echo "PASS: compaction hook capture, recovery, disabled paths and sync contract"
|
|
@@ -11,9 +11,86 @@ for shell in bash zsh fish; do
|
|
|
11
11
|
grep -q 'completion' "$tmp/$shell"
|
|
12
12
|
done
|
|
13
13
|
|
|
14
|
+
capability_ids='memory.write memory.search notes.distill wiki graph security.audit route.check context.explore'
|
|
15
|
+
eval_commands='validate run ablate report prune delete'
|
|
16
|
+
eval_flags='--task-set --adapter --output --repetitions --seed --json --yes --disable --experiment --older-than-days --dry-run'
|
|
17
|
+
for shell in bash zsh fish; do
|
|
18
|
+
grep -q 'capabilities' "$tmp/$shell"
|
|
19
|
+
grep -q 'list.*status.*enable.*disable.*reset.*logging\|list status enable disable reset logging' "$tmp/$shell"
|
|
20
|
+
grep -q -- '--json\|-l json' "$tmp/$shell"
|
|
21
|
+
for capability_id in $capability_ids; do
|
|
22
|
+
grep -qF "$capability_id" "$tmp/$shell"
|
|
23
|
+
done
|
|
24
|
+
if grep -Eq '(^|[[:space:]"'"'"'])(guard|start|finish)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
|
|
25
|
+
echo "completion exposed a private capability operation for $shell" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
grep -q 'eval' "$tmp/$shell"
|
|
29
|
+
grep -q 'validate.*run.*ablate.*report.*prune.*delete\|validate run ablate report prune delete' "$tmp/$shell"
|
|
30
|
+
for eval_flag in $eval_flags; do
|
|
31
|
+
grep -q -- "$eval_flag\|-l ${eval_flag#--}" "$tmp/$shell"
|
|
32
|
+
done
|
|
33
|
+
for capability_id in $capability_ids; do
|
|
34
|
+
grep -qF "$capability_id" "$tmp/$shell"
|
|
35
|
+
done
|
|
36
|
+
if grep -Eq '(^|[[:space:]"'"'"'])(doctor-diagnosis|guard|start|finish)([[:space:]"'"'"']|$)' "$tmp/$shell"; then
|
|
37
|
+
echo "completion exposed a private eval lifecycle operation for $shell" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
"$ROOT/bin/cairn" help >"$tmp/root-help"
|
|
43
|
+
grep -qF 'cairn eval <validate|run|ablate|report|prune|delete>' "$tmp/root-help"
|
|
44
|
+
node "$ROOT/mcp-memory-server/dist/eval-cli.js" --help >"$tmp/eval-help"
|
|
45
|
+
for command in $eval_commands; do
|
|
46
|
+
grep -q "cairn eval $command" "$tmp/eval-help"
|
|
47
|
+
done
|
|
48
|
+
for eval_flag in $eval_flags; do
|
|
49
|
+
grep -q -- "$eval_flag" "$tmp/eval-help"
|
|
50
|
+
done
|
|
51
|
+
if grep -q 'doctor-diagnosis' "$tmp/eval-help"; then
|
|
52
|
+
echo 'eval help exposed a private lifecycle operation' >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
node - "$ROOT/mcp-memory-server/src/capability-registry.ts" "$tmp/bash" "$tmp/zsh" "$tmp/fish" <<'NODE'
|
|
57
|
+
const fs = require("fs");
|
|
58
|
+
const [registryPath, ...completionPaths] = process.argv.slice(2);
|
|
59
|
+
const registry = fs.readFileSync(registryPath, "utf8");
|
|
60
|
+
const ids = [...registry.matchAll(/^\s*id: "([^"]+)",$/gm)].map((match) => match[1]);
|
|
61
|
+
const expected = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
|
|
62
|
+
if (JSON.stringify(ids) !== JSON.stringify(expected)) process.exit(1);
|
|
63
|
+
for (const path of completionPaths) {
|
|
64
|
+
const output = fs.readFileSync(path, "utf8");
|
|
65
|
+
if (!output.includes(ids.join(" "))) process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
NODE
|
|
68
|
+
|
|
14
69
|
grep -q 'complete -F _cairn_complete cairn' "$tmp/bash"
|
|
15
70
|
grep -q '#compdef cairn' "$tmp/zsh"
|
|
16
71
|
grep -q 'complete -c cairn' "$tmp/fish"
|
|
72
|
+
for shell in bash zsh; do
|
|
73
|
+
grep -q 'sync-pi' "$tmp/$shell"
|
|
74
|
+
grep -q -- '--live-root' "$tmp/$shell"
|
|
75
|
+
done
|
|
76
|
+
grep -q 'sync-pi' "$tmp/fish"
|
|
77
|
+
grep -q -- '-l live-root' "$tmp/fish"
|
|
78
|
+
for shell in bash zsh fish; do
|
|
79
|
+
grep -q 'notes' "$tmp/$shell"
|
|
80
|
+
grep -q 'distill.*search-error.*promote.*doctor\|distill search-error promote doctor' "$tmp/$shell"
|
|
81
|
+
grep -q 'artifact' "$tmp/$shell"
|
|
82
|
+
grep -q 'list.*show.*delete.*prune' "$tmp/$shell"
|
|
83
|
+
grep -q -- '--json\|-l json' "$tmp/$shell"
|
|
84
|
+
grep -q -- '--dry-run\|-l dry-run' "$tmp/$shell"
|
|
85
|
+
grep -q -- '--include-protected\|-l include-protected' "$tmp/$shell"
|
|
86
|
+
done
|
|
87
|
+
|
|
88
|
+
grep -q -- '--kind' "$tmp/bash"
|
|
89
|
+
grep -q -- '--session' "$tmp/bash"
|
|
90
|
+
grep -q -- '--kind' "$tmp/zsh"
|
|
91
|
+
grep -q -- '--session' "$tmp/zsh"
|
|
92
|
+
grep -q -- '-l kind' "$tmp/fish"
|
|
93
|
+
grep -q -- '-l session' "$tmp/fish"
|
|
17
94
|
|
|
18
95
|
if "$ROOT/bin/cairn" completion unsupported >/dev/null 2>&1; then
|
|
19
96
|
echo "completion accepted an unsupported shell" >&2
|