model-orchestrator 0.1.5 → 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 +16 -1
- package/README.md +2 -2
- package/bin/cli-run.mjs +24 -4
- package/bin/cli.js +7 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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
|
+
|
|
7
21
|
## [0.1.5] - 2026-09-05
|
|
8
22
|
|
|
9
23
|
First release published by the workflow, with provenance.
|
|
@@ -97,7 +111,8 @@ First release.
|
|
|
97
111
|
- Tests: a case per fix, judges proven to go red, mutation checks; `npm test` prints the current count.
|
|
98
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.
|
|
99
113
|
|
|
100
|
-
[Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.
|
|
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
|
|
101
116
|
[0.1.5]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.4...v0.1.5
|
|
102
117
|
[0.1.4]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.3...v0.1.4
|
|
103
118
|
[0.1.3]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.2...v0.1.3
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Built from a working system, not a diagram: the routing rules, the protocols and
|
|
|
10
10
|
npx model-orchestrator
|
|
11
11
|
```
|
|
12
12
|
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|