@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.
- package/AGENTS.md +45 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +201 -0
- package/README.md +149 -0
- package/SECURITY.md +37 -0
- package/bin/cairn +73 -0
- package/claude/agents/code-reviewer.md +174 -0
- package/claude/agents/security-investigator.md +112 -0
- package/claude/agents/security-target-selector.md +90 -0
- package/claude/agents/security-validator.md +97 -0
- package/claude/agents/wiki-ingester.md +108 -0
- package/claude/agents/wiki-lint-auditor.md +94 -0
- package/claude/agents/wiki-query-analyst.md +84 -0
- package/claude/commands/context-explore.md +40 -0
- package/claude/commands/graphify.md +150 -0
- package/claude/commands/memory-review.md +85 -0
- package/claude/commands/memory-sync.md +71 -0
- package/claude/commands/recall.md +32 -0
- package/claude/commands/remember.md +43 -0
- package/claude/commands/repo-review.md +191 -0
- package/claude/commands/security-audit.md +174 -0
- package/claude/commands/wiki-ingest.md +112 -0
- package/claude/commands/wiki-lint.md +86 -0
- package/claude/commands/wiki-query.md +92 -0
- package/claude/hooks/context-explore-pretask.sh +93 -0
- package/claude/hooks/memory-capture.sh +59 -0
- package/claude/hooks/memory-recall.sh +84 -0
- package/claude/hooks/memory-wakeup.sh +45 -0
- package/docs/building-an-overlay.md +117 -0
- package/docs/domain-knowledge.md +86 -0
- package/docs/git-providers.md +36 -0
- package/docs/operating.md +366 -0
- package/mcp-memory-server/dist/embeddings.js +122 -0
- package/mcp-memory-server/dist/explore-cache.js +97 -0
- package/mcp-memory-server/dist/index.js +1256 -0
- package/mcp-memory-server/package.json +36 -0
- package/opencode/agents/code-reviewer.md +173 -0
- package/opencode/agents/security-investigator.md +111 -0
- package/opencode/agents/security-target-selector.md +89 -0
- package/opencode/agents/security-validator.md +96 -0
- package/opencode/agents/wiki-ingester.md +107 -0
- package/opencode/agents/wiki-lint-auditor.md +93 -0
- package/opencode/agents/wiki-query-analyst.md +83 -0
- package/opencode/command/code-review.md +199 -0
- package/opencode/command/context-explore.md +42 -0
- package/opencode/command/graphify.md +153 -0
- package/opencode/command/memory-review.md +44 -0
- package/opencode/command/memory-sync.md +94 -0
- package/opencode/command/recall.md +38 -0
- package/opencode/command/remember.md +46 -0
- package/opencode/command/security-audit.md +80 -0
- package/opencode/command/wiki-ingest.md +55 -0
- package/opencode/command/wiki-lint.md +51 -0
- package/opencode/command/wiki-query.md +50 -0
- package/opencode/plugins/memory-capture.ts +188 -0
- package/opencode/plugins/memory-recall.ts +132 -0
- package/opencode/plugins/memory-wakeup.ts +102 -0
- package/opencode/templates/security-finding-register.yaml.template +51 -0
- package/opencode/templates/security-issue.md.template +43 -0
- package/opencode/templates/security-policy.md.template +29 -0
- package/opencode/templates/security-report.md.template +43 -0
- package/opencode/templates/wiki-contradictions.md.template +31 -0
- package/opencode/templates/wiki-index.md.template +40 -0
- package/opencode/templates/wiki-lint-report.md.template +38 -0
- package/opencode/templates/wiki-log.md.template +11 -0
- package/opencode/templates/wiki-policy.md.template +47 -0
- package/opencode/templates/wiki-query-answer.md.template +26 -0
- package/opencode/templates/wiki-source-summary.md.template +33 -0
- package/opencode/workflows/memory-review-workflow.md +66 -0
- package/opencode/workflows/security-audit-workflow.md +197 -0
- package/opencode/workflows/wiki-ingest-workflow.md +149 -0
- package/opencode/workflows/wiki-lint-workflow.md +128 -0
- package/opencode/workflows/wiki-query-workflow.md +122 -0
- package/package.json +56 -0
- package/scripts/bootstrap.sh +99 -0
- package/scripts/doctor.sh +91 -0
- package/scripts/install-audit-timer.sh +68 -0
- package/scripts/lib/assert-tool-event.mjs +50 -0
- package/scripts/memory-store.sh +71 -0
- package/scripts/memory-wiki-audit.sh +156 -0
- package/scripts/sync-claude-assets.sh +182 -0
- package/scripts/sync-opencode-explore-assets.sh +187 -0
- package/scripts/sync-opencode-graphify-assets.sh +152 -0
- package/scripts/sync-opencode-memory-assets.sh +160 -0
- package/scripts/sync-opencode-plugin-assets.sh +163 -0
- package/scripts/sync-opencode-security-assets.sh +195 -0
- package/scripts/sync-opencode-wiki-assets.sh +250 -0
- package/scripts/test-audit-timer.sh +29 -0
- package/scripts/test-bootstrap-untracked.sh +41 -0
- package/scripts/test-cli-dispatch.sh +21 -0
- package/scripts/test-doctor.sh +32 -0
- package/scripts/test-launcher-hooks.sh +66 -0
- package/scripts/test-memory-store.sh +44 -0
- package/scripts/test-remember-recall-assertions.sh +70 -0
- package/scripts/transcript-to-text.mjs +53 -0
- package/scripts/verify-docs-parity.sh +112 -0
- package/scripts/verify-explore-maturation.sh +288 -0
- package/scripts/verify-fastcontext-reliability.sh +624 -0
- package/scripts/verify-no-private-references.sh +145 -0
- package/scripts/verify-opencode-live-parity.sh +864 -0
- package/scripts/verify-routing-seam.sh +169 -0
- package/scripts/verify-token-savings-ab.sh +557 -0
- package/templates/PROJECT-INSTRUCTIONS.md.template +42 -0
- package/templates/alignment-gap-register.yaml.template +56 -0
- package/templates/alignment-policy.md.template +28 -0
- package/templates/alignment-pr-template.md.template +18 -0
- package/templates/alignment-report.md.template +56 -0
- package/templates/env.example.template +25 -0
- package/templates/graph-policy.md.template +37 -0
- package/templates/graphs-gitignore.template +3 -0
- package/templates/memory-wiki-audit.service.template +6 -0
- package/templates/memory-wiki-audit.timer.template +9 -0
- package/templates/memory.json.template +4 -0
- package/templates/planning-config.json.template +6 -0
- package/templates/project-brief.md.template +40 -0
- package/templates/security-finding-register.yaml.template +51 -0
- package/templates/security-issue.md.template +43 -0
- package/templates/security-policy.md.template +29 -0
- package/templates/security-report.md.template +43 -0
- package/templates/start-claude.sh.template +46 -0
- package/templates/start-opencode.sh.template +41 -0
- package/templates/structured-change-canvas.md.template +63 -0
- package/templates/wiki-contradictions.md.template +31 -0
- package/templates/wiki-index.md.template +40 -0
- package/templates/wiki-lint-report.md.template +38 -0
- package/templates/wiki-log.md.template +11 -0
- package/templates/wiki-policy.md.template +47 -0
- package/templates/wiki-query-answer.md.template +26 -0
- 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 "$@"
|