@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
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
|