@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
package/docs/native-windows.md
CHANGED
|
@@ -32,6 +32,20 @@ recovery contract as POSIX. It writes a restricted `.ai/cairnkeep.json` setup
|
|
|
32
32
|
record and never syncs machine assets automatically. `cairn bootstrap` remains
|
|
33
33
|
the deterministic compatibility primitive for existing scripts.
|
|
34
34
|
|
|
35
|
+
For Codex memory without a harness-wide sync, select only Codex:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
cairn setup $Project --git init --harness codex --memory local --yes
|
|
39
|
+
Set-Location $Project
|
|
40
|
+
cairn doctor
|
|
41
|
+
.\.ai\start-codex.cmd
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Review `.codex\config.toml` and accept Codex's project-trust prompt. Setup does
|
|
45
|
+
not edit the user-wide Codex configuration or grant trust. It preserves a
|
|
46
|
+
different existing project configuration and doctor verifies a manually merged
|
|
47
|
+
`mcp_servers.cairn-memory` table.
|
|
48
|
+
|
|
35
49
|
Setup and bootstrap write both the established Unix launchers and native Windows
|
|
36
50
|
launchers. Each `.cmd` file enters `start-harness.ps1`, loads `.ai/.env`, changes
|
|
37
51
|
to the project root, and forwards arguments without shell interpolation.
|
package/docs/operating.md
CHANGED
|
@@ -70,7 +70,7 @@ cairn setup /path/to/project --git init --harness claude,pi --memory local --yes
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
The deterministic form accepts `--git init|existing|none`, a comma-separated
|
|
73
|
-
subset of `claude,opencode,pi,kimi,qwen`, and `--memory local|none`. `--git
|
|
73
|
+
subset of `claude,opencode,pi,kimi,qwen,codex`, and `--memory local|none`. `--git
|
|
74
74
|
existing` requires the target to be in an existing work tree. `--git init`
|
|
75
75
|
requires Git and is the only non-interactive authorization to initialize a
|
|
76
76
|
repository. `--git none` is an explicit limited mode: repository-aware features
|
|
@@ -91,8 +91,10 @@ assets. It contains no credentials, endpoints, or absolute paths.
|
|
|
91
91
|
|
|
92
92
|
Setup never installs or refreshes machine-level harness assets. Its
|
|
93
93
|
`machine_sync.automatic` field is always false, and the human output labels the
|
|
94
|
-
reported command as not run automatically.
|
|
95
|
-
|
|
94
|
+
reported command as not run automatically. For selections such as Codex and
|
|
95
|
+
Qwen that need no machine sync, `machine_sync.command` is `null` and the human
|
|
96
|
+
output says that sync is not required. Apply any reported command explicitly,
|
|
97
|
+
check it, then diagnose and launch:
|
|
96
98
|
|
|
97
99
|
```bash
|
|
98
100
|
cairn sync --apply # Claude Code operating assets
|
|
@@ -105,6 +107,25 @@ cd /path/to/project && cairn doctor
|
|
|
105
107
|
`cairn bootstrap [--untracked] PATH` remains available for scripts that depend
|
|
106
108
|
on its original scaffold and Git-exclusion contract.
|
|
107
109
|
|
|
110
|
+
## Setup order (Codex CLI)
|
|
111
|
+
|
|
112
|
+
Codex uses project-scoped configuration, so setup owns the complete local MCP
|
|
113
|
+
wiring without editing user-wide state:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm install -g @cairnkeep/cli
|
|
117
|
+
cairn setup /path/to/project --git init --harness codex --memory local --yes
|
|
118
|
+
cd /path/to/project
|
|
119
|
+
cairn doctor
|
|
120
|
+
./.ai/start-codex.sh # native Windows: .\.ai\start-codex.cmd
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Review `.codex/config.toml` and accept Codex's project-trust prompt before use.
|
|
124
|
+
An existing different file is preserved and reported as skipped; merge the
|
|
125
|
+
generated `mcp_servers.cairn-memory` table into it, then run `cairn doctor`.
|
|
126
|
+
Uninstall leaves that operator-owned file intact. Selecting
|
|
127
|
+
`--memory none` creates the launcher but deliberately omits the MCP entry.
|
|
128
|
+
|
|
108
129
|
## Setup order (Claude Code)
|
|
109
130
|
|
|
110
131
|
The commands below install the default local topology: `cairn-memory` runs as a
|
|
@@ -865,13 +886,13 @@ maintain a running install — without forking the core. All are opt-in.
|
|
|
865
886
|
### Launcher seams
|
|
866
887
|
|
|
867
888
|
The generic launchers (`.ai/start-claude.sh`, `.ai/start-opencode.sh`,
|
|
868
|
-
`.ai/start-kimi.sh`, `.ai/start-qwen.sh`, and `.ai/start-pi.sh`) run optional hooks around the
|
|
889
|
+
`.ai/start-kimi.sh`, `.ai/start-qwen.sh`, `.ai/start-codex.sh`, and `.ai/start-pi.sh`) run optional hooks around the
|
|
869
890
|
harness, each a no-op when absent:
|
|
870
891
|
|
|
871
892
|
| Seam | When | Purpose |
|
|
872
893
|
|---|---|---|
|
|
873
894
|
| `.ai/pre-launch.sh` | sourced after `.env`, before launch | export env (e.g. a provider base URL / auth), refresh credentials, or abort by returning non-zero |
|
|
874
|
-
| `CAIRN_EXTRA_SETTINGS` | read just before launch | path to a settings file layered on Claude Code (`--settings`) or OpenCode (`OPENCODE_CONFIG`); Kimi, Qwen, and Pi leave the variable available to hooks but do not interpret it |
|
|
895
|
+
| `CAIRN_EXTRA_SETTINGS` | read just before launch | path to a settings file layered on Claude Code (`--settings`) or OpenCode (`OPENCODE_CONFIG`); Kimi, Qwen, Codex, and Pi leave the variable available to hooks but do not interpret it |
|
|
875
896
|
| `.ai/post-exit.sh` | sourced after the harness exits | teardown; `CAIRN_EXIT_STATUS` holds the exit code |
|
|
876
897
|
|
|
877
898
|
A wrapper that needs a non-default provider drops a `pre-launch.sh` that renders
|
|
@@ -37,6 +37,7 @@ request unless the corresponding endpoint and credential are configured.
|
|
|
37
37
|
| Explicit artifact read/show | The redacted body only when HTTP is separately enabled and the operator/client explicitly reads it | Local terminal/stdio, or the explicitly registered authenticated server |
|
|
38
38
|
| Opt-in evaluation coordinator | Adapter-defined inference may leave the machine only according to the explicit operator-owned adapter and inherited environment | Local adapter subprocess; Cairnkeep supplies no provider, endpoint, credential, model, or network default |
|
|
39
39
|
| Guided project setup | None | Selected scaffold assets and private `.ai/cairnkeep.json` state in the target; machine sync is never automatic |
|
|
40
|
+
| Codex project setup and launcher | None | Local `.codex/config.toml` starts `cairn memory-server` over stdio only after the operator trusts the project |
|
|
40
41
|
| Pi memory extension | None | Local `cairn memory-server` stdio child; no HTTP transport is inherited |
|
|
41
42
|
|
|
42
43
|
Model endpoints may be local or remote. Cairnkeep cannot determine a provider's
|
|
@@ -93,7 +94,7 @@ and its generated `revert.sh` restores exact bytes, modes, and layout. Explicit
|
|
|
93
94
|
`cairn eval delete` and `prune` remove selected contained experiment trees with
|
|
94
95
|
no hidden tombstone.
|
|
95
96
|
|
|
96
|
-
## Guided setup and Pi bridge
|
|
97
|
+
## Guided setup, Codex, and Pi bridge
|
|
97
98
|
|
|
98
99
|
Guided setup performs a read-only target and Git preflight before project
|
|
99
100
|
writes. Its schema-v1 `.ai/cairnkeep.json` record contains the package version,
|
|
@@ -103,6 +104,12 @@ endpoints, absolute paths, prompts, or memory values. The file is mode `0600`
|
|
|
103
104
|
on POSIX and receives the private managed-file ACL on native Windows. Setup
|
|
104
105
|
never invokes machine sync, and it does not start a harness or server child.
|
|
105
106
|
|
|
107
|
+
Selecting Codex with local memory adds a non-secret project MCP table that
|
|
108
|
+
invokes `cairn memory-server`. Setup never edits user-wide Codex configuration,
|
|
109
|
+
grants project trust, or overwrites a different `.codex/config.toml`. Doctor can
|
|
110
|
+
validate a manually merged Cairnkeep table without claiming ownership of the
|
|
111
|
+
surrounding operator configuration, and uninstall leaves that file intact.
|
|
112
|
+
|
|
106
113
|
When the explicitly installed Pi memory extension starts a session, it spawns
|
|
107
114
|
`cairn memory-server` as a child in the project directory over local stdio. The
|
|
108
115
|
bridge removes `MCP_HTTP_PORT` from the child environment, retains only the
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Cairnkeep quickstart
|
|
2
|
+
|
|
3
|
+
This is the shortest supported route from an empty directory or existing Git
|
|
4
|
+
repository to durable, local agent memory. The default transport is stdio and
|
|
5
|
+
the default store stays on this computer.
|
|
6
|
+
|
|
7
|
+
## Codex CLI
|
|
8
|
+
|
|
9
|
+
Install Cairnkeep, then let guided setup initialize an empty project and write
|
|
10
|
+
Codex's project-scoped MCP entry:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install --global @cairnkeep/cli
|
|
14
|
+
cairn setup /path/to/project --git init --harness codex --memory local --yes
|
|
15
|
+
cd /path/to/project
|
|
16
|
+
cairn doctor
|
|
17
|
+
./.ai/start-codex.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Use `--git existing` instead when the target is already a Git work tree. On
|
|
21
|
+
native Windows, launch with `.\.ai\start-codex.cmd`.
|
|
22
|
+
|
|
23
|
+
Before the first session, review `.codex/config.toml` and accept Codex's project
|
|
24
|
+
trust prompt. The generated entry runs `cairn memory-server` locally. Setup
|
|
25
|
+
does not edit the user-wide Codex configuration and does not enable networking,
|
|
26
|
+
telemetry, embeddings, session capture, or context packs.
|
|
27
|
+
|
|
28
|
+
If `.codex/config.toml` already exists and differs, setup reports it as
|
|
29
|
+
`skipped` and preserves it byte-for-byte. Merge this table into the existing
|
|
30
|
+
file, then run `cairn doctor`:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[mcp_servers.cairn-memory]
|
|
34
|
+
command = "cairn"
|
|
35
|
+
args = ["memory-server"]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Claude Code
|
|
39
|
+
|
|
40
|
+
Claude Code has the full Cairnkeep operating layer, so it needs one explicit
|
|
41
|
+
machine-level sync in addition to project setup:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install --global @cairnkeep/cli
|
|
45
|
+
claude mcp add cairn-memory -s user -- cairn memory-server
|
|
46
|
+
cairn sync --apply
|
|
47
|
+
cairn setup /path/to/project --git init --harness claude --memory local --yes
|
|
48
|
+
cd /path/to/project
|
|
49
|
+
cairn doctor
|
|
50
|
+
./.ai/start-claude.sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Setup reports machine-level commands but never executes them. This keeps
|
|
54
|
+
project scaffolding separate from harness-wide changes.
|
|
55
|
+
|
|
56
|
+
## First memory cycle
|
|
57
|
+
|
|
58
|
+
In a harness with Cairnkeep commands, remember one durable fact and recall it:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
> /remember Use transactional migrations for schema changes
|
|
62
|
+
> /recall transactional migrations
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
In Codex or another MCP client, ask the client to call `memory_write`, then
|
|
66
|
+
`memory_search`. Close the session, start a new one, and repeat the search. The
|
|
67
|
+
result should survive the restart.
|
|
68
|
+
|
|
69
|
+
## What setup owns
|
|
70
|
+
|
|
71
|
+
`cairn setup` creates only the selected harness assets plus the common `.ai`,
|
|
72
|
+
`.planning`, and `.agentfs` scaffold. It initializes Git only with explicit
|
|
73
|
+
`--git init`, preserves unrelated files, does not overwrite modified files it
|
|
74
|
+
does not own, and records managed digests in private `.ai/cairnkeep.json`.
|
|
75
|
+
|
|
76
|
+
Useful follow-ups:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cairn setup . --git existing --harness codex --memory local --yes # reconcile
|
|
80
|
+
cairn doctor # diagnose
|
|
81
|
+
cairn uninstall --dry-run . # preview removal
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For all harnesses, remote HTTP, storage, profiles, context packs, overlays, and
|
|
85
|
+
recovery details, continue with [Operating Cairnkeep](operating.md) or the
|
|
86
|
+
[learning path](learning/README.md).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.12.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"memory",
|
|
65
65
|
"agent",
|
|
66
66
|
"claude-code",
|
|
67
|
+
"codex-cli",
|
|
67
68
|
"opencode",
|
|
68
69
|
"qwen-code",
|
|
69
70
|
"pi-coding-agent",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"type": "array",
|
|
17
17
|
"minItems": 1,
|
|
18
18
|
"uniqueItems": true,
|
|
19
|
-
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen"]}
|
|
19
|
+
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
|
|
20
20
|
},
|
|
21
21
|
"memory": {"enum": ["local", "none"]}
|
|
22
22
|
}
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"type": "array",
|
|
36
36
|
"minItems": 1,
|
|
37
37
|
"uniqueItems": true,
|
|
38
|
-
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen"]}
|
|
38
|
+
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
|
|
39
39
|
},
|
|
40
40
|
"required_harnesses": {
|
|
41
41
|
"type": "array",
|
|
42
42
|
"minItems": 1,
|
|
43
43
|
"uniqueItems": true,
|
|
44
|
-
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen"]}
|
|
44
|
+
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
|
|
45
45
|
},
|
|
46
46
|
"memory": {
|
|
47
47
|
"type": "array",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"type": "array",
|
|
15
15
|
"minItems": 1,
|
|
16
16
|
"uniqueItems": true,
|
|
17
|
-
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen"]}
|
|
17
|
+
"items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
|
|
18
18
|
},
|
|
19
19
|
"assets": {
|
|
20
20
|
"type": "object",
|
|
21
21
|
"propertyNames": {
|
|
22
|
-
"pattern": "^\\.(?:ai|planning|agentfs)/(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$"
|
|
22
|
+
"pattern": "^\\.(?:ai|planning|agentfs|codex)/(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$"
|
|
23
23
|
},
|
|
24
24
|
"additionalProperties": {
|
|
25
25
|
"type": "object",
|
package/scripts/bootstrap.sh
CHANGED
|
@@ -53,11 +53,9 @@ install_file() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
# .ai/ launchers + env
|
|
56
|
-
|
|
57
|
-
install_file "$TPL
|
|
58
|
-
|
|
59
|
-
install_file "$TPL/start-kimi.sh.template" "$target/.ai/start-kimi.sh" 0755
|
|
60
|
-
install_file "$TPL/start-qwen.sh.template" "$target/.ai/start-qwen.sh" 0755
|
|
56
|
+
while IFS=$'\t' read -r template destination mode; do
|
|
57
|
+
install_file "$TPL/$template" "$target/$destination" "0$mode"
|
|
58
|
+
done < <(node "$CAIRN_ROOT/scripts/harness-registry.mjs" bootstrap-assets)
|
|
61
59
|
install_file "$TPL/env.example.template" "$target/.ai/env.example" 0644
|
|
62
60
|
install_file "$TPL/trajectory-redaction.json.template" "$target/.ai/trajectory-redaction.json" 0644
|
|
63
61
|
install_file "$TPL/capabilities.json.template" "$target/.ai/capabilities.json" 0600
|
|
@@ -100,13 +98,7 @@ fi
|
|
|
100
98
|
|
|
101
99
|
echo
|
|
102
100
|
echo "Cairnkeep bootstrapped into $target"
|
|
103
|
-
echo "
|
|
104
|
-
echo "
|
|
105
|
-
echo "
|
|
106
|
-
echo "
|
|
107
|
-
echo " For Pi trajectory + /graphify, also run: cairn sync-pi --apply"
|
|
108
|
-
echo " For Kimi /graphify, also run: cairn sync-kimi --apply"
|
|
109
|
-
echo " From a source clone, use bin/cairn in place of cairn."
|
|
110
|
-
echo " 4. Launch: $target/.ai/start-claude.sh"
|
|
111
|
-
echo " Kimi memory-client launch: $target/.ai/start-kimi.sh"
|
|
112
|
-
echo " Qwen memory-client launch: $target/.ai/start-qwen.sh"
|
|
101
|
+
echo "Codex: run 'cairn setup' in the project and select Codex; then review trust and run .ai/start-codex.sh"
|
|
102
|
+
echo "Claude: claude mcp add cairn-memory -s user -- cairn memory-server; cairn sync --apply"
|
|
103
|
+
echo "Optional project overrides: cp .ai/env.example .ai/.env"
|
|
104
|
+
echo "For new projects, prefer 'cairn setup'; guide: docs/quickstart.md"
|
package/scripts/completion.sh
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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=()
|
|
@@ -18,7 +20,7 @@ _cairn_complete() {
|
|
|
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") ) ;;
|
|
@@ -78,7 +80,7 @@ complete -F _cairn_complete cairn
|
|
|
78
80
|
EOF
|
|
79
81
|
;;
|
|
80
82
|
zsh)
|
|
81
|
-
|
|
83
|
+
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
82
84
|
#compdef cairn
|
|
83
85
|
_cairn() {
|
|
84
86
|
local -a commands
|
|
@@ -112,7 +114,7 @@ _cairn() {
|
|
|
112
114
|
fi
|
|
113
115
|
case $words[2] in
|
|
114
116
|
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:(
|
|
117
|
+
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
118
|
sync) _arguments '--apply[apply changes]' '--live-root[project Claude root]:directory:_files -/' ;;
|
|
117
119
|
sync-pi) _arguments '--apply[apply changes]' '--live-root[Pi agent root]:directory:_files -/' ;;
|
|
118
120
|
sync-kimi) _arguments '--apply[apply changes]' '--live-root[Kimi Code root]:directory:_files -/' ;;
|
|
@@ -175,7 +177,7 @@ _cairn "$@"
|
|
|
175
177
|
EOF
|
|
176
178
|
;;
|
|
177
179
|
fish)
|
|
178
|
-
|
|
180
|
+
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
179
181
|
complete -c cairn -f
|
|
180
182
|
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
183
|
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
|
|
@@ -188,7 +190,7 @@ complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l apply
|
|
|
188
190
|
complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l live-root -r
|
|
189
191
|
complete -c cairn -n "__fish_seen_subcommand_from bootstrap" -l untracked
|
|
190
192
|
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 "
|
|
193
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "__CAIRN_HARNESSES__"
|
|
192
194
|
complete -c cairn -n "__fish_seen_subcommand_from setup" -l memory -r -a "local none"
|
|
193
195
|
complete -c cairn -n "__fish_seen_subcommand_from setup" -l policy -r
|
|
194
196
|
complete -c cairn -n "__fish_seen_subcommand_from setup" -l yes
|
package/scripts/doctor.sh
CHANGED
|
@@ -443,18 +443,19 @@ fi
|
|
|
443
443
|
# repairing project or machine assets. The Node controller returns only bounded
|
|
444
444
|
# states and exact recovery commands; setup remains the sole owner of project
|
|
445
445
|
# reconciliation.
|
|
446
|
-
setup_diagnosis=$(node --input-type=module - "$CAIRN_ROOT/scripts/setup.mjs" "$PWD" <<'NODE' 2>/dev/null
|
|
446
|
+
setup_diagnosis=$(node --input-type=module - "$CAIRN_ROOT/scripts/setup.mjs" "$CAIRN_ROOT/scripts/harness-registry.mjs" "$PWD" <<'NODE' 2>/dev/null
|
|
447
447
|
import { pathToFileURL } from "node:url";
|
|
448
448
|
import { readFileSync } from "node:fs";
|
|
449
449
|
import { join } from "node:path";
|
|
450
450
|
const module = await import(pathToFileURL(process.argv[2]).href);
|
|
451
|
-
const
|
|
451
|
+
const registry = await import(pathToFileURL(process.argv[3]).href);
|
|
452
|
+
const value = module.diagnoseSetup(process.argv[4]);
|
|
452
453
|
const allowed = new Set(["absent", "complete", "limited", "incomplete"]);
|
|
453
454
|
if (value.schema_version !== 1 || !allowed.has(value.status) || !Array.isArray(value.recovery)) process.exit(1);
|
|
454
455
|
let piSelected = false;
|
|
455
456
|
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) => !
|
|
457
|
+
const state = JSON.parse(readFileSync(join(process.argv[4], ".ai", "cairnkeep.json"), "utf8"));
|
|
458
|
+
if (!Array.isArray(state.harnesses) || state.harnesses.some((name) => !registry.HARNESS_IDS.includes(name))) process.exit(1);
|
|
458
459
|
piSelected = state.harnesses.includes("pi");
|
|
459
460
|
}
|
|
460
461
|
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",
|