model-orchestrator 0.1.4 → 0.1.6

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 CHANGED
@@ -4,9 +4,32 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.6] - 2026-09-05
8
+
9
+ Issues #16 to #19, filed against 0.1.5. Each reproduced before the fix; each fix has a test.
10
+
11
+ ### Added
12
+
13
+ - `--dry-run` as an alias for `--dry`; the package script was already named `dry-run` (#16).
14
+ - `cli-run`: `killTree()` ends a lane's process tree with `taskkill /T /F` on Windows instead of killing only the root process. Windows is still not exercised by CI and stays documented as unsupported; the branch is unit-tested by argv capture (#18).
15
+
16
+ ### Fixed
17
+
18
+ - `cli-run --expect-json` accepts a response that is exactly one markdown code fence around JSON (`\`\`\`json ... \`\`\``, CRLF included). Prose before or after the fence still fails the contract, because then the deliverable is not the JSON (#17).
19
+ - `--yes` with several candidate agents and no `--primary` now prefers claude-code, then the first agent that can load subagent definitions, then the first listed. `--ais codex,agy` used to pick codex and write no subagents (#19).
20
+
21
+ ## [0.1.5] - 2026-09-05
22
+
23
+ First release published by the workflow, with provenance.
24
+
25
+ ### Added
26
+
27
+ - Published to npm as `model-orchestrator` (0.1.4 was the first publish, by hand). `npx model-orchestrator` is now the install line; the GitHub route stays for pinned or unreleased runs.
28
+ - `.github/workflows/release.yml`: on a `v*` tag, checks the tag against `package.json`, runs the tests, and publishes with provenance through npm trusted publishing (no stored token). Needs the one-time trusted-publisher setup on npmjs.com described in `RELEASING.md`.
29
+
7
30
  ### Changed
8
31
 
9
- - `RELEASING.md`: the first npm publish is manual and must not pass `--provenance` (npm only generates provenance inside a supported CI runner); the workflow route comes after the package exists.
32
+ - `RELEASING.md`: the first npm publish is manual and must not pass `--provenance` (npm only generates provenance inside a supported CI runner); later releases go through the workflow.
10
33
 
11
34
  ## [0.1.4] - 2026-09-05
12
35
 
@@ -88,7 +111,9 @@ First release.
88
111
  - Tests: a case per fix, judges proven to go red, mutation checks; `npm test` prints the current count.
89
112
  - Adversarial audit: two Codex rounds plus a two-engine review (Codex, Antigravity); findings and fixes in `docs/audit-brief.md`. After the review: subagents go to the project root (`--project`), snippet paths computed from `--dir`, lane sections rendered from the selection, a primary agent required, level 3 asks for API keys separately from CLIs, images and CLI installs pinned, an activation summary at the end of every install.
90
113
 
91
- [Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.4...HEAD
114
+ [Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.6...HEAD
115
+ [0.1.6]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.5...v0.1.6
116
+ [0.1.5]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.4...v0.1.5
92
117
  [0.1.4]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.3...v0.1.4
93
118
  [0.1.3]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.2...v0.1.3
94
119
  [0.1.2]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.1...v0.1.2
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # model-orchestrator
2
2
 
3
- [![test](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml/badge.svg)](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
3
+ [![npm](https://img.shields.io/npm/v/model-orchestrator.svg)](https://www.npmjs.com/package/model-orchestrator) [![test](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml/badge.svg)](https://github.com/aunysillyme/model-orchestrator/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
4
4
 
5
5
  **A model orchestrator you can `npm run`.** Route every task to the cheapest AI that does it well, whether you have one chat app, five agent CLIs, or a virtual machine running them unattended. One installer asks what you have access to and writes only what fits.
6
6
 
7
7
  Built from a working system, not a diagram: the routing rules, the protocols and the lane runner here run in production, generalized so they transfer to any stack.
8
8
 
9
9
  ```bash
10
- npx github:aunysillyme/model-orchestrator#v0.1.4
10
+ npx model-orchestrator
11
11
  ```
12
12
 
13
- That runs the reviewed release straight from GitHub (drop `#v0.1.4` for the current main). `npx model-orchestrator` will work once the package is on the npm registry; until then it is not a command you can run.
13
+ That runs the latest published release from the npm registry. To run a specific release or the current main straight from GitHub: `npx github:aunysillyme/model-orchestrator#v0.1.6` (drop `#v0.1.6` for main).
14
14
 
15
15
  The installer asks a few things, then writes a folder:
16
16
 
@@ -43,7 +43,7 @@ Read the thinking behind each level in [docs/](docs/README.md): [Part 1](docs/pa
43
43
  | `ollama` | local models: the privacy lane | 2+ |
44
44
  | `claude-app`, `chatgpt-app`, `gemini-app` | chat apps with no CLI: level 1 via a paste block | 1 |
45
45
 
46
- `npx github:aunysillyme/model-orchestrator#v0.1.4 --list` prints the catalog with install and sign-in notes. Details: [docs/catalog.md](docs/catalog.md).
46
+ `npx model-orchestrator --list` prints the catalog with install and sign-in notes. Details: [docs/catalog.md](docs/catalog.md).
47
47
 
48
48
  ## Companion tools (both optional)
49
49
 
@@ -59,10 +59,10 @@ Whether or not you select them, every level carries the two rules they serve: `p
59
59
  ## Non-interactive
60
60
 
61
61
  ```bash
62
- npx github:aunysillyme/model-orchestrator#v0.1.4 --yes --level 2 --ais claude-code,codex,grok --primary claude-code --dir ./ai-orchestrator
63
- npx github:aunysillyme/model-orchestrator#v0.1.4 --yes --level 3 --ais claude-code,codex,agy,grok,hermes,qwen,ollama --apis anthropic,openrouter --dry # print the plan, write nothing
64
- npx github:aunysillyme/model-orchestrator#v0.1.4 --yes --level 2 --ais claude-code,codex --project ~/my-app --dir ~/my-app/ai-orchestrator # subagents into ~/my-app/.claude/agents
65
- npx github:aunysillyme/model-orchestrator#v0.1.4 --yes --level 2 --ais claude-code,codex,grok --primary claude-code --update-docs # added a lane: regenerate the docs you never edited
62
+ npx model-orchestrator --yes --level 2 --ais claude-code,codex,grok --primary claude-code --dir ./ai-orchestrator
63
+ npx model-orchestrator --yes --level 3 --ais claude-code,codex,agy,grok,hermes,qwen,ollama --apis anthropic,openrouter --dry # print the plan, write nothing
64
+ npx model-orchestrator --yes --level 2 --ais claude-code,codex --project ~/my-app --dir ~/my-app/ai-orchestrator # subagents into ~/my-app/.claude/agents
65
+ npx model-orchestrator --yes --level 2 --ais claude-code,codex,grok --primary claude-code --update-docs # added a lane: regenerate the docs you never edited
66
66
  ```
67
67
 
68
68
  ## What gets written (level 3, everything)
@@ -120,7 +120,7 @@ If you need a property in the third row to be enforced, that is a router, a poli
120
120
 
121
121
  ## Requirements
122
122
 
123
- Node 18 or newer. No dependencies. Works on macOS and Linux; the level 3 box templates assume Ubuntu.
123
+ Node 18 or newer. No dependencies. Works on macOS and Linux; the level 3 box templates assume Ubuntu. Windows is untested: `cli-run` ends a lane's process tree there with `taskkill`, but nothing in CI runs on Windows, so treat it as unsupported until someone reports otherwise.
124
124
 
125
125
  **Privacy.** The installer makes no network call of its own and sends no telemetry; the only network activity is the `npm install -g` you approve per package. `cli-run` talks to nothing but the vendor CLI you name.
126
126
 
package/bin/cli-run.mjs CHANGED
@@ -244,6 +244,27 @@ export function judge(lane, rc, out, err, outFile) {
244
244
  // an agent CLI that shelled out to a tool must not keep working after the
245
245
  // wrapper has reported 12. A child that calls setsid() itself escapes this
246
246
  // boundary; that is documented, not hidden.
247
+ // Models often wrap JSON in one markdown fence. --expect-json accepts exactly that shape:
248
+ // the whole trimmed response is one fenced block, optionally tagged json. Prose before or
249
+ // after the fence still fails, because then the deliverable is not the JSON (#17).
250
+ export function unfence(text) {
251
+ const t = String(text).trim();
252
+ const m = /^```(?:json|JSON)?[ \t]*\r?\n([\s\S]*?)\r?\n?```$/.exec(t);
253
+ return m ? m[1].trim() : t;
254
+ }
255
+
256
+ // Kill a lane and everything it spawned. POSIX: the detached process group.
257
+ // Windows has no process groups a signal can reach, so taskkill walks the tree (#18).
258
+ // Windows is not exercised by CI; this branch is unit-tested by argv capture only.
259
+ export function killTree(pid, platform = process.platform, deps = { kill: (p, sig) => process.kill(p, sig), spawn }) {
260
+ if (platform === 'win32') {
261
+ deps.spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
262
+ return 'taskkill';
263
+ }
264
+ deps.kill(-pid, 'SIGKILL');
265
+ return 'group';
266
+ }
267
+
247
268
  export function runBounded(argv, timeoutSec, maxBuffer = 16 * 1024 * 1024) {
248
269
  return new Promise((resolveRun) => {
249
270
  const t0 = Date.now();
@@ -257,8 +278,7 @@ export function runBounded(argv, timeoutSec, maxBuffer = 16 * 1024 * 1024) {
257
278
  const killGroup = () => {
258
279
  if (!child) return;
259
280
  try {
260
- if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL');
261
- else child.kill('SIGKILL');
281
+ killTree(child.pid);
262
282
  } catch {
263
283
  try {
264
284
  child.kill('SIGKILL');
@@ -452,9 +472,9 @@ export function checkContracts(opts, text, before) {
452
472
  }
453
473
  if (opts.expectJson) {
454
474
  try {
455
- JSON.parse(text);
475
+ JSON.parse(unfence(text));
456
476
  } catch {
457
- return '--expect-json: the response is not valid JSON';
477
+ return '--expect-json: the response is not valid JSON (a single ```json fence around the whole response is accepted; prose around it is not)';
458
478
  }
459
479
  }
460
480
  return null;
package/bin/cli.js CHANGED
@@ -17,7 +17,7 @@ import { planFiles, writeFiles, resolveSelection, resolveTools, resolveApis, dir
17
17
  // turn a dry run into a real one.
18
18
  const SPEC = {
19
19
  level: 'value', ais: 'value', primary: 'value', dir: 'value', project: 'value', tools: 'value', apis: 'value',
20
- yes: 'bool', force: 'bool', dry: 'bool', 'no-install': 'bool', 'no-tools': 'bool', 'no-apis': 'bool', 'upgrade-runtime': 'bool', 'update-docs': 'bool', list: 'bool', help: 'bool', h: 'bool'
20
+ yes: 'bool', force: 'bool', dry: 'bool', 'dry-run': 'bool', 'no-install': 'bool', 'no-tools': 'bool', 'no-apis': 'bool', 'upgrade-runtime': 'bool', 'update-docs': 'bool', list: 'bool', help: 'bool', h: 'bool'
21
21
  };
22
22
  export function parseArgs(argv) {
23
23
  const out = {};
@@ -93,7 +93,7 @@ Flags
93
93
  --update-docs regenerate the documents a previous run wrote and nobody edited since (hash-checked against
94
94
  MANIFEST.json), so a changed selection reaches ROUTING.md and friends; edited documents are kept
95
95
  and reported, and nothing happens without a manifest
96
- --dry print the plan, write nothing
96
+ --dry, --dry-run print the plan, write nothing
97
97
  --no-install never offer to run npm installs
98
98
  --list print the catalog
99
99
  `);
@@ -178,7 +178,10 @@ async function main() {
178
178
  } else if (candidates.length === 0) {
179
179
  bad('pick at least one agent or chat app to be the orchestrator; a local model runtime on its own cannot run the system');
180
180
  } else if (candidates.length > 1) {
181
- if (yes) primary = candidates.find((a) => a.id === 'claude-code') || candidates[0];
181
+ // --yes picks for the user: claude-code if present, else the first agent that can load subagent
182
+ // definitions (it gets five files written for it), else the first candidate. #19: codex listed
183
+ // before agy used to win and nothing was written to the project root.
184
+ if (yes) primary = candidates.find((a) => a.id === 'claude-code') || candidates.find((a) => a.agentsDir) || candidates[0];
182
185
  else {
183
186
  console.log('\nWhich one is your primary agent (the one that runs the system)?');
184
187
  candidates.forEach((a, i) => console.log(` ${i + 1} ${a.name}`));
@@ -250,7 +253,7 @@ async function main() {
250
253
  const lvl = LEVELS.find((l) => l.id === level);
251
254
  const agentFiles = files.filter((f) => f.root === 'project');
252
255
  console.log(`\nPlan\n level ${lvl.id} ${lvl.name}\n access ${selected.map((a) => a.id).join(', ')}\n primary ${primary ? primary.id : 'none'}\n tools ${tools.map((t) => t.id).join(', ') || 'none'}` + (level >= 3 ? `\n api keys ${apis.map((p) => p.id).join(', ') || 'none'}` : '') + `\n folder ${dir}\n project ${project}${agentFiles.length ? ' (' + agentFiles.length + ' subagent files go here)' : ''}\n files ${files.length}`);
253
- if (flag('dry')) {
256
+ if (flag('dry') || flag('dry-run')) {
254
257
  for (const f of files) console.log(' - ' + (f.root === 'project' ? '[project] ' : '') + f.rel);
255
258
  console.log('\n--dry: nothing written.');
256
259
  rl && rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "model-orchestrator",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A model orchestrator you can npm run: route every task to the cheapest AI that does it well, across one agent, many CLIs, or a whole virtual machine. Three levels, one installer that asks what you have access to.",
5
5
  "type": "module",
6
6
  "bin": {