@cairnkeep/cli 2.11.0 → 2.13.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 (66) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +51 -7
  3. package/docs/building-an-overlay.md +1 -1
  4. package/docs/harness-compatibility.md +40 -1
  5. package/docs/learning/CURRICULUM-MAP.md +10 -7
  6. package/docs/learning/FEATURE-GUIDE.md +14 -5
  7. package/docs/learning/README.md +4 -3
  8. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  9. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  10. package/docs/learning/lessons/L02-installation.md +16 -9
  11. package/docs/learning/lessons/L03-first-project.md +34 -15
  12. package/docs/learning/lessons/L13-session-evidence.md +48 -2
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  15. package/docs/learning/lessons/L21-context-packs.md +1 -1
  16. package/docs/learning/lessons/L22-native-windows.md +9 -9
  17. package/docs/learning/lessons/L23-guided-setup.md +6 -2
  18. package/docs/learning/tracks/evidence-and-evaluation.md +2 -1
  19. package/docs/learning/tracks/quickstart.md +5 -4
  20. package/docs/native-windows.md +23 -0
  21. package/docs/operating.md +69 -5
  22. package/docs/privacy-and-data-flow.md +44 -1
  23. package/docs/quickstart.md +91 -0
  24. package/docs/storage.md +26 -0
  25. package/docs/work-evidence.md +80 -0
  26. package/examples/eval/bundled-fake.json +1 -1
  27. package/mcp-memory-server/dist/artifact-cli.js +6 -2
  28. package/mcp-memory-server/dist/index.js +27 -0
  29. package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
  30. package/mcp-memory-server/dist/trajectory-cli.js +9 -3
  31. package/mcp-memory-server/dist/work-evidence-cli.js +121 -0
  32. package/mcp-memory-server/dist/work-evidence-schema.js +121 -0
  33. package/mcp-memory-server/dist/work-evidence-store.js +516 -0
  34. package/mcp-memory-server/package.json +2 -1
  35. package/package.json +2 -1
  36. package/schemas/cairnkeep-setup-policy.schema.json +3 -3
  37. package/schemas/cairnkeep-setup.schema.json +2 -2
  38. package/scripts/bootstrap.sh +7 -15
  39. package/scripts/cairn-cli.mjs +2 -0
  40. package/scripts/completion.sh +19 -9
  41. package/scripts/doctor.sh +35 -5
  42. package/scripts/harness-registry.mjs +126 -0
  43. package/scripts/setup-core.mjs +4 -2
  44. package/scripts/setup-reconcile.mjs +3 -2
  45. package/scripts/setup.mjs +41 -5
  46. package/scripts/test-bootstrap-untracked.sh +2 -1
  47. package/scripts/test-cli-dispatch.sh +3 -1
  48. package/scripts/test-completion.sh +6 -0
  49. package/scripts/test-harness-registry.sh +94 -0
  50. package/scripts/test-package-install.sh +9 -0
  51. package/scripts/test-phase19-eval-lifecycle.sh +12 -2
  52. package/scripts/test-setup-compatibility.mjs +5 -4
  53. package/scripts/test-uninstall.sh +10 -0
  54. package/scripts/test-windows-native.mjs +27 -1
  55. package/scripts/test-work-evidence-cli.sh +89 -0
  56. package/scripts/uninstall.sh +7 -3
  57. package/scripts/verify-docs-parity.sh +36 -0
  58. package/scripts/windows-platform.mjs +28 -7
  59. package/templates/codex-config.toml.template +5 -0
  60. package/templates/env.example.template +10 -0
  61. package/templates/start-claude.sh.template +18 -0
  62. package/templates/start-codex.sh.template +48 -0
  63. package/templates/start-kimi.sh.template +17 -0
  64. package/templates/start-opencode.sh.template +17 -0
  65. package/templates/start-pi.sh.template +17 -0
  66. package/templates/start-qwen.sh.template +17 -0
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
+ harnesses=$(node "$CAIRN_ROOT/scripts/harness-registry.mjs" ids)
4
6
  shell=${1:-}
5
7
 
6
8
  case "$shell" in
7
9
  bash)
8
- cat <<'EOF'
10
+ sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
9
11
  _cairn_complete() {
10
12
  local current previous commands
11
13
  COMPREPLY=()
12
14
  current=${COMP_WORDS[COMP_CWORD]}
13
15
  previous=${COMP_WORDS[COMP_CWORD-1]:-}
14
- commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help"
16
+ commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help"
15
17
  if (( COMP_CWORD == 1 )); then
16
18
  COMPREPLY=( $(compgen -W "$commands" -- "$current") )
17
19
  return
18
20
  fi
19
21
  case "${COMP_WORDS[1]}" in
20
22
  bootstrap) COMPREPLY=( $(compgen -W "--untracked" -- "$current") ) ;;
21
- setup) COMPREPLY=( $(compgen -W "--git --harness --memory --policy --yes --json init existing none claude opencode pi kimi qwen local" -- "$current") ) ;;
23
+ setup) COMPREPLY=( $(compgen -W "--git --harness --memory --policy --yes --json init existing none __CAIRN_HARNESSES__ local" -- "$current") ) ;;
22
24
  sync) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
23
25
  sync-pi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
24
26
  sync-kimi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
25
27
  doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
26
28
  trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
27
29
  artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
30
+ evidence) COMPREPLY=( $(compgen -W "list show delete prune doctor --status --json --dry-run --repair" -- "$current") ) ;;
28
31
  capabilities)
29
32
  case "${COMP_WORDS[2]:-}" in
30
33
  enable|disable)
@@ -78,7 +81,7 @@ complete -F _cairn_complete cairn
78
81
  EOF
79
82
  ;;
80
83
  zsh)
81
- cat <<'EOF'
84
+ sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
82
85
  #compdef cairn
83
86
  _cairn() {
84
87
  local -a commands
@@ -92,6 +95,7 @@ _cairn() {
92
95
  'doctor:check runtime dependencies and endpoints'
93
96
  'trajectory:inspect and prune local session trajectories'
94
97
  'artifact:inspect, delete, and prune local artifacts'
98
+ 'evidence:inspect and manage local Git-linked work evidence'
95
99
  'capabilities:inspect and manage project capability state'
96
100
  'mcp-tools:inspect and restrict MCP tool exposure'
97
101
  'pack:manage immutable context packs and skill approvals'
@@ -112,7 +116,7 @@ _cairn() {
112
116
  fi
113
117
  case $words[2] in
114
118
  bootstrap) _arguments '--untracked[keep scaffold out of Git]' '*:project directory:_files -/' ;;
115
- setup) _arguments '*:project directory:_files -/' '--git[Git mode]:mode:(init existing none)' '--harness[selected harnesses]:harnesses:(claude opencode pi kimi qwen)' '--memory[memory mode]:mode:(local none)' '--policy[setup policy]:file:_files' '--yes[confirm setup]' '--json[emit JSON]' ;;
119
+ setup) _arguments '*:project directory:_files -/' '--git[Git mode]:mode:(init existing none)' '--harness[selected harnesses]:harnesses:(__CAIRN_HARNESSES__)' '--memory[memory mode]:mode:(local none)' '--policy[setup policy]:file:_files' '--yes[confirm setup]' '--json[emit JSON]' ;;
116
120
  sync) _arguments '--apply[apply changes]' '--live-root[project Claude root]:directory:_files -/' ;;
117
121
  sync-pi) _arguments '--apply[apply changes]' '--live-root[Pi agent root]:directory:_files -/' ;;
118
122
  sync-kimi) _arguments '--apply[apply changes]' '--live-root[Kimi Code root]:directory:_files -/' ;;
@@ -127,6 +131,7 @@ _cairn() {
127
131
  *) _values 'artifact command' list show delete prune ;;
128
132
  esac
129
133
  ;;
134
+ evidence) _values 'work evidence command' list show delete prune doctor '--status[filter records]:status:(pending complete)' '--dry-run[report without mutation]' '--repair[remove safe temporary remnants]' '--json[emit JSON]' ;;
130
135
  mcp-tools) _values 'MCP tool profile command' list status set reset full read-only custom '--tool[allow an exact tool]:tool:' '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
131
136
  pack) _values 'context pack command' init lock validate install list show remove enable disable update skills approve-skill revoke-skill '--ref[pinned Git ref]:ref:' '--project[project root]:directory:_files -/' '--project-id[remote project ID]:project ID:' '--check[inspect update]' '--apply[apply update]' '--confirm[confirm digest]:digest:' '--json[emit JSON]' ;;
132
137
  capabilities)
@@ -175,10 +180,10 @@ _cairn "$@"
175
180
  EOF
176
181
  ;;
177
182
  fish)
178
- cat <<'EOF'
183
+ sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
179
184
  complete -c cairn -f
180
- for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help
181
- complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help" -a $command
185
+ for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help
186
+ complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help" -a $command
182
187
  end
183
188
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
184
189
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
@@ -188,7 +193,7 @@ complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l apply
188
193
  complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l live-root -r
189
194
  complete -c cairn -n "__fish_seen_subcommand_from bootstrap" -l untracked
190
195
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l git -r -a "init existing none"
191
- complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "claude opencode pi kimi qwen"
196
+ complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "__CAIRN_HARNESSES__"
192
197
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l memory -r -a "local none"
193
198
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l policy -r
194
199
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l yes
@@ -202,6 +207,11 @@ complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subc
202
207
  complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from list show delete prune" -l json
203
208
  complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from delete prune" -l dry-run
204
209
  complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from prune" -l include-protected
210
+ complete -c cairn -n "__fish_seen_subcommand_from evidence" -a "list show delete prune doctor"
211
+ complete -c cairn -n "__fish_seen_subcommand_from evidence; and __fish_seen_subcommand_from list" -l status -r -a "pending complete"
212
+ complete -c cairn -n "__fish_seen_subcommand_from evidence; and __fish_seen_subcommand_from list show delete prune doctor" -l json
213
+ complete -c cairn -n "__fish_seen_subcommand_from evidence; and __fish_seen_subcommand_from delete prune" -l dry-run
214
+ complete -c cairn -n "__fish_seen_subcommand_from evidence; and __fish_seen_subcommand_from doctor" -l repair
205
215
  complete -c cairn -n "__fish_seen_subcommand_from capabilities; and not __fish_seen_subcommand_from list status enable disable reset logging" -a "list status enable disable reset logging"
206
216
  complete -c cairn -n "__fish_seen_subcommand_from capabilities; and __fish_seen_subcommand_from enable disable; and not __fish_seen_subcommand_from memory.write memory.search notes.distill wiki graph security.audit route.check context.explore" -a "memory.write memory.search notes.distill wiki graph security.audit route.check context.explore"
207
217
  complete -c cairn -n "__fish_seen_subcommand_from capabilities; and __fish_seen_subcommand_from reset; and not __fish_seen_subcommand_from memory.write memory.search notes.distill wiki graph security.audit route.check context.explore" -a "memory.write memory.search notes.distill wiki graph security.audit route.check context.explore"
package/scripts/doctor.sh CHANGED
@@ -201,7 +201,36 @@ try {
201
201
  [[ $artifact_status -eq 0 || "$artifact_state" == "broken" ]] || true
202
202
  fi
203
203
 
204
- # 9. Typed metadata overlays and canonical-note transaction state. The internal
204
+ # 9. Project-local Git-linked work-evidence records. Absence is healthy because
205
+ # capture is opt-in. Repair removes safe temporary remnants only; it never
206
+ # finalizes an interrupted session or invents a Git state.
207
+ evidence_cli="$CAIRN_ROOT/mcp-memory-server/dist/work-evidence-cli.js"
208
+ if [[ ! -f "$evidence_cli" ]]; then
209
+ fail "work-evidence diagnostics unavailable — rebuild mcp-memory-server"
210
+ else
211
+ evidence_args=(doctor --json)
212
+ [[ $REPAIR_STORES -eq 1 ]] && evidence_args+=(--repair)
213
+ evidence_json=$(node "$evidence_cli" "${evidence_args[@]}" 2>/dev/null)
214
+ evidence_status=$?
215
+ evidence_state=$(node -e '
216
+ try {
217
+ const value = JSON.parse(process.argv[1])
218
+ if (!value.exists) process.stdout.write("absent")
219
+ else if (value.ok && value.repaired) process.stdout.write("repaired")
220
+ else if (value.ok) process.stdout.write("ok")
221
+ else process.stdout.write("broken")
222
+ } catch { process.stdout.write("invalid") }
223
+ ' "$evidence_json" 2>/dev/null)
224
+ case "$evidence_state" in
225
+ absent) skip "work-evidence store (not present — capture is opt-in)" ;;
226
+ repaired) pass "work-evidence store repaired (temporary remnants removed; evidence preserved)" ;;
227
+ ok) pass "work-evidence records and append-only links are valid" ;;
228
+ *) fail "work-evidence validation failed — inspect with: cairn evidence doctor --json" ;;
229
+ esac
230
+ [[ $evidence_status -eq 0 || "$evidence_state" == "broken" ]] || true
231
+ fi
232
+
233
+ # 10. Typed metadata overlays and canonical-note transaction state. The internal
205
234
  # JSON seam enumerates only contained local databases and the one notes root;
206
235
  # raw KV cells and unmarked Markdown bytes are never repair inputs.
207
236
  node_cli="$CAIRN_ROOT/mcp-memory-server/dist/node-cli.js"
@@ -443,18 +472,19 @@ fi
443
472
  # repairing project or machine assets. The Node controller returns only bounded
444
473
  # states and exact recovery commands; setup remains the sole owner of project
445
474
  # reconciliation.
446
- setup_diagnosis=$(node --input-type=module - "$CAIRN_ROOT/scripts/setup.mjs" "$PWD" <<'NODE' 2>/dev/null
475
+ setup_diagnosis=$(node --input-type=module - "$CAIRN_ROOT/scripts/setup.mjs" "$CAIRN_ROOT/scripts/harness-registry.mjs" "$PWD" <<'NODE' 2>/dev/null
447
476
  import { pathToFileURL } from "node:url";
448
477
  import { readFileSync } from "node:fs";
449
478
  import { join } from "node:path";
450
479
  const module = await import(pathToFileURL(process.argv[2]).href);
451
- const value = module.diagnoseSetup(process.argv[3]);
480
+ const registry = await import(pathToFileURL(process.argv[3]).href);
481
+ const value = module.diagnoseSetup(process.argv[4]);
452
482
  const allowed = new Set(["absent", "complete", "limited", "incomplete"]);
453
483
  if (value.schema_version !== 1 || !allowed.has(value.status) || !Array.isArray(value.recovery)) process.exit(1);
454
484
  let piSelected = false;
455
485
  if (value.status === "complete" || value.status === "limited") {
456
- const state = JSON.parse(readFileSync(join(process.argv[3], ".ai", "cairnkeep.json"), "utf8"));
457
- if (!Array.isArray(state.harnesses) || state.harnesses.some((name) => !["claude", "opencode", "pi", "kimi", "qwen"].includes(name))) process.exit(1);
486
+ const state = JSON.parse(readFileSync(join(process.argv[4], ".ai", "cairnkeep.json"), "utf8"));
487
+ if (!Array.isArray(state.harnesses) || state.harnesses.some((name) => !registry.HARNESS_IDS.includes(name))) process.exit(1);
458
488
  piSelected = state.harnesses.includes("pi");
459
489
  }
460
490
  process.stdout.write(`${value.status}:${value.code}:${value.recovery.join("|")}:${piSelected ? "yes" : "no"}`);
@@ -0,0 +1,126 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ /**
4
+ * Declarative registry for every CLI harness supported by project setup.
5
+ *
6
+ * Keep harness-specific project assets and lifecycle guidance here. Consumers
7
+ * must derive choices from this registry instead of maintaining local lists.
8
+ */
9
+ const DEFINITIONS = [
10
+ {
11
+ id: "claude",
12
+ title: "Claude Code",
13
+ launcher: { template: "start-claude.sh.template", path: ".ai/start-claude.sh", mode: 0o755 },
14
+ project_assets: [],
15
+ machine_sync: "cairn sync --apply",
16
+ },
17
+ {
18
+ id: "opencode",
19
+ title: "OpenCode",
20
+ launcher: { template: "start-opencode.sh.template", path: ".ai/start-opencode.sh", mode: 0o755 },
21
+ project_assets: [],
22
+ machine_sync: "cairn sync --apply",
23
+ },
24
+ {
25
+ id: "pi",
26
+ title: "Pi",
27
+ launcher: { template: "start-pi.sh.template", path: ".ai/start-pi.sh", mode: 0o755 },
28
+ project_assets: [],
29
+ machine_sync: "cairn sync-pi --apply",
30
+ },
31
+ {
32
+ id: "kimi",
33
+ title: "Kimi Code",
34
+ launcher: { template: "start-kimi.sh.template", path: ".ai/start-kimi.sh", mode: 0o755 },
35
+ project_assets: [],
36
+ machine_sync: "cairn sync-kimi --apply",
37
+ },
38
+ {
39
+ id: "qwen",
40
+ title: "Qwen Code",
41
+ launcher: { template: "start-qwen.sh.template", path: ".ai/start-qwen.sh", mode: 0o755 },
42
+ project_assets: [],
43
+ machine_sync: null,
44
+ },
45
+ {
46
+ id: "codex",
47
+ title: "Codex CLI",
48
+ launcher: { template: "start-codex.sh.template", path: ".ai/start-codex.sh", mode: 0o755 },
49
+ project_assets: [
50
+ {
51
+ template: "codex-config.toml.template",
52
+ path: ".codex/config.toml",
53
+ mode: 0o644,
54
+ memory: "local",
55
+ // An existing operator-owned TOML file is valid when doctor finds the
56
+ // exact MCP table; it must not become setup/uninstall-owned.
57
+ required: false,
58
+ },
59
+ ],
60
+ machine_sync: null,
61
+ },
62
+ ];
63
+
64
+ function freezeDefinition(definition) {
65
+ const launcher = Object.freeze({ ...definition.launcher });
66
+ const projectAssets = Object.freeze(definition.project_assets.map((asset) => Object.freeze({ ...asset })));
67
+ return Object.freeze({ ...definition, launcher, project_assets: projectAssets });
68
+ }
69
+
70
+ export const HARNESS_REGISTRY = Object.freeze(DEFINITIONS.map(freezeDefinition));
71
+ export const HARNESS_IDS = Object.freeze(HARNESS_REGISTRY.map(({ id }) => id));
72
+
73
+ export function harnessDefinition(id) {
74
+ return HARNESS_REGISTRY.find((definition) => definition.id === id) ?? null;
75
+ }
76
+
77
+ export function harnessProjectAssets(id, memory = "local") {
78
+ const definition = harnessDefinition(id);
79
+ if (!definition) throw new Error(`Unknown harness: ${id}.`);
80
+ return Object.freeze([
81
+ definition.launcher,
82
+ ...definition.project_assets.filter((asset) => !asset.memory || asset.memory === memory),
83
+ ]);
84
+ }
85
+
86
+ export function requiredHarnessAssetPaths(harnesses, memory = "local") {
87
+ return Object.freeze(harnesses.flatMap((id) => harnessProjectAssets(id, memory)
88
+ .filter((asset) => asset.required !== false)
89
+ .map(({ path }) => path)));
90
+ }
91
+
92
+ export function machineSyncCommand(harnesses) {
93
+ const commands = harnesses.map(harnessDefinition).map((definition) => definition?.machine_sync).filter(Boolean);
94
+ if (commands.includes("cairn sync-pi --apply")) return "cairn sync-pi --apply";
95
+ return commands[0] ?? null;
96
+ }
97
+
98
+ function validateRegistry() {
99
+ if (new Set(HARNESS_IDS).size !== HARNESS_IDS.length) throw new Error("Harness registry contains duplicate IDs.");
100
+ for (const definition of HARNESS_REGISTRY) {
101
+ if (!/^[a-z][a-z0-9-]*$/.test(definition.id) || !definition.title) throw new Error("Invalid harness registry entry.");
102
+ const assets = [definition.launcher, ...definition.project_assets];
103
+ if (new Set(assets.map(({ path }) => path)).size !== assets.length) throw new Error(`Duplicate project asset for ${definition.id}.`);
104
+ for (const asset of assets) {
105
+ if (!asset.template || !asset.path || ![0o600, 0o644, 0o755].includes(asset.mode)) throw new Error(`Invalid project asset for ${definition.id}.`);
106
+ }
107
+ }
108
+ }
109
+
110
+ validateRegistry();
111
+
112
+ // The legacy bootstrap is a shell script for Bash 3.2 portability. This bounded
113
+ // output lets it consume the same registry without parsing JavaScript or JSON.
114
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
115
+ if (!new Set(["bootstrap-assets", "ids"]).has(process.argv[2])) {
116
+ console.error("Usage: node harness-registry.mjs bootstrap-assets|ids");
117
+ process.exitCode = 2;
118
+ } else if (process.argv[2] === "ids") {
119
+ process.stdout.write(`${HARNESS_IDS.join(" ")}\n`);
120
+ } else {
121
+ for (const definition of HARNESS_REGISTRY) {
122
+ const asset = definition.launcher;
123
+ process.stdout.write(`${asset.template}\t${asset.path}\t${asset.mode.toString(8)}\n`);
124
+ }
125
+ }
126
+ }
@@ -3,12 +3,13 @@ import { accessSync } from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { dirname, join, parse, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
6
7
 
7
8
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const TEMPLATE_ROOT = join(ROOT, "templates");
9
10
  const POLICY_LIMIT_BYTES = 1024 * 1024;
10
11
  const GIT_MODES = Object.freeze(["init", "existing", "none"]);
11
- const HARNESSES = Object.freeze(["claude", "opencode", "pi", "kimi", "qwen"]);
12
+ const HARNESSES = HARNESS_IDS;
12
13
  const MEMORY_MODES = Object.freeze(["local", "none"]);
13
14
 
14
15
  const COMMON_ASSETS = Object.freeze([
@@ -293,7 +294,8 @@ export function buildSetupPlan({ target, preflight, choices }) {
293
294
  if (choices.git !== "none" && preflight.gitExecutable === "missing") throw operational("missing-git", "The selected Git mode requires the Git executable.");
294
295
  const harnesses = HARNESSES.filter((name) => choices.harnesses.includes(name));
295
296
  const assets = [
296
- ...harnesses.map((harness) => templateAsset(`start-${harness}.sh.template`, `.ai/start-${harness}.sh`, 0o755, harness)),
297
+ ...harnesses.flatMap((harness) => harnessProjectAssets(harness, choices.memory)
298
+ .map(({ template, path, mode }) => templateAsset(template, path, mode, harness))),
297
299
  ...COMMON_ASSETS.map(([template, path, mode]) => templateAsset(template, path, mode)),
298
300
  ];
299
301
  const version = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version;
@@ -16,16 +16,17 @@ import {
16
16
  import { open, rename } from "node:fs/promises";
17
17
  import { spawnSync } from "node:child_process";
18
18
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
19
+ import { HARNESS_IDS } from "./harness-registry.mjs";
19
20
 
20
21
  export const SETUP_STATE_SCHEMA_VERSION = 1;
21
22
 
22
- const HARNESSES = Object.freeze(["claude", "opencode", "pi", "kimi", "qwen"]);
23
+ const HARNESSES = HARNESS_IDS;
23
24
  const GIT_MODES = Object.freeze(["init", "existing", "none"]);
24
25
  const MEMORY_MODES = Object.freeze(["local", "none"]);
25
26
  const STATE_KEYS = Object.freeze(["schema_version", "cairnkeep_version", "git", "memory", "harnesses", "assets"]);
26
27
  const ASSET_KEYS = Object.freeze(["digest", "mode", "template"]);
27
28
  const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
28
- const RELATIVE_PATH_PATTERN = /^\.(?:ai|planning|agentfs)\/(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
29
+ const RELATIVE_PATH_PATTERN = /^\.(?:ai|planning|agentfs|codex)\/(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
29
30
  const TEMPLATE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
30
31
 
31
32
  export function hashSetupAsset(bytes) {
package/scripts/setup.mjs CHANGED
@@ -21,13 +21,14 @@ import {
21
21
  resolveSetupChoices,
22
22
  } from "./setup-core.mjs";
23
23
  import { reconcileSetupPlan } from "./setup-reconcile.mjs";
24
+ import { HARNESS_IDS, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
24
25
 
25
- const HARNESSES = Object.freeze(["claude", "opencode", "pi", "kimi", "qwen"]);
26
+ const HARNESSES = HARNESS_IDS;
26
27
  const GIT_MODES = Object.freeze(["init", "existing", "none"]);
27
28
  const MEMORY_MODES = Object.freeze(["local", "none"]);
28
29
  const STATE_KEYS = Object.freeze(["schema_version", "cairnkeep_version", "git", "memory", "harnesses", "assets"]);
29
30
  const ASSET_KEYS = Object.freeze(["digest", "mode", "template"]);
30
- const SAFE_ASSET = /^\.(?:ai|planning|agentfs)\/(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
31
+ const SAFE_ASSET = /^\.(?:ai|planning|agentfs|codex)\/(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/;
31
32
  const COMMON_SETUP_ASSETS = Object.freeze([
32
33
  ".ai/env.example",
33
34
  ".ai/trajectory-redaction.json",
@@ -108,7 +109,7 @@ function setupRecovery(plan) {
108
109
  }
109
110
 
110
111
  function resultFor(plan, reconciliation, platform) {
111
- const machineCommand = plan.harnesses.includes("pi") ? "cairn sync-pi --apply" : "cairn sync --apply";
112
+ const machineCommand = machineSyncCommand(plan.harnesses);
112
113
  return Object.freeze({
113
114
  schema_version: 1,
114
115
  status: plan.limited ? "limited" : "complete",
@@ -139,7 +140,12 @@ export function renderSetupResult(result, stream) {
139
140
  for (const command of result.launch_commands) writeLine(stream, `Launch: ${command}`);
140
141
  for (const command of result.recovery) writeLine(stream, `Recovery: ${command}`);
141
142
  for (const limitation of result.limitations) writeLine(stream, `Limitation: ${limitation}`);
142
- writeLine(stream, `Machine sync: ${result.machine_sync.command} (not run automatically)`);
143
+ if (result.harnesses.includes("codex") && result.memory === "local") {
144
+ writeLine(stream, "Codex: review .codex/config.toml and accept the project trust prompt before use");
145
+ }
146
+ writeLine(stream, result.machine_sync.command
147
+ ? `Machine sync: ${result.machine_sync.command} (not run automatically)`
148
+ : "Machine sync: not required for the selected harnesses");
143
149
  }
144
150
 
145
151
  function createTarget(target) {
@@ -260,6 +266,33 @@ function readPrivateState(path) {
260
266
  }
261
267
  }
262
268
 
269
+ function hasCodexMemoryConfig(project) {
270
+ const path = join(project, ".codex", "config.toml");
271
+ if (!existsSync(path)) return false;
272
+ const info = lstatSync(path);
273
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return false;
274
+ let descriptor;
275
+ let text;
276
+ try {
277
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
278
+ const opened = fstatSync(descriptor);
279
+ if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) return false;
280
+ text = readFileSync(descriptor, "utf8");
281
+ } finally {
282
+ if (descriptor !== undefined) closeSync(descriptor);
283
+ }
284
+ const lines = text.split(/\r?\n/);
285
+ const headers = lines.flatMap((line, index) => (
286
+ ["[mcp_servers.cairn-memory]", "[mcp_servers.\"cairn-memory\"]"].includes(line.trim()) ? [index] : []
287
+ ));
288
+ if (headers.length !== 1) return false;
289
+ const [start] = headers;
290
+ const next = lines.findIndex((line, index) => index > start && /^\s*\[[^\]]+\]\s*$/.test(line));
291
+ const section = lines.slice(start + 1, next < 0 ? lines.length : next).join("\n");
292
+ return (section.match(/^\s*command\s*=\s*["']cairn["']\s*$/gm) ?? []).length === 1
293
+ && (section.match(/^\s*args\s*=\s*\[\s*["']memory-server["']\s*\]\s*$/gm) ?? []).length === 1;
294
+ }
295
+
263
296
  export function diagnoseSetup(target = ".") {
264
297
  const project = resolve(target);
265
298
  const statePath = join(project, ".ai", "cairnkeep.json");
@@ -292,8 +325,11 @@ export function diagnoseSetup(target = ".") {
292
325
  const digest = createHash("sha256").update(readFileSync(destination)).digest("hex");
293
326
  if (digest !== record.digest || (process.platform !== "win32" && (info.mode & 0o777) !== record.mode)) return incompleteDiagnosis(recovery);
294
327
  }
295
- const requiredAssets = [...COMMON_SETUP_ASSETS, ...state.harnesses.map((harness) => `.ai/start-${harness}.sh`)];
328
+ const requiredAssets = [...COMMON_SETUP_ASSETS, ...requiredHarnessAssetPaths(state.harnesses, state.memory)];
296
329
  if (requiredAssets.some((path) => !Object.hasOwn(state.assets, path))) return incompleteDiagnosis(recovery);
330
+ if (state.memory === "local" && state.harnesses.includes("codex") && !hasCodexMemoryConfig(project)) {
331
+ return incompleteDiagnosis(recovery);
332
+ }
297
333
  return Object.freeze({
298
334
  schema_version: 1,
299
335
  status: state.git === "none" ? "limited" : "complete",
@@ -55,6 +55,7 @@ git -C "$tmp/repo" init -q
55
55
  [[ -f "$tmp/repo/.ai/start-claude.sh" ]] || fail "scaffold missing"
56
56
  [[ -x "$tmp/repo/.ai/start-kimi.sh" ]] || fail "Kimi launcher missing"
57
57
  [[ -x "$tmp/repo/.ai/start-qwen.sh" ]] || fail "Qwen launcher missing"
58
+ [[ -x "$tmp/repo/.ai/start-codex.sh" ]] || fail "Codex launcher missing"
58
59
  [[ -f "$tmp/repo/.planning/config.json" ]] || fail "planning layer missing"
59
60
  graph_policy="$tmp/repo/.planning/graphs/policy.md"
60
61
  grep -qF '`uv tool install graphifyy`' "$graph_policy" ||
@@ -89,7 +90,7 @@ grep -qxF "/.agentfs/" "$tmp/repo/.git/info/exclude" || fail "missing /.agentfs/
89
90
  printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$tmp/repo/.ai/capabilities.json"
90
91
  chmod 600 "$tmp/repo/.ai/capabilities.json"
91
92
  printf '%s\n' '# operator-owned environment' >"$tmp/repo/.ai/env.example"
92
- asset_paths='.ai/start-claude.sh .ai/start-opencode.sh .ai/start-pi.sh .ai/start-kimi.sh .ai/start-qwen.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'
93
+ asset_paths='.ai/start-claude.sh .ai/start-opencode.sh .ai/start-pi.sh .ai/start-kimi.sh .ai/start-qwen.sh .ai/start-codex.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'
93
94
  mkdir -p "$tmp/assets-before"
94
95
  for path in $asset_paths; do
95
96
  cp "$tmp/repo/$path" "$tmp/assets-before/${path//\//__}"
@@ -15,6 +15,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
15
15
  "$cairn" help | grep -q "cairn memory-server" || fail "help missing memory-server"
16
16
  "$cairn" help | grep -q "cairn notes" || fail "help missing notes"
17
17
  "$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
18
+ "$cairn" help | grep -q "cairn evidence <list|show|delete|prune|doctor>" || fail "help missing evidence"
18
19
  "$cairn" help | grep -q "cairn capabilities <list|status|enable|disable|reset|logging>" || fail "help missing capabilities"
19
20
  "$cairn" help | grep -q "cairn mcp-tools <list|status|set|reset>" || fail "help missing mcp-tools"
20
21
  "$cairn" help | grep -q "cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>" || fail "help missing pack"
@@ -24,6 +25,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
24
25
  "$cairn" sync-kimi --help >/dev/null 2>&1 || fail "cairn sync-kimi dispatch"
25
26
  env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
26
27
  "$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
28
+ "$cairn" evidence --help | grep -q "cairn evidence list" || fail "cairn evidence dispatch"
27
29
  "$cairn" capabilities --help >"$tmp/capabilities-help" 2>/dev/null || fail "cairn capabilities dispatch"
28
30
  "$cairn" mcp-tools list --json >"$tmp/mcp-tools.json" || fail "cairn mcp-tools dispatch"
29
31
  CAIRN_PACK_BASE_DIR="$tmp/pack-store" "$cairn" pack list --json >"$tmp/packs.json" || fail "cairn pack dispatch"
@@ -70,4 +72,4 @@ CAIRN_AGENTFS_BASE_DIR="$tmp/store" "$cairn" memory path | grep -qx "$tmp/store"
70
72
  "$cairn" audit-timer --render-only "$tmp/u" >/dev/null || fail "cairn audit-timer dispatch"
71
73
  [[ -f "$tmp/u/memory-wiki-audit.timer" ]] || fail "audit-timer render via cairn produced no units"
72
74
 
73
- echo "PASS: cairn dispatch (doctor, memory, artifact, audit-timer)"
75
+ echo "PASS: cairn dispatch (doctor, memory, artifact, evidence, audit-timer)"
@@ -48,6 +48,9 @@ for shell in bash zsh fish; do
48
48
  grep -q 'pack' "$tmp/$shell"
49
49
  grep -q 'init.*lock.*validate.*install.*list.*show.*remove.*enable.*disable.*update.*skills.*approve-skill.*revoke-skill\|init lock validate install list show remove enable disable update skills approve-skill revoke-skill' "$tmp/$shell"
50
50
  grep -q -- '--purge-packs\|-l purge-packs' "$tmp/$shell"
51
+ grep -q 'evidence' "$tmp/$shell"
52
+ grep -q 'list.*show.*delete.*prune.*doctor\|list show delete prune doctor' "$tmp/$shell"
53
+ grep -q -- '--status\|-l status' "$tmp/$shell"
51
54
  done
52
55
 
53
56
  "$ROOT/bin/cairn" help >"$tmp/root-help"
@@ -64,6 +67,7 @@ if grep -q 'doctor-diagnosis' "$tmp/eval-help"; then
64
67
  exit 1
65
68
  fi
66
69
  grep -qF 'cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>' "$tmp/root-help"
70
+ grep -qF 'cairn evidence <list|show|delete|prune|doctor>' "$tmp/root-help"
67
71
  node "$ROOT/mcp-memory-server/dist/skill-cli.js" --help >"$tmp/skill-help"
68
72
  for command in $skill_commands; do
69
73
  grep -q "cairn skill $command" "$tmp/skill-help"
@@ -87,10 +91,12 @@ grep -q '#compdef cairn' "$tmp/zsh"
87
91
  grep -q 'complete -c cairn' "$tmp/fish"
88
92
  for shell in bash zsh; do
89
93
  grep -q 'sync-pi' "$tmp/$shell"
94
+ grep -q 'codex' "$tmp/$shell"
90
95
  grep -q 'sync-kimi' "$tmp/$shell"
91
96
  grep -q -- '--live-root' "$tmp/$shell"
92
97
  done
93
98
  grep -q 'sync-pi' "$tmp/fish"
99
+ grep -q 'codex' "$tmp/fish"
94
100
  grep -q 'sync-kimi' "$tmp/fish"
95
101
  grep -q 'not __fish_seen_subcommand_from .*sync-pi sync-kimi' "$tmp/fish"
96
102
  grep -q -- '-l live-root' "$tmp/fish"
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+
8
+ node --input-type=module - "$ROOT" "$tmp" <<'NODE'
9
+ import assert from "node:assert/strict";
10
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { spawnSync } from "node:child_process";
13
+ import { pathToFileURL } from "node:url";
14
+
15
+ const [root, sandbox] = process.argv.slice(2);
16
+ const registry = await import(pathToFileURL(join(root, "scripts", "harness-registry.mjs")).href);
17
+ assert.deepEqual(registry.HARNESS_IDS, ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
18
+ assert.equal(new Set(registry.HARNESS_IDS).size, registry.HARNESS_IDS.length);
19
+ for (const id of registry.HARNESS_IDS) {
20
+ const definition = registry.harnessDefinition(id);
21
+ assert.equal(definition.id, id);
22
+ assert.ok(definition.title);
23
+ assert.match(definition.launcher.path, new RegExp(`^\\.ai/start-${id}\\.sh$`));
24
+ }
25
+ assert.deepEqual(registry.harnessProjectAssets("codex", "local").map(({ path }) => path), [
26
+ ".ai/start-codex.sh",
27
+ ".codex/config.toml",
28
+ ]);
29
+ assert.deepEqual(registry.requiredHarnessAssetPaths(["codex"], "local"), [".ai/start-codex.sh"]);
30
+ assert.deepEqual(registry.requiredHarnessAssetPaths(["codex"], "none"), [".ai/start-codex.sh"]);
31
+ assert.equal(registry.machineSyncCommand(["codex"]), null);
32
+
33
+ const schema = JSON.parse(readFileSync(join(root, "schemas", "cairnkeep-setup.schema.json"), "utf8"));
34
+ assert.deepEqual(schema.properties.harnesses.items.enum, registry.HARNESS_IDS, "setup schema drifted from harness registry");
35
+ const policySchema = JSON.parse(readFileSync(join(root, "schemas", "cairnkeep-setup-policy.schema.json"), "utf8"));
36
+ assert.deepEqual(policySchema.properties.defaults.properties.harnesses.items.enum, registry.HARNESS_IDS);
37
+ assert.deepEqual(policySchema.properties.constraints.properties.harnesses.items.enum, registry.HARNESS_IDS);
38
+ assert.deepEqual(policySchema.properties.constraints.properties.required_harnesses.items.enum, registry.HARNESS_IDS);
39
+
40
+ const target = join(sandbox, "codex-project");
41
+ const setup = spawnSync(join(root, "bin", "cairn"), [
42
+ "setup", target, "--git", "init", "--harness", "codex", "--memory", "local", "--yes", "--json",
43
+ ], { encoding: "utf8", shell: false });
44
+ assert.equal(setup.status, 0, setup.stderr);
45
+ const result = JSON.parse(setup.stdout);
46
+ assert.deepEqual(result.harnesses, ["codex"]);
47
+ assert.equal(result.machine_sync.command, null);
48
+ assert.deepEqual(result.launch_commands, [".ai/start-codex.sh"]);
49
+ assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
50
+
51
+ const fakeBin = join(sandbox, "bin");
52
+ mkdirSync(fakeBin);
53
+ const fakeCodex = join(fakeBin, "codex");
54
+ writeFileSync(fakeCodex, "#!/bin/sh\nprintf '%s\\n' \"$PWD\" \"${CAIRN_REGISTRY_TEST:-}\" \"$*\"\n");
55
+ chmodSync(fakeCodex, 0o755);
56
+ writeFileSync(join(target, ".ai", ".env"), "CAIRN_REGISTRY_TEST=loaded\n", { mode: 0o600 });
57
+ const launch = spawnSync(join(target, ".ai", "start-codex.sh"), ["--fixture"], {
58
+ encoding: "utf8",
59
+ shell: false,
60
+ env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}` },
61
+ });
62
+ assert.equal(launch.status, 0, launch.stderr);
63
+ assert.deepEqual(launch.stdout.trim().split("\n"), [target, "loaded", "--fixture"]);
64
+
65
+ const noMemory = join(sandbox, "codex-no-memory");
66
+ const noMemorySetup = spawnSync(join(root, "bin", "cairn"), [
67
+ "setup", noMemory, "--git", "init", "--harness", "codex", "--memory", "none", "--yes",
68
+ ], { encoding: "utf8", shell: false });
69
+ assert.equal(noMemorySetup.status, 0, noMemorySetup.stderr);
70
+ assert.throws(() => readFileSync(join(noMemory, ".codex", "config.toml")), /ENOENT/);
71
+
72
+ const collision = join(sandbox, "codex-existing-config");
73
+ mkdirSync(join(collision, ".codex"), { recursive: true });
74
+ writeFileSync(join(collision, ".codex", "config.toml"), "[features]\nfixture = true\n");
75
+ const collisionSetup = spawnSync(join(root, "bin", "cairn"), [
76
+ "setup", collision, "--git", "init", "--harness", "codex", "--memory", "local", "--yes", "--json",
77
+ ], { encoding: "utf8", shell: false });
78
+ assert.equal(collisionSetup.status, 0, collisionSetup.stderr);
79
+ assert.equal(JSON.parse(collisionSetup.stdout).counts.skipped, 1);
80
+ assert.equal(readFileSync(join(collision, ".codex", "config.toml"), "utf8"), "[features]\nfixture = true\n");
81
+ const setupModule = await import(pathToFileURL(join(root, "scripts", "setup.mjs")).href);
82
+ assert.equal(setupModule.diagnoseSetup(collision).status, "incomplete");
83
+ writeFileSync(join(collision, ".codex", "config.toml"), [
84
+ "[features]", "fixture = true", "", "[mcp_servers.cairn-memory]", 'command = "cairn"', 'args = ["memory-server"]', "",
85
+ ].join("\n"));
86
+ assert.equal(setupModule.diagnoseSetup(collision).status, "complete");
87
+ const uninstall = spawnSync(join(root, "bin", "cairn"), ["uninstall", "--yes", collision], {
88
+ encoding: "utf8", shell: false, env: { ...process.env, HOME: join(sandbox, "home") },
89
+ });
90
+ assert.equal(uninstall.status, 0, uninstall.stderr);
91
+ assert.match(readFileSync(join(collision, ".codex", "config.toml"), "utf8"), /fixture = true/);
92
+ NODE
93
+
94
+ echo "PASS: declarative harness registry and first-class Codex setup contract"
@@ -46,6 +46,7 @@ bootstrap_output=$(cairn bootstrap "$tmp/project")
46
46
  [[ -x "$tmp/project/.ai/start-pi.sh" ]] || fail "bootstrap did not install an executable Pi launcher"
47
47
  [[ -x "$tmp/project/.ai/start-kimi.sh" ]] || fail "bootstrap did not install an executable Kimi launcher"
48
48
  [[ -x "$tmp/project/.ai/start-qwen.sh" ]] || fail "bootstrap did not install an executable Qwen launcher"
49
+ [[ -x "$tmp/project/.ai/start-codex.sh" ]] || fail "bootstrap did not install an executable Codex launcher"
49
50
  [[ -f "$tmp/project/.planning/config.json" ]] || fail "bootstrap did not install the planning scaffold"
50
51
 
51
52
  (cd "$tmp/project" && cairn doctor) >/dev/null || fail "installed package failed cairn doctor"
@@ -267,6 +268,10 @@ NODE
267
268
  fail "npm tarball omitted the Kimi launcher template"
268
269
  [[ -f "$installed_root/templates/start-qwen.sh.template" ]] || \
269
270
  fail "npm tarball omitted the Qwen launcher template"
271
+ [[ -f "$installed_root/templates/start-codex.sh.template" ]] || \
272
+ fail "npm tarball omitted the Codex launcher template"
273
+ [[ -f "$installed_root/templates/codex-config.toml.template" ]] || \
274
+ fail "npm tarball omitted the Codex MCP configuration template"
270
275
  [[ -f "$installed_root/schemas/note.schema.json" ]] || \
271
276
  fail "npm tarball omitted the note schema"
272
277
  [[ -f "$installed_root/schemas/memory-node.schema.json" ]] || \
@@ -287,6 +292,10 @@ for module in artifact-schema artifact-store compaction-normalize artifact-cli;
287
292
  [[ -f "$installed_root/mcp-memory-server/dist/$module.js" ]] || \
288
293
  fail "npm tarball omitted compiled $module"
289
294
  done
295
+ for module in work-evidence-schema work-evidence-store work-evidence-cli; do
296
+ [[ -f "$installed_root/mcp-memory-server/dist/$module.js" ]] || \
297
+ fail "npm tarball omitted compiled $module"
298
+ done
290
299
  [[ -x "$installed_root/claude/hooks/compaction-capture.sh" ]] || \
291
300
  fail "npm tarball omitted the executable Claude compaction hook"
292
301
  [[ -f "$installed_root/opencode/plugins/memory-capture.ts" ]] || \