omp-multi-harness 0.1.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/LICENSE +21 -0
- package/README.md +351 -0
- package/package.json +76 -0
- package/scripts/cli.ts +164 -0
- package/scripts/setup/claude.ts +41 -0
- package/scripts/setup/codex.ts +35 -0
- package/scripts/setup/omp.ts +167 -0
- package/scripts/setup/toolchain.ts +81 -0
- package/scripts/setup/types.ts +76 -0
- package/scripts/setup.ts +116 -0
- package/src/agents/availability.ts +106 -0
- package/src/agents/claude-events.ts +125 -0
- package/src/agents/claude.ts +226 -0
- package/src/agents/codex-events.ts +149 -0
- package/src/agents/codex.ts +236 -0
- package/src/agents/types.ts +81 -0
- package/src/commands/agents.ts +140 -0
- package/src/commands/delegate-command.ts +159 -0
- package/src/commands/harness-setup.ts +94 -0
- package/src/commands/sessions.ts +394 -0
- package/src/config/load.ts +78 -0
- package/src/config/schema.ts +249 -0
- package/src/index.ts +129 -0
- package/src/process/executable.ts +49 -0
- package/src/process/jsonl.ts +124 -0
- package/src/process/process-error.ts +178 -0
- package/src/process/redact.ts +120 -0
- package/src/process/spawn-agent.ts +218 -0
- package/src/routing/handoff.ts +59 -0
- package/src/routing/prompt.ts +72 -0
- package/src/routing/route.ts +286 -0
- package/src/runs/lock.ts +158 -0
- package/src/runs/registry.ts +379 -0
- package/src/runs/ring-buffer.ts +81 -0
- package/src/runs/types.ts +141 -0
- package/src/sessions/resume.ts +163 -0
- package/src/sessions/store.ts +273 -0
- package/src/tools/agent-runs.ts +169 -0
- package/src/tools/ask-agent.ts +230 -0
- package/src/tools/delegate.ts +196 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmad Bakhshi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# omp-multi-harness
|
|
2
|
+
|
|
3
|
+
An [OMP](https://github.com/can1357/oh-my-pi) extension that turns OMP into a **supervisor**
|
|
4
|
+
delegating work to two coding agents you already have:
|
|
5
|
+
|
|
6
|
+
- **Codex CLI** — `codex`
|
|
7
|
+
- **Claude Code CLI** — `claude`
|
|
8
|
+
|
|
9
|
+
It drives their real binaries through their real non-interactive entry points, using the
|
|
10
|
+
logins you already did. **No API keys. No token extraction. No reimplementation of either
|
|
11
|
+
agent.**
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
OMP (supervisor)
|
|
15
|
+
│
|
|
16
|
+
┌───────────────┼───────────────┐
|
|
17
|
+
▼ ▼ ▼
|
|
18
|
+
ask_codex ask_claude /sessions
|
|
19
|
+
│ │ (watch, switch,
|
|
20
|
+
▼ ▼ cancel runs)
|
|
21
|
+
codex CLI claude CLI
|
|
22
|
+
│ │
|
|
23
|
+
your Codex your Claude
|
|
24
|
+
login login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **Status — Phases 0–5 of 6 complete, Phase 6 substantially done.** Everything in the
|
|
28
|
+
> tables below is implemented: delegation, routing, parallel runs, `/sessions`, session
|
|
29
|
+
> continuation, and the background (`--bg`) path. Claude delegation is verified end-to-end
|
|
30
|
+
> against the real CLI; **Codex passes its fake-CLI suite but has never been verified live**,
|
|
31
|
+
> because that account is out of credits — treat it as unproven against the real binary.
|
|
32
|
+
> Model-based `auto` routing is likewise unverified live: OMP itself has no authenticated
|
|
33
|
+
> model here, so it silently falls back to the rule table (which is tested).
|
|
34
|
+
> Progress: [`_plan/PROGRESS.md`](_plan/PROGRESS.md).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### 1. Install the three CLIs
|
|
41
|
+
|
|
42
|
+
| | install | check |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| OMP | `bun add -g @oh-my-pi/pi-coding-agent` | `omp --version` |
|
|
45
|
+
| Codex | `brew install --cask codex` · `npm i -g @openai/codex` | `codex --version` |
|
|
46
|
+
| Claude Code | `npm i -g @anthropic-ai/claude-code` | `claude --version` |
|
|
47
|
+
|
|
48
|
+
OMP loads extensions with [Bun](https://bun.sh), so Bun is required
|
|
49
|
+
(`curl -fsSL https://bun.sh/install | bash`).
|
|
50
|
+
|
|
51
|
+
### 2. Log in to each — three separate, non-transferable logins
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
omp # then /login — OMP's own model (supervisor turns + auto routing)
|
|
55
|
+
codex login # — everything ask_codex does
|
|
56
|
+
claude auth login # — everything ask_claude does
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is not redundancy. Claude Code will not accept credentials supplied by another tool,
|
|
60
|
+
so each CLI keeps its own login and this extension never touches any of them — it only
|
|
61
|
+
reads `codex login status` and `claude auth status`, which print no secrets.
|
|
62
|
+
|
|
63
|
+
If OMP itself is not logged in, delegation still works; only OMP's own turns and
|
|
64
|
+
model-based `auto` routing are affected (routing falls back to rules).
|
|
65
|
+
|
|
66
|
+
### 3. Install the extension
|
|
67
|
+
|
|
68
|
+
OMP discovers extensions from its **agent dir** (`<agentDir>/extensions/`) and from a
|
|
69
|
+
project's `.omp/extensions/`, resolving each directory through that directory's
|
|
70
|
+
`package.json` → `omp.extensions`. It does **not** scan `node_modules`. So installing from
|
|
71
|
+
npm is two steps: install the package, then register it. The bundled CLI does step two.
|
|
72
|
+
|
|
73
|
+
#### From npm — global, for every project (recommended)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install -g omp-multi-harness
|
|
77
|
+
omp-multi-harness link
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`link` symlinks the installed package into `<agentDir>/extensions/multi-harness`. It only
|
|
81
|
+
ever manages its own symlink: it refuses to replace anything that is not a symlink it
|
|
82
|
+
created, and it never edits your OMP config, logs you in, or installs a CLI.
|
|
83
|
+
|
|
84
|
+
#### From npm — one project only
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install --save-dev omp-multi-harness
|
|
88
|
+
npx omp-multi-harness link --project
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That links into `./.omp/extensions/multi-harness`, so the extension loads for this
|
|
92
|
+
repository and nowhere else.
|
|
93
|
+
|
|
94
|
+
#### Using a profile
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
omp-multi-harness link --profile work # ~/.omp/profiles/work/agent/extensions/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`PI_CODING_AGENT_DIR` overrides both the default and `--profile`, and is honored by the CLI
|
|
101
|
+
and by the extension at runtime — nothing hard-codes `~/.omp/agent`.
|
|
102
|
+
|
|
103
|
+
#### Check, and undo
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
omp-multi-harness status # where it is linked, globally and for this project
|
|
107
|
+
omp-multi-harness unlink # remove the symlink (add --project for the project one)
|
|
108
|
+
omp-multi-harness doctor # the full setup check
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### From source
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/abdevts/omp-multi-harness.git
|
|
115
|
+
cd omp-multi-harness
|
|
116
|
+
bun install
|
|
117
|
+
bun scripts/cli.ts link # same linking, straight from the checkout
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or skip installation entirely and load it per run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
omp -e /path/to/omp-multi-harness/src/index.ts
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> **No build step.** OMP loads TypeScript directly, so the package ships its sources and
|
|
127
|
+
> `omp.extensions` points at `./src/index.ts`. There is nothing to compile.
|
|
128
|
+
|
|
129
|
+
### Using it from VS Code
|
|
130
|
+
|
|
131
|
+
The extension lives inside `omp`, not inside VS Code — so "using it in VS Code" means
|
|
132
|
+
running `omp` in VS Code's integrated terminal with the extension registered. Once
|
|
133
|
+
`omp-multi-harness link` has been run, every terminal session picks it up automatically.
|
|
134
|
+
|
|
135
|
+
This repository ships `.vscode/` with the loop already wired:
|
|
136
|
+
|
|
137
|
+
| file | what it gives you |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `tasks.json` | **Terminal → Run Task** for `doctor`, `test`, and `typecheck` |
|
|
140
|
+
| `launch.json` | debug `omp` with the extension loaded via `-e` |
|
|
141
|
+
| `settings.json` | formatting and TypeScript settings matching this codebase |
|
|
142
|
+
| `extensions.json` | the editor extensions this project expects |
|
|
143
|
+
|
|
144
|
+
For a project that merely *consumes* the package, you do not need any of that — install,
|
|
145
|
+
link, and open a terminal:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm install --save-dev omp-multi-harness
|
|
149
|
+
npx omp-multi-harness link --project
|
|
150
|
+
omp # in VS Code's integrated terminal
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Then `/agents` inside OMP confirms it loaded. If you keep a workspace-local agent dir, set
|
|
154
|
+
`PI_CODING_AGENT_DIR` in VS Code's terminal environment (`terminal.integrated.env.osx`,
|
|
155
|
+
`.linux`, or `.windows`) and both the CLI and the extension will follow it.
|
|
156
|
+
|
|
157
|
+
### 4. Verify
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bun run doctor # 14 checks: tools, all three logins, router model, install, config
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
Codex
|
|
165
|
+
✔ Codex CLI codex-cli 0.155.0 (/opt/homebrew/bin/codex)
|
|
166
|
+
✔ Codex authentication Logged in using ChatGPT
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`bun scripts/setup.ts fix` applies the safe repairs (symlink, config block, dependencies).
|
|
170
|
+
Installs and logins are printed for you to run — never executed automatically.
|
|
171
|
+
|
|
172
|
+
Inside OMP, `/agents` and `/harness-setup` report the same thing.
|
|
173
|
+
|
|
174
|
+
### 5. Configure (optional — the defaults are sensible)
|
|
175
|
+
|
|
176
|
+
Add to `~/.omp/agent/config.yml`, or `<project>/.omp/config.yml` to override per project:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
multiHarness:
|
|
180
|
+
enabled: true
|
|
181
|
+
|
|
182
|
+
codex:
|
|
183
|
+
enabled: true
|
|
184
|
+
model: null # null → your ~/.codex/config.toml decides
|
|
185
|
+
timeoutMs: 1800000
|
|
186
|
+
|
|
187
|
+
claude:
|
|
188
|
+
enabled: true
|
|
189
|
+
model: null # null → your Claude Code config decides
|
|
190
|
+
acceptEdits: false
|
|
191
|
+
|
|
192
|
+
routing:
|
|
193
|
+
mode: model # model | rules — how `auto` picks an agent
|
|
194
|
+
model: "@smol" # router model: role alias, provider/id, or bare id
|
|
195
|
+
modeMap:
|
|
196
|
+
plan: claude
|
|
197
|
+
review: claude
|
|
198
|
+
implement: codex
|
|
199
|
+
debug: codex
|
|
200
|
+
|
|
201
|
+
concurrency:
|
|
202
|
+
maxConcurrentRuns: 4
|
|
203
|
+
allowParallelWrites: false # two agents never write one tree at once
|
|
204
|
+
|
|
205
|
+
debug: false
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Every option: [`_spec/09-config.md`](_spec/09-config.md).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Usage
|
|
213
|
+
|
|
214
|
+
Flags for `/codex` and `/claude`: `--read-only`, `--write`, `--new`, `--bg`,
|
|
215
|
+
`--mode <mode>`, `--model <id>`.
|
|
216
|
+
|
|
217
|
+
`--bg` returns a run id immediately and keeps working in the background; track it with
|
|
218
|
+
`/sessions`. Many runs can be alive at once — see [Parallel runs](#parallel-runs).
|
|
219
|
+
|
|
220
|
+
### Commands
|
|
221
|
+
|
|
222
|
+
| command | status | what it does |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `/agents` | ✅ | Availability, version, auth, and readiness for both agents |
|
|
225
|
+
| `/agents auth [codex\|claude]` | ✅ | Auth status plus the exact login command |
|
|
226
|
+
| `/harness-setup` | ✅ | The setup checklist, inside a session |
|
|
227
|
+
| `/codex <task>` | ✅ | Delegate straight to Codex |
|
|
228
|
+
| `/claude <task>` | ✅ | Delegate straight to Claude Code |
|
|
229
|
+
| `/sessions` | ✅ | List, watch, switch focus between, and cancel running delegations |
|
|
230
|
+
|
|
231
|
+
### Tools the supervisor calls on its own
|
|
232
|
+
|
|
233
|
+
| tool | status | typical use |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `ask_codex` | ✅ | implementation, debugging, refactors, tests |
|
|
236
|
+
| `ask_claude` | ✅ | architecture, planning, design review, second opinions |
|
|
237
|
+
| `delegate` | ✅ | `agent: "auto"` routing |
|
|
238
|
+
| `agent_runs` | ✅ | fan several runs out, then join them |
|
|
239
|
+
|
|
240
|
+
### Parallel runs
|
|
241
|
+
|
|
242
|
+
Every delegation — whether from a slash command or a tool — becomes a *run* in a
|
|
243
|
+
session-scoped registry. Runs execute concurrently; `/sessions` is the view over them.
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
/claude --bg review the auth architecture
|
|
247
|
+
/codex --bg --read-only summarize the test suite
|
|
248
|
+
/sessions # both listed, elapsed ticking
|
|
249
|
+
/sessions attach r7c1 # stream that run's output into the widget
|
|
250
|
+
/sessions cancel r7c2 # stops just that run
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Attaching changes only what you are shown. It never pauses, throttles, or reorders a run —
|
|
254
|
+
unfocused runs keep executing and keep filling their own output buffers.
|
|
255
|
+
|
|
256
|
+
What may run at once:
|
|
257
|
+
|
|
258
|
+
| situation | parallel? |
|
|
259
|
+
|---|---|
|
|
260
|
+
| N read-only runs, same repo | yes |
|
|
261
|
+
| read-only + writer, same repo | yes — the reader may observe a moving tree, and the result says so |
|
|
262
|
+
| two writers, same repo | no — serialized by a FIFO workspace write lock |
|
|
263
|
+
| two writers, different repos | yes |
|
|
264
|
+
|
|
265
|
+
`concurrency.maxConcurrentRuns` (default 4) caps live child processes; the rest wait in
|
|
266
|
+
`queued`. Quitting OMP cancels every live run and waits for it to die — no child or
|
|
267
|
+
grandchild outlives the session.
|
|
268
|
+
|
|
269
|
+
> `/sessions` covers **delegated runs only**. OMP's own `/resume` already lists and switches
|
|
270
|
+
> OMP sessions, including `/resume @claude` and `/resume @codex` to import a worker session,
|
|
271
|
+
> so this extension does not duplicate it.
|
|
272
|
+
|
|
273
|
+
### Session continuation
|
|
274
|
+
|
|
275
|
+
Each `(OMP session, repo)` pair remembers the worker session it was talking to, so a second
|
|
276
|
+
`/codex` continues the first one's thread rather than starting cold. `--new` forces a fresh
|
|
277
|
+
one. If a resume fails, the run retries **once** with a compact handoff summary rather than
|
|
278
|
+
replaying history, and says so in the result. Two parallel runs against the same agent and
|
|
279
|
+
repo cannot share one worker session: the second forks (Claude) or starts fresh (Codex).
|
|
280
|
+
|
|
281
|
+
The mapping holds ids, paths, and timestamps only — never task text, tokens, or
|
|
282
|
+
environment — in `0600` files under a `0700` directory in the active agent dir (honoring
|
|
283
|
+
`--profile` and `PI_CODING_AGENT_DIR`).
|
|
284
|
+
|
|
285
|
+
### Model selection
|
|
286
|
+
|
|
287
|
+
Three independent choices:
|
|
288
|
+
|
|
289
|
+
- **Worker models** stay yours. Set nothing and no `-m`/`--model` is passed at all — each
|
|
290
|
+
CLI's own config decides. Override per agent (`codex.model`) or per call (`--model`).
|
|
291
|
+
- **Router model** (`routing.model`, default `@smol`) resolves `agent: "auto"` with one
|
|
292
|
+
small classification call. Any failure falls back to the rule table, so it can never
|
|
293
|
+
block a delegation. `routing.mode: rules` turns it off entirely.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Troubleshooting
|
|
298
|
+
|
|
299
|
+
| symptom | cause and fix |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `/agents` says *not authenticated* for Codex | `codex login`. (Note `codex login status` prints to stderr — tools that read only stdout get this wrong.) |
|
|
302
|
+
| `/agents` says *not authenticated* for Claude | `claude auth login` |
|
|
303
|
+
| `auto` always picks by rules | OMP has no authenticated model — `omp` → `/login`, or set `routing.model` |
|
|
304
|
+
| `unavailable — \`codex\` not found on PATH` | install it, or set `multiHarness.codex.executable` to the full path |
|
|
305
|
+
| `PROVIDER_LIMIT: … out of credits` | the provider account, not the task — top up or switch accounts. Retrying will not help. |
|
|
306
|
+
| doctor shows *repo-local copy shadowing your global omp* | harmless: `bun run` puts `node_modules/.bin` first, and the dev dependency ships an `omp` |
|
|
307
|
+
| Extension not loading | confirm the symlink target, or run `omp -e ./src/index.ts` directly |
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Security posture
|
|
312
|
+
|
|
313
|
+
- Never reads OAuth or token files; auth state comes only from each CLI's own status command.
|
|
314
|
+
- Never prints credentials. `claude auth status --json` returns your email and org — only
|
|
315
|
+
`loggedIn` and `authMethod` are read.
|
|
316
|
+
- Never forwards one provider's credentials to the other.
|
|
317
|
+
- No `shell: true`, ever. Arguments are arrays; prompts go over **stdin**, so task text
|
|
318
|
+
never lands in `ps` or shell history.
|
|
319
|
+
- Permission-bypass flags (`--dangerously-bypass-approvals-and-sandbox`,
|
|
320
|
+
`--dangerously-skip-permissions`) are never passed, and no config option enables them.
|
|
321
|
+
- Working directories are realpath-validated; a run cannot silently target another repo.
|
|
322
|
+
- Two write-capable agents never touch the same working tree at once.
|
|
323
|
+
- `readOnlyEnforced` reports what the adapter's **argv actually enforced**, not what the
|
|
324
|
+
caller requested. An adapter that cannot prove enforcement reports `false`.
|
|
325
|
+
- Every CLI-derived string embedded in an error is redacted first (API keys, tokens, JWTs,
|
|
326
|
+
PEM blocks, `KEY=value` assignments), so a credential a worker echoes back cannot reach
|
|
327
|
+
an error message or a log. Environment views are allowlist-based — a denylist fails open.
|
|
328
|
+
|
|
329
|
+
Details: [`_spec/10-errors-and-security.md`](_spec/10-errors-and-security.md).
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
bun install
|
|
337
|
+
bun test # 110 tests, no live provider calls
|
|
338
|
+
MULTI_HARNESS_LIVE_TESTS=1 bun test test/live.test.ts # opt-in, calls the real CLIs
|
|
339
|
+
bun run typecheck
|
|
340
|
+
bun run dev # omp --no-extensions -e ./src/index.ts
|
|
341
|
+
bun run doctor
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
VS Code config ships in `.vscode/` (tasks for doctor/typecheck/test, launch configs, and
|
|
345
|
+
recommended extensions).
|
|
346
|
+
|
|
347
|
+
- [`_spec/`](_spec/README.md) — the normative specification, 15 documents
|
|
348
|
+
- [`_plan/`](_plan/README.md) — phased plan, decision log, and the task tracker
|
|
349
|
+
|
|
350
|
+
Built and verified against `omp 18.2.6` · `codex-cli 0.155.0` · `claude 2.1.274` · `bun 1.4.2`
|
|
351
|
+
([`_spec/01-environment-findings.md`](_spec/01-environment-findings.md)).
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omp-multi-harness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OMP extension that delegates work to the Codex and Claude Code CLIs \u2014 parallel runs, /sessions, and automatic routing, using each CLI's own login",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ahmad Bakhshi",
|
|
8
|
+
"homepage": "https://github.com/abdevts/omp-multi-harness#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/abdevts/omp-multi-harness.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/abdevts/omp-multi-harness/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"omp",
|
|
18
|
+
"omp-extension",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"codex",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"ai",
|
|
23
|
+
"agent",
|
|
24
|
+
"cli",
|
|
25
|
+
"delegation"
|
|
26
|
+
],
|
|
27
|
+
"main": "src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"omp": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./src/index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"omp-multi-harness": "scripts/cli.ts"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"src",
|
|
42
|
+
"scripts/cli.ts",
|
|
43
|
+
"scripts/setup",
|
|
44
|
+
"scripts/setup.ts",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.4.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"setup": "bun scripts/setup.ts",
|
|
53
|
+
"doctor": "bun scripts/setup.ts check",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "bun test",
|
|
56
|
+
"dev": "omp --no-extensions -e ./src/index.ts",
|
|
57
|
+
"link-extension": "bun scripts/cli.ts link",
|
|
58
|
+
"prepublishOnly": "bun run typecheck && bun test"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@oh-my-pi/pi-coding-agent": ">=18.2.6"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@oh-my-pi/pi-coding-agent": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@oh-my-pi/pi-coding-agent": "^18.2.6",
|
|
70
|
+
"@types/bun": "^1.4.2",
|
|
71
|
+
"typescript": "^7.0.2"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/scripts/cli.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* `omp-multi-harness` CLI — registers this package with OMP.
|
|
4
|
+
*
|
|
5
|
+
* Installing from npm puts the package in node_modules, which OMP does **not** scan. OMP
|
|
6
|
+
* discovers extensions from its agent dir (`<agentDir>/extensions/`) and from a project's
|
|
7
|
+
* `.omp/extensions/`, resolving each directory through its `package.json` `omp.extensions`
|
|
8
|
+
* manifest. So the install step is a symlink from one of those directories to this package.
|
|
9
|
+
*
|
|
10
|
+
* Symlinks only — this never edits your OMP config, never logs in, and never installs a CLI.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const LINK_NAME = "multi-harness";
|
|
18
|
+
|
|
19
|
+
/** This package's root — the directory containing its package.json. */
|
|
20
|
+
function packageRoot(): string {
|
|
21
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The active OMP agent dir, resolved the same way OMP resolves it: `PI_CODING_AGENT_DIR`
|
|
26
|
+
* wins, then `--profile <name>`, then the default. Never hard-coded to `~/.omp/agent`.
|
|
27
|
+
*/
|
|
28
|
+
function agentDir(profile?: string): string {
|
|
29
|
+
const fromEnv = process.env.PI_CODING_AGENT_DIR;
|
|
30
|
+
if (fromEnv) return resolve(fromEnv);
|
|
31
|
+
if (profile) return join(homedir(), ".omp", "profiles", profile, "agent");
|
|
32
|
+
return join(homedir(), ".omp", "agent");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function targetDir(scope: "global" | "project", profile?: string, cwd = process.cwd()): string {
|
|
36
|
+
return scope === "global" ? join(agentDir(profile), "extensions") : join(resolve(cwd), ".omp", "extensions");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Where this package is (or would be) linked from. */
|
|
40
|
+
function linkPath(scope: "global" | "project", profile?: string): string {
|
|
41
|
+
return join(targetDir(scope, profile), LINK_NAME);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function describeExisting(path: string): { kind: "absent" | "symlink" | "other"; target?: string } {
|
|
45
|
+
if (!existsSync(path) && !isDanglingSymlink(path)) return { kind: "absent" };
|
|
46
|
+
try {
|
|
47
|
+
if (lstatSync(path).isSymbolicLink()) return { kind: "symlink", target: readlinkSync(path) };
|
|
48
|
+
} catch {
|
|
49
|
+
// fall through — treat an unreadable entry as "other" so we never clobber it
|
|
50
|
+
}
|
|
51
|
+
return { kind: "other" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `existsSync` follows symlinks, so a broken link reads as absent without this. */
|
|
55
|
+
function isDanglingSymlink(path: string): boolean {
|
|
56
|
+
try {
|
|
57
|
+
return lstatSync(path).isSymbolicLink();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function link(scope: "global" | "project", profile?: string): number {
|
|
64
|
+
const root = packageRoot();
|
|
65
|
+
const dir = targetDir(scope, profile);
|
|
66
|
+
const dest = linkPath(scope, profile);
|
|
67
|
+
const existing = describeExisting(dest);
|
|
68
|
+
|
|
69
|
+
if (existing.kind === "other") {
|
|
70
|
+
console.error(`Refusing to replace ${dest} — it exists and is not a symlink.`);
|
|
71
|
+
console.error("Move it aside yourself, then re-run. This tool only ever manages its own symlink.");
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
if (existing.kind === "symlink" && resolve(dirname(dest), existing.target ?? "") === root) {
|
|
75
|
+
console.log(`Already linked: ${dest} -> ${root}`);
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
if (existing.kind === "symlink") rmSync(dest);
|
|
81
|
+
symlinkSync(root, dest, "dir");
|
|
82
|
+
|
|
83
|
+
console.log(`Linked: ${dest} -> ${root}`);
|
|
84
|
+
console.log("Start `omp` and run /agents to confirm it loaded.");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function unlink(scope: "global" | "project", profile?: string): number {
|
|
89
|
+
const dest = linkPath(scope, profile);
|
|
90
|
+
const existing = describeExisting(dest);
|
|
91
|
+
|
|
92
|
+
if (existing.kind === "absent") {
|
|
93
|
+
console.log(`Nothing to remove at ${dest}.`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
if (existing.kind === "other") {
|
|
97
|
+
console.error(`Refusing to remove ${dest} — it is not a symlink.`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
rmSync(dest);
|
|
101
|
+
console.log(`Unlinked: ${dest}`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function status(profile?: string): number {
|
|
106
|
+
for (const scope of ["global", "project"] as const) {
|
|
107
|
+
const dest = linkPath(scope, profile);
|
|
108
|
+
const existing = describeExisting(dest);
|
|
109
|
+
const state =
|
|
110
|
+
existing.kind === "absent"
|
|
111
|
+
? "not linked"
|
|
112
|
+
: existing.kind === "other"
|
|
113
|
+
? "occupied by a non-symlink"
|
|
114
|
+
: `-> ${existing.target}`;
|
|
115
|
+
console.log(`${scope.padEnd(8)} ${dest}\n ${state}`);
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const USAGE = `omp-multi-harness — register this OMP extension
|
|
121
|
+
|
|
122
|
+
Usage:
|
|
123
|
+
omp-multi-harness link [--project] [--profile <name>] symlink into OMP's extensions dir
|
|
124
|
+
omp-multi-harness unlink [--project] [--profile <name>] remove that symlink
|
|
125
|
+
omp-multi-harness status [--profile <name>] show where it is linked
|
|
126
|
+
omp-multi-harness doctor run the full setup check
|
|
127
|
+
|
|
128
|
+
--project links into ./.omp/extensions (this repo only) instead of the agent dir.
|
|
129
|
+
--profile <name> targets ~/.omp/profiles/<name>/agent. PI_CODING_AGENT_DIR overrides both.`;
|
|
130
|
+
|
|
131
|
+
const args = process.argv.slice(2);
|
|
132
|
+
const command = args[0] ?? "help";
|
|
133
|
+
const scope: "global" | "project" = args.includes("--project") ? "project" : "global";
|
|
134
|
+
const profileIndex = args.indexOf("--profile");
|
|
135
|
+
const profile = profileIndex >= 0 ? args[profileIndex + 1] : undefined;
|
|
136
|
+
|
|
137
|
+
if (profileIndex >= 0 && !profile) {
|
|
138
|
+
console.error("--profile needs a name");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
switch (command) {
|
|
143
|
+
case "link":
|
|
144
|
+
process.exit(link(scope, profile));
|
|
145
|
+
break;
|
|
146
|
+
case "unlink":
|
|
147
|
+
process.exit(unlink(scope, profile));
|
|
148
|
+
break;
|
|
149
|
+
case "status":
|
|
150
|
+
process.exit(status(profile));
|
|
151
|
+
break;
|
|
152
|
+
case "doctor": {
|
|
153
|
+
// setup.ts runs its checks on import via its own entrypoint, and does not export a
|
|
154
|
+
// callable main — so spawn it rather than pretending it has an API it does not.
|
|
155
|
+
const proc = Bun.spawn(["bun", join(packageRoot(), "scripts", "setup.ts"), "check"], {
|
|
156
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
157
|
+
});
|
|
158
|
+
process.exit(await proc.exited);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
console.log(USAGE);
|
|
163
|
+
process.exit(command === "help" || command === "--help" || command === "-h" ? 0 : 1);
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Claude Code CLI setup: presence + authentication. Mirrors src/agents/claude.ts. */
|
|
2
|
+
import { type SetupGroup, executableStep, sh, which } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const claudeSetup: SetupGroup = {
|
|
5
|
+
id: "claude",
|
|
6
|
+
title: "Claude Code",
|
|
7
|
+
steps: [
|
|
8
|
+
executableStep({
|
|
9
|
+
id: "claude-cli",
|
|
10
|
+
title: "Claude Code CLI",
|
|
11
|
+
bin: "claude",
|
|
12
|
+
install: {
|
|
13
|
+
description: "Install Claude Code",
|
|
14
|
+
command: "npm install -g @anthropic-ai/claude-code",
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
{
|
|
18
|
+
id: "claude-auth",
|
|
19
|
+
title: "Claude Code authentication",
|
|
20
|
+
run() {
|
|
21
|
+
if (!which("claude")) return { status: "skip", detail: "claude not installed" };
|
|
22
|
+
const r = sh("claude", ["auth", "status", "--json"], 20_000);
|
|
23
|
+
if (r.ok) {
|
|
24
|
+
try {
|
|
25
|
+
// The payload also carries email, org id and org name — deliberately
|
|
26
|
+
// read only these two fields, and never persist or print the rest.
|
|
27
|
+
const j = JSON.parse(r.out) as { loggedIn?: boolean; authMethod?: string };
|
|
28
|
+
if (j.loggedIn) return { status: "ok", detail: `logged in via ${j.authMethod ?? "unknown method"}` };
|
|
29
|
+
} catch {
|
|
30
|
+
return { status: "warn", detail: "unexpected `claude auth status` output" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
status: "fail",
|
|
35
|
+
detail: "not authenticated",
|
|
36
|
+
fix: { description: "Complete Claude Code's own login flow", command: "claude auth login" },
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Codex CLI setup: presence + authentication. Mirrors src/agents/codex.ts. */
|
|
2
|
+
import { IS_MAC, type SetupGroup, executableStep, sh, which } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const codexSetup: SetupGroup = {
|
|
5
|
+
id: "codex",
|
|
6
|
+
title: "Codex",
|
|
7
|
+
steps: [
|
|
8
|
+
executableStep({
|
|
9
|
+
id: "codex-cli",
|
|
10
|
+
title: "Codex CLI",
|
|
11
|
+
bin: "codex",
|
|
12
|
+
install: {
|
|
13
|
+
description: "Install the Codex CLI",
|
|
14
|
+
command: IS_MAC ? "brew install --cask codex" : "npm install -g @openai/codex",
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
{
|
|
18
|
+
id: "codex-auth",
|
|
19
|
+
title: "Codex authentication",
|
|
20
|
+
run() {
|
|
21
|
+
if (!which("codex")) return { status: "skip", detail: "codex not installed" };
|
|
22
|
+
const r = sh("codex", ["login", "status"], 20_000);
|
|
23
|
+
// `codex login status` prints to STDERR, not stdout — read both (_spec/14).
|
|
24
|
+
// Only the CLI's own status line is read; no credential file is ever touched.
|
|
25
|
+
const line = [r.out, r.err].join("\n").split("\n").find((l) => /logged in/i.test(l));
|
|
26
|
+
if (r.ok && line) return { status: "ok", detail: line.trim() };
|
|
27
|
+
return {
|
|
28
|
+
status: "fail",
|
|
29
|
+
detail: "not authenticated",
|
|
30
|
+
fix: { description: "Complete Codex's own login flow", command: "codex login" },
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|