@cairnkeep/cli 2.17.3 → 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 (47) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/docs/harness-compatibility.md +48 -0
  4. package/docs/learning/CURRICULUM-MAP.md +2 -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 +13 -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/pi-bridge-child.js +109 -0
  21. package/mcp-memory-server/dist/pi-mcp-bridge.js +15 -3
  22. package/mcp-memory-server/dist/work-evidence-schema.js +1 -1
  23. package/mcp-memory-server/dist/work-evidence-store.js +1 -1
  24. package/mcp-memory-server/package.json +5 -3
  25. package/omp/extensions/cairnkeep-capture.ts +226 -0
  26. package/omp/extensions/cairnkeep-memory.ts +288 -0
  27. package/package.json +77 -76
  28. package/schemas/artifact.schema.json +1 -1
  29. package/schemas/cairnkeep-setup-policy.schema.json +3 -3
  30. package/schemas/cairnkeep-setup.schema.json +1 -1
  31. package/scripts/cairn-cli.mjs +2 -0
  32. package/scripts/completion.sh +8 -3
  33. package/scripts/harness-registry.mjs +15 -3
  34. package/scripts/lib/omp-capture-stage.mjs +103 -0
  35. package/scripts/setup.mjs +10 -5
  36. package/scripts/sync-omp-assets.sh +96 -0
  37. package/scripts/test-bootstrap-untracked.sh +1 -1
  38. package/scripts/test-cli-dispatch.sh +2 -0
  39. package/scripts/test-completion.sh +4 -2
  40. package/scripts/test-harness-registry.sh +28 -1
  41. package/scripts/test-setup-output.sh +20 -1
  42. package/scripts/test-setup-preflight.mjs +1 -1
  43. package/scripts/test-work-evidence-cli.sh +1 -1
  44. package/scripts/uninstall.sh +1 -0
  45. package/scripts/verify-omp-capture.mjs +370 -0
  46. package/scripts/verify-omp-mcp-bridge.mjs +333 -0
  47. package/templates/start-omp.sh.template +47 -0
@@ -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",
@@ -30,6 +30,7 @@ const POSIX_COMMANDS = new Map([
30
30
  ["sync", "sync-claude-assets.sh"],
31
31
  ["sync-pi", "sync-pi-assets.sh"],
32
32
  ["sync-kimi", "sync-kimi-assets.sh"],
33
+ ["sync-omp", "sync-omp-assets.sh"],
33
34
  ["audit-timer", "install-audit-timer.sh"],
34
35
  ["completion", "completion.sh"],
35
36
  ]);
@@ -44,6 +45,7 @@ Usage:
44
45
  cairn sync [--check|--apply] [--live-root DIR]
45
46
  cairn sync-pi [--check|--apply] [--live-root DIR]
46
47
  cairn sync-kimi [--check|--apply] [--live-root DIR]
48
+ cairn sync-omp [--check|--apply] [--live-root DIR]
47
49
  cairn doctor [--repair]
48
50
  cairn trajectory <list|show|prune>
49
51
  cairn proposals <create|list|show|apply|doctor>
@@ -13,7 +13,7 @@ _cairn_complete() {
13
13
  COMPREPLY=()
14
14
  current=${COMP_WORDS[COMP_CWORD]}
15
15
  previous=${COMP_WORDS[COMP_CWORD-1]:-}
16
- commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
16
+ commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
17
17
  if (( COMP_CWORD == 1 )); then
18
18
  COMPREPLY=( $(compgen -W "$commands" -- "$current") )
19
19
  return
@@ -24,6 +24,7 @@ _cairn_complete() {
24
24
  sync) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
25
25
  sync-pi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
26
26
  sync-kimi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
27
+ sync-omp) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
27
28
  doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
28
29
  trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
29
30
  artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
@@ -94,6 +95,7 @@ _cairn() {
94
95
  'sync:install the Claude operating layer'
95
96
  'sync-pi:install the Pi trajectory extension and graph prompt'
96
97
  'sync-kimi:install the Kimi graph Skill'
98
+ 'sync-omp:install the oh-my-pi memory extension'
97
99
  'doctor:check runtime dependencies and endpoints'
98
100
  'trajectory:inspect and prune local session trajectories'
99
101
  'artifact:inspect, delete, and prune local artifacts'
@@ -124,6 +126,7 @@ _cairn() {
124
126
  sync) _arguments '--apply[apply changes]' '--live-root[project Claude root]:directory:_files -/' ;;
125
127
  sync-pi) _arguments '--apply[apply changes]' '--live-root[Pi agent root]:directory:_files -/' ;;
126
128
  sync-kimi) _arguments '--apply[apply changes]' '--live-root[Kimi Code root]:directory:_files -/' ;;
129
+ sync-omp) _arguments '--apply[apply changes]' '--live-root[omp agent root]:directory:_files -/' ;;
127
130
  doctor) _arguments '--repair[repair trajectory metadata and indexes]' ;;
128
131
  trajectory) _values 'trajectory command' list show prune ;;
129
132
  artifact)
@@ -188,8 +191,8 @@ EOF
188
191
  fish)
189
192
  sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
190
193
  complete -c cairn -f
191
- for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
192
- complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
194
+ for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
195
+ complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
193
196
  end
194
197
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
195
198
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
@@ -197,6 +200,8 @@ complete -c cairn -n "__fish_seen_subcommand_from sync-pi" -l apply
197
200
  complete -c cairn -n "__fish_seen_subcommand_from sync-pi" -l live-root -r
198
201
  complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l apply
199
202
  complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l live-root -r
203
+ complete -c cairn -n "__fish_seen_subcommand_from sync-omp" -l apply
204
+ complete -c cairn -n "__fish_seen_subcommand_from sync-omp" -l live-root -r
200
205
  complete -c cairn -n "__fish_seen_subcommand_from bootstrap" -l untracked
201
206
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l git -r -a "init existing none"
202
207
  complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "__CAIRN_HARNESSES__"
@@ -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() {
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // omp capture staging child for the cairnkeep-capture extension.
3
+ //
4
+ // Reads transcript text on stdin, runs the shared extraction
5
+ // (`node <server-entry> extract <model>`, stdin-piped — transcript text is
6
+ // never interpolated into a shell line), and stages the candidate JSON under
7
+ // <repo>/.planning/memory-staging/ with the exact same filename, JSON, and
8
+ // retention-cap-5 contract as stageCandidates in opencode/plugins/memory-capture.ts.
9
+ //
10
+ // Invoked detached + unref'd by the extension so staging completes even when
11
+ // the agent process exits before extraction returns. Fail-open: always exits 0.
12
+ //
13
+ // Usage: node omp-capture-stage.mjs <repo> <server-entry> <model>
14
+
15
+ import { spawn } from "node:child_process";
16
+ import { mkdirSync, readdirSync, writeFileSync, unlinkSync, statSync, existsSync } from "node:fs";
17
+ import { join } from "node:path";
18
+
19
+ const RETENTION_CAP = 5;
20
+ const EXTRACT_TIMEOUT_MS = 120_000;
21
+
22
+ const [, , repo, serverEntry, model] = process.argv;
23
+
24
+ function stageCandidates(repoRoot, candidatesJson) {
25
+ const stagingDir = join(repoRoot, ".planning", "memory-staging");
26
+ mkdirSync(stagingDir, { recursive: true });
27
+ const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
28
+ const stageFile = join(stagingDir, `${ts}.json`);
29
+ writeFileSync(stageFile, `${candidatesJson}\n`);
30
+
31
+ // Keep the staging dir bounded — drop the oldest beyond 5 sessions.
32
+ const staged = readdirSync(stagingDir)
33
+ .filter((f) => f.endsWith(".json"))
34
+ .map((f) => ({ f, mtime: statSync(join(stagingDir, f)).mtimeMs }))
35
+ .sort((a, b) => b.mtime - a.mtime);
36
+ for (const { f } of staged.slice(RETENTION_CAP)) {
37
+ unlinkSync(join(stagingDir, f));
38
+ }
39
+ return stageFile;
40
+ }
41
+
42
+ function runExtract(entry, extractModel, input) {
43
+ return new Promise((resolvePromise) => {
44
+ let stdout = "";
45
+ let settled = false;
46
+ const finish = () => {
47
+ if (settled) return;
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ resolvePromise(stdout);
51
+ };
52
+ const child = spawn("node", [entry, "extract", extractModel], { stdio: ["pipe", "pipe", "ignore"] });
53
+ const timer = setTimeout(() => {
54
+ try { child.kill("SIGKILL"); } catch { /* best-effort kill only */ }
55
+ }, EXTRACT_TIMEOUT_MS);
56
+ child.stdout?.on("data", (chunk) => {
57
+ stdout += chunk.toString("utf8");
58
+ if (stdout.length > 1024 * 1024) {
59
+ stdout = "";
60
+ try { child.kill("SIGKILL"); } catch { /* best-effort kill only */ }
61
+ }
62
+ });
63
+ child.on("close", finish);
64
+ child.on("error", finish);
65
+ child.stdin.on("error", () => {
66
+ // EPIPE after a failed/terminated child is part of the fail-open path.
67
+ });
68
+ child.stdin.write(input);
69
+ child.stdin.end();
70
+ });
71
+ }
72
+
73
+ async function main() {
74
+ if (!repo || !serverEntry || !model || !existsSync(serverEntry)) return;
75
+ let input = "";
76
+ try {
77
+ for await (const chunk of process.stdin) input += chunk.toString("utf8");
78
+ } catch {
79
+ return;
80
+ }
81
+ input = input.trim();
82
+ if (!input) return;
83
+
84
+ const candidatesJson = (await runExtract(serverEntry, model, input)).trim();
85
+ if (!candidatesJson) return;
86
+
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(candidatesJson);
90
+ } catch {
91
+ return;
92
+ }
93
+ if (!Array.isArray(parsed.candidates) || parsed.candidates.length === 0) return;
94
+
95
+ stageCandidates(repo, candidatesJson);
96
+ }
97
+
98
+ try {
99
+ await main();
100
+ } catch {
101
+ // Fail open — staging is best-effort.
102
+ }
103
+ process.exit(0);
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) {
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: sync-omp-assets.sh [--check|--apply] [--live-root PATH]
7
+
8
+ Compare or sync the repo-managed oh-my-pi (omp) extension asset against the
9
+ live omp agent configuration tree.
10
+
11
+ Options:
12
+ --check Verify managed live assets match the rendered repo copies (default)
13
+ --apply Install managed assets, then verify
14
+ --live-root PATH Override the omp agent root (default: $PI_CODING_AGENT_DIR or ~/.omp/agent)
15
+ -h, --help Show this help text
16
+ EOF
17
+ }
18
+
19
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
20
+ SOURCE_ROOT="$ROOT_DIR/omp"
21
+ LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.omp/agent}"
22
+ MODE="check"
23
+ ASSETS=(
24
+ "extensions/cairnkeep-memory.ts"
25
+ "extensions/cairnkeep-capture.ts"
26
+ )
27
+
28
+ while [[ $# -gt 0 ]]; do
29
+ case "$1" in
30
+ --check) MODE="check"; shift ;;
31
+ --apply) MODE="apply"; shift ;;
32
+ --live-root) LIVE_ROOT="$2"; shift 2 ;;
33
+ -h|--help) usage; exit 0 ;;
34
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
35
+ esac
36
+ done
37
+
38
+ render_asset() {
39
+ local rel="$1"
40
+ if [[ "$rel" == extensions/*.ts ]]; then
41
+ sed "s|@@INFRA_ROOT@@|$ROOT_DIR|g" "$SOURCE_ROOT/$rel"
42
+ else
43
+ cat "$SOURCE_ROOT/$rel"
44
+ fi
45
+ }
46
+
47
+ ensure_sources() {
48
+ local rel
49
+ for rel in "${ASSETS[@]}"; do
50
+ [[ -f "$SOURCE_ROOT/$rel" ]] || {
51
+ echo "Missing repo-managed source asset: $SOURCE_ROOT/$rel" >&2
52
+ return 2
53
+ }
54
+ done
55
+ }
56
+
57
+ check_assets() {
58
+ local rel
59
+ local failed=0
60
+ for rel in "${ASSETS[@]}"; do
61
+ if [[ ! -f "$LIVE_ROOT/$rel" ]]; then
62
+ echo "Missing live omp asset under $LIVE_ROOT: $rel" >&2
63
+ failed=1
64
+ elif ! cmp -s <(render_asset "$rel") "$LIVE_ROOT/$rel"; then
65
+ echo "Out-of-sync live omp asset under $LIVE_ROOT: $rel" >&2
66
+ failed=1
67
+ fi
68
+ done
69
+ return "$failed"
70
+ }
71
+
72
+ ensure_sources
73
+
74
+ if [[ "$MODE" == "apply" ]]; then
75
+ updated=0
76
+ unchanged=0
77
+ for rel in "${ASSETS[@]}"; do
78
+ mkdir -p "$(dirname "$LIVE_ROOT/$rel")"
79
+ if [[ -f "$LIVE_ROOT/$rel" ]] && cmp -s <(render_asset "$rel") "$LIVE_ROOT/$rel"; then
80
+ unchanged=$((unchanged + 1))
81
+ continue
82
+ fi
83
+ rendered_tmp=$(mktemp)
84
+ render_asset "$rel" > "$rendered_tmp"
85
+ install -m 0644 "$rendered_tmp" "$LIVE_ROOT/$rel"
86
+ rm -f "$rendered_tmp"
87
+ updated=$((updated + 1))
88
+ done
89
+ printf 'Applied %s omp asset(s); %s already matched.\n' "$updated" "$unchanged"
90
+ fi
91
+
92
+ if ! check_assets; then
93
+ echo "Recovery: sync-omp-assets.sh --apply --live-root PATH" >&2
94
+ exit 1
95
+ fi
96
+ printf 'omp extensions are in sync under %s\n' "$LIVE_ROOT"
@@ -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//\//__}"
@@ -12,6 +12,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
12
12
  "$cairn" help | grep -q "cairn sync" || fail "help missing sync"
13
13
  "$cairn" help | grep -q "cairn sync-pi" || fail "help missing sync-pi"
14
14
  "$cairn" help | grep -q "cairn sync-kimi" || fail "help missing sync-kimi"
15
+ "$cairn" help | grep -q "cairn sync-omp" || fail "help missing sync-omp"
15
16
  "$cairn" help | grep -q "cairn memory-server" || fail "help missing memory-server"
16
17
  "$cairn" help | grep -q "cairn notes" || fail "help missing notes"
17
18
  "$cairn" help | grep -q "cairn artifact <list|show|delete|prune>" || fail "help missing artifact"
@@ -24,6 +25,7 @@ tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
24
25
  "$cairn" sync --help >/dev/null 2>&1 || fail "cairn sync dispatch"
25
26
  "$cairn" sync-pi --help >/dev/null 2>&1 || fail "cairn sync-pi dispatch"
26
27
  "$cairn" sync-kimi --help >/dev/null 2>&1 || fail "cairn sync-kimi dispatch"
28
+ "$cairn" sync-omp --help >/dev/null 2>&1 || fail "cairn sync-omp dispatch"
27
29
  env -u CAIRN_NOTE_DISTILLATION "$cairn" notes --help >/dev/null 2>&1 || fail "cairn notes dispatch"
28
30
  "$cairn" artifact --help | grep -q "cairn artifact list" || fail "cairn artifact dispatch"
29
31
  "$cairn" evidence --help | grep -q "cairn evidence list" || fail "cairn evidence dispatch"
@@ -109,12 +109,14 @@ for shell in bash zsh; do
109
109
  grep -q 'sync-pi' "$tmp/$shell"
110
110
  grep -q 'codex' "$tmp/$shell"
111
111
  grep -q 'sync-kimi' "$tmp/$shell"
112
+ grep -q 'sync-omp' "$tmp/$shell"
112
113
  grep -q -- '--live-root' "$tmp/$shell"
113
114
  done
114
115
  grep -q 'sync-pi' "$tmp/fish"
115
116
  grep -q 'codex' "$tmp/fish"
116
117
  grep -q 'sync-kimi' "$tmp/fish"
117
- grep -q 'not __fish_seen_subcommand_from .*sync-pi sync-kimi' "$tmp/fish"
118
+ grep -q 'sync-omp' "$tmp/fish"
119
+ grep -q 'not __fish_seen_subcommand_from .*sync-pi sync-kimi sync-omp' "$tmp/fish"
118
120
  grep -q -- '-l live-root' "$tmp/fish"
119
121
  for shell in bash zsh fish; do
120
122
  grep -q 'notes' "$tmp/$shell"
@@ -163,7 +165,7 @@ if [[ "$completion_surface_complete" != true ]]; then
163
165
  fi
164
166
 
165
167
  setup_flags='--git --harness --memory --policy --yes --json'
166
- 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'
167
169
  for shell in bash zsh fish powershell; do
168
170
  for flag in $setup_flags; do
169
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