@cairnkeep/cli 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +221 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +1 -1
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +2 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/operating.md +531 -4
  16. package/docs/privacy-and-data-flow.md +417 -0
  17. package/docs/storage.md +252 -0
  18. package/examples/eval/README.md +42 -0
  19. package/examples/eval/adapter.json +12 -0
  20. package/examples/eval/bundled-fake.json +1 -0
  21. package/examples/eval/task-set.json +1 -0
  22. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  23. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  24. package/mcp-memory-server/dist/artifact-store.js +812 -0
  25. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  26. package/mcp-memory-server/dist/capability-cli.js +361 -0
  27. package/mcp-memory-server/dist/capability-config.js +310 -0
  28. package/mcp-memory-server/dist/capability-harness.js +496 -0
  29. package/mcp-memory-server/dist/capability-registry.js +123 -0
  30. package/mcp-memory-server/dist/capability-schema.js +65 -0
  31. package/mcp-memory-server/dist/capability-store.js +431 -0
  32. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  33. package/mcp-memory-server/dist/eval-cli.js +523 -0
  34. package/mcp-memory-server/dist/eval-plan.js +346 -0
  35. package/mcp-memory-server/dist/eval-process.js +273 -0
  36. package/mcp-memory-server/dist/eval-report.js +609 -0
  37. package/mcp-memory-server/dist/eval-runner.js +827 -0
  38. package/mcp-memory-server/dist/eval-schema.js +368 -0
  39. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  40. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  41. package/mcp-memory-server/dist/failure-signature.js +141 -0
  42. package/mcp-memory-server/dist/index.js +600 -55
  43. package/mcp-memory-server/dist/node-cli.js +84 -0
  44. package/mcp-memory-server/dist/node-schema.js +170 -0
  45. package/mcp-memory-server/dist/node-store.js +455 -0
  46. package/mcp-memory-server/dist/note-cli.js +235 -0
  47. package/mcp-memory-server/dist/note-distiller.js +177 -0
  48. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  49. package/mcp-memory-server/dist/note-schema.js +4 -0
  50. package/mcp-memory-server/dist/note-store.js +985 -0
  51. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  52. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  53. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  54. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  55. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  56. package/mcp-memory-server/package.json +24 -2
  57. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  58. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  59. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  60. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  61. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  62. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  63. package/opencode/command/graphify.md +25 -14
  64. package/opencode/plugins/memory-capture.ts +76 -10
  65. package/opencode/plugins/memory-wakeup.ts +58 -6
  66. package/package.json +8 -4
  67. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  68. package/schemas/artifact.schema.json +133 -0
  69. package/schemas/capability-callback.schema.json +68 -0
  70. package/schemas/capability-contract.schema.json +126 -0
  71. package/schemas/eval-adapter.schema.json +43 -0
  72. package/schemas/eval-protocol.schema.json +114 -0
  73. package/schemas/eval-report.schema.json +423 -0
  74. package/schemas/eval-task-set.schema.json +114 -0
  75. package/schemas/memory-node.schema.json +90 -0
  76. package/schemas/note.schema.json +112 -0
  77. package/schemas/trajectory-redaction.schema.json +25 -0
  78. package/schemas/trajectory.schema.json +82 -0
  79. package/scripts/bootstrap.sh +4 -0
  80. package/scripts/completion.sh +119 -5
  81. package/scripts/doctor.sh +284 -0
  82. package/scripts/fake-eval-adapter.mjs +120 -0
  83. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  84. package/scripts/install-audit-timer.sh +3 -1
  85. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  86. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  87. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  88. package/scripts/memory-wiki-audit.sh +49 -0
  89. package/scripts/smoke-bash32.sh +9 -0
  90. package/scripts/sync-claude-assets.sh +99 -6
  91. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  92. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  93. package/scripts/sync-opencode-security-assets.sh +24 -3
  94. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  95. package/scripts/sync-pi-assets.sh +72 -0
  96. package/scripts/test-audit-timer.sh +98 -0
  97. package/scripts/test-bootstrap-untracked.sh +48 -0
  98. package/scripts/test-cli-dispatch.sh +41 -2
  99. package/scripts/test-compaction-hooks.sh +495 -0
  100. package/scripts/test-completion.sh +77 -0
  101. package/scripts/test-doctor.sh +348 -1
  102. package/scripts/test-eval-cli.sh +175 -0
  103. package/scripts/test-launcher-hooks.sh +602 -3
  104. package/scripts/test-notes-cli.sh +114 -0
  105. package/scripts/test-package-install.sh +251 -0
  106. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  107. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  108. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  109. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  110. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  111. package/scripts/test-trajectory-cli.sh +87 -0
  112. package/scripts/test-trajectory-hooks.sh +142 -0
  113. package/scripts/test-uninstall.sh +231 -5
  114. package/scripts/uninstall.sh +37 -5
  115. package/scripts/verify-dependency-security.mjs +121 -0
  116. package/scripts/verify-docs-parity.sh +527 -1
  117. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  118. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  119. package/templates/capabilities.json.template +7 -0
  120. package/templates/env.example.template +51 -0
  121. package/templates/memory-wiki-audit.service.template +1 -1
  122. package/templates/start-claude.sh.template +36 -0
  123. package/templates/start-opencode.sh.template +47 -5
  124. package/templates/start-pi.sh.template +30 -0
  125. package/templates/trajectory-redaction.json.template +4 -0
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  usage() {
5
5
  cat <<'EOF'
6
- Usage: sync-opencode-graphify-assets.sh [--check|--apply] [--live-root PATH]
6
+ Usage: sync-opencode-graphify-assets.sh [--check|--apply] [--capability-overlay] [--live-root PATH]
7
7
 
8
8
  Compare or sync the repo-managed Graphify OpenCode command asset against the
9
9
  live OpenCode config tree.
@@ -11,12 +11,17 @@ live OpenCode config tree.
11
11
  Options:
12
12
  --check Verify that the managed live asset matches the repo copy (default)
13
13
  --apply Copy the repo-managed asset into the live OpenCode tree, then verify
14
+ --capability-overlay
15
+ Select the guarded command overlay for a contract-enabled isolated root
14
16
  --live-root PATH Override the live OpenCode root (default: $OPENCODE_CONFIG_DIR or $HOME/.config/opencode)
15
17
  -h, --help Show this help text
16
18
 
17
19
  Notes:
18
20
  - The repo-managed source of truth lives under ./opencode/
19
21
  - This script manages only the /graphify command asset
22
+ - Plugin installation is delegated to sync-opencode-plugin-assets.sh;
23
+ --capability-overlay selects its native plugin only when the master switch is on
24
+ - The command asset always retains its legacy owner and bytes
20
25
  EOF
21
26
  }
22
27
 
@@ -24,6 +29,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
24
29
  SOURCE_ROOT="$ROOT_DIR/opencode"
25
30
  LIVE_ROOT="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"
26
31
  MODE="check"
32
+ CAPABILITY_OVERLAY=0
27
33
 
28
34
  ASSETS=(
29
35
  "command/graphify.md"
@@ -39,6 +45,10 @@ while [[ $# -gt 0 ]]; do
39
45
  MODE="apply"
40
46
  shift
41
47
  ;;
48
+ --capability-overlay)
49
+ CAPABILITY_OVERLAY=1
50
+ shift
51
+ ;;
42
52
  --live-root)
43
53
  LIVE_ROOT="$2"
44
54
  shift 2
@@ -55,6 +65,11 @@ while [[ $# -gt 0 ]]; do
55
65
  esac
56
66
  done
57
67
 
68
+ source_for() {
69
+ local rel="$1"
70
+ printf '%s\n' "$SOURCE_ROOT/$rel"
71
+ }
72
+
58
73
  ensure_source_assets_exist() {
59
74
  local rel
60
75
  local missing=0
@@ -79,7 +94,7 @@ check_asset_sync() {
79
94
  local -a mismatched=()
80
95
 
81
96
  for rel in "${ASSETS[@]}"; do
82
- src="$SOURCE_ROOT/$rel"
97
+ src=$(source_for "$rel")
83
98
  dst="$LIVE_ROOT/$rel"
84
99
 
85
100
  if [[ ! -f "$dst" ]]; then
@@ -125,7 +140,7 @@ run_apply() {
125
140
  ensure_source_assets_exist
126
141
 
127
142
  for rel in "${ASSETS[@]}"; do
128
- src="$SOURCE_ROOT/$rel"
143
+ src=$(source_for "$rel")
129
144
  dst="$LIVE_ROOT/$rel"
130
145
 
131
146
  mkdir -p "$(dirname "$dst")"
@@ -150,3 +165,9 @@ case "$MODE" in
150
165
  run_apply
151
166
  ;;
152
167
  esac
168
+
169
+ plugin_args=("--$MODE" "--live-root" "$LIVE_ROOT")
170
+ if [[ "$CAPABILITY_OVERLAY" -eq 1 ]]; then
171
+ plugin_args+=("--capability-overlay")
172
+ fi
173
+ "$ROOT_DIR/scripts/sync-opencode-plugin-assets.sh" "${plugin_args[@]}"
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  usage() {
5
5
  cat <<'EOF'
6
- Usage: sync-opencode-plugin-assets.sh [--check|--apply] [--live-root PATH]
6
+ Usage: sync-opencode-plugin-assets.sh [--check|--apply] [--capability-overlay] [--live-root PATH]
7
7
 
8
8
  Compare or sync the repo-managed OpenCode plugin assets against the live
9
9
  OpenCode config tree.
@@ -11,6 +11,8 @@ OpenCode config tree.
11
11
  Options:
12
12
  --check Verify that the managed live assets match the repo copies (default)
13
13
  --apply Copy the repo-managed assets into the live OpenCode tree, then verify
14
+ --capability-overlay
15
+ Include the native capability plugin only when CAIRN_CAPABILITY_CONTRACT is enabled
14
16
  --live-root PATH Override the live OpenCode root (default: $OPENCODE_CONFIG_DIR or $HOME/.config/opencode)
15
17
  -h, --help Show this help text
16
18
 
@@ -24,6 +26,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
24
26
  SOURCE_ROOT="$ROOT_DIR/opencode"
25
27
  LIVE_ROOT="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"
26
28
  MODE="check"
29
+ CAPABILITY_OVERLAY=0
27
30
 
28
31
  ASSETS=(
29
32
  "plugins/memory-wakeup.ts"
@@ -41,6 +44,10 @@ while [[ $# -gt 0 ]]; do
41
44
  MODE="apply"
42
45
  shift
43
46
  ;;
47
+ --capability-overlay)
48
+ CAPABILITY_OVERLAY=1
49
+ shift
50
+ ;;
44
51
  --live-root)
45
52
  LIVE_ROOT="$2"
46
53
  shift 2
@@ -57,6 +64,23 @@ while [[ $# -gt 0 ]]; do
57
64
  esac
58
65
  done
59
66
 
67
+ case "${CAIRN_CAPABILITY_CONTRACT:-}" in
68
+ 1|true|TRUE|yes|YES|on|ON)
69
+ if [[ "$CAPABILITY_OVERLAY" -eq 1 ]]; then
70
+ ASSETS+=("plugins/capability-command.ts")
71
+ fi
72
+ ;;
73
+ esac
74
+
75
+ source_asset() {
76
+ local rel="$1"
77
+ if [[ "$rel" == "plugins/capability-command.ts" ]]; then
78
+ printf '%s\n' "$SOURCE_ROOT/capability-contract/plugins/capability-command.ts"
79
+ else
80
+ printf '%s\n' "$SOURCE_ROOT/$rel"
81
+ fi
82
+ }
83
+
60
84
  render_asset() {
61
85
  local src="$1"
62
86
  sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$src"
@@ -67,8 +91,10 @@ ensure_source_assets_exist() {
67
91
  local missing=0
68
92
 
69
93
  for rel in "${ASSETS[@]}"; do
70
- if [[ ! -f "$SOURCE_ROOT/$rel" ]]; then
71
- echo "Missing repo-managed source asset: $SOURCE_ROOT/$rel" >&2
94
+ local src
95
+ src=$(source_asset "$rel")
96
+ if [[ ! -f "$src" ]]; then
97
+ echo "Missing repo-managed source asset: $src" >&2
72
98
  missing=1
73
99
  fi
74
100
  done
@@ -86,7 +112,7 @@ check_asset_sync() {
86
112
  local -a mismatched=()
87
113
 
88
114
  for rel in "${ASSETS[@]}"; do
89
- src="$SOURCE_ROOT/$rel"
115
+ src=$(source_asset "$rel")
90
116
  dst="$LIVE_ROOT/$rel"
91
117
 
92
118
  if [[ ! -f "$dst" ]]; then
@@ -133,7 +159,7 @@ run_apply() {
133
159
  ensure_source_assets_exist
134
160
 
135
161
  for rel in "${ASSETS[@]}"; do
136
- src="$SOURCE_ROOT/$rel"
162
+ src=$(source_asset "$rel")
137
163
  dst="$LIVE_ROOT/$rel"
138
164
 
139
165
  mkdir -p "$(dirname "$dst")"
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  usage() {
5
5
  cat <<'EOF'
6
- Usage: sync-opencode-security-assets.sh [--check|--apply] [--live-root PATH]
6
+ Usage: sync-opencode-security-assets.sh [--check|--apply] [--capability-overlay] [--live-root PATH]
7
7
 
8
8
  Compare or sync the repo-managed OpenCode security-audit assets against the live
9
9
  OpenCode config tree.
@@ -11,12 +11,17 @@ OpenCode config tree.
11
11
  Options:
12
12
  --check Verify that the managed live assets match the repo copy (default)
13
13
  --apply Copy the repo-managed assets into the live OpenCode tree, then verify
14
+ --capability-overlay
15
+ Select guarded command/workflow overlays for a contract-enabled isolated root
14
16
  --live-root PATH Override the live OpenCode root (default: $OPENCODE_CONFIG_DIR or $HOME/.config/opencode)
15
17
  -h, --help Show this help text
16
18
 
17
19
  Notes:
18
20
  - The repo-managed source of truth lives under ./opencode/
19
21
  - This script manages only the security-audit OpenCode assets
22
+ - Plugin installation is delegated to sync-opencode-plugin-assets.sh;
23
+ --capability-overlay selects its native plugin only when the master switch is on
24
+ - Command and workflow assets always retain their legacy owners and bytes
20
25
  - --apply also removes the legacy gsd-overrides security-audit workflow/template files
21
26
  EOF
22
27
  }
@@ -25,6 +30,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
25
30
  SOURCE_ROOT="$ROOT_DIR/opencode"
26
31
  LIVE_ROOT="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"
27
32
  MODE="check"
33
+ CAPABILITY_OVERLAY=0
28
34
 
29
35
  ASSETS=(
30
36
  "command/security-audit.md"
@@ -56,6 +62,10 @@ while [[ $# -gt 0 ]]; do
56
62
  MODE="apply"
57
63
  shift
58
64
  ;;
65
+ --capability-overlay)
66
+ CAPABILITY_OVERLAY=1
67
+ shift
68
+ ;;
59
69
  --live-root)
60
70
  LIVE_ROOT="$2"
61
71
  shift 2
@@ -72,6 +82,11 @@ while [[ $# -gt 0 ]]; do
72
82
  esac
73
83
  done
74
84
 
85
+ source_for() {
86
+ local rel="$1"
87
+ printf '%s\n' "$SOURCE_ROOT/$rel"
88
+ }
89
+
75
90
  ensure_source_assets_exist() {
76
91
  local rel
77
92
  local missing=0
@@ -116,7 +131,7 @@ run_check() {
116
131
  ensure_source_assets_exist
117
132
 
118
133
  for rel in "${ASSETS[@]}"; do
119
- src="$SOURCE_ROOT/$rel"
134
+ src=$(source_for "$rel")
120
135
  dst="$LIVE_ROOT/$rel"
121
136
 
122
137
  if [[ ! -f "$dst" ]]; then
@@ -160,7 +175,7 @@ run_apply() {
160
175
  ensure_source_assets_exist
161
176
 
162
177
  for rel in "${ASSETS[@]}"; do
163
- src="$SOURCE_ROOT/$rel"
178
+ src=$(source_for "$rel")
164
179
  dst="$LIVE_ROOT/$rel"
165
180
 
166
181
  mkdir -p "$(dirname "$dst")"
@@ -193,3 +208,9 @@ case "$MODE" in
193
208
  run_apply
194
209
  ;;
195
210
  esac
211
+
212
+ plugin_args=("--$MODE" "--live-root" "$LIVE_ROOT")
213
+ if [[ "$CAPABILITY_OVERLAY" -eq 1 ]]; then
214
+ plugin_args+=("--capability-overlay")
215
+ fi
216
+ "$ROOT_DIR/scripts/sync-opencode-plugin-assets.sh" "${plugin_args[@]}"
@@ -3,7 +3,7 @@ set -euo pipefail
3
3
 
4
4
  usage() {
5
5
  cat <<'EOF'
6
- Usage: sync-opencode-wiki-assets.sh [--check|--apply] [--live-root PATH]
6
+ Usage: sync-opencode-wiki-assets.sh [--check|--apply] [--capability-overlay] [--live-root PATH]
7
7
 
8
8
  Compare or sync the repo-managed OpenCode wiki assets against the live
9
9
  OpenCode config tree.
@@ -11,12 +11,17 @@ OpenCode config tree.
11
11
  Options:
12
12
  --check Verify that the managed live assets match the repo copy (default)
13
13
  --apply Copy the repo-managed assets into the live OpenCode tree, then verify
14
+ --capability-overlay
15
+ Select guarded command/workflow overlays for a contract-enabled isolated root
14
16
  --live-root PATH Override the live OpenCode root (default: $OPENCODE_CONFIG_DIR or $HOME/.config/opencode)
15
17
  -h, --help Show this help text
16
18
 
17
19
  Notes:
18
20
  - The repo-managed source of truth lives under ./opencode/
19
21
  - This script manages only the wiki-specific OpenCode assets
22
+ - Plugin installation is delegated to sync-opencode-plugin-assets.sh;
23
+ --capability-overlay selects its native plugin only when the master switch is on
24
+ - Command and workflow assets always retain their legacy owners and bytes
20
25
  - Extra live wiki assets are reported as warnings but are not deleted automatically
21
26
  - --apply also removes the legacy gsd-overrides wiki workflow/template files
22
27
  EOF
@@ -26,6 +31,7 @@ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
26
31
  SOURCE_ROOT="$ROOT_DIR/opencode"
27
32
  LIVE_ROOT="${OPENCODE_CONFIG_DIR:-$HOME/.config/opencode}"
28
33
  MODE="check"
34
+ CAPABILITY_OVERLAY=0
29
35
 
30
36
  ASSETS=(
31
37
  "command/wiki-ingest.md"
@@ -68,6 +74,10 @@ while [[ $# -gt 0 ]]; do
68
74
  MODE="apply"
69
75
  shift
70
76
  ;;
77
+ --capability-overlay)
78
+ CAPABILITY_OVERLAY=1
79
+ shift
80
+ ;;
71
81
  --live-root)
72
82
  LIVE_ROOT="$2"
73
83
  shift 2
@@ -84,6 +94,11 @@ while [[ $# -gt 0 ]]; do
84
94
  esac
85
95
  done
86
96
 
97
+ source_for() {
98
+ local rel="$1"
99
+ printf '%s\n' "$SOURCE_ROOT/$rel"
100
+ }
101
+
87
102
  collect_extra_live_assets() {
88
103
  local root="$1"
89
104
  local -A managed=()
@@ -174,7 +189,7 @@ run_check() {
174
189
  ensure_source_assets_exist
175
190
 
176
191
  for rel in "${ASSETS[@]}"; do
177
- src="$SOURCE_ROOT/$rel"
192
+ src=$(source_for "$rel")
178
193
  dst="$LIVE_ROOT/$rel"
179
194
 
180
195
  if [[ ! -f "$dst" ]]; then
@@ -219,7 +234,7 @@ run_apply() {
219
234
  ensure_source_assets_exist
220
235
 
221
236
  for rel in "${ASSETS[@]}"; do
222
- src="$SOURCE_ROOT/$rel"
237
+ src=$(source_for "$rel")
223
238
  dst="$LIVE_ROOT/$rel"
224
239
 
225
240
  mkdir -p "$(dirname "$dst")"
@@ -252,3 +267,9 @@ case "$MODE" in
252
267
  run_apply
253
268
  ;;
254
269
  esac
270
+
271
+ plugin_args=("--$MODE" "--live-root" "$LIVE_ROOT")
272
+ if [[ "$CAPABILITY_OVERLAY" -eq 1 ]]; then
273
+ plugin_args+=("--capability-overlay")
274
+ fi
275
+ "$ROOT_DIR/scripts/sync-opencode-plugin-assets.sh" "${plugin_args[@]}"
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: sync-pi-assets.sh [--check|--apply] [--live-root PATH]
7
+
8
+ Compare or sync the repo-managed Pi extension assets against the live Pi agent
9
+ configuration tree.
10
+
11
+ Options:
12
+ --check Verify managed live assets match the rendered repo copies (default)
13
+ --apply Install managed assets, then verify
14
+ --live-root PATH Override the Pi agent root (default: $PI_CODING_AGENT_DIR or ~/.pi/agent)
15
+ -h, --help Show this help text
16
+ EOF
17
+ }
18
+
19
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
20
+ SOURCE_ROOT="$ROOT_DIR/pi"
21
+ LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
22
+ MODE="check"
23
+ ASSET="extensions/cairnkeep-trajectory.ts"
24
+
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --check) MODE="check"; shift ;;
28
+ --apply) MODE="apply"; shift ;;
29
+ --live-root) LIVE_ROOT="$2"; shift 2 ;;
30
+ -h|--help) usage; exit 0 ;;
31
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
32
+ esac
33
+ done
34
+
35
+ render_asset() {
36
+ sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$SOURCE_ROOT/$ASSET"
37
+ }
38
+
39
+ check_asset() {
40
+ [[ -f "$SOURCE_ROOT/$ASSET" ]] || {
41
+ echo "Missing repo-managed source asset: $SOURCE_ROOT/$ASSET" >&2
42
+ return 2
43
+ }
44
+ [[ -f "$LIVE_ROOT/$ASSET" ]] || {
45
+ echo "Missing live Pi extension under $LIVE_ROOT: $ASSET" >&2
46
+ return 1
47
+ }
48
+ cmp -s <(render_asset) "$LIVE_ROOT/$ASSET" || {
49
+ echo "Out-of-sync live Pi extension under $LIVE_ROOT: $ASSET" >&2
50
+ return 1
51
+ }
52
+ }
53
+
54
+ if [[ "$MODE" == "apply" ]]; then
55
+ [[ -f "$SOURCE_ROOT/$ASSET" ]] || {
56
+ echo "Missing repo-managed source asset: $SOURCE_ROOT/$ASSET" >&2
57
+ exit 2
58
+ }
59
+ mkdir -p "$(dirname "$LIVE_ROOT/$ASSET")"
60
+ if [[ -f "$LIVE_ROOT/$ASSET" ]] && cmp -s <(render_asset) "$LIVE_ROOT/$ASSET"; then
61
+ echo "Applied 0 Pi extension asset(s); 1 already matched."
62
+ else
63
+ rendered_tmp=$(mktemp)
64
+ render_asset > "$rendered_tmp"
65
+ install -m 0644 "$rendered_tmp" "$LIVE_ROOT/$ASSET"
66
+ rm -f "$rendered_tmp"
67
+ echo "Applied 1 Pi extension asset(s); 0 already matched."
68
+ fi
69
+ fi
70
+
71
+ check_asset
72
+ printf 'Pi extension assets are in sync under %s\n' "$LIVE_ROOT"
@@ -26,4 +26,102 @@ grep -q "scripts/memory-wiki-audit.sh --para-root $tmp/PARA --report $tmp/audit.
26
26
  grep -q "OnCalendar=Mon \*-\*-\* 04:00:00" "$tmr" || fail "timer OnCalendar not substituted"
27
27
  grep -q "WantedBy=timers.target" "$tmr" || fail "timer missing [Install] WantedBy"
28
28
 
29
+ # Scheduled note distillation stays in the same separate audit process and is
30
+ # activated only by the effective note capability state.
31
+ npm --prefix "$ROOT/mcp-memory-server" run build >/dev/null
32
+ mkdir -p "$tmp/PARA/Projects/empty-project"
33
+ [[ -x "$ROOT/scripts/memory-wiki-audit.sh" ]] || fail "audit script is not executable for systemd"
34
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store-off" env -u CAIRN_NOTE_DISTILLATION \
35
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$tmp/PARA" >"$tmp/audit-off.md"
36
+ [[ ! -e "$tmp/store-off/notes" ]] || fail "flag-off audit created note storage"
37
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store-on" CAIRN_NOTE_DISTILLATION=1 \
38
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$tmp/PARA" >"$tmp/audit-on.md"
39
+ grep -q "Note distillation" "$tmp/audit-on.md" || fail "flag-on audit omitted note distillation status"
40
+
41
+ managed="$tmp/managed"
42
+ managed_para="$managed/PARA"
43
+ mkdir -p "$managed_para/Projects/empty-project"
44
+
45
+ # Master-off keeps the inherited compatibility path even when staged managed
46
+ # state says otherwise.
47
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities enable notes.distill --json >/dev/null)
48
+ (cd "$managed" && env -u CAIRN_CAPABILITY_CONTRACT -u CAIRN_NOTE_DISTILLATION -u CAIRN_CAPABILITY_NOTES_DISTILL \
49
+ CAIRN_AGENTFS_BASE_DIR="$tmp/store-master-off" \
50
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-master-off.md")
51
+ ! grep -q "Note distillation" "$tmp/audit-master-off.md" || fail "master-off audit consumed staged managed state"
52
+
53
+ # Project state and the derived environment override follow the central
54
+ # environment > project > compatibility precedence.
55
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities disable notes.distill --json >/dev/null)
56
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_NOTE_DISTILLATION=1 \
57
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-managed-disabled.md")
58
+ ! grep -q "Note distillation" "$tmp/audit-managed-disabled.md" || fail "managed disable did not override compatibility enable"
59
+
60
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_NOTES_DISTILL=1 \
61
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-env-enabled.md")
62
+ grep -q "Note distillation: completed" "$tmp/audit-env-enabled.md" || fail "environment enable did not override managed disable"
63
+
64
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities enable notes.distill --json >/dev/null)
65
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_NOTES_DISTILL=0 CAIRN_NOTE_DISTILLATION=1 \
66
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-env-disabled.md")
67
+ ! grep -q "Note distillation" "$tmp/audit-env-disabled.md" || fail "environment disable did not override managed enable"
68
+
69
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities reset notes.distill --json >/dev/null)
70
+ (cd "$managed" && env -u CAIRN_NOTE_DISTILLATION CAIRN_CAPABILITY_CONTRACT=1 \
71
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-reset-off.md")
72
+ ! grep -q "Note distillation" "$tmp/audit-reset-off.md" || fail "reset did not restore inherited default-off state"
73
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_NOTE_DISTILLATION=1 \
74
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-reset-on.md")
75
+ grep -q "Note distillation: completed" "$tmp/audit-reset-on.md" || fail "reset did not restore compatibility enable"
76
+
77
+ # Disabled state performs no note or callback-store work even when logging and
78
+ # trajectory capture are both consented.
79
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities disable notes.distill --json >/dev/null)
80
+ (cd "$managed" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 CAIRN_TRAJECTORY_CAPTURE=1 \
81
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$managed_para" >"$tmp/audit-disabled-consented.md")
82
+ [[ ! -e "$managed/.agentfs/trajectory.db" ]] || fail "disabled audit created callback storage"
83
+
84
+ # Enabled jobs create callback metadata only when all three consents are on.
85
+ logging="$tmp/logging"
86
+ logging_para="$logging/PARA"
87
+ mkdir -p "$logging_para/Projects/empty-project"
88
+ (cd "$logging" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities enable notes.distill --json >/dev/null)
89
+ (cd "$logging" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 \
90
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$logging_para" >"$tmp/audit-no-capture.md")
91
+ [[ ! -e "$logging/.agentfs/trajectory.db" ]] || fail "capture-off audit created callback storage"
92
+ (cd "$logging" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_TRAJECTORY_CAPTURE=1 \
93
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$logging_para" >"$tmp/audit-no-logging.md")
94
+ [[ ! -e "$logging/.agentfs/trajectory.db" ]] || fail "logging-off audit created callback storage"
95
+ (cd "$logging" && CAIRN_CAPABILITY_CONTRACT=1 CAIRN_CAPABILITY_LOGGING=1 CAIRN_TRAJECTORY_CAPTURE=1 \
96
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$logging_para" >"$tmp/audit-logged.md")
97
+ PROJECT="$logging" node --input-type=module -e '
98
+ const { listCapabilityRecords } = await import("./mcp-memory-server/dist/capability-store.js");
99
+ const rows = (await listCapabilityRecords(process.env.PROJECT)).records;
100
+ if (rows.length !== 1 || rows[0].capability_id !== "notes.distill" || rows[0].outcome !== "success"
101
+ || rows[0].source !== "notes-cli" || rows[0].transport !== "local-process") process.exit(1);
102
+ ' || fail "all-consent audit did not create one exact note callback record"
103
+
104
+ # A note child failure remains visible but cannot mask deterministic wiki
105
+ # findings or change their established exit code.
106
+ failure="$tmp/failure"
107
+ failure_para="$failure/PARA"
108
+ wiki="$failure_para/Projects/stale/.planning/wiki"
109
+ mkdir -p "$wiki/sources" "$failure/fake-bin"
110
+ printf '%s\n' '- Last reviewed: 2000-01-01' >"$wiki/sources/stale.md"
111
+ printf '%s\n' '# Index' >"$wiki/index.md"
112
+ (cd "$failure" && CAIRN_CAPABILITY_CONTRACT=1 "$ROOT/bin/cairn" capabilities enable notes.distill --json >/dev/null)
113
+ real_node=$(command -v node)
114
+ printf '%s\n' '#!/usr/bin/env bash' \
115
+ 'if [[ "$1" == *"/note-cli.js" ]]; then exit 42; fi' \
116
+ 'exec "$REAL_NODE" "$@"' >"$failure/fake-bin/node"
117
+ chmod +x "$failure/fake-bin/node"
118
+ set +e
119
+ (cd "$failure" && REAL_NODE="$real_node" PATH="$failure/fake-bin:$PATH" CAIRN_CAPABILITY_CONTRACT=1 \
120
+ "$ROOT/scripts/memory-wiki-audit.sh" --para-root "$failure_para" >"$tmp/audit-failed-note.md")
121
+ audit_status=$?
122
+ set -e
123
+ [[ "$audit_status" == 3 ]] || fail "note failure changed deterministic wiki audit exit status"
124
+ grep -q "stale source pages" "$tmp/audit-failed-note.md" || fail "note failure masked deterministic wiki findings"
125
+ grep -q "Note distillation: failed" "$tmp/audit-failed-note.md" || fail "note failure status was not reported"
126
+
29
127
  echo "PASS: audit-timer render (placeholders substituted, units well-formed)"
@@ -11,6 +11,36 @@ fail() {
11
11
  exit 1
12
12
  }
13
13
 
14
+ capability_env_keys='CAIRN_CAPABILITY_CONTRACT CAIRN_CAPABILITY_LOGGING CAIRN_CAPABILITY_MEMORY_WRITE CAIRN_CAPABILITY_MEMORY_SEARCH CAIRN_CAPABILITY_NOTES_DISTILL CAIRN_CAPABILITY_WIKI CAIRN_CAPABILITY_GRAPH CAIRN_CAPABILITY_SECURITY_AUDIT CAIRN_CAPABILITY_ROUTE_CHECK CAIRN_CAPABILITY_CONTEXT_EXPLORE'
15
+ for key in $capability_env_keys; do
16
+ [[ $(grep -cxF "# $key=" "$ROOT/templates/env.example.template") -eq 1 ]] ||
17
+ fail "environment template must document exactly one commented $key entry"
18
+ if grep -q "^[[:space:]]*$key=" "$ROOT/templates/env.example.template"; then
19
+ fail "environment template enabled $key by default"
20
+ fi
21
+ done
22
+ node - "$ROOT/mcp-memory-server/src/capability-registry.ts" "$ROOT/templates/env.example.template" <<'NODE'
23
+ const fs = require("fs");
24
+ const [registryPath, templatePath] = process.argv.slice(2);
25
+ const registry = fs.readFileSync(registryPath, "utf8");
26
+ const template = fs.readFileSync(templatePath, "utf8");
27
+ const ids = [...registry.matchAll(/^\s*id: "([^"]+)",$/gm)].map((match) => match[1]);
28
+ const derived = ids.map((id) => `CAIRN_CAPABILITY_${id.toUpperCase().replaceAll(".", "_")}`);
29
+ const documented = [...template.matchAll(/^# (CAIRN_CAPABILITY_[A-Z_]+)=$/gm)]
30
+ .map((match) => match[1])
31
+ .filter((name) => name !== "CAIRN_CAPABILITY_CONTRACT" && name !== "CAIRN_CAPABILITY_LOGGING");
32
+ if (JSON.stringify(documented) !== JSON.stringify(derived)) process.exit(1);
33
+ NODE
34
+ grep -qF 'Strict booleans: 1 | true | yes | on; 0 | false | no | off.' "$ROOT/templates/env.example.template" ||
35
+ fail "environment template omitted strict capability boolean guidance"
36
+ grep -qF 'CAIRN_NOTE_DISTILLATION remains a compatibility input' "$ROOT/templates/env.example.template" ||
37
+ fail "environment template omitted the existing note compatibility input"
38
+ grep -qF 'CAIRN_TRAJECTORY_CAPTURE is separate required consent' "$ROOT/templates/env.example.template" ||
39
+ fail "environment template omitted independent trajectory consent"
40
+ if grep -Eqi '(prisma|drizzle|typeorm|sequelize|db[[:space:]]+push|schema[[:space:]-]+push|migrat(e|ion))' "$ROOT/scripts/bootstrap.sh"; then
41
+ fail "bootstrap contains an ORM, schema-push, or migration path"
42
+ fi
43
+
14
44
  # --untracked outside a git repo fails before creating anything
15
45
  mkdir "$tmp/plain"
16
46
  if "$ROOT/scripts/bootstrap.sh" --untracked "$tmp/plain" >/dev/null 2>&1; then
@@ -29,11 +59,28 @@ git -C "$tmp/repo" init -q
29
59
  grep -qxF "/.ai/" "$tmp/repo/.git/info/exclude" || fail "missing /.ai/ exclude entry"
30
60
  grep -qxF "/.planning/" "$tmp/repo/.git/info/exclude" || fail "missing /.planning/ exclude entry"
31
61
  grep -qxF "/.agentfs/" "$tmp/repo/.git/info/exclude" || fail "missing /.agentfs/ exclude entry"
62
+ [[ -f "$tmp/repo/.ai/capabilities.json" ]] || fail "capability configuration missing"
63
+ [[ ! -e "$tmp/repo/.agentfs/trajectory.db" ]] || fail "fresh bootstrap created a callback database"
64
+
65
+ # Existing scaffold and operator configuration bytes are preserved on rerun.
66
+ printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$tmp/repo/.ai/capabilities.json"
67
+ chmod 600 "$tmp/repo/.ai/capabilities.json"
68
+ printf '%s\n' '# operator-owned environment' >"$tmp/repo/.ai/env.example"
69
+ asset_paths='.ai/start-claude.sh .ai/start-opencode.sh .ai/start-pi.sh .ai/env.example .ai/trajectory-redaction.json .ai/capabilities.json .agentfs/.gitignore .planning/config.json .planning/wiki/index.md .planning/wiki/policy.md .planning/wiki/CONTRADICTIONS.md .planning/wiki/LOG.md .planning/alignment/policy.md .planning/alignment/gap-register.yaml .planning/graphs/policy.md .planning/graphs/.gitignore .planning/security/policy.md'
70
+ mkdir -p "$tmp/assets-before"
71
+ for path in $asset_paths; do
72
+ cp "$tmp/repo/$path" "$tmp/assets-before/${path//\//__}"
73
+ done
32
74
 
33
75
  # Re-run is idempotent: no duplicate exclude entries
34
76
  "$ROOT/scripts/bootstrap.sh" --untracked "$tmp/repo" >/dev/null
35
77
  [[ $(grep -cxF "/.ai/" "$tmp/repo/.git/info/exclude") -eq 1 ]] || fail "duplicate exclude entries"
36
78
  [[ $(grep -cxF "/.agentfs/" "$tmp/repo/.git/info/exclude") -eq 1 ]] || fail "duplicate .agentfs exclude entry"
79
+ for path in $asset_paths; do
80
+ [[ -e "$tmp/repo/$path" ]] || fail "bootstrap rerun deleted installed asset: $path"
81
+ cmp -s "$tmp/assets-before/${path//\//__}" "$tmp/repo/$path" || fail "bootstrap rerun rewrote installed asset: $path"
82
+ done
83
+ [[ ! -e "$tmp/repo/.agentfs/trajectory.db" ]] || fail "bootstrap rerun created a callback database"
37
84
 
38
85
  # Default mode is unchanged: scaffold stays visible to git
39
86
  mkdir "$tmp/repo2"
@@ -41,5 +88,6 @@ git -C "$tmp/repo2" init -q
41
88
  "$ROOT/scripts/bootstrap.sh" "$tmp/repo2" >/dev/null
42
89
  git -C "$tmp/repo2" status --porcelain | grep -q "\.ai/" || fail "default mode should leave the scaffold tracked"
43
90
  git -C "$tmp/repo2" status --porcelain | grep -q "\.agentfs/" || fail "default mode should track the memory ignore guard"
91
+ [[ ! -e "$tmp/repo2/.agentfs/trajectory.db" ]] || fail "default bootstrap created a callback database"
44
92
 
45
93
  echo "PASS: bootstrap --untracked contributor mode"
@@ -4,21 +4,60 @@ set -euo pipefail
4
4
  ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
5
  fail() { echo "FAIL: $1" >&2; exit 1; }
6
6
  cairn="$ROOT/bin/cairn"
7
+ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
7
8
 
8
9
  "$cairn" help | grep -q "cairn doctor" || fail "help missing doctor"
9
10
  "$cairn" help | grep -q "cairn memory" || fail "help missing memory"
10
11
  "$cairn" help | grep -q "cairn audit-timer" || fail "help missing audit-timer"
11
12
  "$cairn" help | grep -q "cairn sync" || fail "help missing sync"
13
+ "$cairn" help | grep -q "cairn sync-pi" || fail "help missing sync-pi"
12
14
  "$cairn" help | grep -q "cairn memory-server" || fail "help missing memory-server"
15
+ "$cairn" help | grep -q "cairn notes" || fail "help missing notes"
16
+ "$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
17
+ "$cairn" help | grep -q "cairn capabilities <list|status|enable|disable|reset|logging>" || fail "help missing capabilities"
13
18
  "$cairn" sync --help >/dev/null 2>&1 || fail "cairn sync dispatch"
19
+ "$cairn" sync-pi --help >/dev/null 2>&1 || fail "cairn sync-pi dispatch"
20
+ env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
21
+ "$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
22
+ "$cairn" capabilities --help >"$tmp/capabilities-help" 2>/dev/null || fail "cairn capabilities dispatch"
23
+ grep -qxF 'MCP capability changes require a memory-server restart. Operating capability' "$tmp/capabilities-help" || fail "capability help missing exact restart wording"
24
+ grep -qxF 'changes apply on the next invocation.' "$tmp/capabilities-help" || fail "capability help missing exact invocation wording"
25
+ if grep -Eq '^ cairn capabilities (guard|start|finish|doctor)' "$tmp/capabilities-help"; then
26
+ fail "capability help exposed a private operation"
27
+ fi
14
28
  if "$cairn" not-a-command >/dev/null 2>&1; then
15
29
  fail "unknown command should exit non-zero"
16
30
  fi
17
31
 
18
- tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
32
+ mkdir -p "$tmp/prefix/bin" "$tmp/project"
33
+ ln -s "$ROOT/bin/cairn" "$tmp/prefix/bin/cairn"
34
+ (
35
+ cd "$tmp/project"
36
+ env -u CAIRN_CAPABILITY_CONTRACT "$tmp/prefix/bin/cairn" capabilities status --json >"$tmp/status-one.json"
37
+ env -u CAIRN_CAPABILITY_CONTRACT "$tmp/prefix/bin/cairn" capabilities status --json >"$tmp/status-two.json"
38
+ )
39
+ cmp -s "$tmp/status-one.json" "$tmp/status-two.json" || fail "installed capability status JSON is unstable"
40
+ node - "$tmp/status-one.json" <<'NODE'
41
+ const fs = require("fs");
42
+ const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
43
+ const ids = ["memory.write", "memory.search", "notes.distill", "wiki", "graph", "security.audit", "route.check", "context.explore"];
44
+ if (value.schema_version !== 1 || value.contract_enabled !== false) process.exit(1);
45
+ if (JSON.stringify(value.capabilities.map(({ id }) => id)) !== JSON.stringify(ids)) process.exit(1);
46
+ if (!/^[0-9a-f]{64}$/.test(value.configuration_digest)) process.exit(1);
47
+ NODE
48
+ set +e
49
+ (cd "$tmp/project" && "$tmp/prefix/bin/cairn" capabilities PHASE18_RAW_SENTINEL) >"$tmp/unknown.out" 2>"$tmp/unknown.err"
50
+ unknown_status=$?
51
+ set -e
52
+ [[ "$unknown_status" -eq 2 ]] || fail "unknown capability operation should exit 2"
53
+ [[ ! -s "$tmp/unknown.out" ]] || fail "unknown capability operation wrote stdout"
54
+ grep -qxF 'cairn capabilities: invalid command, arguments, or managed state.' "$tmp/unknown.err" || fail "unknown capability operation was not value-free"
55
+ if grep -qF 'PHASE18_RAW_SENTINEL' "$tmp/unknown.out" "$tmp/unknown.err"; then
56
+ fail "unknown capability operation exposed its raw value"
57
+ fi
19
58
  CAIRN_AGENTFS_BASE_DIR="$tmp/store" "$cairn" memory path | grep -qx "$tmp/store" || fail "cairn memory path dispatch"
20
59
  ( cd "$tmp" && "$cairn" doctor ) >/dev/null || fail "cairn doctor dispatch (clean env should exit 0)"
21
60
  "$cairn" audit-timer --render-only "$tmp/u" >/dev/null || fail "cairn audit-timer dispatch"
22
61
  [[ -f "$tmp/u/memory-wiki-audit.timer" ]] || fail "audit-timer render via cairn produced no units"
23
62
 
24
- echo "PASS: cairn dispatch (doctor, memory, audit-timer)"
63
+ echo "PASS: cairn dispatch (doctor, memory, artifact, audit-timer)"