@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.
- package/CHANGELOG.md +41 -1
- package/CONTRIBUTING.md +1 -1
- package/docs/harness-compatibility.md +48 -0
- package/docs/learning/CURRICULUM-MAP.md +2 -1
- package/docs/learning/FEATURE-GUIDE.md +2 -1
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +1 -1
- package/docs/learning/lessons/L02-installation.md +1 -1
- package/docs/learning/lessons/L03-first-project.md +1 -1
- 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 +1 -1
- package/docs/learning/lessons/L23-guided-setup.md +18 -7
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/operating.md +13 -6
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-schema.js +1 -1
- package/mcp-memory-server/dist/pi-bridge-child.js +109 -0
- package/mcp-memory-server/dist/pi-mcp-bridge.js +15 -3
- package/mcp-memory-server/dist/work-evidence-schema.js +1 -1
- package/mcp-memory-server/dist/work-evidence-store.js +1 -1
- package/mcp-memory-server/package.json +5 -3
- package/omp/extensions/cairnkeep-capture.ts +226 -0
- package/omp/extensions/cairnkeep-memory.ts +288 -0
- package/package.json +77 -76
- package/schemas/artifact.schema.json +1 -1
- package/schemas/cairnkeep-setup-policy.schema.json +3 -3
- package/schemas/cairnkeep-setup.schema.json +1 -1
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +8 -3
- package/scripts/harness-registry.mjs +15 -3
- package/scripts/lib/omp-capture-stage.mjs +103 -0
- package/scripts/setup.mjs +10 -5
- package/scripts/sync-omp-assets.sh +96 -0
- package/scripts/test-bootstrap-untracked.sh +1 -1
- package/scripts/test-cli-dispatch.sh +2 -0
- package/scripts/test-completion.sh +4 -2
- package/scripts/test-harness-registry.sh +28 -1
- package/scripts/test-setup-output.sh +20 -1
- package/scripts/test-setup-preflight.mjs +1 -1
- package/scripts/test-work-evidence-cli.sh +1 -1
- package/scripts/uninstall.sh +1 -0
- package/scripts/verify-omp-capture.mjs +370 -0
- package/scripts/verify-omp-mcp-bridge.mjs +333 -0
- 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",
|
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -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>
|
package/scripts/completion.sh
CHANGED
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 '
|
|
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 -
|
|
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"
|