@cairnkeep/cli 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/docs/harness-compatibility.md +2 -1
  4. package/docs/learning/CURRICULUM-MAP.md +1 -1
  5. package/docs/learning/FEATURE-GUIDE.md +2 -1
  6. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  7. package/docs/learning/lessons/L01-safe-trial.md +1 -1
  8. package/docs/learning/lessons/L02-installation.md +1 -1
  9. package/docs/learning/lessons/L03-first-project.md +1 -1
  10. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  11. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  12. package/docs/learning/lessons/L21-context-packs.md +1 -1
  13. package/docs/learning/lessons/L22-native-windows.md +1 -1
  14. package/docs/learning/lessons/L23-guided-setup.md +18 -7
  15. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  16. package/docs/learning/lessons/L25-playbooks.md +1 -1
  17. package/docs/operating.md +11 -6
  18. package/examples/eval/bundled-fake.json +1 -1
  19. package/mcp-memory-server/dist/artifact-schema.js +1 -1
  20. package/mcp-memory-server/dist/work-evidence-schema.js +1 -1
  21. package/mcp-memory-server/dist/work-evidence-store.js +1 -1
  22. package/package.json +1 -1
  23. package/schemas/artifact.schema.json +1 -1
  24. package/schemas/cairnkeep-setup-policy.schema.json +3 -3
  25. package/schemas/cairnkeep-setup.schema.json +1 -1
  26. package/scripts/harness-registry.mjs +15 -3
  27. package/scripts/setup.mjs +10 -5
  28. package/scripts/test-bootstrap-untracked.sh +1 -1
  29. package/scripts/test-completion.sh +1 -1
  30. package/scripts/test-harness-registry.sh +28 -1
  31. package/scripts/test-setup-output.sh +20 -1
  32. package/scripts/test-setup-preflight.mjs +1 -1
  33. package/scripts/test-work-evidence-cli.sh +1 -1
  34. package/scripts/uninstall.sh +1 -0
  35. package/templates/start-omp.sh.template +47 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable user-facing changes are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.19.0] - 2026-09-28
9
+
10
+ ### Added
11
+
12
+ - Include oh-my-pi (OMP) in guided setup with a project launcher and explicit,
13
+ opt-in machine sync instructions.
14
+ - Report every required machine sync command when multiple harnesses are
15
+ selected, while retaining the singular JSON field for compatibility.
16
+ - Support OMP in opt-in Git-linked work evidence and preserve its diff-artifact
17
+ provenance.
18
+
8
19
  ## [2.18.0] - 2026-09-28
9
20
 
10
21
  ### Added
@@ -561,7 +572,8 @@ stored memories.
561
572
  - Preserve executable permissions and Bash 3.2 portability on macOS.
562
573
  - Add backup-first uninstall and SQLite-safe memory export/import guidance.
563
574
 
564
- [Unreleased]: https://github.com/cairnkeep/cairnkeep/compare/v2.18.0...HEAD
575
+ [Unreleased]: https://github.com/cairnkeep/cairnkeep/compare/v2.19.0...HEAD
576
+ [2.19.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.18.0...v2.19.0
565
577
  [2.18.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.17.3...v2.18.0
566
578
  [2.15.1]: https://github.com/cairnkeep/cairnkeep/compare/v2.15.0...v2.15.1
567
579
  [2.15.0]: https://github.com/cairnkeep/cairnkeep/compare/v2.14.0...v2.15.0
package/CONTRIBUTING.md CHANGED
@@ -43,7 +43,7 @@ endpoints. They are not part of the default offline test suite.
43
43
  Node-based CLI implementations.
44
44
  - `bin/cairn` is the installed command entry point.
45
45
  - `templates/` contains project scaffolding and managed instruction assets.
46
- - `claude/`, `opencode/`, `pi/`, and `kimi/` contain harness-specific operating
46
+ - `claude/`, `opencode/`, `pi/`, `omp/`, and `kimi/` contain harness-specific operating
47
47
  assets.
48
48
  - `scripts/` contains sync, lifecycle, packaging, and contract checks.
49
49
  - `docs/` contains the versioned user and operator references indexed from
@@ -80,7 +80,8 @@ registry-current release.
80
80
 
81
81
  ## oh-my-pi (omp)
82
82
 
83
- Run `cairn sync-omp --apply` explicitly to install
83
+ Select OMP in `cairn setup` to create the project launcher, then run
84
+ `cairn sync-omp --apply` explicitly to install
84
85
  `extensions/cairnkeep-memory.ts` and `extensions/cairnkeep-capture.ts` under
85
86
  the omp agent root (default
86
87
  `~/.omp/agent`; `PI_CODING_AGENT_DIR` overrides it, matching omp's agent
@@ -1,6 +1,6 @@
1
1
  # Curriculum Coverage Map
2
2
 
3
- **Baseline:** Cairnkeep 2.18.0
3
+ **Baseline:** Cairnkeep 2.19.0
4
4
  **Last reviewed:** 2026-08-19
5
5
 
6
6
  This map prevents the course from drifting behind the product. It assigns each
@@ -1,6 +1,6 @@
1
1
  # Cairnkeep feature guide
2
2
 
3
- **Baseline:** Cairnkeep 2.18.0
3
+ **Baseline:** Cairnkeep 2.19.0
4
4
 
5
5
  This is the step-back map for choosing and operating Cairnkeep features. The
6
6
  [course labs](https://github.com/cairnkeep/cairnkeep-course-labs) provide one
@@ -86,6 +86,7 @@ prerequisites for ordinary memory.
86
86
  | Context packs | Manual local or commit-pinned Git install plus project enablement | Immutable objects and atomic pointers under `CAIRN_PACK_BASE_DIR`; no background sync | Validate, inspect digest, check/apply update, disable/remove | `course-10-trust-context` |
87
87
  | OKF exchange | `cairn pack validate-okf`, `import-okf`, and preview-confirm `export-okf` | Local import/export by default; explicit file/shared-note allowlist; redacted output | Inspect diagnostics/provenance/output list; exact-digest apply; disable/remove imported pack | `course-10-trust-context` |
88
88
  | Pi local stdio memory | Select Pi during setup, then explicitly run `cairn sync-pi --apply` | Local memory-server child; dynamic effective tool catalog; annotations retained in trusted details, not a native Pi field | `cairn sync-pi --check`, `cairn doctor`, cancel one call, then observe awaited shutdown with no orphan | `course-12-guided-setup` |
89
+ | OMP local stdio memory | Select OMP during setup, then explicitly run `cairn sync-omp --apply` | Local memory-server child; dynamic effective tool catalog; capture stages candidates for review | `cairn sync-omp --check`, `cairn doctor`, cancel one call, then verify session-stop staging and no orphan | `course-12-guided-setup` |
89
90
 
90
91
  ## Evidence, governance, and measurement
91
92
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 10 minutes
6
- **Tested with:** Cairnkeep 2.18.0
6
+ **Tested with:** Cairnkeep 2.19.0
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 15 minutes
6
- **Tested with:** Cairnkeep 2.18.0
6
+ **Tested with:** Cairnkeep 2.19.0
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.18.0 and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.19.0 and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.18.0
6
+ **Tested with:** Cairnkeep 2.19.0
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Evidence and Evaluation
5
5
  **Time:** 50 minutes
6
- **Tested with:** Cairnkeep 2.18.0
6
+ **Tested with:** Cairnkeep 2.19.0
7
7
 
8
8
  ## Outcome
9
9
 
@@ -1,7 +1,7 @@
1
1
  # L20 - Least-authority MCP tool profiles
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.18.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.19.0 and Node.js 22 or newer
5
5
  **Time:** 25 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L21 - Immutable context packs and skill approval
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.18.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.19.0 and Node.js 22 or newer
5
5
  **Time:** 40 minutes
6
6
 
7
7
  ## Outcome
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Operator
5
5
  **Time:** 30 minutes
6
- **Tested with:** Cairnkeep 2.18.0, Windows x64, and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.19.0, Windows x64, and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart and Operator
5
5
  **Time:** 35 minutes
6
- **Tested with:** Cairnkeep 2.18.0; Pi 0.84.1; Node.js 22, 24, and 26
6
+ **Tested with:** Cairnkeep 2.19.0; Pi 0.84.1; Node.js 22, 24, and 26
7
7
 
8
8
  ## Outcome
9
9
 
@@ -13,13 +13,14 @@ Pi's local stdio memory bridge, and recover or uninstall every managed asset
13
13
  without hidden machine changes.
14
14
 
15
15
  The selected harnesses come from Cairnkeep's declarative registry. Codex is a
16
- supported selection: it receives `.codex/config.toml` and a launcher, while Pi
17
- continues to require its separate explicit machine sync.
16
+ supported selection: it receives `.codex/config.toml` and a launcher. Pi and
17
+ OMP receive project launchers and each require a separate, explicit machine
18
+ sync; selecting both reports both sync commands.
18
19
 
19
20
  ## Prerequisites
20
21
 
21
22
  - Node.js 22 or newer, npm, and Git for the main exercise.
22
- - Pi 0.84.1 or newer.
23
+ - Pi 0.84.1 or newer; OMP is optional.
23
24
  - A disposable parent directory containing no credentials, secrets, private
24
25
  state, or identifying project data.
25
26
 
@@ -32,7 +33,7 @@ cairn setup /tmp/cairnkeep-guided-selector-lab
32
33
  ```
33
34
 
34
35
  Choose Git with Up/Down and Enter, tick Pi with Space in the harness checklist,
35
- keep local memory, then inspect the plan before confirming. Pressing Escape or
36
+ optionally tick OMP as well, keep local memory, then inspect the plan before confirming. Pressing Escape or
36
37
  Ctrl-C cancels; redirected and limited terminals use the documented text-input
37
38
  fallback. Remove that disposable selector lab before continuing if you applied
38
39
  the plan.
@@ -63,6 +64,14 @@ cairn doctor
63
64
  ./.ai/start-pi.sh
64
65
  ```
65
66
 
67
+ If OMP was selected, run its separate sync and use the generated launcher:
68
+
69
+ ```bash
70
+ cairn sync-omp --apply
71
+ cairn sync-omp --check
72
+ ./.ai/start-omp.sh
73
+ ```
74
+
66
75
  In the disposable Pi session, list the registered Cairnkeep tools and make one
67
76
  harmless read-only memory call. The extension starts `cairn memory-server` as a
68
77
  local stdio child in this project; it does not select HTTP or a remote server.
@@ -107,9 +116,10 @@ cairn sync-pi --check
107
116
  cairn doctor
108
117
  ```
109
118
 
110
- Doctor reports project-state drift separately from missing or drifted Pi
119
+ Doctor reports project-state drift separately from missing or drifted harness
111
120
  machine assets. Setup repairs only recorded project assets; `cairn sync-pi
112
- --apply` repairs only the package-owned Pi extension and prompt paths.
121
+ --apply` and `cairn sync-omp --apply` repair only their respective package-owned
122
+ machine paths.
113
123
 
114
124
  Preview backup-first removal before confirming it:
115
125
 
@@ -128,6 +138,7 @@ and context packs remain unless their separate purge flags are supplied.
128
138
  | Setup refuses a missing target | The non-interactive command omitted an explicit choice or `--yes` | Run `cairn setup PATH` in a terminal, or supply the target, `--git`, `--harness`, `--memory`, and `--yes` together |
129
139
  | Setup refuses `--git existing` | The target is not inside a Git work tree | Choose an existing repository, or explicitly authorize `--git init`; use `--git none` only when limited mode is intended |
130
140
  | Doctor reports missing or drifted Pi assets | Project setup selected Pi but machine sync is incomplete | Run `cairn sync-pi --apply`, then `cairn sync-pi --check` and `cairn doctor` |
141
+ | OMP is missing an extension | Project setup selected OMP but its explicit machine sync was not applied | Run `cairn sync-omp --apply`, then `cairn sync-omp --check` and `cairn doctor` |
131
142
  | A cancelled call ends the session | The Pi/bridge version does not satisfy the per-call cancellation contract | Preserve sanitized diagnostics, close the session, and do not claim release readiness |
132
143
  | Pi exits but a child process remains | Shutdown did not complete or the tested extension drifted | Preserve process-state evidence, run the sync check, and treat the release gate as failed |
133
144
  | A client expects native annotation hints | Pi's public tool API has no annotations field | Inspect trusted bridge metadata/result `details`; never claim native propagation |
@@ -1,7 +1,7 @@
1
1
  # L24 - Reviewed knowledge exchange
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.18.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.19.0 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L25 - Bounded workflow playbooks
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.18.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.19.0 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
package/docs/operating.md CHANGED
@@ -75,7 +75,7 @@ cairn setup /path/to/project --git init --harness claude,pi --memory local --yes
75
75
  ```
76
76
 
77
77
  The deterministic form accepts `--git init|existing|none`, a comma-separated
78
- subset of `claude,opencode,pi,kimi,qwen,codex`, and `--memory local|none`. `--git
78
+ subset of `claude,opencode,pi,omp,kimi,qwen,codex`, and `--memory local|none`. `--git
79
79
  existing` requires the target to be in an existing work tree. `--git init`
80
80
  requires Git and is the only non-interactive authorization to initialize a
81
81
  repository. `--git none` is an explicit limited mode: repository-aware features
@@ -95,16 +95,21 @@ selected harnesses, and digests/modes/template identifiers for setup-owned
95
95
  assets. It contains no credentials, endpoints, or absolute paths.
96
96
 
97
97
  Setup never installs or refreshes machine-level harness assets. Its
98
- `machine_sync.automatic` field is always false, and the human output labels the
99
- reported command as not run automatically. For selections such as Codex and
100
- Qwen that need no machine sync, `machine_sync.command` is `null` and the human
101
- output says that sync is not required. Apply any reported command explicitly,
102
- check it, then diagnose and launch:
98
+ `machine_sync.automatic` field is always false, and the human output labels each
99
+ reported command as not run automatically. The JSON result's
100
+ `machine_sync.commands` lists every required command; the older singular
101
+ `machine_sync.command` field remains as the first command for compatibility.
102
+ For selections such as Codex and Qwen that need no machine sync,
103
+ `machine_sync.commands` is empty and the human output says that sync is not
104
+ required. Apply each reported command explicitly, check the corresponding
105
+ assets, then diagnose and launch:
103
106
 
104
107
  ```bash
105
108
  cairn sync --apply # Claude Code operating assets
106
109
  cairn sync-pi --apply # Pi extension and prompt
107
110
  cairn sync-pi --check
111
+ cairn sync-omp --apply # OMP extensions
112
+ cairn sync-omp --check
108
113
  cd /path/to/project && cairn doctor
109
114
  ./.ai/start-pi.sh
110
115
  ```
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.18.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.19.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
@@ -32,7 +32,7 @@ export const artifactNodeRefSchema = z.object({
32
32
  export const artifactProvenanceSchema = z.object({
33
33
  producer: z.string().min(1).max(256).refine((value) => !/[\u0000-\u001f\u007f]/.test(value)),
34
34
  source_event: z.string().min(1).max(256).optional(),
35
- harness: z.enum(["claude-code", "opencode", "pi"]).optional(),
35
+ harness: z.enum(["claude-code", "opencode", "pi", "omp"]).optional(),
36
36
  harness_version: z.string().min(1).max(128).optional(),
37
37
  native_id: z.string().min(1).max(256).optional(),
38
38
  }).strict();
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export const WORK_EVIDENCE_SCHEMA_VERSION = 1;
3
- export const WORK_EVIDENCE_HARNESSES = ["claude", "opencode", "pi", "kimi", "qwen", "codex"];
3
+ export const WORK_EVIDENCE_HARNESSES = ["claude", "opencode", "pi", "omp", "kimi", "qwen", "codex"];
4
4
  export const WORK_EVIDENCE_DEFAULT_RETENTION_DAYS = 30;
5
5
  export const WORK_EVIDENCE_DEFAULT_STORE_MAX_BYTES = 64 * 1024 * 1024;
6
6
  export const WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS = 4096;
@@ -292,7 +292,7 @@ export async function finishWorkEvidence(projectRoot, evidenceId, exitStatus, op
292
292
  provenance: {
293
293
  producer: "cairn-work-evidence",
294
294
  source_event: "launcher-exit",
295
- harness: start.harness === "claude" ? "claude-code" : ["opencode", "pi"].includes(start.harness) ? start.harness : undefined,
295
+ harness: start.harness === "claude" ? "claude-code" : ["opencode", "pi", "omp"].includes(start.harness) ? start.harness : undefined,
296
296
  },
297
297
  content: { text: patchCapture.text },
298
298
  }, { ...artifactLimits, artifactMaxBytes: Math.min(artifactLimits.artifactMaxBytes, limits.patchMaxBytes) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.18.0",
3
+ "version": "2.19.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",
@@ -27,7 +27,7 @@
27
27
  "properties": {
28
28
  "producer": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" },
29
29
  "source_event": { "type": "string", "minLength": 1, "maxLength": 256 },
30
- "harness": { "enum": ["claude-code", "opencode", "pi"] },
30
+ "harness": { "enum": ["claude-code", "opencode", "pi", "omp"] },
31
31
  "harness_version": { "type": "string", "minLength": 1, "maxLength": 128 },
32
32
  "native_id": { "type": "string", "minLength": 1, "maxLength": 256 }
33
33
  }
@@ -16,7 +16,7 @@
16
16
  "type": "array",
17
17
  "minItems": 1,
18
18
  "uniqueItems": true,
19
- "items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
19
+ "items": {"enum": ["claude", "opencode", "pi", "omp", "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", "codex"]}
38
+ "items": {"enum": ["claude", "opencode", "pi", "omp", "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", "codex"]}
44
+ "items": {"enum": ["claude", "opencode", "pi", "omp", "kimi", "qwen", "codex"]}
45
45
  },
46
46
  "memory": {
47
47
  "type": "array",
@@ -14,7 +14,7 @@
14
14
  "type": "array",
15
15
  "minItems": 1,
16
16
  "uniqueItems": true,
17
- "items": {"enum": ["claude", "opencode", "pi", "kimi", "qwen", "codex"]}
17
+ "items": {"enum": ["claude", "opencode", "pi", "omp", "kimi", "qwen", "codex"]}
18
18
  },
19
19
  "assets": {
20
20
  "type": "object",
@@ -28,6 +28,13 @@ const DEFINITIONS = [
28
28
  project_assets: [],
29
29
  machine_sync: "cairn sync-pi --apply",
30
30
  },
31
+ {
32
+ id: "omp",
33
+ title: "Oh My Pi (OMP)",
34
+ launcher: { template: "start-omp.sh.template", path: ".ai/start-omp.sh", mode: 0o755 },
35
+ project_assets: [],
36
+ machine_sync: "cairn sync-omp --apply",
37
+ },
31
38
  {
32
39
  id: "kimi",
33
40
  title: "Kimi Code",
@@ -90,9 +97,14 @@ export function requiredHarnessAssetPaths(harnesses, memory = "local") {
90
97
  }
91
98
 
92
99
  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;
100
+ const commands = machineSyncCommands(harnesses);
101
+ return commands.find((command) => command === "cairn sync-pi --apply") ?? commands[0] ?? null;
102
+ }
103
+
104
+ export function machineSyncCommands(harnesses) {
105
+ return Object.freeze([...new Set(
106
+ harnesses.map(harnessDefinition).map((definition) => definition?.machine_sync).filter(Boolean),
107
+ )]);
96
108
  }
97
109
 
98
110
  function validateRegistry() {
package/scripts/setup.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  resolveSetupChoices,
23
23
  } from "./setup-core.mjs";
24
24
  import { reconcileSetupPlan } from "./setup-reconcile.mjs";
25
- import { HARNESS_IDS, HARNESS_REGISTRY, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
25
+ import { HARNESS_IDS, HARNESS_REGISTRY, machineSyncCommand, machineSyncCommands, requiredHarnessAssetPaths } from "./harness-registry.mjs";
26
26
  import { reconcilePlaybookInstructions } from "./playbook-instructions.mjs";
27
27
  import { selectManyPrompt, selectOnePrompt, supportsTerminalPrompts } from "./terminal-prompts.mjs";
28
28
 
@@ -189,6 +189,7 @@ function setupRecovery(plan) {
189
189
  }
190
190
 
191
191
  function resultFor(plan, reconciliation, platform) {
192
+ const machineCommands = machineSyncCommands(plan.harnesses);
192
193
  const machineCommand = machineSyncCommand(plan.harnesses);
193
194
  return Object.freeze({
194
195
  schema_version: 1,
@@ -203,7 +204,7 @@ function resultFor(plan, reconciliation, platform) {
203
204
  launch_commands: Object.freeze(launchCommands(plan, platform)),
204
205
  recovery: Object.freeze(setupRecovery(plan)),
205
206
  limitations: Object.freeze(plan.limited ? ["Git integration is disabled; repository-aware features are limited."] : []),
206
- machine_sync: Object.freeze({ automatic: false, command: machineCommand }),
207
+ machine_sync: Object.freeze({ automatic: false, command: machineCommand, commands: machineCommands }),
207
208
  });
208
209
  }
209
210
 
@@ -223,9 +224,13 @@ export function renderSetupResult(result, stream) {
223
224
  if (result.harnesses.includes("codex") && result.memory === "local") {
224
225
  writeLine(stream, "Codex: review .codex/config.toml and accept the project trust prompt before use");
225
226
  }
226
- writeLine(stream, result.machine_sync.command
227
- ? `Machine sync: ${result.machine_sync.command} (not run automatically)`
228
- : "Machine sync: not required for the selected harnesses");
227
+ if (result.machine_sync.commands.length > 0) {
228
+ for (const command of result.machine_sync.commands) {
229
+ writeLine(stream, `Machine sync: ${command} (not run automatically)`);
230
+ }
231
+ } else {
232
+ writeLine(stream, "Machine sync: not required for the selected harnesses");
233
+ }
229
234
  }
230
235
 
231
236
  function createTarget(target) {
@@ -90,7 +90,7 @@ grep -qxF "/.agentfs/" "$tmp/repo/.git/info/exclude" || fail "missing /.agentfs/
90
90
  printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$tmp/repo/.ai/capabilities.json"
91
91
  chmod 600 "$tmp/repo/.ai/capabilities.json"
92
92
  printf '%s\n' '# operator-owned environment' >"$tmp/repo/.ai/env.example"
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
+ asset_paths='.ai/start-claude.sh .ai/start-opencode.sh .ai/start-pi.sh .ai/start-omp.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'
94
94
  mkdir -p "$tmp/assets-before"
95
95
  for path in $asset_paths; do
96
96
  cp "$tmp/repo/$path" "$tmp/assets-before/${path//\//__}"
@@ -165,7 +165,7 @@ if [[ "$completion_surface_complete" != true ]]; then
165
165
  fi
166
166
 
167
167
  setup_flags='--git --harness --memory --policy --yes --json'
168
- setup_values='init existing none claude opencode pi kimi qwen local'
168
+ setup_values='init existing none claude opencode pi omp kimi qwen codex local'
169
169
  for shell in bash zsh fish powershell; do
170
170
  for flag in $setup_flags; do
171
171
  grep -q -- "$flag\|${flag#--}" "$tmp/$shell" || {
@@ -25,7 +25,7 @@ for (const path of [
25
25
  assert.match(content, /approval/);
26
26
  }
27
27
  const registry = await import(pathToFileURL(join(root, "scripts", "harness-registry.mjs")).href);
28
- assert.deepEqual(registry.HARNESS_IDS, ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
28
+ assert.deepEqual(registry.HARNESS_IDS, ["claude", "opencode", "pi", "omp", "kimi", "qwen", "codex"]);
29
29
  assert.equal(new Set(registry.HARNESS_IDS).size, registry.HARNESS_IDS.length);
30
30
  for (const id of registry.HARNESS_IDS) {
31
31
  const definition = registry.harnessDefinition(id);
@@ -40,6 +40,14 @@ assert.deepEqual(registry.harnessProjectAssets("codex", "local").map(({ path })
40
40
  assert.deepEqual(registry.requiredHarnessAssetPaths(["codex"], "local"), [".ai/start-codex.sh"]);
41
41
  assert.deepEqual(registry.requiredHarnessAssetPaths(["codex"], "none"), [".ai/start-codex.sh"]);
42
42
  assert.equal(registry.machineSyncCommand(["codex"]), null);
43
+ assert.deepEqual(registry.machineSyncCommands(["claude", "opencode", "pi", "omp", "kimi"]), [
44
+ "cairn sync --apply",
45
+ "cairn sync-pi --apply",
46
+ "cairn sync-omp --apply",
47
+ "cairn sync-kimi --apply",
48
+ ]);
49
+ assert.equal(registry.machineSyncCommand(["claude", "pi"]), "cairn sync-pi --apply", "legacy singular machine sync should retain its Pi preference");
50
+ assert.deepEqual(registry.machineSyncCommands(["codex"]), []);
43
51
 
44
52
  const schema = JSON.parse(readFileSync(join(root, "schemas", "cairnkeep-setup.schema.json"), "utf8"));
45
53
  assert.deepEqual(schema.properties.harnesses.items.enum, registry.HARNESS_IDS, "setup schema drifted from harness registry");
@@ -56,6 +64,7 @@ assert.equal(setup.status, 0, setup.stderr);
56
64
  const result = JSON.parse(setup.stdout);
57
65
  assert.deepEqual(result.harnesses, ["codex"]);
58
66
  assert.equal(result.machine_sync.command, null);
67
+ assert.deepEqual(result.machine_sync.commands, []);
59
68
  assert.deepEqual(result.launch_commands, [".ai/start-codex.sh"]);
60
69
  assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory-local\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
61
70
 
@@ -73,6 +82,24 @@ const launch = spawnSync(join(target, ".ai", "start-codex.sh"), ["--fixture"], {
73
82
  assert.equal(launch.status, 0, launch.stderr);
74
83
  assert.deepEqual(launch.stdout.trim().split("\n"), [target, "loaded", "--fixture"]);
75
84
 
85
+ const ompTarget = join(sandbox, "omp-project");
86
+ const ompSetup = spawnSync(join(root, "bin", "cairn"), [
87
+ "setup", ompTarget, "--git", "init", "--harness", "omp", "--memory", "local", "--yes", "--json",
88
+ ], { encoding: "utf8", shell: false });
89
+ assert.equal(ompSetup.status, 0, ompSetup.stderr);
90
+ assert.deepEqual(JSON.parse(ompSetup.stdout).harnesses, ["omp"]);
91
+ const fakeOmp = join(fakeBin, "omp");
92
+ writeFileSync(fakeOmp, "#!/bin/sh\nprintf '%s\\n' \"$PWD\" \"${CAIRN_REGISTRY_TEST:-}\" \"$*\"\n");
93
+ chmodSync(fakeOmp, 0o755);
94
+ writeFileSync(join(ompTarget, ".ai", ".env"), "CAIRN_REGISTRY_TEST=omp-loaded\n", { mode: 0o600 });
95
+ const ompLaunch = spawnSync(join(ompTarget, ".ai", "start-omp.sh"), ["--fixture"], {
96
+ encoding: "utf8",
97
+ shell: false,
98
+ env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}` },
99
+ });
100
+ assert.equal(ompLaunch.status, 0, ompLaunch.stderr);
101
+ assert.deepEqual(ompLaunch.stdout.trim().split("\n"), [ompTarget, "omp-loaded", "--fixture"]);
102
+
76
103
  const noMemory = join(sandbox, "codex-no-memory");
77
104
  const noMemorySetup = spawnSync(join(root, "bin", "cairn"), [
78
105
  "setup", noMemory, "--git", "init", "--harness", "codex", "--memory", "none", "--yes",
@@ -68,6 +68,7 @@ assert.equal(result.launch_commands.some((line) => /start-pi\.sh/.test(line)), t
68
68
  assert.equal(Array.isArray(result.recovery), true);
69
69
  assert.equal(result.machine_sync.automatic, false);
70
70
  assert.match(result.machine_sync.command, /^cairn sync(?:-pi)? --(?:check|apply)/);
71
+ assert.deepEqual(result.machine_sync.commands, ["cairn sync --apply", "cairn sync-pi --apply"]);
71
72
  NODE
72
73
 
73
74
  complete_human="$tmp/complete.txt"
@@ -86,7 +87,25 @@ for expected in \
86
87
  "Recovery:"; do
87
88
  grep -qF "$expected" "$complete_human" || fail "human output missing $expected"
88
89
  done
89
- grep -qE 'cairn sync(-pi)? --(check|apply)' "$complete_human" || fail "human output omitted explicit machine sync direction"
90
+ grep -qF 'Machine sync: cairn sync --apply (not run automatically)' "$complete_human" || fail "human output omitted Claude machine sync"
91
+ grep -qF 'Machine sync: cairn sync-pi --apply (not run automatically)' "$complete_human" || fail "human output omitted Pi machine sync"
92
+
93
+ omp_json="$tmp/omp.json"
94
+ "$ROOT/bin/cairn" setup "$tmp/omp target" \
95
+ --git init --harness omp --memory local --yes --json >"$omp_json"
96
+ node --input-type=module - "$omp_json" <<'NODE'
97
+ import assert from "node:assert/strict";
98
+ import fs from "node:fs";
99
+
100
+ const result = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
101
+ assert.deepEqual(result.harnesses, ["omp"]);
102
+ assert.deepEqual(result.launch_commands, [".ai/start-omp.sh"]);
103
+ assert.equal(result.machine_sync.automatic, false);
104
+ assert.equal(result.machine_sync.command, "cairn sync-omp --apply");
105
+ assert.deepEqual(result.machine_sync.commands, ["cairn sync-omp --apply"]);
106
+ NODE
107
+ [[ -x "$tmp/omp target/.ai/start-omp.sh" ]] || fail "OMP launcher missing"
108
+ grep -qF 'cairn evidence run --harness omp -- omp' "$tmp/omp target/.ai/start-omp.sh" || fail "OMP launcher work-evidence integration missing"
90
109
 
91
110
  limited_json="$tmp/limited.json"
92
111
  "$ROOT/bin/cairn" setup "$tmp/limited target" \
@@ -345,7 +345,7 @@ async function testStructuredSetupSelectors(setup, fixture) {
345
345
  "Apply this setup plan?",
346
346
  ]);
347
347
  const harnessPrompt = selections.find(({ message }) => message === "Coding harnesses");
348
- assert.deepEqual(harnessPrompt.choices.map(({ value }) => value), ["claude", "opencode", "pi", "kimi", "qwen", "codex"]);
348
+ assert.deepEqual(harnessPrompt.choices.map(({ value }) => value), ["claude", "opencode", "pi", "omp", "kimi", "qwen", "codex"]);
349
349
  assert.match(harnessPrompt.choices.find(({ value }) => value === "codex").label, /Codex CLI/);
350
350
  assertNoManagedPaths(fixture.empty, "cancelled structured setup");
351
351
  }
@@ -16,7 +16,7 @@ exec node "$ROOT/bin/cairn" "\$@"
16
16
  EOF
17
17
  chmod +x "$fake_bin/cairn"
18
18
 
19
- for harness in claude opencode pi kimi qwen codex; do
19
+ for harness in claude opencode pi omp kimi qwen codex; do
20
20
  project="$tmp/$harness"
21
21
  mkdir -p "$project/.ai" "$project/.agentfs"
22
22
  cp "$ROOT/templates/start-$harness.sh.template" "$project/.ai/start-$harness.sh"
@@ -43,6 +43,7 @@ PROJECT_AI_FILES=(
43
43
  start-claude.sh
44
44
  start-opencode.sh
45
45
  start-pi.sh
46
+ start-omp.sh
46
47
  start-kimi.sh
47
48
  start-qwen.sh
48
49
  start-codex.sh
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # Cairnkeep launcher for oh-my-pi (OMP). Loads this repo's private .ai/.env,
3
+ # runs the optional pre/post seams, and launches OMP from the project root.
4
+ set -euo pipefail
5
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
6
+
7
+ if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
8
+ set -a
9
+ # shellcheck disable=SC1091
10
+ . "$ROOT_DIR/.ai/.env"
11
+ set +a
12
+ fi
13
+ if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
14
+ # shellcheck disable=SC1091
15
+ . "$ROOT_DIR/.ai/pre-launch.sh"
16
+ fi
17
+
18
+ cd "$ROOT_DIR"
19
+
20
+ case "${CAIRN_WORK_EVIDENCE:-}" in
21
+ 1|true|TRUE|yes|YES|on|ON)
22
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
23
+ set +e
24
+ cairn evidence run --harness omp -- omp "$@"
25
+ CAIRN_EXIT_STATUS=$?
26
+ set -e
27
+ export CAIRN_EXIT_STATUS
28
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
29
+ # shellcheck disable=SC1091
30
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
31
+ fi
32
+ exit "$CAIRN_EXIT_STATUS"
33
+ fi
34
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
35
+ ;;
36
+ esac
37
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
38
+ set +e
39
+ omp "$@"
40
+ CAIRN_EXIT_STATUS=$?
41
+ set -e
42
+ export CAIRN_EXIT_STATUS
43
+ # shellcheck disable=SC1091
44
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
45
+ exit "$CAIRN_EXIT_STATUS"
46
+ fi
47
+ exec omp "$@"