omnilane 0.8.3 → 0.9.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 +50 -1
- package/README.ja.md +26 -6
- package/README.ko.md +25 -5
- package/README.md +40 -8
- package/README.zh-CN.md +19 -8
- package/README.zh-TW.md +31 -6
- package/VERSION +1 -1
- package/bin/omnilane +4 -3
- package/bin/omnilane-mcp +291 -0
- package/completions/omnilane.fish +50 -0
- package/package.json +1 -1
- package/scripts/check.sh +125 -0
- package/scripts/configure.sh +149 -1
- package/scripts/dispatch.sh +4 -4
- package/scripts/doctor.sh +38 -0
- package/scripts/jobs.sh +132 -10
- package/scripts/lib/common.sh +57 -3
- package/scripts/runners/run-cerebras.sh +7 -0
- package/scripts/runners/run-deepseek.sh +7 -0
- package/scripts/runners/run-groq.sh +7 -0
- package/scripts/runners/run-mistral.sh +7 -0
- package/scripts/runners/run-openai-compat.sh +102 -0
- package/scripts/runners/run-openrouter.sh +5 -84
- package/scripts/runners/run-zai.sh +7 -0
package/bin/omnilane-mcp
CHANGED
|
@@ -9,6 +9,7 @@ const { spawn } = require('child_process');
|
|
|
9
9
|
const ROOT = path.resolve(__dirname, '..');
|
|
10
10
|
const DISPATCH_SCRIPT = path.join(ROOT, 'scripts', 'dispatch.sh');
|
|
11
11
|
const JOBS_SCRIPT = path.join(ROOT, 'scripts', 'jobs.sh');
|
|
12
|
+
const DOCTOR_SCRIPT = path.join(ROOT, 'scripts', 'doctor.sh');
|
|
12
13
|
const LATEST_PROTOCOL_VERSION = '2025-11-25';
|
|
13
14
|
const SUPPORTED_PROTOCOL_VERSIONS = new Set([
|
|
14
15
|
'2025-11-25',
|
|
@@ -146,6 +147,196 @@ const tools = [
|
|
|
146
147
|
openWorldHint: false,
|
|
147
148
|
},
|
|
148
149
|
},
|
|
150
|
+
{
|
|
151
|
+
name: 'explain',
|
|
152
|
+
description: 'Explain one lane offline: every fallback candidate and the selected vendor, with no provider call and no job state.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
lane: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
pattern: '^[a-z][a-z0-9-]*$',
|
|
159
|
+
description: 'Configured omnilane lane name.',
|
|
160
|
+
},
|
|
161
|
+
json: {
|
|
162
|
+
type: 'boolean',
|
|
163
|
+
default: false,
|
|
164
|
+
description: 'Return the versioned JSON envelope instead of the human table.',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
required: ['lane'],
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
},
|
|
170
|
+
annotations: {
|
|
171
|
+
readOnlyHint: true,
|
|
172
|
+
destructiveHint: false,
|
|
173
|
+
idempotentHint: true,
|
|
174
|
+
openWorldHint: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'validate',
|
|
179
|
+
description: 'Lint the effective routing table offline and report PASS/WARN/FAIL per lane, with no provider call and no job state.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
json: {
|
|
184
|
+
type: 'boolean',
|
|
185
|
+
default: false,
|
|
186
|
+
description: 'Return the versioned JSON envelope instead of the human report.',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
},
|
|
191
|
+
annotations: {
|
|
192
|
+
readOnlyHint: true,
|
|
193
|
+
destructiveHint: false,
|
|
194
|
+
idempotentHint: true,
|
|
195
|
+
openWorldHint: false,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'dry_run',
|
|
200
|
+
description: 'Resolve a dispatch plan (vendor, model, mode, timeouts, side-effect decision) without calling any provider or creating job state. Work mode requires an explicit workdir.',
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
lane: {
|
|
205
|
+
type: 'string',
|
|
206
|
+
pattern: '^[a-z][a-z0-9-]*$',
|
|
207
|
+
description: 'Configured omnilane lane name.',
|
|
208
|
+
},
|
|
209
|
+
task: { type: 'string', description: 'Task text used only to resolve the plan; it is never sent to a provider.' },
|
|
210
|
+
mode: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
enum: ['advise', 'work'],
|
|
213
|
+
default: 'advise',
|
|
214
|
+
description: 'Read-only advice or write-enabled work.',
|
|
215
|
+
},
|
|
216
|
+
workdir: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
minLength: 1,
|
|
219
|
+
description: 'Working directory. Required when mode is work.',
|
|
220
|
+
},
|
|
221
|
+
vendor: { type: 'string', description: 'Optional configured vendor override.' },
|
|
222
|
+
model: { type: 'string', description: 'Optional routed model override.' },
|
|
223
|
+
effort: { type: 'string', description: 'Optional routed effort override.' },
|
|
224
|
+
timeout: {
|
|
225
|
+
type: 'integer',
|
|
226
|
+
minimum: 1,
|
|
227
|
+
description: 'Per-call timeout in seconds forwarded to dispatch.sh.',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
required: ['lane', 'task'],
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
allOf: [
|
|
233
|
+
{
|
|
234
|
+
if: { properties: { mode: { const: 'work' } }, required: ['mode'] },
|
|
235
|
+
then: { required: ['workdir'] },
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
annotations: {
|
|
240
|
+
readOnlyHint: true,
|
|
241
|
+
destructiveHint: false,
|
|
242
|
+
idempotentHint: true,
|
|
243
|
+
openWorldHint: false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'jobs_list',
|
|
248
|
+
description: 'List omnilane background jobs. Metadata only; task and result bodies stay private.',
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
json: {
|
|
253
|
+
type: 'boolean',
|
|
254
|
+
default: false,
|
|
255
|
+
description: 'Return the versioned JSON envelope instead of the human list.',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
},
|
|
260
|
+
annotations: {
|
|
261
|
+
readOnlyHint: true,
|
|
262
|
+
destructiveHint: false,
|
|
263
|
+
idempotentHint: true,
|
|
264
|
+
openWorldHint: false,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'doctor',
|
|
269
|
+
description: 'Read-only local health report for routing, state, watchdog, and optional UI support.',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
json: {
|
|
274
|
+
type: 'boolean',
|
|
275
|
+
default: false,
|
|
276
|
+
description: 'Return the versioned JSON envelope instead of the human report.',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
},
|
|
281
|
+
annotations: {
|
|
282
|
+
readOnlyHint: true,
|
|
283
|
+
destructiveHint: false,
|
|
284
|
+
idempotentHint: true,
|
|
285
|
+
openWorldHint: false,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'jobs_stats',
|
|
290
|
+
description: 'Aggregate local background-job outcomes and lane/vendor distribution from bounded public metadata. Never reads task or result bodies.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
last: {
|
|
295
|
+
type: 'integer',
|
|
296
|
+
minimum: 1,
|
|
297
|
+
description: 'Limit the aggregate to the most recent N jobs.',
|
|
298
|
+
},
|
|
299
|
+
json: {
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
default: false,
|
|
302
|
+
description: 'Return the versioned JSON envelope instead of the human report.',
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
additionalProperties: false,
|
|
306
|
+
},
|
|
307
|
+
annotations: {
|
|
308
|
+
readOnlyHint: true,
|
|
309
|
+
destructiveHint: false,
|
|
310
|
+
idempotentHint: true,
|
|
311
|
+
openWorldHint: false,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: 'jobs_audit',
|
|
316
|
+
description: 'Bounded read-only integrity and privacy scan of the local job store. Reports findings without printing task or result content.',
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
last: {
|
|
321
|
+
type: 'integer',
|
|
322
|
+
minimum: 1,
|
|
323
|
+
description: 'Limit the scan to the most recent N jobs.',
|
|
324
|
+
},
|
|
325
|
+
json: {
|
|
326
|
+
type: 'boolean',
|
|
327
|
+
default: false,
|
|
328
|
+
description: 'Return the versioned JSON envelope instead of the human report.',
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
},
|
|
333
|
+
annotations: {
|
|
334
|
+
readOnlyHint: true,
|
|
335
|
+
destructiveHint: false,
|
|
336
|
+
idempotentHint: true,
|
|
337
|
+
openWorldHint: false,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
149
340
|
];
|
|
150
341
|
|
|
151
342
|
class RpcError extends Error {
|
|
@@ -237,6 +428,60 @@ function validateJobArguments(args) {
|
|
|
237
428
|
return null;
|
|
238
429
|
}
|
|
239
430
|
|
|
431
|
+
function validateJsonOnlyArguments(args) {
|
|
432
|
+
const error = validationError(args, ['json']);
|
|
433
|
+
if (error) return error;
|
|
434
|
+
if (OWN(args, 'json') && typeof args.json !== 'boolean') return 'json must be a boolean';
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function validateJobsQueryArguments(args) {
|
|
439
|
+
const error = validationError(args, ['last', 'json']);
|
|
440
|
+
if (error) return error;
|
|
441
|
+
if (OWN(args, 'last') && (!Number.isInteger(args.last) || args.last < 1)) {
|
|
442
|
+
return 'last must be a positive integer';
|
|
443
|
+
}
|
|
444
|
+
if (OWN(args, 'json') && typeof args.json !== 'boolean') return 'json must be a boolean';
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateExplainArguments(args) {
|
|
449
|
+
const error = validationError(args, ['lane', 'json']);
|
|
450
|
+
if (error) return error;
|
|
451
|
+
if (typeof args.lane !== 'string' || !LANE_PATTERN.test(args.lane)) {
|
|
452
|
+
return 'lane must match ^[a-z][a-z0-9-]*$';
|
|
453
|
+
}
|
|
454
|
+
if (OWN(args, 'json') && typeof args.json !== 'boolean') return 'json must be a boolean';
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function validateDryRunArguments(args) {
|
|
459
|
+
const error = validationError(args, [
|
|
460
|
+
'lane', 'task', 'mode', 'workdir', 'vendor', 'model', 'effort', 'timeout',
|
|
461
|
+
]);
|
|
462
|
+
if (error) return error;
|
|
463
|
+
if (typeof args.lane !== 'string' || !LANE_PATTERN.test(args.lane)) {
|
|
464
|
+
return 'lane must match ^[a-z][a-z0-9-]*$';
|
|
465
|
+
}
|
|
466
|
+
if (typeof args.task !== 'string') return 'task must be a string';
|
|
467
|
+
if (OWN(args, 'mode') && args.mode !== 'advise' && args.mode !== 'work') {
|
|
468
|
+
return 'mode must be advise or work';
|
|
469
|
+
}
|
|
470
|
+
if (OWN(args, 'workdir') && (typeof args.workdir !== 'string' || args.workdir.length === 0)) {
|
|
471
|
+
return 'workdir must be a non-empty string';
|
|
472
|
+
}
|
|
473
|
+
for (const key of ['vendor', 'model', 'effort']) {
|
|
474
|
+
if (OWN(args, key) && typeof args[key] !== 'string') return `${key} must be a string`;
|
|
475
|
+
}
|
|
476
|
+
if (OWN(args, 'timeout') && (!Number.isInteger(args.timeout) || args.timeout < 1)) {
|
|
477
|
+
return 'timeout must be a positive integer';
|
|
478
|
+
}
|
|
479
|
+
if (args.mode === 'work' && !OWN(args, 'workdir')) {
|
|
480
|
+
return 'work mode requires an explicit workdir';
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
240
485
|
function createCapture() {
|
|
241
486
|
const chunks = { stdout: [], stderr: [] };
|
|
242
487
|
let collected = 0;
|
|
@@ -363,6 +608,52 @@ async function callTool(name, args) {
|
|
|
363
608
|
const command = name === 'jobs_status' ? 'status' : 'result';
|
|
364
609
|
return runToolScript(name, JOBS_SCRIPT, [command, args.id]);
|
|
365
610
|
}
|
|
611
|
+
if (name === 'jobs_list') {
|
|
612
|
+
const error = validateJsonOnlyArguments(args);
|
|
613
|
+
if (error) return toolError(error);
|
|
614
|
+
const argv = args.json === true ? ['--json', 'list'] : ['list'];
|
|
615
|
+
return runToolScript('jobs_list', JOBS_SCRIPT, argv);
|
|
616
|
+
}
|
|
617
|
+
if (name === 'explain') {
|
|
618
|
+
const error = validateExplainArguments(args);
|
|
619
|
+
if (error) return toolError(error);
|
|
620
|
+
const argv = ['--explain', args.lane];
|
|
621
|
+
if (args.json === true) argv.push('--json');
|
|
622
|
+
return runToolScript('explain', DISPATCH_SCRIPT, argv);
|
|
623
|
+
}
|
|
624
|
+
if (name === 'validate') {
|
|
625
|
+
const error = validateJsonOnlyArguments(args);
|
|
626
|
+
if (error) return toolError(error);
|
|
627
|
+
const argv = ['--validate'];
|
|
628
|
+
if (args.json === true) argv.push('--json');
|
|
629
|
+
return runToolScript('validate', DISPATCH_SCRIPT, argv);
|
|
630
|
+
}
|
|
631
|
+
if (name === 'dry_run') {
|
|
632
|
+
const error = validateDryRunArguments(args);
|
|
633
|
+
if (error) return toolError(error);
|
|
634
|
+
const mode = OWN(args, 'mode') ? args.mode : 'advise';
|
|
635
|
+
const argv = ['--dry-run', '--mode', mode];
|
|
636
|
+
for (const key of ['workdir', 'vendor', 'model', 'effort', 'timeout']) {
|
|
637
|
+
if (OWN(args, key)) argv.push(`--${key}`, String(args[key]));
|
|
638
|
+
}
|
|
639
|
+
argv.push(args.lane, args.task);
|
|
640
|
+
return runToolScript('dry_run', DISPATCH_SCRIPT, argv);
|
|
641
|
+
}
|
|
642
|
+
if (name === 'doctor') {
|
|
643
|
+
const error = validateJsonOnlyArguments(args);
|
|
644
|
+
if (error) return toolError(error);
|
|
645
|
+
const argv = args.json === true ? ['--json'] : [];
|
|
646
|
+
return runToolScript('doctor', DOCTOR_SCRIPT, argv);
|
|
647
|
+
}
|
|
648
|
+
if (name === 'jobs_stats' || name === 'jobs_audit') {
|
|
649
|
+
const error = validateJobsQueryArguments(args);
|
|
650
|
+
if (error) return toolError(error);
|
|
651
|
+
const argv = [];
|
|
652
|
+
if (args.json === true) argv.push('--json');
|
|
653
|
+
argv.push(name === 'jobs_stats' ? 'stats' : 'audit');
|
|
654
|
+
if (OWN(args, 'last')) argv.push('--last', String(args.last));
|
|
655
|
+
return runToolScript(name, JOBS_SCRIPT, argv);
|
|
656
|
+
}
|
|
366
657
|
throw new RpcError(-32602, `Unknown tool: ${name}`);
|
|
367
658
|
}
|
|
368
659
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Fish completion for omnilane. Static command/option completion only; it never
|
|
2
|
+
# invokes dispatch, provider CLIs, or the executable machine-local overlay.
|
|
3
|
+
# Dynamic lane-name and job-id completion remain Bash/Zsh-only for now.
|
|
4
|
+
|
|
5
|
+
# Top-level subcommands (offered only before a subcommand is chosen).
|
|
6
|
+
complete -c omnilane -f -n __fish_use_subcommand -a version -d 'installed release version'
|
|
7
|
+
complete -c omnilane -f -n __fish_use_subcommand -a list -d 'effective routing table'
|
|
8
|
+
complete -c omnilane -f -n __fish_use_subcommand -a route -d 'dispatch or consult a model'
|
|
9
|
+
complete -c omnilane -f -n __fish_use_subcommand -a dispatch -d 'dispatch or consult a model'
|
|
10
|
+
complete -c omnilane -f -n __fish_use_subcommand -a jobs -d 'inspect background jobs'
|
|
11
|
+
complete -c omnilane -f -n __fish_use_subcommand -a mcp -d 'MCP stdio server'
|
|
12
|
+
complete -c omnilane -f -n __fish_use_subcommand -a doctor -d 'read-only health report'
|
|
13
|
+
complete -c omnilane -f -n __fish_use_subcommand -a release-audit -d 'offline release gate'
|
|
14
|
+
complete -c omnilane -f -n __fish_use_subcommand -a ui -d 'Live Board server'
|
|
15
|
+
complete -c omnilane -f -n __fish_use_subcommand -a configure -d 'lane routing (menu or set/get/unset/list/diff)'
|
|
16
|
+
complete -c omnilane -f -n __fish_use_subcommand -a completion -d 'print a shell completion script'
|
|
17
|
+
complete -c omnilane -f -n __fish_use_subcommand -a help -d 'usage'
|
|
18
|
+
|
|
19
|
+
# route / dispatch options.
|
|
20
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l mode -x -a 'advise work' -d 'read-only advise or write-enabled work'
|
|
21
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l vendor -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras exec vote' -d 'pin one vendor, no fallback'
|
|
22
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l effort -x -a 'low medium high xhigh max' -d 'reasoning effort'
|
|
23
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l workdir -r -d 'working directory'
|
|
24
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l model -x -d 'routed model override'
|
|
25
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l timeout -x -d 'per-call timeout in seconds'
|
|
26
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l job-timeout -x -d 'whole-job timeout in seconds'
|
|
27
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l background -d 'run as a background job'
|
|
28
|
+
complete -c omnilane -n '__fish_seen_subcommand_from route dispatch' -l dry-run -d 'resolve the plan and stop'
|
|
29
|
+
|
|
30
|
+
# jobs subcommands (only before a jobs subcommand is chosen).
|
|
31
|
+
complete -c omnilane -f \
|
|
32
|
+
-n '__fish_seen_subcommand_from jobs; and not __fish_seen_subcommand_from list status result tail retry stats wait audit prune cancel rm help' \
|
|
33
|
+
-a 'list status result tail retry stats wait audit prune cancel rm help' -d 'jobs subcommand'
|
|
34
|
+
complete -c omnilane -n '__fish_seen_subcommand_from jobs' -l json -d 'JSON output'
|
|
35
|
+
# jobs list / stats filters.
|
|
36
|
+
complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list stats' -l lane -x -d 'filter by lane'
|
|
37
|
+
complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list stats' -l vendor -x -a 'codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras exec' -d 'filter by vendor'
|
|
38
|
+
complete -c omnilane -n '__fish_seen_subcommand_from jobs; and __fish_seen_subcommand_from list' -l status -x -a 'running done' -d 'filter by status'
|
|
39
|
+
|
|
40
|
+
# configure subcommands (non-interactive).
|
|
41
|
+
complete -c omnilane -f -n '__fish_seen_subcommand_from configure; and not __fish_seen_subcommand_from set get unset list diff' -a 'set get unset list diff' -d 'configure action'
|
|
42
|
+
|
|
43
|
+
# doctor / release-audit / ui / completion.
|
|
44
|
+
complete -c omnilane -n '__fish_seen_subcommand_from doctor' -l json -d 'JSON output'
|
|
45
|
+
complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l target -x -d 'target version'
|
|
46
|
+
complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l allow-dirty -d 'permit a dirty tree'
|
|
47
|
+
complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l require-tag -d 'require an annotated tag'
|
|
48
|
+
complete -c omnilane -n '__fish_seen_subcommand_from release-audit' -l json -d 'JSON output'
|
|
49
|
+
complete -c omnilane -f -n '__fish_seen_subcommand_from ui' -a 'start status url stop' -d 'Live Board action'
|
|
50
|
+
complete -c omnilane -f -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'target shell'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnilane",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "One routing table, every harness — classify subtasks into lanes and dispatch each lane to the best vendor's agentic CLI (Codex, Claude, Gemini, Grok) using your existing subscription logins.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"omnilane": "bin/omnilane"
|
package/scripts/check.sh
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
# omnilane self-check: run the CONTRIBUTING required checks in one command.
|
|
4
|
+
# Usage: check.sh [--quick] [REPO_DIR]
|
|
5
|
+
# --quick skip the two slow suite runs (python unittest, tests/run.sh)
|
|
6
|
+
# A SKIP (tool unavailable or target absent) is not a failure; the script exits
|
|
7
|
+
# non-zero only when a check actually FAILs. No -e: checks may fail and are
|
|
8
|
+
# tallied rather than aborting the run.
|
|
9
|
+
|
|
10
|
+
QUICK=0
|
|
11
|
+
REPO=""
|
|
12
|
+
while [[ $# -gt 0 ]]; do
|
|
13
|
+
case "$1" in
|
|
14
|
+
--quick) QUICK=1; shift ;;
|
|
15
|
+
-h|--help) echo "usage: check.sh [--quick] [REPO_DIR]"; exit 0 ;;
|
|
16
|
+
--) shift; break ;;
|
|
17
|
+
-*) echo "check.sh: unknown option: $1" >&2; exit 2 ;;
|
|
18
|
+
*) if [[ -n "$REPO" ]]; then echo "check.sh: too many arguments" >&2; exit 2; fi
|
|
19
|
+
REPO="$1"; shift ;;
|
|
20
|
+
esac
|
|
21
|
+
done
|
|
22
|
+
if [[ $# -gt 0 ]]; then
|
|
23
|
+
if [[ -n "$REPO" ]]; then echo "check.sh: too many arguments" >&2; exit 2; fi
|
|
24
|
+
REPO="$1"
|
|
25
|
+
fi
|
|
26
|
+
[[ -n "$REPO" ]] || REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
|
|
28
|
+
PASS_N=0; FAIL_N=0; SKIP_N=0
|
|
29
|
+
_pass() { PASS_N=$((PASS_N + 1)); printf 'PASS %s\n' "$1"; }
|
|
30
|
+
_skip() { SKIP_N=$((SKIP_N + 1)); printf 'SKIP %s (%s)\n' "$1" "$2"; }
|
|
31
|
+
_fail() { FAIL_N=$((FAIL_N + 1)); printf 'FAIL %s\n' "$1"; [[ -z "${2:-}" ]] || printf ' %s\n' "$2"; }
|
|
32
|
+
|
|
33
|
+
# Shell files present in the repo (CONTRIBUTING's bash -n / shellcheck set).
|
|
34
|
+
shell_files=()
|
|
35
|
+
for pattern in 'bin/omnilane' 'scripts/*.sh' 'scripts/lib/*.sh' 'scripts/runners/*.sh' 'install.sh'; do
|
|
36
|
+
# shellcheck disable=SC2086 # $pattern is an intentional glob against $REPO
|
|
37
|
+
for f in $REPO/$pattern; do
|
|
38
|
+
[[ -f "$f" ]] && shell_files+=("$f")
|
|
39
|
+
done
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# 1) bash -n on every tracked shell file
|
|
43
|
+
if [[ ${#shell_files[@]} -eq 0 ]]; then
|
|
44
|
+
_skip bash-syntax "no shell files found"
|
|
45
|
+
else
|
|
46
|
+
bad=""
|
|
47
|
+
for f in "${shell_files[@]}"; do
|
|
48
|
+
bash -n "$f" 2>/dev/null || bad="$bad ${f##*/}"
|
|
49
|
+
done
|
|
50
|
+
if [[ -z "$bad" ]]; then _pass bash-syntax; else _fail bash-syntax "invalid:$bad"; fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 2) shellcheck -S warning (skip if unavailable)
|
|
54
|
+
if ! command -v shellcheck >/dev/null 2>&1; then
|
|
55
|
+
_skip shellcheck "not installed"
|
|
56
|
+
elif [[ ${#shell_files[@]} -eq 0 ]]; then
|
|
57
|
+
_skip shellcheck "no shell files"
|
|
58
|
+
elif shellcheck -S warning "${shell_files[@]}" >/dev/null 2>&1; then
|
|
59
|
+
_pass shellcheck
|
|
60
|
+
else
|
|
61
|
+
_fail shellcheck "shellcheck -S warning reported findings"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3) perl -c on the job-timeout supervisor
|
|
65
|
+
perl_file="$REPO/scripts/lib/job-timeout.pl"
|
|
66
|
+
if ! command -v perl >/dev/null 2>&1; then
|
|
67
|
+
_skip perl-syntax "perl not installed"
|
|
68
|
+
elif [[ ! -f "$perl_file" ]]; then
|
|
69
|
+
_skip perl-syntax "job-timeout.pl absent"
|
|
70
|
+
elif perl -c "$perl_file" >/dev/null 2>&1; then
|
|
71
|
+
_pass perl-syntax
|
|
72
|
+
else
|
|
73
|
+
_fail perl-syntax "perl -c failed"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# 4) python compile of the UI and test modules
|
|
77
|
+
py_files=()
|
|
78
|
+
for rel in scripts/ui.py tests/test_ui.py tests/test_ci_policy.py tests/ui_browser_harness.py; do
|
|
79
|
+
[[ -f "$REPO/$rel" ]] && py_files+=("$REPO/$rel")
|
|
80
|
+
done
|
|
81
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
82
|
+
_skip py-compile "python3 not installed"
|
|
83
|
+
elif [[ ${#py_files[@]} -eq 0 ]]; then
|
|
84
|
+
_skip py-compile "no python files"
|
|
85
|
+
elif python3 -m py_compile "${py_files[@]}" >/dev/null 2>&1; then
|
|
86
|
+
_pass py-compile
|
|
87
|
+
else
|
|
88
|
+
_fail py-compile "py_compile failed"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# 5) python unit tests (slow; --quick skips)
|
|
92
|
+
if [[ "$QUICK" -eq 1 ]]; then
|
|
93
|
+
_skip unittest "--quick"
|
|
94
|
+
elif ! command -v python3 >/dev/null 2>&1; then
|
|
95
|
+
_skip unittest "python3 not installed"
|
|
96
|
+
elif [[ ! -d "$REPO/tests" ]]; then
|
|
97
|
+
_skip unittest "no tests dir"
|
|
98
|
+
elif ( cd "$REPO" && python3 -m unittest discover -s tests -p 'test_*.py' >/dev/null 2>&1 ); then
|
|
99
|
+
_pass unittest
|
|
100
|
+
else
|
|
101
|
+
_fail unittest "python unittest suite failed"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# 6) shell test suite (slow; --quick skips)
|
|
105
|
+
if [[ "$QUICK" -eq 1 ]]; then
|
|
106
|
+
_skip shell-suite "--quick"
|
|
107
|
+
elif [[ ! -f "$REPO/tests/run.sh" ]]; then
|
|
108
|
+
_skip shell-suite "tests/run.sh absent"
|
|
109
|
+
elif bash "$REPO/tests/run.sh" >/dev/null 2>&1; then
|
|
110
|
+
_pass shell-suite
|
|
111
|
+
else
|
|
112
|
+
_fail shell-suite "bash tests/run.sh failed"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# 7) effective routing table resolves
|
|
116
|
+
if [[ ! -f "$REPO/scripts/dispatch.sh" ]]; then
|
|
117
|
+
_skip dispatch-list "dispatch.sh absent"
|
|
118
|
+
elif bash "$REPO/scripts/dispatch.sh" --list >/dev/null 2>&1; then
|
|
119
|
+
_pass dispatch-list
|
|
120
|
+
else
|
|
121
|
+
_fail dispatch-list "dispatch.sh --list failed"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
printf '\nsummary: %d passed, %d skipped, %d failed\n' "$PASS_N" "$SKIP_N" "$FAIL_N"
|
|
125
|
+
[[ "$FAIL_N" -eq 0 ]]
|
package/scripts/configure.sh
CHANGED
|
@@ -9,6 +9,142 @@ source "$(dirname "${BASH_SOURCE[0]}")/lib/i18n.sh"
|
|
|
9
9
|
|
|
10
10
|
LOCAL_FILE="$OMNILANE_HOME/routing.local.yaml"
|
|
11
11
|
|
|
12
|
+
# --- Non-interactive subcommands -------------------------------------------
|
|
13
|
+
# `configure set|get|unset|list` script the same routing.local.yaml the menu
|
|
14
|
+
# writes, so automation never needs a tty. No subcommand => interactive menu.
|
|
15
|
+
|
|
16
|
+
cfg_usage() {
|
|
17
|
+
cat >&2 <<'EOF'
|
|
18
|
+
usage: configure interactive lane menu
|
|
19
|
+
configure set LANE SPEC set/override one lane. SPEC = "vendor model effort [| ...]" or "off".
|
|
20
|
+
Quote the whole SPEC (and the model) when a model name has spaces.
|
|
21
|
+
configure get LANE show the effective routing line for LANE
|
|
22
|
+
configure unset LANE remove LANE's local override
|
|
23
|
+
configure list show current local overrides
|
|
24
|
+
configure diff show how local overrides change the effective table vs the defaults
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Reject shell-dangerous bytes while allowing the routing RHS grammar
|
|
29
|
+
# (chains with |, quoted "models with spaces", slashes, effort tokens).
|
|
30
|
+
cfg_spec_is_safe() {
|
|
31
|
+
case "$1" in
|
|
32
|
+
'') return 1 ;;
|
|
33
|
+
*'$'*|*'`'*|*'\'*|*'#'*|*';'*|*'&'*|*'<'*|*'>'*|*$'\r'*|*$'\n'*) return 1 ;;
|
|
34
|
+
*) return 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cfg_lane_is_known() {
|
|
39
|
+
local want="$1" line
|
|
40
|
+
while IFS= read -r line; do
|
|
41
|
+
case "$line" in "$want":*) return 0 ;; esac
|
|
42
|
+
done < "$OMNILANE_REPO/routing.yaml"
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cfg_valid_lane() { [[ "$1" =~ ^[a-z][a-z0-9-]*$ ]]; }
|
|
47
|
+
|
|
48
|
+
cfg_set() {
|
|
49
|
+
local lane="${1:-}"; shift || true
|
|
50
|
+
local spec="$*"
|
|
51
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
52
|
+
cfg_lane_is_known "$lane" || { echo "omnilane: unknown lane '$lane' (see: omnilane list)" >&2; exit 2; }
|
|
53
|
+
[[ -n "$spec" ]] || { echo "omnilane: missing routing spec for '$lane'" >&2; cfg_usage; exit 2; }
|
|
54
|
+
cfg_spec_is_safe "$spec" || { echo 'omnilane: unsafe routing spec (not allowed: $ ` \ # ; & < > newlines)' >&2; exit 2; }
|
|
55
|
+
|
|
56
|
+
mkdir -p "$OMNILANE_HOME"
|
|
57
|
+
local had_file=0
|
|
58
|
+
if [[ -f "$LOCAL_FILE" ]]; then had_file=1; cp "$LOCAL_FILE" "$LOCAL_FILE.bak"; fi
|
|
59
|
+
local tmp="$LOCAL_FILE.tmp.$$"
|
|
60
|
+
{
|
|
61
|
+
echo "# updated by 'configure set' on $(date +%F) — first match per lane wins"
|
|
62
|
+
echo "$lane: $spec"
|
|
63
|
+
[[ "$had_file" -eq 1 ]] && grep -v '^#' "$LOCAL_FILE.bak" | grep -v "^$lane:" || true
|
|
64
|
+
} > "$tmp"
|
|
65
|
+
mv "$tmp" "$LOCAL_FILE"
|
|
66
|
+
|
|
67
|
+
# Semantic guard: reject only a structural FAIL on THIS lane. Availability
|
|
68
|
+
# WARN (validate exit 4) is fine — the vendor CLI may be legitimately absent.
|
|
69
|
+
local validate_out
|
|
70
|
+
validate_out="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --validate 2>&1)" || true
|
|
71
|
+
if printf '%s\n' "$validate_out" | grep -q "^FAIL $lane "; then
|
|
72
|
+
if [[ "$had_file" -eq 1 ]]; then mv "$LOCAL_FILE.bak" "$LOCAL_FILE"; else /bin/rm -f "$LOCAL_FILE"; fi
|
|
73
|
+
echo "omnilane: rejected — $(printf '%s\n' "$validate_out" | grep "^FAIL $lane " | head -1)" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
[[ "$had_file" -eq 1 ]] && /bin/rm -f "$LOCAL_FILE.bak"
|
|
77
|
+
echo "set $lane -> $spec"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cfg_get() {
|
|
81
|
+
local lane="${1:-}"
|
|
82
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
83
|
+
local line
|
|
84
|
+
line="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null | grep "^$lane:" | head -1 || true)"
|
|
85
|
+
[[ -n "$line" ]] || { echo "omnilane: unknown lane '$lane' (see: omnilane list)" >&2; exit 2; }
|
|
86
|
+
printf '%s\n' "$line"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cfg_unset() {
|
|
90
|
+
local lane="${1:-}"
|
|
91
|
+
cfg_valid_lane "$lane" || { echo "omnilane: invalid lane '$lane'" >&2; exit 2; }
|
|
92
|
+
if [[ ! -f "$LOCAL_FILE" ]] || ! grep -q "^$lane:" "$LOCAL_FILE"; then
|
|
93
|
+
echo "no local override for '$lane'"; return 0
|
|
94
|
+
fi
|
|
95
|
+
cp "$LOCAL_FILE" "$LOCAL_FILE.bak"
|
|
96
|
+
grep -v "^$lane:" "$LOCAL_FILE.bak" > "$LOCAL_FILE" || true
|
|
97
|
+
/bin/rm -f "$LOCAL_FILE.bak"
|
|
98
|
+
echo "unset $lane (local override removed)"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
cfg_list() {
|
|
102
|
+
if [[ -f "$LOCAL_FILE" ]] && grep -qE '^[a-z]' "$LOCAL_FILE"; then
|
|
103
|
+
cat "$LOCAL_FILE"
|
|
104
|
+
else
|
|
105
|
+
echo "no local overrides in $LOCAL_FILE"
|
|
106
|
+
fi
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Diff the effective table (local wins) against a defaults-only resolution, so
|
|
110
|
+
# the user sees exactly which lanes their overrides change. Reuses dispatch.sh
|
|
111
|
+
# --list for both, so availability annotation and formatting stay consistent; a
|
|
112
|
+
# throwaway empty OMNILANE_HOME yields the defaults-only table.
|
|
113
|
+
cfg_diff() {
|
|
114
|
+
if [[ ! -f "$LOCAL_FILE" ]] || ! grep -qE '^[a-z]' "$LOCAL_FILE"; then
|
|
115
|
+
echo "no local overrides ($LOCAL_FILE); effective table equals the defaults"
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
local empty eff def changed=0 lane eff_line def_line
|
|
119
|
+
empty="$(mktemp -d "${TMPDIR:-/tmp}/omnilane-diff.XXXXXX")"
|
|
120
|
+
eff="$(OMNILANE_HOME="$OMNILANE_HOME" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
|
|
121
|
+
def="$(OMNILANE_HOME="$empty" bash "$OMNILANE_REPO/scripts/dispatch.sh" --list 2>/dev/null || true)"
|
|
122
|
+
/bin/rm -rf "$empty"
|
|
123
|
+
while IFS= read -r eff_line; do
|
|
124
|
+
[[ "$eff_line" =~ ^([a-z][a-z0-9-]*): ]] || continue
|
|
125
|
+
lane="${BASH_REMATCH[1]}"
|
|
126
|
+
def_line="$(printf '%s\n' "$def" | grep "^$lane:" | head -1 || true)"
|
|
127
|
+
if [[ "$eff_line" != "$def_line" ]]; then
|
|
128
|
+
changed=1
|
|
129
|
+
printf 'default> %s\n' "${def_line:-($lane not in defaults)}"
|
|
130
|
+
printf 'local > %s\n' "$eff_line"
|
|
131
|
+
echo
|
|
132
|
+
fi
|
|
133
|
+
done <<DIFF_EFF
|
|
134
|
+
$eff
|
|
135
|
+
DIFF_EFF
|
|
136
|
+
[[ "$changed" -eq 1 ]] || echo "local overrides present, but the effective table matches the defaults"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case "${1:-}" in
|
|
140
|
+
set) shift; cfg_set "$@"; exit $? ;;
|
|
141
|
+
get) shift; cfg_get "$@"; exit $? ;;
|
|
142
|
+
unset) shift; cfg_unset "$@"; exit $? ;;
|
|
143
|
+
list) shift; cfg_list "$@"; exit $? ;;
|
|
144
|
+
diff) cfg_diff; exit $? ;;
|
|
145
|
+
-h|--help|help) cfg_usage; exit 0 ;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
12
148
|
# Curated suggestions only — "c" always allows free text so new models work.
|
|
13
149
|
CODEX_MODELS=("gpt-5.6-sol" "gpt-5.6-terra" "gpt-5.6-luna")
|
|
14
150
|
CODEX_EFFORTS=("xhigh" "max" "ultra" "high" "medium" "low" "minimal" "none")
|
|
@@ -21,6 +157,13 @@ QWEN_MODELS=("qwen3-coder-plus" "qwen3-coder-flash")
|
|
|
21
157
|
# OpenCode models use provider/model form; OpenRouter models are catalog slugs.
|
|
22
158
|
OPENCODE_MODELS=("openrouter/anthropic/claude-sonnet-5" "openrouter/openai/gpt-5.6-sol" "opencode/default (leave model to opencode)")
|
|
23
159
|
OPENROUTER_MODELS=("anthropic/claude-sonnet-5" "openai/gpt-5.6-sol" "moonshotai/kimi-k3" "qwen/qwen3-coder-plus")
|
|
160
|
+
# Direct-API OpenAI-compatible vendors (curl + <VENDOR>_API_KEY); slugs are
|
|
161
|
+
# suggestions — "c" free text covers anything each provider's /models lists.
|
|
162
|
+
DEEPSEEK_MODELS=("deepseek-chat" "deepseek-reasoner" "deepseek-v4-flash")
|
|
163
|
+
ZAI_MODELS=("glm-4.6")
|
|
164
|
+
MISTRAL_MODELS=("devstral-latest" "codestral-latest" "mistral-medium-latest")
|
|
165
|
+
GROQ_MODELS=("openai/gpt-oss-120b" "qwen/qwen3.6-27b" "openai/gpt-oss-20b")
|
|
166
|
+
CEREBRAS_MODELS=("gpt-oss-120b" "qwen-3-32b" "llama-3.3-70b")
|
|
24
167
|
|
|
25
168
|
custom_value_is_safe() {
|
|
26
169
|
case "$1" in
|
|
@@ -79,7 +222,7 @@ while true; do
|
|
|
79
222
|
echo "$(msgf cfg_pick_range "${#LANES[@]}")"; continue
|
|
80
223
|
fi
|
|
81
224
|
lane="${LANES[$((n - 1))]}"
|
|
82
|
-
vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen opencode openrouter "vote (multi-model panel)" "exec (your own script/gate)" off)"
|
|
225
|
+
vendor="$(pick "$(msgf cfg_vendor_for "$lane")" codex claude grok gemini kimi qwen opencode openrouter deepseek zai mistral groq cerebras "vote (multi-model panel)" "exec (your own script/gate)" off)"
|
|
83
226
|
[[ "$vendor" == exec* ]] && vendor="exec"
|
|
84
227
|
[[ "$vendor" == vote* ]] && vendor="vote"
|
|
85
228
|
if [[ "$vendor" == "off" ]]; then
|
|
@@ -96,6 +239,11 @@ while true; do
|
|
|
96
239
|
opencode) model="$(pick "$(msg cfg_model)" "${OPENCODE_MODELS[@]}")"; effort="-"
|
|
97
240
|
[[ "$model" == "opencode/default"* ]] && model="-" ;;
|
|
98
241
|
openrouter) model="$(pick "$(msg cfg_model)" "${OPENROUTER_MODELS[@]}")"; effort="-" ;;
|
|
242
|
+
deepseek) model="$(pick "$(msg cfg_model)" "${DEEPSEEK_MODELS[@]}")"; effort="-" ;;
|
|
243
|
+
zai) model="$(pick "$(msg cfg_model)" "${ZAI_MODELS[@]}")"; effort="-" ;;
|
|
244
|
+
mistral) model="$(pick "$(msg cfg_model)" "${MISTRAL_MODELS[@]}")"; effort="-" ;;
|
|
245
|
+
groq) model="$(pick "$(msg cfg_model)" "${GROQ_MODELS[@]}")"; effort="-" ;;
|
|
246
|
+
cerebras) model="$(pick "$(msg cfg_model)" "${CEREBRAS_MODELS[@]}")"; effort="-" ;;
|
|
99
247
|
vote) while true; do
|
|
100
248
|
count="$(pick "$(msg cfg_voters_count)" "1" "2" "3" "4")"
|
|
101
249
|
[[ "$count" =~ ^[1-4]$ ]] && break
|