@cairnkeep/cli 2.11.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.
- package/CHANGELOG.md +21 -1
- package/README.md +31 -6
- package/docs/building-an-overlay.md +1 -1
- package/docs/harness-compatibility.md +34 -1
- package/docs/learning/CURRICULUM-MAP.md +8 -7
- package/docs/learning/FEATURE-GUIDE.md +9 -4
- package/docs/learning/README.md +3 -2
- 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/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/quickstart.md +5 -4
- package/docs/native-windows.md +14 -0
- package/docs/operating.md +26 -5
- package/docs/privacy-and-data-flow.md +8 -1
- package/docs/quickstart.md +86 -0
- package/examples/eval/bundled-fake.json +1 -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/completion.sh +8 -6
- package/scripts/doctor.sh +5 -4
- 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-completion.sh +2 -0
- package/scripts/test-harness-registry.sh +94 -0
- package/scripts/test-package-install.sh +5 -0
- package/scripts/test-phase19-eval-lifecycle.sh +12 -2
- package/scripts/test-setup-compatibility.mjs +5 -4
- package/scripts/test-windows-native.mjs +24 -1
- package/scripts/uninstall.sh +7 -3
- package/scripts/windows-platform.mjs +13 -5
- package/templates/codex-config.toml.template +5 -0
- package/templates/start-codex.sh.template +30 -0
|
@@ -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"
|
|
@@ -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" ]] || \
|
|
@@ -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
|
-
|
|
340
|
-
|
|
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);
|
|
@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
|
|
|
18
18
|
import { dirname, join, relative, resolve } from "node:path";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
20
|
import { bootstrapWindows } from "./windows-platform.mjs";
|
|
21
|
+
import { HARNESS_IDS } from "./harness-registry.mjs";
|
|
21
22
|
|
|
22
23
|
const EXPECTED_RED_EXIT = 86;
|
|
23
24
|
const RED_MARKER = "PHASE26_RED:SETUP_RECONCILE_MISSING";
|
|
@@ -25,8 +26,8 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
25
26
|
const root = resolve(here, "..");
|
|
26
27
|
const setupCorePath = join(here, "setup-core.mjs");
|
|
27
28
|
const setupReconcilePath = join(here, "setup-reconcile.mjs");
|
|
28
|
-
const HARNESSES =
|
|
29
|
-
const MANAGED_ROOTS = [".ai", ".planning", ".agentfs"];
|
|
29
|
+
const HARNESSES = HARNESS_IDS;
|
|
30
|
+
const MANAGED_ROOTS = [".ai", ".codex", ".planning", ".agentfs"];
|
|
30
31
|
|
|
31
32
|
function run(executable, args, options = {}) {
|
|
32
33
|
const result = spawnSync(executable, args, {
|
|
@@ -141,9 +142,9 @@ async function testSetupCompatibility(production, sandbox, legacy) {
|
|
|
141
142
|
choices: Object.freeze({ git: "existing", harnesses: HARNESSES, memory: "local", confirmed: true }),
|
|
142
143
|
});
|
|
143
144
|
const result = await production.reconcile.reconcileSetupPlan(plan);
|
|
144
|
-
assert.deepEqual(result.counts, { created: legacy.posixBefore.size, updated: 0, unchanged: 0, skipped: 0 });
|
|
145
|
+
assert.deepEqual(result.counts, { created: legacy.posixBefore.size + 1, updated: 0, unchanged: 0, skipped: 0 });
|
|
145
146
|
const current = snapshot(target);
|
|
146
|
-
assertSnapshotEqual(current, legacy.posixBefore, "deterministic setup", [".ai/cairnkeep.json"]);
|
|
147
|
+
assertSnapshotEqual(current, legacy.posixBefore, "deterministic setup", [".ai/cairnkeep.json", ".codex/config.toml"]);
|
|
147
148
|
assert.ok(current.has(".ai/cairnkeep.json"), "setup state missing");
|
|
148
149
|
const state = JSON.parse(current.get(".ai/cairnkeep.json").bytes.toString("utf8"));
|
|
149
150
|
assert.deepEqual(state.harnesses, HARNESSES);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runWindowsCommand,
|
|
17
17
|
} from "./windows-platform.mjs";
|
|
18
18
|
import { runNativeContainer } from "./cairn-container-cli.mjs";
|
|
19
|
+
import { HARNESS_IDS } from "./harness-registry.mjs";
|
|
19
20
|
|
|
20
21
|
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
21
22
|
const sandbox = mkdtempSync(join(tmpdir(), "cairn-windows-native-"));
|
|
@@ -28,7 +29,7 @@ try {
|
|
|
28
29
|
const project = join(sandbox, "Project with spaces – Unicode");
|
|
29
30
|
mkdirSync(project, { recursive: true });
|
|
30
31
|
bootstrapWindows(root, [project]);
|
|
31
|
-
for (const harness of
|
|
32
|
+
for (const harness of HARNESS_IDS) {
|
|
32
33
|
assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
|
|
33
34
|
}
|
|
34
35
|
assert.match(readFileSync(join(project, ".ai", "start-claude.cmd"), "utf8"), /start-harness\.ps1/);
|
|
@@ -125,6 +126,28 @@ try {
|
|
|
125
126
|
assert.match(powershellCompletion(), /--policy/);
|
|
126
127
|
assert.match(powershellCompletion(), /--yes/);
|
|
127
128
|
assert.match(powershellCompletion(), /--json/);
|
|
129
|
+
assert.match(powershellCompletion(), /codex/);
|
|
130
|
+
|
|
131
|
+
const codexProject = join(sandbox, "Guided Codex Project");
|
|
132
|
+
let codexOutput = "";
|
|
133
|
+
process.stdout.write = ((chunk) => {
|
|
134
|
+
codexOutput += String(chunk);
|
|
135
|
+
return true;
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
await runWindowsCommand({
|
|
139
|
+
command: "setup",
|
|
140
|
+
args: [codexProject, "--git", "none", "--harness", "codex", "--memory", "local", "--yes", "--json"],
|
|
141
|
+
root,
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
process.stdout.write = originalWrite;
|
|
145
|
+
}
|
|
146
|
+
const codexResult = JSON.parse(codexOutput);
|
|
147
|
+
assert.deepEqual(codexResult.harnesses, ["codex"]);
|
|
148
|
+
assert.equal(codexResult.machine_sync.command, null);
|
|
149
|
+
assert.ok(existsSync(join(codexProject, ".ai", "start-codex.cmd")));
|
|
150
|
+
assert.match(readFileSync(join(codexProject, ".codex", "config.toml"), "utf8"), /mcp_servers\.cairn-memory/);
|
|
128
151
|
console.log("PASS: simulated Windows setup parity, private state, recovery, and completion contract");
|
|
129
152
|
}
|
|
130
153
|
} finally {
|
package/scripts/uninstall.sh
CHANGED
|
@@ -45,6 +45,7 @@ PROJECT_AI_FILES=(
|
|
|
45
45
|
start-pi.sh
|
|
46
46
|
start-kimi.sh
|
|
47
47
|
start-qwen.sh
|
|
48
|
+
start-codex.sh
|
|
48
49
|
env.example
|
|
49
50
|
trajectory-redaction.json
|
|
50
51
|
capabilities.json
|
|
@@ -129,9 +130,11 @@ remove_path() {
|
|
|
129
130
|
# record. The record identifies candidates, but never grants authority to an
|
|
130
131
|
# arbitrary path. Modified candidates are still backed up before removal.
|
|
131
132
|
setup_owned_paths() {
|
|
132
|
-
node --input-type=module - "$1" <<'NODE'
|
|
133
|
+
node --input-type=module - "$1" "$CAIRN_ROOT/scripts/harness-registry.mjs" <<'NODE'
|
|
133
134
|
import { lstatSync, readFileSync } from "node:fs";
|
|
135
|
+
import { pathToFileURL } from "node:url";
|
|
134
136
|
const path = process.argv[2];
|
|
137
|
+
const registry = await import(pathToFileURL(process.argv[3]).href);
|
|
135
138
|
const info = lstatSync(path);
|
|
136
139
|
if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) process.exit(1);
|
|
137
140
|
if (process.platform !== "win32" && (info.mode & 0o777) !== 0o600) process.exit(1);
|
|
@@ -153,7 +156,8 @@ const allowed = new Set([
|
|
|
153
156
|
".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
|
|
154
157
|
".planning/graphs/policy.md", ".planning/graphs/.gitignore", ".planning/security/policy.md",
|
|
155
158
|
".ai/start-harness.mjs", ".ai/start-harness.ps1",
|
|
156
|
-
...
|
|
159
|
+
...registry.HARNESS_IDS.flatMap((name) => [`.ai/start-${name}.sh`, `.ai/start-${name}.cmd`]),
|
|
160
|
+
...registry.HARNESS_IDS.flatMap((name) => registry.harnessProjectAssets(name, "local").map(({ path }) => path)),
|
|
157
161
|
]);
|
|
158
162
|
for (const [asset, record] of Object.entries(state.assets)) {
|
|
159
163
|
if (!allowed.has(asset)
|
|
@@ -321,7 +325,7 @@ for proj in "${PROJECTS[@]:-}"; do
|
|
|
321
325
|
if [[ $DRY_RUN -eq 0 ]]; then
|
|
322
326
|
for managed_dir in \
|
|
323
327
|
"$proj/.planning/wiki" "$proj/.planning/alignment" "$proj/.planning/graphs" \
|
|
324
|
-
"$proj/.planning/security" "$proj/.planning" "$proj/.ai" "$proj/.agentfs"; do
|
|
328
|
+
"$proj/.planning/security" "$proj/.planning" "$proj/.codex" "$proj/.ai" "$proj/.agentfs"; do
|
|
325
329
|
rmdir "$managed_dir" 2>/dev/null || true
|
|
326
330
|
done
|
|
327
331
|
fi
|
|
@@ -20,6 +20,7 @@ import { gzipSync, gunzipSync } from "node:zlib";
|
|
|
20
20
|
import { spawnSync } from "node:child_process";
|
|
21
21
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
22
22
|
import { homedir, tmpdir } from "node:os";
|
|
23
|
+
import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
|
|
23
24
|
|
|
24
25
|
const BOOTSTRAP_FILES = [
|
|
25
26
|
["env.example.template", ".ai/env.example"],
|
|
@@ -39,7 +40,7 @@ const BOOTSTRAP_FILES = [
|
|
|
39
40
|
["security-policy.md.template", ".planning/security/policy.md"],
|
|
40
41
|
];
|
|
41
42
|
|
|
42
|
-
const HARNESSES =
|
|
43
|
+
const HARNESSES = HARNESS_IDS;
|
|
43
44
|
const HOOK_EVENTS = new Map([
|
|
44
45
|
["memory-wakeup", ["SessionStart", ""]],
|
|
45
46
|
["memory-capture", ["SessionEnd", ""]],
|
|
@@ -249,7 +250,8 @@ export function bootstrapWindows(root, args) {
|
|
|
249
250
|
installFile(join(templates, source), join(target, ...destination.split("/")), destination.endsWith("capabilities.json") ? 0o600 : 0o644);
|
|
250
251
|
}
|
|
251
252
|
for (const harness of HARNESSES) {
|
|
252
|
-
|
|
253
|
+
const asset = harnessProjectAssets(harness, "local")[0];
|
|
254
|
+
installFile(join(templates, asset.template), join(target, ...asset.path.split("/")), asset.mode);
|
|
253
255
|
const cmdPath = join(target, ".ai", `start-${harness}.cmd`);
|
|
254
256
|
if (!existsSync(cmdPath)) atomicWrite(cmdPath, windowsLauncher(harness), 0o755);
|
|
255
257
|
}
|
|
@@ -267,7 +269,9 @@ export function bootstrapWindows(root, args) {
|
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
console.log(`\nCairnkeep bootstrapped into ${target}`);
|
|
270
|
-
console.log(`
|
|
272
|
+
console.log(`Codex: run 'cairn setup' in the project and select Codex, review trust, then use ${join(target, ".ai", "start-codex.cmd")}`);
|
|
273
|
+
console.log(`Claude: register 'cairn memory-server', run 'cairn sync --apply', then use ${join(target, ".ai", "start-claude.cmd")}`);
|
|
274
|
+
console.log("For new projects, prefer 'cairn setup'; guide: docs/quickstart.md");
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
function walkFiles(root, predicate = () => true, prefix = "") {
|
|
@@ -616,7 +620,10 @@ function uninstallWindows(root, args) {
|
|
|
616
620
|
for (const project of projects) {
|
|
617
621
|
const base = resolve(project);
|
|
618
622
|
for (const [, rel] of BOOTSTRAP_FILES) targets.push(join(base, ...rel.split("/")));
|
|
619
|
-
for (const harness of HARNESSES)
|
|
623
|
+
for (const harness of HARNESSES) {
|
|
624
|
+
for (const asset of harnessProjectAssets(harness, "local")) targets.push(join(base, ...asset.path.split("/")));
|
|
625
|
+
targets.push(join(base, ".ai", `start-${harness}.cmd`));
|
|
626
|
+
}
|
|
620
627
|
targets.push(join(base, ".ai", "start-harness.mjs"));
|
|
621
628
|
targets.push(join(base, ".ai", "start-harness.ps1"));
|
|
622
629
|
if (options.has("--purge-memory")) targets.push(join(base, ".agentfs"));
|
|
@@ -685,10 +692,11 @@ foreach ($Item in $Manifest.items) {
|
|
|
685
692
|
}
|
|
686
693
|
|
|
687
694
|
export function powershellCompletion() {
|
|
695
|
+
const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
|
|
688
696
|
return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
|
|
689
697
|
param($wordToComplete, $commandAst, $cursorPosition)
|
|
690
698
|
$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'
|
|
691
|
-
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none','
|
|
699
|
+
$setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
|
|
692
700
|
$candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } else { $commands }
|
|
693
701
|
$candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
694
702
|
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Cairnkeep launcher for Codex CLI. Loads this project's private .ai/.env and
|
|
3
|
+
# starts Codex in the project root. The project-scoped MCP registration lives in
|
|
4
|
+
# .codex/config.toml and is only loaded after Codex trusts the project.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
7
|
+
|
|
8
|
+
if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
|
|
9
|
+
set -a
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
. "$ROOT_DIR/.ai/.env"
|
|
12
|
+
set +a
|
|
13
|
+
fi
|
|
14
|
+
if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
. "$ROOT_DIR/.ai/pre-launch.sh"
|
|
17
|
+
fi
|
|
18
|
+
cd "$ROOT_DIR"
|
|
19
|
+
|
|
20
|
+
if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
|
|
21
|
+
set +e
|
|
22
|
+
codex "$@"
|
|
23
|
+
CAIRN_EXIT_STATUS=$?
|
|
24
|
+
set -e
|
|
25
|
+
export CAIRN_EXIT_STATUS
|
|
26
|
+
# shellcheck disable=SC1091
|
|
27
|
+
. "$ROOT_DIR/.ai/post-exit.sh" || true
|
|
28
|
+
exit "$CAIRN_EXIT_STATUS"
|
|
29
|
+
fi
|
|
30
|
+
exec codex "$@"
|