@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
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cairnkeep/cli",
3
+ "version": "1.0.2",
4
+ "description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "cairn": "bin/cairn"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "scripts/",
16
+ "templates/",
17
+ "claude/",
18
+ "opencode/",
19
+ "docs/",
20
+ "CONTRIBUTING.md",
21
+ "SECURITY.md",
22
+ "mcp-memory-server/dist/",
23
+ "mcp-memory-server/package.json",
24
+ "AGENTS.md"
25
+ ],
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.29.0",
31
+ "agentfs-sdk": "^0.6.4",
32
+ "zod": "^3.25.76"
33
+ },
34
+ "scripts": {
35
+ "test": "for test in scripts/test-*.sh; do \"$test\" || exit 1; done",
36
+ "check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm test",
37
+ "prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server ci && npm --prefix mcp-memory-server test"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/cairnkeep/cairnkeep.git"
42
+ },
43
+ "homepage": "https://github.com/cairnkeep/cairnkeep#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/cairnkeep/cairnkeep/issues"
46
+ },
47
+ "keywords": [
48
+ "mcp",
49
+ "memory",
50
+ "agent",
51
+ "claude-code",
52
+ "opencode",
53
+ "context",
54
+ "rag"
55
+ ]
56
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Cairnkeep project bootstrap (core). Scaffolds a project's .ai/ launchers and a
3
+ # .planning/ derived-knowledge layer from the bundled templates. Generic and
4
+ # provider-neutral; variants can wrap this and add their own launchers/env.
5
+ set -euo pipefail
6
+
7
+ CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
8
+ TPL="$CAIRN_ROOT/templates"
9
+
10
+ untracked=0
11
+ target="."
12
+ for arg in "$@"; do
13
+ case "$arg" in
14
+ --untracked) untracked=1 ;;
15
+ -*)
16
+ echo "Unknown option: $arg" >&2
17
+ echo "Usage: bootstrap.sh [--untracked] [path]" >&2
18
+ exit 1
19
+ ;;
20
+ *) target="$arg" ;;
21
+ esac
22
+ done
23
+ if [[ ! -d "$target" ]]; then
24
+ echo "Bootstrap target is not a directory: $target" >&2
25
+ exit 1
26
+ fi
27
+ target=$(cd "$target" && pwd)
28
+
29
+ # Contributor mode: keep the scaffold invisible to git via .git/info/exclude
30
+ # (per-clone, never touches the repo's .gitignore). Resolve everything up
31
+ # front so a non-repo target fails before any files are created.
32
+ exclude_file=""
33
+ exclude_prefix=""
34
+ if [[ $untracked -eq 1 ]]; then
35
+ if ! exclude_file=$(cd "$target" && git rev-parse --git-path info/exclude 2>/dev/null); then
36
+ echo "--untracked requires the target to be inside a git repository: $target" >&2
37
+ exit 1
38
+ fi
39
+ [[ "$exclude_file" == /* ]] || exclude_file="$target/$exclude_file"
40
+ exclude_prefix=$(cd "$target" && git rev-parse --show-prefix)
41
+ fi
42
+
43
+ install_file() {
44
+ local src="$1" dest="$2" mode="$3"
45
+ if [[ -e "$dest" ]]; then
46
+ echo "skip (exists): ${dest#"$target"/}"
47
+ return 0
48
+ fi
49
+ mkdir -p "$(dirname "$dest")"
50
+ cp "$src" "$dest"
51
+ chmod "$mode" "$dest"
52
+ echo "created: ${dest#"$target"/}"
53
+ }
54
+
55
+ # .ai/ launchers + env
56
+ install_file "$TPL/start-claude.sh.template" "$target/.ai/start-claude.sh" 0755
57
+ install_file "$TPL/start-opencode.sh.template" "$target/.ai/start-opencode.sh" 0755
58
+ install_file "$TPL/env.example.template" "$target/.ai/env.example" 0644
59
+
60
+ # .planning/ derived-knowledge layer (template : destination)
61
+ planning_pairs=(
62
+ "planning-config.json.template:.planning/config.json"
63
+ "project-brief.md.template:.planning/PROJECT-BRIEF.md"
64
+ "wiki-index.md.template:.planning/wiki/index.md"
65
+ "wiki-policy.md.template:.planning/wiki/policy.md"
66
+ "wiki-contradictions.md.template:.planning/wiki/CONTRADICTIONS.md"
67
+ "wiki-log.md.template:.planning/wiki/LOG.md"
68
+ "alignment-policy.md.template:.planning/alignment/policy.md"
69
+ "alignment-gap-register.yaml.template:.planning/alignment/gap-register.yaml"
70
+ "graph-policy.md.template:.planning/graphs/policy.md"
71
+ "graphs-gitignore.template:.planning/graphs/.gitignore"
72
+ "security-policy.md.template:.planning/security/policy.md"
73
+ )
74
+ for pair in "${planning_pairs[@]}"; do
75
+ install_file "$TPL/${pair%%:*}" "$target/${pair#*:}" 0644
76
+ done
77
+
78
+ if [[ $untracked -eq 1 ]]; then
79
+ mkdir -p "$(dirname "$exclude_file")"
80
+ for entry in "/${exclude_prefix}.ai/" "/${exclude_prefix}.planning/"; do
81
+ if grep -qxF "$entry" "$exclude_file" 2>/dev/null; then
82
+ echo "skip (already excluded): $entry"
83
+ else
84
+ echo "$entry" >>"$exclude_file"
85
+ echo "excluded: $entry"
86
+ fi
87
+ done
88
+ echo "Contributor mode: scaffold is local-only (${exclude_file#"$target"/});"
89
+ echo "it is never committed, so it lives on this machine only."
90
+ fi
91
+
92
+ echo
93
+ echo "Cairnkeep bootstrapped into $target"
94
+ echo "Next steps (full guide: docs/operating.md):"
95
+ echo " 1. cp .ai/env.example .ai/.env and edit"
96
+ echo " 2. Build + register the memory server: cd mcp-memory-server && npm install && npm run build,"
97
+ echo " then: claude mcp add cairn-memory -s user -- node \"\$PWD/dist/index.js\""
98
+ echo " 3. Install the operating layer (commands/agents/hooks): scripts/sync-claude-assets.sh --apply"
99
+ echo " 4. Launch: $target/.ai/start-claude.sh"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # cairn doctor — post-bootstrap health check.
3
+ #
4
+ # Reports pass/warn/skip/fail for the pieces cairnkeep needs, reading only what
5
+ # is already configured in ./.ai/.env (or the current environment). Unconfigured
6
+ # optional dependencies are SKIPPED, never failed. Exits non-zero only if a
7
+ # CONFIGURED dependency is unreachable, so a green run means "the things you
8
+ # turned on actually work".
9
+ set -uo pipefail
10
+
11
+ CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
12
+
13
+ # Load the project's .ai/.env if present (does not override already-set env).
14
+ if [[ -f "$PWD/.ai/.env" ]]; then
15
+ set -a
16
+ # shellcheck disable=SC1091
17
+ . "$PWD/.ai/.env"
18
+ set +a
19
+ fi
20
+
21
+ fails=0
22
+ pass() { printf ' [PASS] %s\n' "$1"; }
23
+ warn() { printf ' [WARN] %s\n' "$1"; }
24
+ skip() { printf ' [SKIP] %s\n' "$1"; }
25
+ fail() { printf ' [FAIL] %s\n' "$1"; fails=$((fails + 1)); }
26
+
27
+ # curl returns 0 once the endpoint answers at all (any HTTP status counts as
28
+ # reachable); a connect/timeout failure returns non-zero.
29
+ reachable() { curl -sS -m 5 -o /dev/null "$1" >/dev/null 2>&1; }
30
+
31
+ echo "cairn doctor"
32
+
33
+ # 1. Memory server built (local prerequisite; actionable warning, not a failure).
34
+ if [[ -f "$CAIRN_ROOT/mcp-memory-server/dist/index.js" ]]; then
35
+ pass "memory server built (mcp-memory-server/dist/index.js)"
36
+ else
37
+ warn "memory server not built — run: (cd \"$CAIRN_ROOT/mcp-memory-server\" && npm install && npm run build)"
38
+ fi
39
+
40
+ # 2. LLM extraction endpoint (optional; unset → substring-only memory search).
41
+ if [[ -n "${CAIRN_LLM_API_URL:-}" ]]; then
42
+ if ! command -v curl >/dev/null 2>&1; then
43
+ skip "LLM endpoint: curl not installed, cannot probe ${CAIRN_LLM_API_URL}"
44
+ elif reachable "$CAIRN_LLM_API_URL"; then
45
+ pass "LLM endpoint reachable (${CAIRN_LLM_API_URL})"
46
+ else
47
+ fail "LLM endpoint unreachable (${CAIRN_LLM_API_URL})"
48
+ fi
49
+ else
50
+ skip "LLM endpoint (CAIRN_LLM_API_URL unset — memory search degrades to substring)"
51
+ fi
52
+
53
+ # 3. Embedding endpoint (optional).
54
+ if [[ -n "${CAIRN_MEMORY_EMBEDDING_URL:-}" ]]; then
55
+ if ! command -v curl >/dev/null 2>&1; then
56
+ skip "embedding endpoint: curl not installed, cannot probe ${CAIRN_MEMORY_EMBEDDING_URL}"
57
+ elif reachable "$CAIRN_MEMORY_EMBEDDING_URL"; then
58
+ pass "embedding endpoint reachable (${CAIRN_MEMORY_EMBEDDING_URL})"
59
+ else
60
+ fail "embedding endpoint unreachable (${CAIRN_MEMORY_EMBEDDING_URL})"
61
+ fi
62
+ else
63
+ skip "embedding endpoint (CAIRN_MEMORY_EMBEDDING_URL unset)"
64
+ fi
65
+
66
+ # 4. Git provider (collaboration commands). Report configuration only — auth is
67
+ # provider- and host-specific and lives in the wrapper, not the core.
68
+ case "${CAIRN_GIT_PROVIDER:-}" in
69
+ "" | none) skip "git provider (CAIRN_GIT_PROVIDER unset/none — collaboration commands off)" ;;
70
+ github | gitlab | codeberg | forgejo) pass "git provider configured: ${CAIRN_GIT_PROVIDER}" ;;
71
+ *) warn "git provider '${CAIRN_GIT_PROVIDER}' is not one of github|gitlab|codeberg|forgejo|none" ;;
72
+ esac
73
+
74
+ # 5. Memory store location (created on first write; report + writability).
75
+ store_dir="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
76
+ store_dir="${store_dir/#\~/$HOME}"
77
+ if [[ -d "$store_dir" ]]; then
78
+ if [[ -w "$store_dir" ]]; then pass "memory store: $store_dir (exists, writable)"
79
+ else fail "memory store not writable: $store_dir"; fi
80
+ else
81
+ parent=$(dirname "$store_dir")
82
+ if [[ -w "$parent" ]]; then pass "memory store: $store_dir (will be created on first write)"
83
+ else fail "memory store parent not writable: $parent"; fi
84
+ fi
85
+
86
+ echo
87
+ if [[ "$fails" -gt 0 ]]; then
88
+ echo "cairn doctor: $fails configured dependency check(s) failed."
89
+ exit 1
90
+ fi
91
+ echo "cairn doctor: OK (configured dependencies healthy)."
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # install-audit-timer.sh — opt-in scheduler for memory-wiki-audit.sh.
3
+ #
4
+ # Renders a systemd user service + timer from templates/ and enables the timer,
5
+ # so the deterministic memory/wiki invalidation audit runs on a schedule. A cron
6
+ # alternative is printed for hosts without systemd.
7
+ #
8
+ # Usage: install-audit-timer.sh [--para-root PATH] [--on-calendar SPEC]
9
+ # [--report PATH] [--render-only DIR]
10
+ # --para-root PATH root the audit scans (default: $HOME/PARA)
11
+ # --on-calendar SPEC systemd OnCalendar schedule (default: daily)
12
+ # --report PATH where the audit writes its report
13
+ # --render-only DIR render the unit files into DIR and exit (no systemctl)
14
+ set -euo pipefail
15
+
16
+ CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
17
+ TPL="$CAIRN_ROOT/templates"
18
+ AUDIT_SCRIPT="$CAIRN_ROOT/scripts/memory-wiki-audit.sh"
19
+
20
+ PARA_ROOT="${HOME}/PARA"
21
+ ON_CALENDAR="daily"
22
+ REPORT_PATH="${HOME}/.cairnkeep/memory-wiki-audit.report.md"
23
+ RENDER_ONLY=""
24
+
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --para-root) PARA_ROOT="$2"; shift 2 ;;
28
+ --on-calendar) ON_CALENDAR="$2"; shift 2 ;;
29
+ --report) REPORT_PATH="$2"; shift 2 ;;
30
+ --render-only) RENDER_ONLY="$2"; shift 2 ;;
31
+ -h|--help) sed -n '2,16p' "$0"; exit 0 ;;
32
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
33
+ esac
34
+ done
35
+
36
+ render() {
37
+ sed -e "s|@@AUDIT_SCRIPT@@|${AUDIT_SCRIPT}|g" \
38
+ -e "s|@@PARA_ROOT@@|${PARA_ROOT}|g" \
39
+ -e "s|@@REPORT_PATH@@|${REPORT_PATH}|g" \
40
+ -e "s|@@ON_CALENDAR@@|${ON_CALENDAR}|g" \
41
+ "$1"
42
+ }
43
+
44
+ cron_line="@daily ${AUDIT_SCRIPT} --para-root ${PARA_ROOT} --report ${REPORT_PATH}"
45
+
46
+ if [[ -n "$RENDER_ONLY" ]]; then
47
+ mkdir -p "$RENDER_ONLY"
48
+ render "$TPL/memory-wiki-audit.service.template" > "$RENDER_ONLY/memory-wiki-audit.service"
49
+ render "$TPL/memory-wiki-audit.timer.template" > "$RENDER_ONLY/memory-wiki-audit.timer"
50
+ echo "rendered unit files into $RENDER_ONLY"
51
+ exit 0
52
+ fi
53
+
54
+ if ! command -v systemctl >/dev/null 2>&1; then
55
+ echo "systemctl not found. Use --render-only DIR, or schedule via cron:" >&2
56
+ echo " $cron_line" >&2
57
+ exit 1
58
+ fi
59
+
60
+ UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
61
+ mkdir -p "$UNIT_DIR"
62
+ mkdir -p "$(dirname "$REPORT_PATH")"
63
+ render "$TPL/memory-wiki-audit.service.template" > "$UNIT_DIR/memory-wiki-audit.service"
64
+ render "$TPL/memory-wiki-audit.timer.template" > "$UNIT_DIR/memory-wiki-audit.timer"
65
+ systemctl --user daemon-reload
66
+ systemctl --user enable --now memory-wiki-audit.timer
67
+ echo "installed + enabled memory-wiki-audit.timer (OnCalendar=${ON_CALENDAR})"
68
+ echo "cron alternative: ${cron_line}"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // scripts/lib/assert-tool-event.mjs
3
+ //
4
+ // Reads NDJSON (opencode `run --format json` style events) from stdin and
5
+ // exits 0 the moment a genuine tool_use event matches TOOL_EVENT_REGEX (and,
6
+ // if TOOL_EVENT_CANARY is set, whose part.state.output contains it); exits 1
7
+ // otherwise. A narrated-but-unexecuted mention of a tool name inside a
8
+ // "text" event's part.text never matches — only a real top-level
9
+ // type === "tool_use" event with part.state.status === "completed" does
10
+ // (D-08/D-09, 13-RESEARCH.md Pattern 2).
11
+ //
12
+ // TOOL_EVENT_REGEX and TOOL_EVENT_CANARY are read ONLY from process.env
13
+ // (never argv string-interpolation), so an adversarial canary value can't
14
+ // inject into the matcher — mirrors seed_canary()'s env-var-passing idiom
15
+ // in scripts/verify-opencode-live-parity.sh.
16
+
17
+ const toolEventRegexSource = process.env.TOOL_EVENT_REGEX;
18
+ if (!toolEventRegexSource) {
19
+ process.stderr.write("assert-tool-event: TOOL_EVENT_REGEX env var is required\n");
20
+ process.exit(1);
21
+ }
22
+ const toolEventRegex = new RegExp(toolEventRegexSource);
23
+ const canary = process.env.TOOL_EVENT_CANARY || "";
24
+
25
+ let data = "";
26
+ process.stdin.on("data", (c) => { data += c; });
27
+ process.stdin.on("end", () => {
28
+ for (const line of data.split("\n")) {
29
+ if (!line.trim()) continue;
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(line);
33
+ } catch {
34
+ // Not a JSON line (e.g. interleaved log noise) — skip, never abort
35
+ // the scan on a malformed line (T-13-01).
36
+ continue;
37
+ }
38
+ if (
39
+ parsed.type === "tool_use" &&
40
+ toolEventRegex.test(parsed.part?.tool || "") &&
41
+ parsed.part?.state?.status === "completed"
42
+ ) {
43
+ if (canary && !String(parsed.part?.state?.output ?? "").includes(canary)) {
44
+ continue;
45
+ }
46
+ process.exit(0);
47
+ }
48
+ }
49
+ process.exit(1);
50
+ });
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # cairn memory <path|export|import> — relocate the durable memory store between
3
+ # machines or backends.
4
+ #
5
+ # The store is one SQLite .db per scope under CAIRN_AGENTFS_BASE_DIR
6
+ # (default ~/.cairnkeep). `export` takes a WAL-safe snapshot of every scope db
7
+ # into a portable archive; `import` restores them on another machine; `path`
8
+ # prints the store location so nothing has to guess where it lives.
9
+ set -euo pipefail
10
+
11
+ base="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
12
+ base="${base/#\~/$HOME}"
13
+
14
+ usage() {
15
+ cat <<'EOF'
16
+ Usage: cairn memory <path|export|import>
17
+ path Print the memory store base directory.
18
+ export <file.tgz> WAL-safe snapshot of every scope .db into a portable archive.
19
+ import <file.tgz> Restore scope .db files from an archive (backs up existing).
20
+ EOF
21
+ }
22
+
23
+ sub="${1:-help}"
24
+ [[ $# -gt 0 ]] && shift
25
+
26
+ case "$sub" in
27
+ path)
28
+ echo "$base"
29
+ ;;
30
+ export)
31
+ out="${1:?usage: cairn memory export <file.tgz>}"
32
+ command -v sqlite3 >/dev/null 2>&1 || { echo "sqlite3 is required for a WAL-safe export" >&2; exit 1; }
33
+ [[ -d "$base" ]] || { echo "no memory store directory at $base" >&2; exit 1; }
34
+ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
35
+ n=0
36
+ for db in "$base"/*.db; do
37
+ [[ -e "$db" ]] || continue
38
+ # .backup yields a consistent copy even while the store is in WAL mode.
39
+ sqlite3 "$db" ".backup '$tmp/$(basename "$db")'"
40
+ n=$((n + 1))
41
+ done
42
+ [[ $n -gt 0 ]] || { echo "no scope .db files under $base — nothing to export" >&2; exit 1; }
43
+ tar -C "$tmp" -czf "$out" .
44
+ echo "exported $n scope db file(s) from $base -> $out"
45
+ ;;
46
+ import)
47
+ in="${1:?usage: cairn memory import <file.tgz>}"
48
+ [[ -f "$in" ]] || { echo "no such archive: $in" >&2; exit 1; }
49
+ mkdir -p "$base"
50
+ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
51
+ tar -C "$tmp" -xzf "$in"
52
+ n=0
53
+ for db in "$tmp"/*.db; do
54
+ [[ -e "$db" ]] || continue
55
+ dest="$base/$(basename "$db")"
56
+ [[ -f "$dest" ]] && cp -f "$dest" "$dest.bak-pre-import"
57
+ cp -f "$db" "$dest"
58
+ n=$((n + 1))
59
+ done
60
+ [[ $n -gt 0 ]] || { echo "archive contained no scope .db files: $in" >&2; exit 1; }
61
+ echo "imported $n scope db file(s) -> $base"
62
+ ;;
63
+ help | -h | --help)
64
+ usage
65
+ ;;
66
+ *)
67
+ echo "Unknown: cairn memory $sub" >&2
68
+ usage >&2
69
+ exit 1
70
+ ;;
71
+ esac
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ # memory-wiki-audit.sh — periodic invalidation scanner for the memory + wiki layers.
3
+ #
4
+ # Scans every project under PARA_ROOT that has a .planning/wiki/ for:
5
+ # 1. stale wiki source pages (Last reviewed older than STALE_DAYS)
6
+ # 2. open hard contradictions in CONTRADICTIONS.md
7
+ # 3. orphan source pages not linked from index.md
8
+ # 4. unreviewed memory candidates staged longer than STALE_DAYS
9
+ #
10
+ # Output: a consolidated markdown report on stdout (and optionally to REPORT_PATH).
11
+ # Designed for a systemd timer or cron. Pure bash/grep — no model round-trip, so
12
+ # it is reliable and cheap. The model-driven deep lint (/wiki-lint) still needs
13
+ # to run inside a session; this is the deterministic backstop that runs on a
14
+ # schedule and tells you where to focus.
15
+ #
16
+ # Usage: memory-wiki-audit.sh [--para-root PATH] [--stale-days N] [--report PATH]
17
+ # --para-root PATH root to scan (default: $HOME/PARA)
18
+ # --stale-days N staleness threshold in days (default: 30)
19
+ # --report PATH also write the report to this file
20
+ set -euo pipefail
21
+
22
+ PARA_ROOT="${HOME}/PARA"
23
+ STALE_DAYS=30
24
+ REPORT_PATH=""
25
+
26
+ while [[ $# -gt 0 ]]; do
27
+ case "$1" in
28
+ --para-root) PARA_ROOT="$2"; shift 2 ;;
29
+ --stale-days) STALE_DAYS="$2"; shift 2 ;;
30
+ --report) REPORT_PATH="$2"; shift 2 ;;
31
+ -h|--help)
32
+ sed -n '2,20p' "$0"; exit 0 ;;
33
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
34
+ esac
35
+ done
36
+
37
+ cutoff_epoch="$(date -u -d "$STALE_DAYS days ago" +%s 2>/dev/null || date -u -v-${STALE_DAYS}d +%s 2>/dev/null || echo 0)"
38
+
39
+ stale_total=0
40
+ hard_open_total=0
41
+ orphan_total=0
42
+ staged_stale_total=0
43
+ sections=()
44
+
45
+ # Find every project with a wiki.
46
+ while IFS= read -r wiki_dir; do
47
+ proj_dir="$(dirname "$(dirname "$wiki_dir")")"
48
+ proj_name="$(basename "$proj_dir")"
49
+ proj_findings=()
50
+
51
+ # 1. Stale source pages (Last reviewed older than cutoff).
52
+ stale_pages=()
53
+ if [[ -d "$wiki_dir/sources" ]]; then
54
+ while IFS= read -r page; do
55
+ [[ -n "$page" ]] || continue
56
+ lr="$(grep -m1 -iE '^[-*] \*?\*?Last reviewed\*?\*?:' "$page" 2>/dev/null | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || true)"
57
+ if [[ -n "$lr" ]]; then
58
+ lr_epoch="$(date -u -d "$lr" +%s 2>/dev/null || echo 9999999999)"
59
+ if (( lr_epoch < cutoff_epoch )); then
60
+ stale_pages+=("$(basename "$page") (reviewed $lr)")
61
+ fi
62
+ fi
63
+ done < <(find "$wiki_dir/sources" -maxdepth 1 -name '*.md' -type f 2>/dev/null)
64
+ fi
65
+ if (( ${#stale_pages[@]} > 0 )); then
66
+ proj_findings+=("- stale source pages (${#stale_pages[@]}): ${stale_pages[*]}")
67
+ stale_total=$((stale_total + ${#stale_pages[@]}))
68
+ fi
69
+
70
+ # 2. Open hard contradictions.
71
+ contras="$wiki_dir/CONTRADICTIONS.md"
72
+ hard_open=0
73
+ if [[ -f "$contras" ]]; then
74
+ # Count open entries tagged hard in the Open section.
75
+ open_block="$(sed -n '/wiki:contradictions:open:start/,/wiki:contradictions:open:end/p' "$contras" 2>/dev/null || true)"
76
+ hard_open="$(printf '%s' "$open_block" | grep -ciE 'severity: hard|hard contradiction' 2>/dev/null || true)"
77
+ hard_open="$(printf '%s' "${hard_open:-0}" | tr -dc '0-9')"
78
+ hard_open="${hard_open:-0}"
79
+ fi
80
+ if (( hard_open > 0 )); then
81
+ proj_findings+=("- open HARD contradictions: $hard_open")
82
+ hard_open_total=$((hard_open_total + hard_open))
83
+ fi
84
+
85
+ # 3. Orphan source pages (in sources/ but not named in index.md).
86
+ orphans=()
87
+ if [[ -f "$wiki_dir/index.md" ]] && [[ -d "$wiki_dir/sources" ]]; then
88
+ while IFS= read -r page; do
89
+ [[ -n "$page" ]] || continue
90
+ base="$(basename "$page")"
91
+ grep -qF "$base" "$wiki_dir/index.md" 2>/dev/null || orphans+=("$base")
92
+ done < <(find "$wiki_dir/sources" -maxdepth 1 -name '*.md' -type f 2>/dev/null)
93
+ fi
94
+ if (( ${#orphans[@]} > 0 )); then
95
+ proj_findings+=("- orphan source pages (${#orphans[@]}): ${orphans[*]}")
96
+ orphan_total=$((orphan_total + ${#orphans[@]}))
97
+ fi
98
+
99
+ # 4. Unreviewed staged memory candidates older than threshold.
100
+ staging="$proj_dir/.planning/memory-staging"
101
+ staged_stale=0
102
+ if [[ -d "$staging" ]]; then
103
+ while IFS= read -r sf; do
104
+ [[ -n "$sf" ]] || continue
105
+ sf_epoch="$(stat -c %Y "$sf" 2>/dev/null || stat -f %m "$sf" 2>/dev/null || echo 9999999999)"
106
+ if (( sf_epoch < cutoff_epoch )); then
107
+ staged_stale=$((staged_stale + 1))
108
+ fi
109
+ done < <(find "$staging" -maxdepth 1 -name '*.json' -type f 2>/dev/null)
110
+ fi
111
+ if (( staged_stale > 0 )); then
112
+ proj_findings+=("- unreviewed staged memory candidates (>$STALE_DAYS days): $staged_stale")
113
+ staged_stale_total=$((staged_stale_total + staged_stale))
114
+ fi
115
+
116
+ if (( ${#proj_findings[@]} > 0 )); then
117
+ sections+=("### $proj_name" "" "${proj_findings[@]}" "")
118
+ fi
119
+ done < <(find "$PARA_ROOT" -maxdepth 4 -type d -name wiki -path '*/.planning/wiki' 2>/dev/null)
120
+
121
+ # Build report once, output to stdout (and REPORT_PATH if set).
122
+ report_body="$(
123
+ {
124
+ echo "# Memory + Wiki Audit Report"
125
+ echo
126
+ echo "- Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
127
+ echo "- Scope: $PARA_ROOT (staleness threshold: $STALE_DAYS days)"
128
+ echo "- Stale source pages: $stale_total"
129
+ echo "- Open hard contradictions: $hard_open_total"
130
+ echo "- Orphan source pages: $orphan_total"
131
+ echo "- Unreviewed staged candidates: $staged_stale_total"
132
+ echo
133
+ if (( ${#sections[@]} > 0 )); then
134
+ echo "## Findings by project"
135
+ echo
136
+ printf '%s\n' "${sections[@]}"
137
+ else
138
+ echo "## No findings"
139
+ echo
140
+ echo "All scanned projects are within thresholds."
141
+ fi
142
+ }
143
+ )"
144
+
145
+ printf '%s\n' "$report_body"
146
+
147
+ if [[ -n "$REPORT_PATH" ]]; then
148
+ mkdir -p "$(dirname "$REPORT_PATH")"
149
+ printf '%s\n' "$report_body" > "$REPORT_PATH"
150
+ fi
151
+
152
+ # Exit non-zero if there are actionable findings (useful for cron alerting).
153
+ if (( stale_total + hard_open_total + orphan_total + staged_stale_total > 0 )); then
154
+ exit 3
155
+ fi
156
+ exit 0