@cairnkeep/cli 2.9.0 → 2.12.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 (86) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +83 -29
  3. package/bin/cairn +6 -165
  4. package/bin/cairn-container +8 -0
  5. package/docs/building-an-overlay.md +42 -5
  6. package/docs/containers.md +10 -0
  7. package/docs/harness-compatibility.md +64 -3
  8. package/docs/learning/CURRICULUM-MAP.md +13 -6
  9. package/docs/learning/FEATURE-GUIDE.md +14 -3
  10. package/docs/learning/README.md +5 -2
  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 +24 -8
  14. package/docs/learning/lessons/L03-first-project.md +34 -15
  15. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  16. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  17. package/docs/learning/lessons/L21-context-packs.md +1 -1
  18. package/docs/learning/lessons/L22-native-windows.md +83 -0
  19. package/docs/learning/lessons/L23-guided-setup.md +142 -0
  20. package/docs/learning/tracks/operator.md +8 -3
  21. package/docs/learning/tracks/quickstart.md +9 -4
  22. package/docs/native-windows.md +136 -0
  23. package/docs/operating.md +142 -20
  24. package/docs/privacy-and-data-flow.md +41 -1
  25. package/docs/quickstart.md +86 -0
  26. package/docs/storage.md +14 -2
  27. package/examples/eval/bundled-fake.json +1 -1
  28. package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
  29. package/mcp-memory-server/dist/artifact-store.js +196 -55
  30. package/mcp-memory-server/dist/capability-cli.js +2 -1
  31. package/mcp-memory-server/dist/capability-config.js +4 -3
  32. package/mcp-memory-server/dist/capability-harness.js +34 -7
  33. package/mcp-memory-server/dist/context-pack.js +84 -25
  34. package/mcp-memory-server/dist/eval-cli.js +6 -5
  35. package/mcp-memory-server/dist/eval-plan.js +35 -13
  36. package/mcp-memory-server/dist/eval-process.js +11 -4
  37. package/mcp-memory-server/dist/eval-report.js +7 -5
  38. package/mcp-memory-server/dist/graph-cli.js +7 -1
  39. package/mcp-memory-server/dist/index.js +19 -20
  40. package/mcp-memory-server/dist/mcp-tool-profile.js +6 -4
  41. package/mcp-memory-server/dist/note-store.js +12 -3
  42. package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
  43. package/mcp-memory-server/dist/platform-security.js +102 -0
  44. package/mcp-memory-server/dist/skill-store.js +27 -12
  45. package/mcp-memory-server/package.json +2 -1
  46. package/package.json +6 -4
  47. package/pi/extensions/cairnkeep-memory.ts +61 -0
  48. package/schemas/cairnkeep-setup-policy.schema.json +55 -0
  49. package/schemas/cairnkeep-setup.schema.json +36 -0
  50. package/scripts/bootstrap.sh +7 -15
  51. package/scripts/cairn-cli.mjs +119 -0
  52. package/scripts/cairn-container-cli.mjs +121 -0
  53. package/scripts/cairn-hook.mjs +199 -0
  54. package/scripts/completion.sh +17 -6
  55. package/scripts/doctor.sh +58 -0
  56. package/scripts/fix-permissions.mjs +12 -0
  57. package/scripts/harness-registry.mjs +126 -0
  58. package/scripts/memory-wiki-audit.mjs +95 -0
  59. package/scripts/phase26-test-manifest.mjs +26 -0
  60. package/scripts/run-repository-tests.mjs +76 -0
  61. package/scripts/setup-core.mjs +312 -0
  62. package/scripts/setup-reconcile.mjs +332 -0
  63. package/scripts/setup.mjs +342 -0
  64. package/scripts/sync-pi-assets.sh +19 -4
  65. package/scripts/test-bootstrap-untracked.sh +2 -1
  66. package/scripts/test-completion.sh +41 -0
  67. package/scripts/test-doctor.sh +66 -0
  68. package/scripts/test-harness-registry.sh +94 -0
  69. package/scripts/test-learning-docs.sh +52 -3
  70. package/scripts/test-package-install.sh +69 -0
  71. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  72. package/scripts/test-phase18-capability-lifecycle.sh +1 -1
  73. package/scripts/test-phase19-eval-lifecycle.sh +12 -2
  74. package/scripts/test-pi-lifecycle.mjs +190 -0
  75. package/scripts/test-setup-compatibility.mjs +187 -0
  76. package/scripts/test-setup-output.sh +131 -0
  77. package/scripts/test-setup-overlay.sh +113 -0
  78. package/scripts/test-setup-preflight.mjs +281 -0
  79. package/scripts/test-setup-reconcile.mjs +257 -0
  80. package/scripts/test-uninstall.sh +50 -0
  81. package/scripts/test-windows-native.mjs +159 -0
  82. package/scripts/uninstall.sh +73 -6
  83. package/scripts/verify-pi-mcp-bridge.mjs +588 -0
  84. package/scripts/windows-platform.mjs +733 -0
  85. package/templates/codex-config.toml.template +5 -0
  86. package/templates/start-codex.sh.template +30 -0
@@ -21,6 +21,7 @@ SOURCE_ROOT="$ROOT_DIR/pi"
21
21
  LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
22
22
  MODE="check"
23
23
  ASSETS=(
24
+ "extensions/cairnkeep-memory.ts"
24
25
  "extensions/cairnkeep-trajectory.ts"
25
26
  "prompts/graphify.md"
26
27
  )
@@ -37,7 +38,7 @@ done
37
38
 
38
39
  render_asset() {
39
40
  local rel="$1"
40
- if [[ "$rel" == "extensions/cairnkeep-trajectory.ts" ]]; then
41
+ if [[ "$rel" == extensions/*.ts ]]; then
41
42
  sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$SOURCE_ROOT/$rel"
42
43
  else
43
44
  cat "$SOURCE_ROOT/$rel"
@@ -74,20 +75,34 @@ ensure_sources
74
75
  if [[ "$MODE" == "apply" ]]; then
75
76
  updated=0
76
77
  unchanged=0
78
+ memory_updated=0
79
+ memory_unchanged=0
77
80
  for rel in "${ASSETS[@]}"; do
78
81
  mkdir -p "$(dirname "$LIVE_ROOT/$rel")"
79
82
  if [[ -f "$LIVE_ROOT/$rel" ]] && cmp -s <(render_asset "$rel") "$LIVE_ROOT/$rel"; then
80
- unchanged=$((unchanged + 1))
83
+ if [[ "$rel" == "extensions/cairnkeep-memory.ts" ]]; then
84
+ memory_unchanged=1
85
+ else
86
+ unchanged=$((unchanged + 1))
87
+ fi
81
88
  continue
82
89
  fi
83
90
  rendered_tmp=$(mktemp)
84
91
  render_asset "$rel" > "$rendered_tmp"
85
92
  install -m 0644 "$rendered_tmp" "$LIVE_ROOT/$rel"
86
93
  rm -f "$rendered_tmp"
87
- updated=$((updated + 1))
94
+ if [[ "$rel" == "extensions/cairnkeep-memory.ts" ]]; then
95
+ memory_updated=1
96
+ else
97
+ updated=$((updated + 1))
98
+ fi
88
99
  done
89
100
  printf 'Applied %s Pi asset(s); %s already matched.\n' "$updated" "$unchanged"
101
+ printf 'Pi memory extension: %s updated; %s already matched.\n' "$memory_updated" "$memory_unchanged"
90
102
  fi
91
103
 
92
- check_assets
104
+ if ! check_assets; then
105
+ echo "Recovery: sync-pi-assets.sh --apply --live-root PATH" >&2
106
+ exit 1
107
+ fi
93
108
  printf 'Pi extension and graph prompt are in sync under %s\n' "$LIVE_ROOT"
@@ -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//\//__}"
@@ -87,10 +87,12 @@ grep -q '#compdef cairn' "$tmp/zsh"
87
87
  grep -q 'complete -c cairn' "$tmp/fish"
88
88
  for shell in bash zsh; do
89
89
  grep -q 'sync-pi' "$tmp/$shell"
90
+ grep -q 'codex' "$tmp/$shell"
90
91
  grep -q 'sync-kimi' "$tmp/$shell"
91
92
  grep -q -- '--live-root' "$tmp/$shell"
92
93
  done
93
94
  grep -q 'sync-pi' "$tmp/fish"
95
+ grep -q 'codex' "$tmp/fish"
94
96
  grep -q 'sync-kimi' "$tmp/fish"
95
97
  grep -q 'not __fish_seen_subcommand_from .*sync-pi sync-kimi' "$tmp/fish"
96
98
  grep -q -- '-l live-root' "$tmp/fish"
@@ -118,5 +120,44 @@ if "$ROOT/bin/cairn" completion unsupported >/dev/null 2>&1; then
118
120
  exit 1
119
121
  fi
120
122
 
123
+ completion_surface_complete=true
124
+ for shell in bash zsh fish; do
125
+ if ! grep -qw 'setup' "$tmp/$shell"; then
126
+ completion_surface_complete=false
127
+ fi
128
+ done
129
+ "$ROOT/bin/cairn" completion powershell >"$tmp/powershell"
130
+ if ! grep -qE "['\"]setup['\"]" "$tmp/powershell"; then
131
+ completion_surface_complete=false
132
+ fi
133
+
134
+ if [[ "$completion_surface_complete" != true ]]; then
135
+ if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
136
+ echo "PHASE26_RED:SETUP_COMPLETION_MISSING"
137
+ exit 86
138
+ fi
139
+ echo "SKIP: guided setup completion surface is not complete"
140
+ git -C "$ROOT" diff --check
141
+ echo "PASS: shell completion generation"
142
+ exit 0
143
+ fi
144
+
145
+ setup_flags='--git --harness --memory --policy --yes --json'
146
+ setup_values='init existing none claude opencode pi kimi qwen local'
147
+ for shell in bash zsh fish powershell; do
148
+ for flag in $setup_flags; do
149
+ grep -q -- "$flag\|${flag#--}" "$tmp/$shell" || {
150
+ echo "setup completion omitted $flag for $shell" >&2
151
+ exit 1
152
+ }
153
+ done
154
+ for value in $setup_values; do
155
+ grep -qF "$value" "$tmp/$shell" || {
156
+ echo "setup completion omitted $value for $shell" >&2
157
+ exit 1
158
+ }
159
+ done
160
+ done
161
+
121
162
  git -C "$ROOT" diff --check
122
163
  echo "PASS: shell completion generation"
@@ -454,3 +454,69 @@ for sentinel in prompt-sentinel model-output-sentinel adapter-stderr-sentinel en
454
454
  done
455
455
 
456
456
  echo "PASS: cairn doctor (existing probes plus value-free capability/evaluation diagnostics)"
457
+
458
+ phase26_doctor_complete=true
459
+ for required in \
460
+ "$ROOT/scripts/setup.mjs" \
461
+ "$ROOT/schemas/cairnkeep-setup.schema.json" \
462
+ "$ROOT/pi/extensions/cairnkeep-memory.ts"; do
463
+ [[ -f "$required" ]] || phase26_doctor_complete=false
464
+ done
465
+ grep -qF 'cairnkeep.json' "$doctor" || phase26_doctor_complete=false
466
+ grep -qF 'cairnkeep-memory.ts' "$doctor" || phase26_doctor_complete=false
467
+
468
+ if [[ "$phase26_doctor_complete" != true ]]; then
469
+ if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
470
+ echo "PHASE26_RED:SETUP_DOCTOR_MISSING"
471
+ exit 86
472
+ fi
473
+ echo "SKIP: setup and Pi doctor diagnostics are not complete"
474
+ exit 0
475
+ fi
476
+
477
+ setup_home="$tmp/setup-home"
478
+ setup_pi="$setup_home/.pi/agent"
479
+ mkdir -p "$setup_home" "$setup_pi"
480
+ HOME="$setup_home" "$ROOT/scripts/sync-pi-assets.sh" --apply --live-root "$setup_pi" >/dev/null
481
+
482
+ complete_project="$tmp/setup-complete"
483
+ HOME="$setup_home" "$ROOT/bin/cairn" setup "$complete_project" \
484
+ --git init --harness claude,pi --memory local --yes --json >/dev/null
485
+ (cd "$complete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-complete.out" 2>&1 \
486
+ || fail "doctor rejected complete guided setup"
487
+ grep -Eq '\[PASS\].*(project )?setup.*complete' "$tmp/setup-complete.out" || fail "doctor omitted complete setup state"
488
+
489
+ limited_project="$tmp/setup-limited"
490
+ HOME="$setup_home" "$ROOT/bin/cairn" setup "$limited_project" \
491
+ --git none --harness claude --memory none --yes --json >/dev/null
492
+ (cd "$limited_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-limited.out" 2>&1 \
493
+ || fail "doctor treated intentional Git-less setup as fatal"
494
+ grep -Eq '\[WARN\].*(setup.*limited|Git-less)' "$tmp/setup-limited.out" || fail "doctor omitted Git-less limited state"
495
+
496
+ incomplete_project="$tmp/setup-incomplete"
497
+ mkdir -p "$incomplete_project/.ai"
498
+ printf '%s\n' '{"schema_version":1,"git":"existing","memory":"local","harnesses":["pi"],"assets":{}}' >"$incomplete_project/.ai/cairnkeep.json"
499
+ set +e
500
+ (cd "$incomplete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-incomplete.out" 2>&1
501
+ incomplete_status=$?
502
+ set -e
503
+ [[ "$incomplete_status" -ne 0 ]] || fail "doctor accepted incomplete setup state"
504
+ grep -Eq '\[FAIL\].*(setup.*incomplete|setup state)' "$tmp/setup-incomplete.out" || fail "doctor omitted incomplete setup diagnosis"
505
+ grep -Eq 'cairn setup .*--git|cairn setup --git' "$tmp/setup-incomplete.out" || fail "incomplete setup lacks exact setup recovery"
506
+
507
+ rm -f "$setup_pi/extensions/cairnkeep-memory.ts"
508
+ set +e
509
+ (cd "$complete_project" && HOME="$setup_home" PI_CODING_AGENT_DIR="$setup_pi" "$doctor") >"$tmp/setup-pi-drift.out" 2>&1
510
+ pi_drift_status=$?
511
+ set -e
512
+ [[ "$pi_drift_status" -ne 0 ]] || fail "doctor accepted missing selected Pi memory extension"
513
+ grep -Eq '\[FAIL\].*Pi.*memory extension|\[FAIL\].*cairnkeep-memory' "$tmp/setup-pi-drift.out" || fail "doctor omitted Pi drift diagnosis"
514
+ grep -qF 'cairn sync-pi --apply' "$tmp/setup-pi-drift.out" || fail "Pi drift lacks exact sync recovery"
515
+
516
+ for output in "$tmp/setup-complete.out" "$tmp/setup-limited.out" "$tmp/setup-incomplete.out" "$tmp/setup-pi-drift.out"; do
517
+ if grep -Eqi 'token[=:]|secret[=:]|credential[=:]|https?://' "$output"; then
518
+ fail "setup doctor exposed unbounded private values"
519
+ fi
520
+ done
521
+
522
+ echo "PASS: complete, limited, incomplete, Pi drift, and recovery doctor contract"
@@ -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"
@@ -16,13 +16,13 @@ grep -qF "**Baseline:** Cairnkeep $version" "$feature_guide"
16
16
  grep -qF 'https://github.com/cairnkeep/cairnkeep-course-labs' "$landing"
17
17
  for checkpoint in course-00-app course-01-bootstrap course-02-memory course-03-quality \
18
18
  course-04-operation course-05-evidence course-06-governance course-07-evaluation \
19
- course-08-graph course-09-skill course-10-trust-context; do
19
+ course-08-graph course-09-skill course-10-trust-context course-11-windows; do
20
20
  grep -qF "\`$checkpoint\`" "$coverage"
21
21
  done
22
22
 
23
23
  ready=0
24
24
  brief=0
25
- for number in $(seq -w 0 21); do
25
+ for number in $(seq -w 0 22); do
26
26
  matches=(docs/learning/lessons/L"$number"-*.md)
27
27
  [[ ${#matches[@]} -eq 1 && -f ${matches[0]} ]] || {
28
28
  echo "learning path must contain exactly one L$number lesson" >&2
@@ -53,7 +53,7 @@ grep -qF '`cairn sync-kimi --apply`' "$graph_lesson"
53
53
  grep -qF '`cairn sync-pi --apply`' "$graph_lesson"
54
54
  grep -qF '`graphify-out/`' "$graph_lesson"
55
55
 
56
- [[ $ready -eq 7 && $brief -eq 15 ]] || {
56
+ [[ $ready -eq 8 && $brief -eq 15 ]] || {
57
57
  echo "unexpected learning status totals: ready=$ready brief=$brief" >&2
58
58
  exit 1
59
59
  }
@@ -94,4 +94,53 @@ for (const file of markdownFiles("docs/learning")) {
94
94
  }
95
95
  NODE
96
96
 
97
+ l23=docs/learning/lessons/L23-guided-setup.md
98
+ quickstart=docs/learning/tracks/quickstart.md
99
+ operator=docs/learning/tracks/operator.md
100
+ phase26_learning_complete=true
101
+ [[ -f "$l23" ]] || phase26_learning_complete=false
102
+ grep -qF 'L23-guided-setup.md' "$quickstart" || phase26_learning_complete=false
103
+ grep -qF 'L23-guided-setup.md' "$operator" || phase26_learning_complete=false
104
+
105
+ if [[ "$phase26_learning_complete" != true ]]; then
106
+ if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
107
+ echo "PHASE26_RED:GUIDED_SETUP_LEARNING_MISSING"
108
+ exit 86
109
+ fi
110
+ echo "SKIP: Ready L23 guided-setup lesson and track routing are not complete"
111
+ echo "PASS: public learning path structure, readiness, links, and version alignment"
112
+ exit 0
113
+ fi
114
+
115
+ grep -q '^\*\*Status:\*\* Ready' "$l23"
116
+ grep -qF "Tested with:** Cairnkeep $version" "$l23"
117
+ for heading in '## Outcome' '## Exercise' '## Recovery exercise' '## Common failures' '## Privacy and trust boundary'; do
118
+ grep -qxF "$heading" "$l23" || {
119
+ echo "L23 missing required heading: $heading" >&2
120
+ exit 1
121
+ }
122
+ done
123
+ for command in \
124
+ 'cairn setup' \
125
+ '--git init' \
126
+ '--git none' \
127
+ '--harness' \
128
+ '--memory' \
129
+ '--yes' \
130
+ 'cairn sync-pi --apply' \
131
+ 'cairn doctor' \
132
+ 'cairn uninstall'; do
133
+ grep -qF -- "$command" "$l23" || {
134
+ echo "L23 missing guided setup command or mode: $command" >&2
135
+ exit 1
136
+ }
137
+ done
138
+ grep -Eqi 'missing|empty' "$l23"
139
+ grep -Eqi 'deterministic|non-interactive|non-TTY' "$l23"
140
+ grep -Eqi 'local.*stdio|stdio.*local' "$l23"
141
+ grep -Eqi 'limited|Git-less' "$l23"
142
+ grep -Eqi 'cancel|shutdown|child process' "$l23"
143
+ grep -Eqi '0\.84\.1' "$l23"
144
+ grep -Eqi 'credentials|secrets|private state' "$l23"
145
+
97
146
  echo "PASS: public learning path structure, readiness, links, and version alignment"
@@ -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" ]] || \
@@ -331,4 +336,68 @@ fi
331
336
  [[ "$(sha256sum "$ROOT/package-lock.json" | cut -d' ' -f1)" == "$root_lock_before" ]] || fail "root lockfile changed during pack/install"
332
337
  [[ "$(sha256sum "$ROOT/mcp-memory-server/package-lock.json" | cut -d' ' -f1)" == "$server_lock_before" ]] || fail "server lockfile changed during pack/install"
333
338
 
339
+ phase26_surface_complete=true
340
+ for required in \
341
+ scripts/setup.mjs \
342
+ scripts/setup-core.mjs \
343
+ scripts/setup-reconcile.mjs \
344
+ schemas/cairnkeep-setup.schema.json \
345
+ schemas/cairnkeep-setup-policy.schema.json \
346
+ mcp-memory-server/dist/pi-mcp-bridge.js \
347
+ pi/extensions/cairnkeep-memory.ts \
348
+ docs/operating.md \
349
+ docs/harness-compatibility.md \
350
+ docs/native-windows.md \
351
+ docs/building-an-overlay.md
352
+ do
353
+ [[ -f "$ROOT/$required" ]] || phase26_surface_complete=false
354
+ done
355
+
356
+ if [[ "$phase26_surface_complete" != true ]]; then
357
+ if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
358
+ echo "PHASE26_RED:SETUP_PACKAGE_MISSING"
359
+ exit 86
360
+ fi
361
+ echo "SKIP: packaged guided setup and Pi bridge surface is not complete"
362
+ echo "PASS: npm tarball installs a self-contained CLI and MCP server"
363
+ exit 0
364
+ fi
365
+
366
+ for required in \
367
+ scripts/setup.mjs \
368
+ scripts/setup-core.mjs \
369
+ scripts/setup-reconcile.mjs \
370
+ schemas/cairnkeep-setup.schema.json \
371
+ schemas/cairnkeep-setup-policy.schema.json \
372
+ mcp-memory-server/dist/pi-mcp-bridge.js \
373
+ pi/extensions/cairnkeep-memory.ts \
374
+ docs/operating.md \
375
+ docs/harness-compatibility.md \
376
+ docs/native-windows.md \
377
+ docs/building-an-overlay.md
378
+ do
379
+ [[ -f "$installed_root/$required" ]] || fail "npm tarball omitted Phase 26 asset $required"
380
+ done
381
+
382
+ installed_setup_target="$tmp/Installed Setup – Unicode"
383
+ (cd "$tmp" && cairn setup "$installed_setup_target" \
384
+ --git none --harness claude,pi --memory local --yes --json) >"$tmp/installed-setup.json" \
385
+ || fail "installed cairn setup failed outside the source checkout"
386
+ node - "$tmp/installed-setup.json" "$installed_setup_target" <<'NODE'
387
+ const fs = require("fs");
388
+ const [resultPath, target] = process.argv.slice(2);
389
+ const result = JSON.parse(fs.readFileSync(resultPath, "utf8"));
390
+ if (result.schema_version !== 1 || result.status !== "limited") process.exit(1);
391
+ if (result.git !== "none" || result.memory !== "local") process.exit(1);
392
+ if (JSON.stringify(result.harnesses) !== JSON.stringify(["claude", "pi"])) process.exit(1);
393
+ if (!fs.existsSync(`${target}/.ai/cairnkeep.json`)) process.exit(1);
394
+ NODE
395
+
396
+ if find "$installed_root" -type f \( \
397
+ -name '.env' -o -name '*.tmp' -o -name '*.bak' -o -name 'cairnkeep.json' \
398
+ -o -path '*/.planning/*' -o -path '*/.agentfs/*' \
399
+ \) | grep -q .; then
400
+ fail "npm tarball included temporary or project-private setup state"
401
+ fi
402
+
334
403
  echo "PASS: npm tarball installs a self-contained CLI and MCP server"
@@ -48,7 +48,7 @@ run_shell_syntax_baseline() {
48
48
  local script
49
49
  while IFS= read -r script; do
50
50
  bash -n "$script" || return 1
51
- done < <(find "$ROOT/bin" "$ROOT/scripts" -type f \( -name '*.sh' -o -name 'cairn' \) | sort)
51
+ done < <(find "$ROOT/bin" "$ROOT/scripts" -type f -name '*.sh' | sort)
52
52
  }
53
53
 
54
54
  run_baseline() {
@@ -142,7 +142,7 @@ run_baseline() {
142
142
  run_baseline_check "$ROOT/scripts/test-completion.sh"
143
143
  run_baseline_check "$ROOT/scripts/test-package-install.sh"
144
144
  run_baseline_check "$ROOT/scripts/test-uninstall.sh"
145
- run_baseline_check bash -n "$ROOT/bin/cairn"
145
+ run_baseline_check node --check "$ROOT/bin/cairn"
146
146
  run_master_off_identity
147
147
  echo "PASS: Phase 18 master-off CLI/package/lifecycle identity baseline"
148
148
  }
@@ -336,8 +336,18 @@ const cancelTask={...tasks[0],id:"task-cancel",input:"cancel"};
336
336
  const cancelPlan=makePlan([cancelTask]);
337
337
  const cancelStore=await reports.createEvalReportStore({root:join(fixtureRoot,"reports"),experiment_id:"cancel-fixture"});
338
338
  const controller=new AbortController();
339
- setTimeout(()=>controller.abort(),100);
340
- const cancelled=await runner.runTwoPassExperiment({plan:cancelPlan,report_store:cancelStore,temporary_root:fixtureRoot,signal:controller.signal,distill_command:{program:process.execPath,args:[distillerScript]}});
339
+ const cancellation=runner.runTwoPassExperiment({plan:cancelPlan,report_store:cancelStore,temporary_root:fixtureRoot,signal:controller.signal,distill_command:{program:process.execPath,args:[distillerScript]}});
340
+ let adapterStarted=false;
341
+ for (let attempt=0; attempt<200; attempt+=1) {
342
+ await new Promise((resolve)=>setTimeout(resolve,25));
343
+ if (existsSync(invocationLog) && readFileSync(invocationLog,"utf8").includes("task-cancel:run1")) {
344
+ adapterStarted=true;
345
+ break;
346
+ }
347
+ }
348
+ assert.equal(adapterStarted,true,"cancellation fixture adapter did not start");
349
+ controller.abort();
350
+ const cancelled=await cancellation;
341
351
  assert.equal(cancelled.report.status,"partial");
342
352
  assert.deepEqual(cancelled.report.observations.map(({terminal_state})=>terminal_state),["cancelled"]);
343
353
  assert.equal(cancelled.report.observations[0].process.cleanup!=="pending",true);