@cairnkeep/cli 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/AGENTS.md +45 -0
  2. package/CONTRIBUTING.md +49 -0
  3. package/LICENSE +201 -0
  4. package/README.md +149 -0
  5. package/SECURITY.md +37 -0
  6. package/bin/cairn +73 -0
  7. package/claude/agents/code-reviewer.md +174 -0
  8. package/claude/agents/security-investigator.md +112 -0
  9. package/claude/agents/security-target-selector.md +90 -0
  10. package/claude/agents/security-validator.md +97 -0
  11. package/claude/agents/wiki-ingester.md +108 -0
  12. package/claude/agents/wiki-lint-auditor.md +94 -0
  13. package/claude/agents/wiki-query-analyst.md +84 -0
  14. package/claude/commands/context-explore.md +40 -0
  15. package/claude/commands/graphify.md +150 -0
  16. package/claude/commands/memory-review.md +85 -0
  17. package/claude/commands/memory-sync.md +71 -0
  18. package/claude/commands/recall.md +32 -0
  19. package/claude/commands/remember.md +43 -0
  20. package/claude/commands/repo-review.md +191 -0
  21. package/claude/commands/security-audit.md +174 -0
  22. package/claude/commands/wiki-ingest.md +112 -0
  23. package/claude/commands/wiki-lint.md +86 -0
  24. package/claude/commands/wiki-query.md +92 -0
  25. package/claude/hooks/context-explore-pretask.sh +93 -0
  26. package/claude/hooks/memory-capture.sh +59 -0
  27. package/claude/hooks/memory-recall.sh +84 -0
  28. package/claude/hooks/memory-wakeup.sh +45 -0
  29. package/docs/building-an-overlay.md +117 -0
  30. package/docs/domain-knowledge.md +86 -0
  31. package/docs/git-providers.md +36 -0
  32. package/docs/operating.md +366 -0
  33. package/mcp-memory-server/dist/embeddings.js +122 -0
  34. package/mcp-memory-server/dist/explore-cache.js +97 -0
  35. package/mcp-memory-server/dist/index.js +1256 -0
  36. package/mcp-memory-server/package.json +36 -0
  37. package/opencode/agents/code-reviewer.md +173 -0
  38. package/opencode/agents/security-investigator.md +111 -0
  39. package/opencode/agents/security-target-selector.md +89 -0
  40. package/opencode/agents/security-validator.md +96 -0
  41. package/opencode/agents/wiki-ingester.md +107 -0
  42. package/opencode/agents/wiki-lint-auditor.md +93 -0
  43. package/opencode/agents/wiki-query-analyst.md +83 -0
  44. package/opencode/command/code-review.md +199 -0
  45. package/opencode/command/context-explore.md +42 -0
  46. package/opencode/command/graphify.md +153 -0
  47. package/opencode/command/memory-review.md +44 -0
  48. package/opencode/command/memory-sync.md +94 -0
  49. package/opencode/command/recall.md +38 -0
  50. package/opencode/command/remember.md +46 -0
  51. package/opencode/command/security-audit.md +80 -0
  52. package/opencode/command/wiki-ingest.md +55 -0
  53. package/opencode/command/wiki-lint.md +51 -0
  54. package/opencode/command/wiki-query.md +50 -0
  55. package/opencode/plugins/memory-capture.ts +188 -0
  56. package/opencode/plugins/memory-recall.ts +132 -0
  57. package/opencode/plugins/memory-wakeup.ts +102 -0
  58. package/opencode/templates/security-finding-register.yaml.template +51 -0
  59. package/opencode/templates/security-issue.md.template +43 -0
  60. package/opencode/templates/security-policy.md.template +29 -0
  61. package/opencode/templates/security-report.md.template +43 -0
  62. package/opencode/templates/wiki-contradictions.md.template +31 -0
  63. package/opencode/templates/wiki-index.md.template +40 -0
  64. package/opencode/templates/wiki-lint-report.md.template +38 -0
  65. package/opencode/templates/wiki-log.md.template +11 -0
  66. package/opencode/templates/wiki-policy.md.template +47 -0
  67. package/opencode/templates/wiki-query-answer.md.template +26 -0
  68. package/opencode/templates/wiki-source-summary.md.template +33 -0
  69. package/opencode/workflows/memory-review-workflow.md +66 -0
  70. package/opencode/workflows/security-audit-workflow.md +197 -0
  71. package/opencode/workflows/wiki-ingest-workflow.md +149 -0
  72. package/opencode/workflows/wiki-lint-workflow.md +128 -0
  73. package/opencode/workflows/wiki-query-workflow.md +122 -0
  74. package/package.json +56 -0
  75. package/scripts/bootstrap.sh +99 -0
  76. package/scripts/doctor.sh +91 -0
  77. package/scripts/install-audit-timer.sh +68 -0
  78. package/scripts/lib/assert-tool-event.mjs +50 -0
  79. package/scripts/memory-store.sh +71 -0
  80. package/scripts/memory-wiki-audit.sh +156 -0
  81. package/scripts/sync-claude-assets.sh +182 -0
  82. package/scripts/sync-opencode-explore-assets.sh +187 -0
  83. package/scripts/sync-opencode-graphify-assets.sh +152 -0
  84. package/scripts/sync-opencode-memory-assets.sh +160 -0
  85. package/scripts/sync-opencode-plugin-assets.sh +163 -0
  86. package/scripts/sync-opencode-security-assets.sh +195 -0
  87. package/scripts/sync-opencode-wiki-assets.sh +250 -0
  88. package/scripts/test-audit-timer.sh +29 -0
  89. package/scripts/test-bootstrap-untracked.sh +41 -0
  90. package/scripts/test-cli-dispatch.sh +21 -0
  91. package/scripts/test-doctor.sh +32 -0
  92. package/scripts/test-launcher-hooks.sh +66 -0
  93. package/scripts/test-memory-store.sh +44 -0
  94. package/scripts/test-remember-recall-assertions.sh +70 -0
  95. package/scripts/transcript-to-text.mjs +53 -0
  96. package/scripts/verify-docs-parity.sh +112 -0
  97. package/scripts/verify-explore-maturation.sh +288 -0
  98. package/scripts/verify-fastcontext-reliability.sh +624 -0
  99. package/scripts/verify-no-private-references.sh +145 -0
  100. package/scripts/verify-opencode-live-parity.sh +864 -0
  101. package/scripts/verify-routing-seam.sh +169 -0
  102. package/scripts/verify-token-savings-ab.sh +557 -0
  103. package/templates/PROJECT-INSTRUCTIONS.md.template +42 -0
  104. package/templates/alignment-gap-register.yaml.template +56 -0
  105. package/templates/alignment-policy.md.template +28 -0
  106. package/templates/alignment-pr-template.md.template +18 -0
  107. package/templates/alignment-report.md.template +56 -0
  108. package/templates/env.example.template +25 -0
  109. package/templates/graph-policy.md.template +37 -0
  110. package/templates/graphs-gitignore.template +3 -0
  111. package/templates/memory-wiki-audit.service.template +6 -0
  112. package/templates/memory-wiki-audit.timer.template +9 -0
  113. package/templates/memory.json.template +4 -0
  114. package/templates/planning-config.json.template +6 -0
  115. package/templates/project-brief.md.template +40 -0
  116. package/templates/security-finding-register.yaml.template +51 -0
  117. package/templates/security-issue.md.template +43 -0
  118. package/templates/security-policy.md.template +29 -0
  119. package/templates/security-report.md.template +43 -0
  120. package/templates/start-claude.sh.template +46 -0
  121. package/templates/start-opencode.sh.template +41 -0
  122. package/templates/structured-change-canvas.md.template +63 -0
  123. package/templates/wiki-contradictions.md.template +31 -0
  124. package/templates/wiki-index.md.template +40 -0
  125. package/templates/wiki-lint-report.md.template +38 -0
  126. package/templates/wiki-log.md.template +11 -0
  127. package/templates/wiki-policy.md.template +47 -0
  128. package/templates/wiki-query-answer.md.template +26 -0
  129. package/templates/wiki-source-summary.md.template +33 -0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Smoke test for `cairn doctor`: unconfigured deps SKIP (exit 0); a configured
3
+ # but unreachable endpoint FAILs (exit non-zero).
4
+ set -uo pipefail
5
+
6
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
7
+ tmp=$(mktemp -d)
8
+ trap 'rm -rf "$tmp"' EXIT
9
+ fail() { echo "FAIL: $1" >&2; exit 1; }
10
+
11
+ doctor="$ROOT/scripts/doctor.sh"
12
+ # Clean the inherited env so the test controls what is "configured".
13
+ unset CAIRN_LLM_API_URL CAIRN_MEMORY_EMBEDDING_URL CAIRN_GIT_PROVIDER CAIRN_AGENTFS_BASE_DIR
14
+
15
+ # 1. Nothing configured → only SKIP/PASS/WARN, exit 0.
16
+ proj="$tmp/clean"; mkdir -p "$proj"
17
+ ( cd "$proj" && "$doctor" ) >"$tmp/out1" 2>&1 || fail "doctor exited non-zero with nothing configured:\n$(cat "$tmp/out1")"
18
+ grep -q "\[SKIP\]" "$tmp/out1" || fail "expected SKIP lines when nothing is configured"
19
+
20
+ # 2. A configured but unreachable endpoint → FAIL + non-zero exit.
21
+ if command -v curl >/dev/null 2>&1; then
22
+ proj2="$tmp/broken"; mkdir -p "$proj2/.ai"
23
+ echo 'CAIRN_LLM_API_URL=http://127.0.0.1:1' > "$proj2/.ai/.env"
24
+ if ( cd "$proj2" && "$doctor" ) >"$tmp/out2" 2>&1; then
25
+ fail "doctor should exit non-zero for an unreachable configured endpoint:\n$(cat "$tmp/out2")"
26
+ fi
27
+ grep -q "\[FAIL\] LLM endpoint unreachable" "$tmp/out2" || fail "expected a FAIL line for the unreachable endpoint"
28
+ else
29
+ echo " (curl absent — skipped the unreachable-endpoint case)"
30
+ fi
31
+
32
+ echo "PASS: cairn doctor (skip unconfigured, fail configured-unreachable)"
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Smoke test for the launcher wrapper seams: .ai/pre-launch.sh (source + abort),
3
+ # CAIRN_EXTRA_SETTINGS layering, and .ai/post-exit.sh.
4
+ set -euo pipefail
5
+
6
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
7
+ tmp=$(mktemp -d)
8
+ trap 'rm -rf "$tmp"' EXIT
9
+
10
+ fail() { echo "FAIL: $1" >&2; exit 1; }
11
+
12
+ # A git repo scaffolded with the real templates.
13
+ repo="$tmp/repo"; mkdir "$repo"; git -C "$repo" init -q
14
+ "$ROOT/scripts/bootstrap.sh" "$repo" >/dev/null
15
+ launcher="$repo/.ai/start-claude.sh"
16
+ [[ -x "$launcher" ]] || fail "launcher not scaffolded"
17
+
18
+ # Fake `claude` on PATH: records its args + a marker env var, exits FAKE_EXIT.
19
+ mkdir "$tmp/bin"
20
+ cat > "$tmp/bin/claude" <<'FAKE'
21
+ #!/usr/bin/env bash
22
+ { echo "args:$*"; echo "prelaunch:${PRELAUNCH_RAN:-0}"; } > "$CLAUDE_LOG"
23
+ exit "${FAKE_EXIT:-0}"
24
+ FAKE
25
+ chmod +x "$tmp/bin/claude"
26
+ export PATH="$tmp/bin:$PATH"
27
+ export CLAUDE_LOG="$tmp/claude.log"
28
+
29
+ # 1. No hooks: launcher execs claude with the passed args (baseline unchanged).
30
+ rm -f "$CLAUDE_LOG"
31
+ "$launcher" --foo bar >/dev/null 2>&1 || fail "baseline launch failed"
32
+ grep -qx "args:--foo bar" "$CLAUDE_LOG" || fail "baseline did not pass args through"
33
+ grep -qx "prelaunch:0" "$CLAUDE_LOG" || fail "prelaunch marker leaked without a hook"
34
+
35
+ # 2. pre-launch.sh is sourced (can export env) + CAIRN_EXTRA_SETTINGS layers in.
36
+ rm -f "$CLAUDE_LOG"
37
+ cat > "$repo/.ai/pre-launch.sh" <<EOF
38
+ export PRELAUNCH_RAN=1
39
+ export CAIRN_EXTRA_SETTINGS="$tmp/settings.json"
40
+ EOF
41
+ echo '{}' > "$tmp/settings.json"
42
+ "$launcher" >/dev/null 2>&1 || fail "launch with pre-launch failed"
43
+ grep -qx "prelaunch:1" "$CLAUDE_LOG" || fail "pre-launch env not visible to harness"
44
+ grep -q -- "--settings $tmp/settings.json" "$CLAUDE_LOG" || fail "CAIRN_EXTRA_SETTINGS not passed as --settings"
45
+
46
+ # 3. pre-launch.sh non-zero aborts the launch before the harness runs.
47
+ rm -f "$CLAUDE_LOG"
48
+ cat > "$repo/.ai/pre-launch.sh" <<'EOF'
49
+ echo "pre-launch says no" >&2
50
+ return 1
51
+ EOF
52
+ if "$launcher" >/dev/null 2>&1; then fail "non-zero pre-launch should abort the launch"; fi
53
+ [[ ! -f "$CLAUDE_LOG" ]] || fail "harness ran despite pre-launch abort"
54
+ rm -f "$repo/.ai/pre-launch.sh"
55
+
56
+ # 4. post-exit.sh runs after the harness with $CAIRN_EXIT_STATUS.
57
+ rm -f "$CLAUDE_LOG" "$tmp/post.log"
58
+ cat > "$repo/.ai/post-exit.sh" <<EOF
59
+ echo "post:\${CAIRN_EXIT_STATUS}" > "$tmp/post.log"
60
+ EOF
61
+ FAKE_EXIT=7 "$launcher" >/dev/null 2>&1 && fail "launcher should propagate harness exit code"
62
+ status=$?
63
+ [[ "$status" -eq 7 ]] || fail "expected exit 7 from launcher, got $status"
64
+ grep -qx "post:7" "$tmp/post.log" || fail "post-exit hook did not see CAIRN_EXIT_STATUS"
65
+
66
+ echo "PASS: launcher hook seams (pre-launch source/abort, settings layering, post-exit)"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # Round-trip test for `cairn memory export/import/path`.
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
+ mem="$ROOT/scripts/memory-store.sh"
11
+
12
+ # path prints the resolved base dir.
13
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store" "$mem" path | grep -qx "$tmp/store" \
14
+ || fail "path did not print the resolved base dir"
15
+
16
+ if ! command -v sqlite3 >/dev/null 2>&1; then
17
+ echo " (sqlite3 absent — skipped export/import round-trip)"
18
+ echo "PASS: cairn memory path (export/import skipped)"
19
+ exit 0
20
+ fi
21
+
22
+ # Seed a store with two scopes and known rows.
23
+ mkdir -p "$tmp/store"
24
+ sqlite3 "$tmp/store/identity.db" "CREATE TABLE m(k TEXT, v TEXT); INSERT INTO m VALUES('alpha','one');"
25
+ sqlite3 "$tmp/store/work-memory.db" "CREATE TABLE m(k TEXT, v TEXT); INSERT INTO m VALUES('beta','two');"
26
+
27
+ # Export from store A.
28
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store" "$mem" export "$tmp/export.tgz" >/dev/null
29
+ [[ -f "$tmp/export.tgz" ]] || fail "export archive not created"
30
+
31
+ # Import into a fresh store B and verify both scopes + rows survived.
32
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store2" "$mem" import "$tmp/export.tgz" >/dev/null
33
+ [[ -f "$tmp/store2/identity.db" ]] || fail "identity.db missing after import"
34
+ [[ -f "$tmp/store2/work-memory.db" ]] || fail "work-memory.db missing after import"
35
+ got=$(sqlite3 "$tmp/store2/identity.db" "SELECT v FROM m WHERE k='alpha';")
36
+ [[ "$got" == "one" ]] || fail "identity row not preserved (got '$got')"
37
+ got=$(sqlite3 "$tmp/store2/work-memory.db" "SELECT v FROM m WHERE k='beta';")
38
+ [[ "$got" == "two" ]] || fail "work-memory row not preserved (got '$got')"
39
+
40
+ # Import over an existing store backs up the prior file.
41
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store2" "$mem" import "$tmp/export.tgz" >/dev/null
42
+ [[ -f "$tmp/store2/identity.db.bak-pre-import" ]] || fail "existing store not backed up on re-import"
43
+
44
+ echo "PASS: cairn memory export/import round-trip + path"
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Offline fixture test for scripts/lib/assert-tool-event.mjs (D-08/D-09,
5
+ # T-13-01). Proves the NDJSON tool-event matcher distinguishes a genuine
6
+ # tool_use event from a narrated-but-unexecuted text-event mention, with
7
+ # and without canary linkage, and that it never crashes on malformed/empty
8
+ # input. Fixtures are the verbatim live-captured NDJSON lines from
9
+ # 13-RESEARCH.md's Code Examples section, embedded inline (single-quoted,
10
+ # no shell interpolation) so this test is self-contained and runs offline.
11
+
12
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
13
+ MATCHER="$ROOT_DIR/scripts/lib/assert-tool-event.mjs"
14
+
15
+ # Verbatim live capture, 13-RESEARCH.md Pattern 2:
16
+ # `opencode run "remember that the test canary value is XYZ123" --format json --auto`
17
+ WRITE_EVENT='{"type":"tool_use","timestamp":1783468394936,"sessionID":"ses_0c1000285ffeB3LgTc2lL6lpX7","part":{"type":"tool","tool":"cairn-memory_memory_write","callID":"call_00_HwYj7yo6GPefkOwdragE3435","state":{"status":"completed","input":{"scope":"work","key":"test-canary","value":"The test canary value is XYZ123."},"output":"{\n \"ok\": true,\n \"scope\": \"work\",\n \"key\": \"test-canary\",\n \"collisions\": []\n}","metadata":{"truncated":false},"title":"","time":{"start":1783468394919,"end":1783468394935}},"id":"prt_f3f000825001ZCly3HCQBHAmWC","sessionID":"ses_0c1000285ffeB3LgTc2lL6lpX7","messageID":"msg_f3efffdec001R3DoelYr4jBzyz"}}'
18
+
19
+ # Verbatim live capture, 13-RESEARCH.md Pattern 2:
20
+ # `opencode run "search your memory for the test canary value..." --format json --auto`
21
+ # Its part.state.output (a JSON-encoded string) contains the literal substring
22
+ # "XYZ123" — the D-09 canary-linkage fixture.
23
+ SEARCH_EVENT='{"type":"tool_use","timestamp":1783468410962,"sessionID":"ses_0c0ffc42fffean1LIPrMRiv0Gj","part":{"type":"tool","tool":"cairn-memory_memory_search","callID":"call_00_ojFUlRfeg7RdkhxkZudF4674","state":{"status":"completed","input":{"scope":"work","query":"test canary value"},"output":"{\n \"mode\": \"substring\",\n \"count\": 1,\n \"results\": [\n {\n \"scope\": \"work\",\n \"key\": \"test-canary\",\n \"value\": \"The test canary value is XYZ123.\",\n \"score\": 1\n }\n ]\n}","metadata":{"truncated":false},"title":"","time":{"start":1783468410951,"end":1783468410961}},"id":"prt_f3f004761001QGw1GlWtp0nfgx","sessionID":"ses_0c0ffc42fffean1LIPrMRiv0Gj","messageID":"msg_f3f003c45001tYJ94eWqStsR7W"}}'
24
+
25
+ # Synthesized (not live-captured): a "text" event whose part.text merely
26
+ # narrates the write tool name in prose. Top-level type is "text", not
27
+ # "tool_use" — must NOT match (the D-08 narrated-but-unexecuted case).
28
+ TEXT_NARRATION_EVENT='{"type":"text","timestamp":1783468394000,"sessionID":"ses_test0000000000000000000","part":{"id":"prt_test0000000000000000001","messageID":"msg_test0000000000000000001","sessionID":"ses_test0000000000000000000","type":"text","text":"I will now call cairn-memory_memory_write to remember this fact.","time":{"start":1,"end":2}}}'
29
+
30
+ failures=0
31
+
32
+ # check(name, expected_exit, input, env_assignments...)
33
+ check() {
34
+ local name="$1" expected_exit="$2" input="$3"
35
+ shift 3
36
+ local actual_exit=0
37
+ printf '%s' "$input" | env "$@" node "$MATCHER" >/dev/null 2>&1 || actual_exit=$?
38
+ if [[ "$actual_exit" -eq "$expected_exit" ]]; then
39
+ echo "[test-remember-recall-assertions] PASS: $name"
40
+ else
41
+ echo "[test-remember-recall-assertions] FAIL: $name (expected exit $expected_exit, got $actual_exit)" >&2
42
+ failures=1
43
+ fi
44
+ }
45
+
46
+ check "genuine write tool_use event, no canary required -> exit 0" \
47
+ 0 "$WRITE_EVENT" \
48
+ TOOL_EVENT_REGEX='cairn-memory_memory_(write|supersede)'
49
+
50
+ check "genuine search tool_use event with matching canary -> exit 0" \
51
+ 0 "$SEARCH_EVENT" \
52
+ TOOL_EVENT_REGEX='cairn-memory_memory_(search|read)' TOOL_EVENT_CANARY='XYZ123'
53
+
54
+ check "genuine search tool_use event with a canary absent from output -> exit 1" \
55
+ 1 "$SEARCH_EVENT" \
56
+ TOOL_EVENT_REGEX='cairn-memory_memory_(search|read)' TOOL_EVENT_CANARY='CANARY-NOT-PRESENT-ANYWHERE'
57
+
58
+ check "narrated-only text event (D-08 false-positive class) -> exit 1" \
59
+ 1 "$TEXT_NARRATION_EVENT" \
60
+ TOOL_EVENT_REGEX='cairn-memory_memory_(write|supersede)'
61
+
62
+ check "empty stdin -> exit 1" \
63
+ 1 "" \
64
+ TOOL_EVENT_REGEX='cairn-memory_memory_(write|supersede)'
65
+
66
+ if [[ "$failures" -ne 0 ]]; then
67
+ echo "[test-remember-recall-assertions] FAIL: one or more fixture cases failed" >&2
68
+ exit 1
69
+ fi
70
+ echo "[test-remember-recall-assertions] PASS: all 5 fixture cases passed"
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ // transcript-to-text.mjs — read a Claude/OpenCode JSONL transcript and emit a
3
+ // readable plain-text stream of the conversation (user + assistant text only,
4
+ // tool calls/results trimmed). Piped into `mcp-memory-server extract` so the
5
+ // extraction model sees what actually happened in the session.
6
+ //
7
+ // Usage: node transcript-to-text.mjs <transcript.jsonl> [max-chars]
8
+ // max-chars defaults to 12000; keeps the most recent turns when truncating.
9
+ 'use strict';
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ const [, , transcriptPath, maxCharsArg] = process.argv;
13
+ const maxChars = parseInt(maxCharsArg ?? '12000', 10);
14
+
15
+ if (!transcriptPath) {
16
+ process.stderr.write('transcript-to-text: no transcript path given\n');
17
+ process.exit(0);
18
+ }
19
+
20
+ let raw;
21
+ try {
22
+ raw = readFileSync(transcriptPath, 'utf8');
23
+ } catch {
24
+ process.exit(0);
25
+ }
26
+
27
+ const lines = raw.split('\n').filter(Boolean);
28
+ const turns = [];
29
+ for (const line of lines) {
30
+ let obj;
31
+ try { obj = JSON.parse(line); } catch { continue; }
32
+ const role = obj.type || obj.role || obj.message?.role;
33
+ // Extract text content from common transcript shapes.
34
+ let text = '';
35
+ const content = obj.message?.content ?? obj.content;
36
+ if (typeof content === 'string') {
37
+ text = content;
38
+ } else if (Array.isArray(content)) {
39
+ text = content
40
+ .filter((b) => typeof b === 'object' && b && (b.type === 'text' || typeof b.text === 'string'))
41
+ .map((b) => b.text ?? '')
42
+ .join('\n');
43
+ }
44
+ text = text.trim();
45
+ if (!text) continue;
46
+ // Skip pure tool-result noise and system reminders.
47
+ if (role === 'user' || role === 'human') turns.push(`USER: ${text}`);
48
+ else if (role === 'assistant' || role === 'ai') turns.push(`ASSISTANT: ${text}`);
49
+ }
50
+
51
+ let out = turns.join('\n\n');
52
+ if (out.length > maxChars) out = out.slice(-maxChars);
53
+ process.stdout.write(out);
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Docs-vs-code parity checker (SC-02; D-10). Two one-directional checks --
5
+ # code surface must be documented; doc-only names are allowed (e.g.
6
+ # CAIRN_GIT_PROVIDER is read by the CLI/provider layer, not the server, and
7
+ # script-local vars like CAIRN_GUARD_DENYLIST/CAIRN_ROUTE_BINARY are never
8
+ # server config -- neither is a failure):
9
+ #
10
+ # 1. Env-key check: every CAIRN_[A-Z_]+/MCP_HTTP_[A-Z_]+ token read in
11
+ # mcp-memory-server/src/*.ts (the runtime server config surface --
12
+ # NOT scripts/ or bin/) must be named somewhere in docs/operating.md
13
+ # or README.md. `comm -23` on two sorted lists -- code-keys-not-in-
14
+ # docs -- not a symmetric diff.
15
+ # 2. Command check: every claude/commands/*.md basename must appear in
16
+ # docs/operating.md.
17
+ #
18
+ # Reports both check results even when the first fails, so a single run
19
+ # surfaces the full drift set. Exits non-zero on any drift, 0 on none.
20
+
21
+ usage() {
22
+ cat <<'EOF'
23
+ Usage: verify-docs-parity.sh
24
+ verify-docs-parity.sh -h|--help
25
+
26
+ Checks that every CAIRN_*/MCP_HTTP_* env key read by the cairn-memory MCP
27
+ server (mcp-memory-server/src/*.ts) is named in docs/operating.md or
28
+ README.md, and that every claude/commands/*.md command is named in
29
+ docs/operating.md. One-directional: doc-only names are not a failure.
30
+ Prints every missing key/command by name, then exits non-zero on any
31
+ drift, or 0 if the docs are fully in sync with the shipped code.
32
+ EOF
33
+ }
34
+
35
+ ENV_KEY_PATTERN='(CAIRN_[A-Z_]+|MCP_HTTP_[A-Z_]+)'
36
+
37
+ # check_env_keys(): comm -23 of sorted code-keys vs sorted doc-keys --
38
+ # code-keys-not-in-docs only (one-directional, per D-10).
39
+ check_env_keys() {
40
+ local code_keys doc_keys missing
41
+
42
+ code_keys=$(grep -ohE "\\b${ENV_KEY_PATTERN}\\b" mcp-memory-server/src/*.ts | sort -u)
43
+ doc_keys=$(grep -ohE "\\b${ENV_KEY_PATTERN}\\b" docs/operating.md README.md | sort -u)
44
+
45
+ missing=$(comm -23 <(printf '%s\n' "$code_keys") <(printf '%s\n' "$doc_keys"))
46
+
47
+ if [[ -n "$missing" ]]; then
48
+ echo "FATAL: env keys read in mcp-memory-server/src but undocumented:" >&2
49
+ while IFS= read -r key; do
50
+ [[ -n "$key" ]] && echo " - $key" >&2
51
+ done <<<"$missing"
52
+ return 1
53
+ fi
54
+
55
+ echo "[env-keys] OK: every mcp-memory-server/src env key is named in docs/operating.md or README.md"
56
+ return 0
57
+ }
58
+
59
+ # check_commands(): every claude/commands/*.md basename must appear as a
60
+ # documented command reference in docs/operating.md -- either backtick-
61
+ # quoted (the "N commands" enumeration, e.g. `graphify`) or slash-prefixed
62
+ # (the workflow section, e.g. `/remember <fact>`). A bare substring match
63
+ # is too loose: "context-explore" appears in docs/operating.md today only
64
+ # inside an unrelated sync-script filename comment, not as a documented
65
+ # command -- that incidental mention must not count as "documented".
66
+ check_commands() {
67
+ local failed=0
68
+ local file name
69
+
70
+ for file in claude/commands/*.md; do
71
+ name=$(basename "$file" .md)
72
+ if ! grep -qF "\`${name}\`" docs/operating.md && ! grep -qF "/${name}" docs/operating.md; then
73
+ echo "FATAL: command '$name' (claude/commands/${name}.md) is not named in docs/operating.md" >&2
74
+ failed=1
75
+ fi
76
+ done
77
+
78
+ if [[ "$failed" -ne 0 ]]; then
79
+ return 1
80
+ fi
81
+
82
+ echo "[commands] OK: every claude/commands/*.md command is named in docs/operating.md"
83
+ return 0
84
+ }
85
+
86
+ main() {
87
+ while [[ $# -gt 0 ]]; do
88
+ case "$1" in
89
+ -h|--help)
90
+ usage
91
+ exit 0
92
+ ;;
93
+ *)
94
+ usage >&2
95
+ exit 2
96
+ ;;
97
+ esac
98
+ done
99
+
100
+ local failed=0
101
+ check_env_keys || failed=1
102
+ check_commands || failed=1
103
+
104
+ if [[ "$failed" -ne 0 ]]; then
105
+ echo "FATAL: docs-parity check found drift (see above) -- SC-02 not yet satisfied" >&2
106
+ exit 1
107
+ fi
108
+
109
+ echo "[parity] OK: docs match shipped code -- no drift found"
110
+ }
111
+
112
+ main "$@"
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ # Phase 12 composed end-to-end proof: context_explore maturation
5
+ # (CTX-08 cross-referencing, CTX-09 pre-task hook auto-invoke,
6
+ # CTX-10 result caching).
7
+ #
8
+ # Fully offline (no network, no real token_miser) -- every stage drives the
9
+ # real `explore` CLI subcommand or the real hook script against a fake
10
+ # token_miser fixture binary, so it runs in CI. Mirrors
11
+ # verify-token-savings-ab.sh's wrapper/logging-binary technique for proving
12
+ # "the binary was NOT invoked" on a cache hit, and reuses Plan 01/02's
13
+ # smoke-test fixtures (fake-tokenmiser-logging.sh, fake-tokenmiser-crossref.sh)
14
+ # and temp-git-repo + XDG_CACHE_HOME isolation setup.
15
+ #
16
+ # Stages:
17
+ # --crossref CTX-08: a seeded memory/wiki entry produces memory_refs/
18
+ # wiki_refs on the matching citation; the non-matching citation
19
+ # and a fully unseeded repo stay plain (D-02/D-03/D-04).
20
+ # --cache CTX-10: a second identical call returns cached:true and the
21
+ # binary's invocation counter does NOT increment; a repo change
22
+ # (tracked-file edit or new untracked file) forces cached:false
23
+ # and re-invokes the binary.
24
+ # --hook CTX-09: a scripted UserPromptSubmit JSON piped through
25
+ # claude/hooks/context-explore-pretask.sh emits
26
+ # hookSpecificOutput.additionalContext when double-opted-in
27
+ # with citations found, and stays silent when gated off or the
28
+ # prompt is low-signal.
29
+ # (no stage flag) runs all three stages in sequence.
30
+ #
31
+ # Exits non-zero if any check in any run stage failed.
32
+
33
+ usage() {
34
+ cat <<'EOF'
35
+ Usage: verify-explore-maturation.sh [--crossref|--cache|--hook] [-h|--help]
36
+
37
+ Composed, offline, re-runnable proof of Phase 12's three context_explore
38
+ maturation success criteria: citation cross-referencing (CTX-08), result
39
+ caching (CTX-10), and pre-task hook auto-invoke (CTX-09). Drives the real
40
+ `explore` CLI subcommand and the real hook script against fake token_miser
41
+ fixture binaries -- no network, no live binary required.
42
+
43
+ Options:
44
+ --crossref Run only the CTX-08 cross-reference stage.
45
+ --cache Run only the CTX-10 cache stage.
46
+ --hook Run only the CTX-09 hook stage.
47
+ (default) Run all three stages.
48
+ -h, --help Show this help text.
49
+
50
+ Prerequisite: mcp-memory-server must already be built
51
+ (`cd mcp-memory-server && npm install && npm run build`).
52
+ EOF
53
+ }
54
+
55
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
56
+ SERVER_DIR="$ROOT_DIR/mcp-memory-server"
57
+ SERVER_ENTRY="$SERVER_DIR/dist/index.js"
58
+ HOOK_SRC="$ROOT_DIR/claude/hooks/context-explore-pretask.sh"
59
+ FIXTURES_DIR="$SERVER_DIR/scripts/fixtures"
60
+
61
+ FAILURES=0
62
+ TMP_PATHS=()
63
+
64
+ cleanup() {
65
+ local p
66
+ for p in "${TMP_PATHS[@]:-}"; do
67
+ [[ -n "$p" ]] && rm -rf "$p"
68
+ done
69
+ }
70
+ trap cleanup EXIT
71
+
72
+ mktmp() {
73
+ local p
74
+ p=$(mktemp -d)
75
+ TMP_PATHS+=("$p")
76
+ printf '%s' "$p"
77
+ }
78
+
79
+ # check(name, ok): ok is a shell "true"/"false" 0/1-style status string.
80
+ check() {
81
+ local name="$1" status="$2"
82
+ if [[ "$status" -eq 0 ]]; then
83
+ echo "ok: $name"
84
+ else
85
+ echo "FAIL: $name" >&2
86
+ FAILURES=$((FAILURES + 1))
87
+ fi
88
+ }
89
+
90
+ require_build() {
91
+ if [[ ! -f "$SERVER_ENTRY" ]]; then
92
+ echo "FATAL: $SERVER_ENTRY not found -- build the server first:" >&2
93
+ echo " cd mcp-memory-server && npm install && npm run build" >&2
94
+ exit 1
95
+ fi
96
+ }
97
+
98
+ git_init_repo() {
99
+ local repo="$1"
100
+ git -C "$repo" init -q
101
+ git -C "$repo" config user.email "verify@example.com"
102
+ git -C "$repo" config user.name "verify"
103
+ echo "one" > "$repo/a.txt"
104
+ git -C "$repo" add a.txt
105
+ git -C "$repo" commit -q -m init
106
+ }
107
+
108
+ # seed_memory_wiki(repoRoot, stem): seeds .agentfs/project.db (via a
109
+ # short-lived node subprocess, so the db is unlocked before any later
110
+ # consumer opens it) and a .planning/wiki/sources page mentioning $stem.
111
+ seed_memory_wiki() {
112
+ local repo="$1" stem="$2"
113
+ mkdir -p "$repo/.agentfs"
114
+ node -e "
115
+ const { AgentFS } = require('$SERVER_DIR/node_modules/agentfs-sdk');
116
+ (async () => {
117
+ const agent = await AgentFS.open({ id: 'project', path: '$repo/.agentfs/project.db' });
118
+ await agent.kv.set('patterns/${stem}-handling', 'notes about ${stem} module behavior');
119
+ await agent.close();
120
+ })();
121
+ " >/dev/null
122
+ mkdir -p "$repo/.planning/wiki/sources"
123
+ printf '# %s notes\n\n- **%s module behavior is documented here.**\n' "$stem" "$stem" \
124
+ > "$repo/.planning/wiki/sources/${stem}-notes.md"
125
+ }
126
+
127
+ # --- Stage: CTX-08 cross-reference ---
128
+ stage_crossref() {
129
+ echo "=== CTX-08: citation cross-referencing ==="
130
+ local fixture="$FIXTURES_DIR/fake-tokenmiser-crossref.sh"
131
+ chmod +x "$fixture"
132
+
133
+ local seeded bare xdg out widget gadget st
134
+ seeded=$(mktmp)
135
+ git_init_repo "$seeded"
136
+ seed_memory_wiki "$seeded" "widget"
137
+ xdg=$(mktmp)
138
+
139
+ # The CLI takes the query as argv[3]; repo_root is resolved from
140
+ # CAIRN_EXPLORE_REPO_ROOT (no --repo-root flag on the CLI subcommand).
141
+ out=$(XDG_CACHE_HOME="$xdg" CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_CACHE=0 \
142
+ CAIRN_EXPLORE_REPO_ROOT="$seeded" node "$SERVER_ENTRY" explore "anything" 2>/dev/null || true)
143
+
144
+ [[ "$(echo "$out" | jq -r '.ok // false')" == "true" ]]; check "seeded run is ok:true" $?
145
+ widget=$(echo "$out" | jq -c '.citations[] | select(.path == "src/widget.rs")')
146
+ gadget=$(echo "$out" | jq -c '.citations[] | select(.path == "src/gadget.rs")')
147
+
148
+ [[ "$(echo "$widget" | jq -r '(.memory_refs // []) | length')" -gt 0 ]]; check "matching citation (src/widget.rs) has non-empty memory_refs" $?
149
+ [[ "$(echo "$widget" | jq -r '(.wiki_refs // []) | length')" -gt 0 ]]; check "matching citation (src/widget.rs) has non-empty wiki_refs" $?
150
+ [[ "$(echo "$gadget" | jq -r 'has("memory_refs")')" == "false" ]]; check "non-matching citation (src/gadget.rs) has no memory_refs" $?
151
+ [[ "$(echo "$gadget" | jq -r 'has("wiki_refs")')" == "false" ]]; check "non-matching citation (src/gadget.rs) has no wiki_refs" $?
152
+
153
+ bare=$(mktmp)
154
+ git_init_repo "$bare"
155
+ out=$(XDG_CACHE_HOME="$xdg" CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_CACHE=0 \
156
+ CAIRN_EXPLORE_REPO_ROOT="$bare" node "$SERVER_ENTRY" explore "anything" 2>/dev/null || true)
157
+ [[ "$(echo "$out" | jq -r '.ok // false')" == "true" ]]; check "bare (no-seed) run is ok:true" $?
158
+ [[ "$(echo "$out" | jq -r '[.citations[] | (has("memory_refs") or has("wiki_refs"))] | any')" == "false" ]]
159
+ check "bare run: no citation has memory_refs/wiki_refs (fail-open, byte-identical output)" $?
160
+ }
161
+
162
+ # --- Stage: CTX-10 cache ---
163
+ stage_cache() {
164
+ echo "=== CTX-10: result cache ==="
165
+ local fixture="$FIXTURES_DIR/fake-tokenmiser-logging.sh"
166
+ chmod +x "$fixture"
167
+
168
+ local repo xdg hitlog
169
+ repo=$(mktmp)
170
+ git_init_repo "$repo"
171
+ xdg=$(mktmp)
172
+ hitlog="$(mktmp)/hits.log"
173
+
174
+ run_explore() {
175
+ XDG_CACHE_HOME="$xdg" CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_REPO_ROOT="$repo" \
176
+ EXPLORE_HIT_LOG="$hitlog" node "$SERVER_ENTRY" explore "find the foo function" 2>/dev/null
177
+ }
178
+ invocation_count() {
179
+ [[ -f "$hitlog" ]] && grep -c . "$hitlog" || echo 0
180
+ }
181
+
182
+ local first second third
183
+ first=$(run_explore)
184
+ [[ "$(echo "$first" | jq -r '.ok // false')" == "true" ]]; check "first call is ok:true" $?
185
+ [[ "$(echo "$first" | jq -r '.cached')" == "false" ]]; check "first call is cached:false (cache miss)" $?
186
+ [[ "$(invocation_count)" -eq 1 ]]; check "first call spawned the binary exactly once" $?
187
+
188
+ second=$(run_explore)
189
+ [[ "$(echo "$second" | jq -r '.cached')" == "true" ]]; check "second identical call is cached:true" $?
190
+ [[ "$(invocation_count)" -eq 1 ]]; check "second identical call did NOT re-spawn the binary" $?
191
+
192
+ echo "two" > "$repo/a.txt"
193
+ third=$(run_explore)
194
+ [[ "$(echo "$third" | jq -r '.cached')" == "false" ]]; check "call after a tracked-file edit is cached:false" $?
195
+ [[ "$(invocation_count)" -eq 2 ]]; check "call after a tracked-file edit re-spawns the binary" $?
196
+
197
+ echo "one" > "$repo/a.txt"
198
+ echo "new" > "$repo/untracked.txt"
199
+ local untracked_call
200
+ untracked_call=$(run_explore)
201
+ [[ "$(echo "$untracked_call" | jq -r '.cached')" == "false" ]]; check "call after a new untracked file is cached:false" $?
202
+ [[ "$(invocation_count)" -eq 3 ]]; check "call after a new untracked file re-spawns the binary" $?
203
+ }
204
+
205
+ # --- Stage: CTX-09 pre-task hook ---
206
+ stage_hook() {
207
+ echo "=== CTX-09: pre-task auto-invoke hook ==="
208
+ local fixture="$FIXTURES_DIR/fake-tokenmiser-crossref.sh"
209
+ chmod +x "$fixture"
210
+
211
+ local rendered repo xdg
212
+ rendered=$(mktmp)/hook.sh
213
+ mkdir -p "$(dirname "$rendered")"
214
+ sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$HOOK_SRC" > "$rendered"
215
+ chmod +x "$rendered"
216
+
217
+ repo=$(mktmp)
218
+ git_init_repo "$repo"
219
+ xdg=$(mktmp)
220
+
221
+ local prompt='where is the widget module implemented in this repo'
222
+ local stdin_json
223
+ stdin_json=$(jq -n --arg prompt "$prompt" '{prompt: $prompt}')
224
+
225
+ local gated_on
226
+ gated_on=$(CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_AUTOINVOKE=1 CAIRN_EXPLORE_CACHE=0 \
227
+ CAIRN_EXPLORE_REPO_ROOT="$repo" XDG_CACHE_HOME="$xdg" \
228
+ bash -c "printf '%s' '$stdin_json' | bash '$rendered'")
229
+ [[ -n "$gated_on" ]] && echo "$gated_on" | jq -e '.hookSpecificOutput.additionalContext | length > 0' >/dev/null 2>&1
230
+ check "gated-on high-signal prompt injects hookSpecificOutput.additionalContext" $?
231
+
232
+ local gated_off
233
+ gated_off=$(CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_CACHE=0 \
234
+ CAIRN_EXPLORE_REPO_ROOT="$repo" XDG_CACHE_HOME="$xdg" \
235
+ bash -c "printf '%s' '$stdin_json' | bash '$rendered'")
236
+ [[ -z "$gated_off" ]]; check "gated-off (CAIRN_EXPLORE_AUTOINVOKE unset) stays silent" $?
237
+
238
+ local low_signal_json low_signal_out
239
+ low_signal_json=$(jq -n '{prompt: "ok"}')
240
+ low_signal_out=$(CAIRN_EXPLORE_BINARY="$fixture" CAIRN_EXPLORE_AUTOINVOKE=1 CAIRN_EXPLORE_CACHE=0 \
241
+ CAIRN_EXPLORE_REPO_ROOT="$repo" XDG_CACHE_HOME="$xdg" \
242
+ bash -c "printf '%s' '$low_signal_json' | bash '$rendered'")
243
+ [[ -z "$low_signal_out" ]]; check "low-signal prompt (bare ack) stays silent even when gated on" $?
244
+ }
245
+
246
+ main() {
247
+ local stage=""
248
+
249
+ while [[ $# -gt 0 ]]; do
250
+ case "$1" in
251
+ --crossref|--cache|--hook)
252
+ stage="$1"
253
+ shift
254
+ ;;
255
+ -h|--help)
256
+ usage
257
+ exit 0
258
+ ;;
259
+ *)
260
+ usage >&2
261
+ exit 2
262
+ ;;
263
+ esac
264
+ done
265
+
266
+ require_build
267
+
268
+ case "$stage" in
269
+ --crossref) stage_crossref ;;
270
+ --cache) stage_cache ;;
271
+ --hook) stage_hook ;;
272
+ "")
273
+ stage_crossref
274
+ stage_cache
275
+ stage_hook
276
+ ;;
277
+ esac
278
+
279
+ if [[ "$FAILURES" -ne 0 ]]; then
280
+ echo "" >&2
281
+ echo "FATAL: $FAILURES check(s) failed" >&2
282
+ exit 1
283
+ fi
284
+ echo ""
285
+ echo "All Phase 12 context_explore maturation checks passed (CTX-08/09/10)."
286
+ }
287
+
288
+ main "$@"