@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.
- package/CHANGELOG.md +33 -1
- package/README.md +51 -7
- package/docs/building-an-overlay.md +1 -1
- package/docs/harness-compatibility.md +40 -1
- package/docs/learning/CURRICULUM-MAP.md +10 -7
- package/docs/learning/FEATURE-GUIDE.md +14 -5
- package/docs/learning/README.md +4 -3
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +16 -9
- package/docs/learning/lessons/L03-first-project.md +34 -15
- package/docs/learning/lessons/L13-session-evidence.md +48 -2
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +9 -9
- package/docs/learning/lessons/L23-guided-setup.md +6 -2
- package/docs/learning/tracks/evidence-and-evaluation.md +2 -1
- package/docs/learning/tracks/quickstart.md +5 -4
- package/docs/native-windows.md +23 -0
- package/docs/operating.md +69 -5
- package/docs/privacy-and-data-flow.md +44 -1
- package/docs/quickstart.md +91 -0
- package/docs/storage.md +26 -0
- package/docs/work-evidence.md +80 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-cli.js +6 -2
- package/mcp-memory-server/dist/index.js +27 -0
- package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
- package/mcp-memory-server/dist/trajectory-cli.js +9 -3
- package/mcp-memory-server/dist/work-evidence-cli.js +121 -0
- package/mcp-memory-server/dist/work-evidence-schema.js +121 -0
- package/mcp-memory-server/dist/work-evidence-store.js +516 -0
- package/mcp-memory-server/package.json +2 -1
- package/package.json +2 -1
- package/schemas/cairnkeep-setup-policy.schema.json +3 -3
- package/schemas/cairnkeep-setup.schema.json +2 -2
- package/scripts/bootstrap.sh +7 -15
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +19 -9
- package/scripts/doctor.sh +35 -5
- package/scripts/harness-registry.mjs +126 -0
- package/scripts/setup-core.mjs +4 -2
- package/scripts/setup-reconcile.mjs +3 -2
- package/scripts/setup.mjs +41 -5
- package/scripts/test-bootstrap-untracked.sh +2 -1
- package/scripts/test-cli-dispatch.sh +3 -1
- package/scripts/test-completion.sh +6 -0
- package/scripts/test-harness-registry.sh +94 -0
- package/scripts/test-package-install.sh +9 -0
- package/scripts/test-phase19-eval-lifecycle.sh +12 -2
- package/scripts/test-setup-compatibility.mjs +5 -4
- package/scripts/test-uninstall.sh +10 -0
- package/scripts/test-windows-native.mjs +27 -1
- package/scripts/test-work-evidence-cli.sh +89 -0
- package/scripts/uninstall.sh +7 -3
- package/scripts/verify-docs-parity.sh +36 -0
- package/scripts/windows-platform.mjs +28 -7
- package/templates/codex-config.toml.template +5 -0
- package/templates/env.example.template +10 -0
- package/templates/start-claude.sh.template +18 -0
- package/templates/start-codex.sh.template +48 -0
- package/templates/start-kimi.sh.template +17 -0
- package/templates/start-opencode.sh.template +17 -0
- package/templates/start-pi.sh.template +17 -0
- package/templates/start-qwen.sh.template +17 -0
package/scripts/completion.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:(
|
|
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
|
-
|
|
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 "
|
|
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.
|
|
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
|
|
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[
|
|
457
|
-
if (!Array.isArray(state.harnesses) || state.harnesses.some((name) => !
|
|
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
|
+
}
|
package/scripts/setup-core.mjs
CHANGED
|
@@ -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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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" ]] || \
|