project-autopilot 12.0.0 → 12.0.1
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/README.md +10 -10
- package/SKILL.md +1 -3
- package/adapters/agents.json +11 -1
- package/config.example.json +5 -3
- package/config.schema.json +28 -9
- package/docs/configuration.md +5 -3
- package/package.json +1 -1
- package/scripts/adapter-drivers.mjs +209 -22
- package/scripts/agent-contract.mjs +24 -2
- package/scripts/agent-invocation.mjs +32 -5
- package/scripts/autopilot.mjs +787 -222
- package/scripts/config-layout.mjs +6 -0
- package/scripts/config-migration.mjs +3 -3
- package/scripts/config-validation.mjs +12 -4
- package/scripts/e2e-detect.mjs +30 -0
- package/scripts/escalation-naming.mjs +19 -0
- package/scripts/failure-contract.mjs +25 -9
- package/scripts/gen-config-schema.mjs +7 -0
- package/scripts/idle-hang-watchdog.mjs +34 -7
- package/scripts/implementation-baseline.mjs +95 -4
- package/scripts/model-discovery.mjs +176 -38
- package/scripts/opencode-generation.mjs +116 -0
- package/scripts/per-task-budget.mjs +11 -7
- package/scripts/plan-review-rounds.mjs +164 -9
- package/scripts/planning-boundary.mjs +16 -6
- package/scripts/planning-continuation.mjs +43 -23
- package/scripts/planning-review-decision.mjs +3 -0
- package/scripts/planning-review-wiring.mjs +12 -8
- package/scripts/relay-guards.mjs +133 -11
- package/scripts/review-canary.mjs +5 -2
- package/scripts/role-fallback.mjs +23 -8
- package/scripts/run-history.mjs +32 -1
- package/scripts/run-observability.mjs +46 -2
- package/scripts/setup-discovery.mjs +2 -1
- package/scripts/setup.mjs +183 -81
- package/scripts/skill-copy.mjs +49 -0
- package/scripts/task-requirements.mjs +24 -0
- package/scripts/watch-report.mjs +146 -21
- package/scripts/watch-summary.mjs +26 -3
- package/scripts/plan-mode-model-note.mjs +0 -27
|
@@ -5,12 +5,44 @@ import {resolveAdapterTransport} from './adapter-transport.mjs';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import {fileURLToPath} from 'node:url';
|
|
8
|
+
import {
|
|
9
|
+
isOpenCodeAdapter,
|
|
10
|
+
openCodeHelpArgs,
|
|
11
|
+
cachedOpenCodeGeneration,
|
|
12
|
+
rememberOpenCodeGeneration
|
|
13
|
+
} from './opencode-generation.mjs';
|
|
8
14
|
|
|
9
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
16
|
const DOCUMENTED_MODELS_DIR = path.join(__dirname, '..', 'adapters', 'documented-models');
|
|
11
17
|
const DOCUMENTED_EFFORT_DIR = path.join(__dirname, '..', 'adapters', 'documented-effort');
|
|
12
18
|
|
|
19
|
+
// Short probe limit for --help/--version capability detection. Model-list
|
|
20
|
+
// discovery is a different, slower call (a real `opencode models --verbose`
|
|
21
|
+
// can take >10 s on a busy machine and print thousands of lines), so it gets
|
|
22
|
+
// its own, more realistic limit below.
|
|
13
23
|
export const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
24
|
+
export const MODEL_DISCOVERY_TIMEOUT_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
// Tests (and operators with unusually slow providers) can override the
|
|
27
|
+
// model-list limit. Read at call time so tests can set it after importing
|
|
28
|
+
// this module.
|
|
29
|
+
export function modelDiscoveryTimeoutMs(){
|
|
30
|
+
const raw = Number(process.env.AP_MODEL_DISCOVERY_TIMEOUT_MS);
|
|
31
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : MODEL_DISCOVERY_TIMEOUT_MS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function modelDiscoveryTimeoutLabel(ms = modelDiscoveryTimeoutMs()){
|
|
35
|
+
const seconds = ms / 1000;
|
|
36
|
+
return `${Number.isInteger(seconds) ? seconds : seconds.toFixed(1)} s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Backlog #5: a cold first start (opencode rebuilding its data for a new
|
|
40
|
+
// project) can exceed the normal model-list budget even though the CLI is
|
|
41
|
+
// working. Setup/config retry that timed-out probe ONCE with twice the
|
|
42
|
+
// effective budget; the env override still scales both attempts.
|
|
43
|
+
export function modelDiscoveryRetryTimeoutMs(){
|
|
44
|
+
return modelDiscoveryTimeoutMs() * 2;
|
|
45
|
+
}
|
|
14
46
|
|
|
15
47
|
function cleanLines(text){
|
|
16
48
|
return String(text||'').split(/\r?\n/).map(s=>s.trim()).filter(Boolean);
|
|
@@ -133,14 +165,22 @@ function flagFromHelp(text, aliases){
|
|
|
133
165
|
export async function discoverCapabilities(meta, deadline){
|
|
134
166
|
if(!deadline) deadline = newDeadline();
|
|
135
167
|
const help=await collectHelpText(meta, deadline);
|
|
168
|
+
const openCodeGeneration = isOpenCodeAdapter(meta)
|
|
169
|
+
? rememberOpenCodeGeneration(meta, help)
|
|
170
|
+
: null;
|
|
171
|
+
// OpenCode 2.x has no reasoning/effort flag: the level is a model suffix.
|
|
172
|
+
// Its help names --model's value as "provider/model#variant" instead.
|
|
173
|
+
const openCodeModelSuffix = openCodeGeneration === "2";
|
|
136
174
|
const modelFlag = flagFromHelp(help, ['--model','-m']);
|
|
137
175
|
// Try the adapter's OWN declared effort_flag first (e.g. OpenCode's is
|
|
138
176
|
// "--variant" — its reasoning/effort levels ARE its model variants, there
|
|
139
177
|
// is no separate "--effort" flag), then the two generic literal names as
|
|
140
178
|
// a fallback for adapters that don't declare one.
|
|
141
|
-
const effortFlag =
|
|
142
|
-
?
|
|
143
|
-
:
|
|
179
|
+
const effortFlag = openCodeModelSuffix
|
|
180
|
+
? null
|
|
181
|
+
: meta.effort_mode === 'config'
|
|
182
|
+
? flagFromHelp(help, ['--config'])
|
|
183
|
+
: flagFromHelp(help, [meta.effort_flag, '--effort', '--reasoning-effort'].filter(Boolean));
|
|
144
184
|
const variantFlagRaw = flagFromHelp(help, ['--variant']);
|
|
145
185
|
// If the flag just detected for "variant" is the exact same literal flag
|
|
146
186
|
// already claimed by "effort" above (OpenCode's case once help text from
|
|
@@ -159,16 +199,26 @@ export async function discoverCapabilities(meta, deadline){
|
|
|
159
199
|
const flag = String(arg).replace(/^.*?--/, "--");
|
|
160
200
|
return help.includes(flag);
|
|
161
201
|
});
|
|
162
|
-
|
|
202
|
+
const capabilities = {
|
|
163
203
|
model: !!modelFlag,
|
|
164
204
|
model_flag: modelFlag,
|
|
165
|
-
effort: !!effortFlag,
|
|
205
|
+
effort: openCodeModelSuffix ? !!modelFlag : !!effortFlag,
|
|
166
206
|
effort_flag: effortFlag,
|
|
167
|
-
variant: !!variantFlag,
|
|
168
|
-
variant_flag: variantFlag,
|
|
207
|
+
variant: openCodeModelSuffix ? false : !!variantFlag,
|
|
208
|
+
variant_flag: openCodeModelSuffix ? null : variantFlag,
|
|
169
209
|
resume: resumeSupported,
|
|
170
210
|
discovery: meta.model_discovery || 'manual'
|
|
171
211
|
};
|
|
212
|
+
if (isOpenCodeAdapter(meta)) {
|
|
213
|
+
capabilities.opencode_generation = openCodeGeneration;
|
|
214
|
+
if (openCodeModelSuffix) {
|
|
215
|
+
capabilities.effort_mode = 'model_suffix';
|
|
216
|
+
capabilities.standalone = true;
|
|
217
|
+
} else {
|
|
218
|
+
capabilities.standalone = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return capabilities;
|
|
172
222
|
}
|
|
173
223
|
|
|
174
224
|
export async function discoverAdapter(meta){
|
|
@@ -258,20 +308,83 @@ export function parseOpenCodeVerboseOutput(text){
|
|
|
258
308
|
return out;
|
|
259
309
|
}
|
|
260
310
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
311
|
+
// OpenCode 2.x's machine-readable catalog: `api GET /api/model` returns
|
|
312
|
+
// {location, data:[{providerID,id,name,variants:[{id,settings}],status,enabled}]}.
|
|
313
|
+
// The per-model variant ids are the reasoning/effort levels (the same
|
|
314
|
+
// dimension 1.x reports through `models --verbose`). A disabled or
|
|
315
|
+
// non-active entry is not selectable, mirroring the usable-only 1.x list.
|
|
316
|
+
export function parseOpenCodeApiModels(text){
|
|
317
|
+
let parsed;
|
|
318
|
+
try{ parsed=JSON.parse(String(text||'').trim()); }catch{ return []; }
|
|
319
|
+
const data=Array.isArray(parsed) ? parsed : parsed?.data;
|
|
320
|
+
if(!Array.isArray(data)) return [];
|
|
321
|
+
const out=[];
|
|
322
|
+
const seen=new Set();
|
|
323
|
+
for(const model of data){
|
|
324
|
+
if(!model || typeof model!=='object') continue;
|
|
325
|
+
const provider=typeof model.providerID==='string' ? model.providerID : '';
|
|
326
|
+
const modelId=typeof model.id==='string' ? model.id : (typeof model.modelID==='string' ? model.modelID : '');
|
|
327
|
+
if(!provider || !modelId) continue;
|
|
328
|
+
if(model.enabled === false) continue;
|
|
329
|
+
if(typeof model.status === 'string' && model.status !== 'active') continue;
|
|
330
|
+
const id=`${provider}/${modelId}`;
|
|
331
|
+
if(seen.has(id)) continue;
|
|
332
|
+
seen.add(id);
|
|
333
|
+
const variants=Array.isArray(model.variants)
|
|
334
|
+
? model.variants.map(v=>typeof v==='string' ? v : v?.id).filter(Boolean)
|
|
335
|
+
: [];
|
|
336
|
+
out.push({id,label:model.name||model.modelID||modelId,reasoning:variants,variants});
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function resolveOpenCodeGeneration(meta, deadline){
|
|
342
|
+
const cached=cachedOpenCodeGeneration(meta);
|
|
343
|
+
if(cached !== undefined) return {generation:cached, timedOut:false};
|
|
344
|
+
const r=await spawnWithDeadline(meta.binary,openCodeHelpArgs(meta),{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
345
|
+
if(r.timedOut) return {generation:null, timedOut:true};
|
|
346
|
+
return {generation:rememberOpenCodeGeneration(meta, `${r.stdout||''}\n${r.stderr||''}`), timedOut:false};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function discoverFromOpenCode(meta, deadline){
|
|
350
|
+
if(!deadline) deadline = newDeadline(modelDiscoveryTimeoutMs());
|
|
351
|
+
const generation=await resolveOpenCodeGeneration(meta, deadline);
|
|
352
|
+
if(generation.timedOut) return {items:[], status:'timeout'};
|
|
353
|
+
let timedOut=false;
|
|
354
|
+
if(generation.generation === '2'){
|
|
355
|
+
let r=await spawnWithDeadline(meta.binary,['api','GET','/api/model'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
356
|
+
if(r.timedOut) timedOut=true;
|
|
357
|
+
if(r.status===0 && r.stdout){
|
|
358
|
+
const items=parseOpenCodeApiModels(r.stdout);
|
|
359
|
+
if(items.length) return {items, status:'reported'};
|
|
360
|
+
}
|
|
361
|
+
if(remaining(deadline) <= 0) return {items:[], status:timedOut ? 'timeout' : 'unsupported'};
|
|
362
|
+
// 2.x `models --verbose` no longer exists; the plain list still gives
|
|
363
|
+
// ids, so it remains the last-resort fallback (levels unavailable).
|
|
364
|
+
r=await spawnWithDeadline(meta.binary,['models'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
365
|
+
if(r.timedOut) timedOut=true;
|
|
366
|
+
if(r.status!==0 || !r.stdout) return {items:[], status:timedOut ? 'timeout' : 'unsupported'};
|
|
367
|
+
const items=unique(cleanLines(r.stdout)
|
|
368
|
+
.filter(line=>/^[A-Za-z0-9._-]+\/[A-Za-z0-9._:@+-]+(?:\s|$)/.test(line))
|
|
369
|
+
.map(line=>line.split(/\s+/)[0]))
|
|
370
|
+
.map(id=>({id,label:id,reasoning:[],variants:discoverOpenCodeVariants(id)}));
|
|
371
|
+
return {items, status:items.length ? 'reported' : 'unsupported'};
|
|
372
|
+
}
|
|
373
|
+
let r=await spawnWithDeadline(meta.binary,['models','--verbose'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
374
|
+
if(r.timedOut) timedOut=true;
|
|
264
375
|
if(r.status===0 && r.stdout){
|
|
265
376
|
const verbose=parseOpenCodeVerboseOutput(r.stdout);
|
|
266
|
-
if(verbose.length) return verbose;
|
|
377
|
+
if(verbose.length) return {items:verbose, status:'reported'};
|
|
267
378
|
}
|
|
268
|
-
if(remaining(deadline) <= 0) return [];
|
|
269
|
-
r=await spawnWithDeadline(binary,['models'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
270
|
-
if(r.
|
|
271
|
-
|
|
379
|
+
if(remaining(deadline) <= 0) return {items:[], status:timedOut ? 'timeout' : 'unsupported'};
|
|
380
|
+
r=await spawnWithDeadline(meta.binary,['models'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
381
|
+
if(r.timedOut) timedOut=true;
|
|
382
|
+
if(r.status!==0 || !r.stdout) return {items:[], status:timedOut ? 'timeout' : 'unsupported'};
|
|
383
|
+
const items=unique(cleanLines(r.stdout)
|
|
272
384
|
.filter(line=>/^[A-Za-z0-9._-]+\/[A-Za-z0-9._:@+-]+(?:\s|$)/.test(line))
|
|
273
385
|
.map(line=>line.split(/\s+/)[0]))
|
|
274
386
|
.map(id=>({id,label:id,reasoning:[],variants:discoverOpenCodeVariants(id)}));
|
|
387
|
+
return {items, status:items.length ? 'reported' : 'unsupported'};
|
|
275
388
|
}
|
|
276
389
|
|
|
277
390
|
|
|
@@ -280,12 +393,14 @@ export function parseAntigravityModels(text){
|
|
|
280
393
|
}
|
|
281
394
|
|
|
282
395
|
async function discoverAntigravityModels(binary, deadline){
|
|
283
|
-
if(!deadline) deadline = newDeadline();
|
|
396
|
+
if(!deadline) deadline = newDeadline(modelDiscoveryTimeoutMs());
|
|
284
397
|
const transport=resolveAdapterTransport({driver:adapterDriver('antigravity'),request:{binary,args:['models']}});
|
|
285
|
-
if(!transport.available) return [];
|
|
398
|
+
if(!transport.available) return {items:[], status:'unsupported'};
|
|
286
399
|
const r=await spawnWithDeadline(transport.invocation.binary,transport.invocation.args,{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
287
|
-
if(r.
|
|
288
|
-
|
|
400
|
+
if(r.timedOut) return {items:[], status:'timeout'};
|
|
401
|
+
if(r.status!==0 || !r.stdout) return {items:[], status:'unsupported'};
|
|
402
|
+
const items=parseAntigravityOutput(r.stdout);
|
|
403
|
+
return {items, status:items.length ? 'reported' : 'unsupported'};
|
|
289
404
|
}
|
|
290
405
|
|
|
291
406
|
function parseAntigravityOutput(text){
|
|
@@ -316,14 +431,20 @@ function parseAntigravityOutput(text){
|
|
|
316
431
|
}
|
|
317
432
|
|
|
318
433
|
async function discoverFromCursor(binary, deadline){
|
|
319
|
-
if(!deadline) deadline = newDeadline();
|
|
434
|
+
if(!deadline) deadline = newDeadline(modelDiscoveryTimeoutMs());
|
|
435
|
+
let timedOut=false;
|
|
320
436
|
let r=await spawnWithDeadline(binary,['models'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
321
|
-
if(r.
|
|
322
|
-
if(r.status!==0
|
|
323
|
-
|
|
437
|
+
if(r.timedOut) timedOut=true;
|
|
438
|
+
if(r.status!==0 && remaining(deadline) > 0){
|
|
439
|
+
r=await spawnWithDeadline(binary,['--list-models'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}, deadline);
|
|
440
|
+
if(r.timedOut) timedOut=true;
|
|
441
|
+
}
|
|
442
|
+
if(r.status!==0 || !r.stdout) return {items:[], status:timedOut ? 'timeout' : 'unsupported'};
|
|
443
|
+
const items=unique(cleanLines(r.stdout)
|
|
324
444
|
.filter(line=>!/^(available|models|model|name|id|usage|help|press|cursor agent|available models|model list)\b/i.test(line))
|
|
325
445
|
.map(line=>line.replace(/^[-*•]\s*/,'').split(/\s{2,}|\t/)[0].trim())
|
|
326
446
|
.filter(line=>line && line.length<160));
|
|
447
|
+
return {items, status:items.length ? 'reported' : 'unsupported'};
|
|
327
448
|
}
|
|
328
449
|
|
|
329
450
|
function normalizeCodexModels(result){
|
|
@@ -336,7 +457,8 @@ function normalizeCodexModels(result){
|
|
|
336
457
|
})).filter(m=>m.id);
|
|
337
458
|
}
|
|
338
459
|
|
|
339
|
-
function discoverFromCodexAppServer(binary){
|
|
460
|
+
function discoverFromCodexAppServer(binary, deadline){
|
|
461
|
+
if(!deadline) deadline = newDeadline(modelDiscoveryTimeoutMs());
|
|
340
462
|
return new Promise(resolve => {
|
|
341
463
|
const child = spawnAgent(binary,['app-server','--stdio'],{encoding:'utf8',stdio:['pipe','pipe','ignore']});
|
|
342
464
|
let done = false;
|
|
@@ -344,8 +466,8 @@ function discoverFromCodexAppServer(binary){
|
|
|
344
466
|
if(done) return;
|
|
345
467
|
done = true;
|
|
346
468
|
killTree(child);
|
|
347
|
-
resolve([]);
|
|
348
|
-
},
|
|
469
|
+
resolve({items:[], status:'timeout'});
|
|
470
|
+
}, remaining(deadline));
|
|
349
471
|
const finish = (result) => {
|
|
350
472
|
if(done) return;
|
|
351
473
|
done = true;
|
|
@@ -363,15 +485,18 @@ function discoverFromCodexAppServer(binary){
|
|
|
363
485
|
try{ const msg=JSON.parse(line); replies.push(msg); }catch{}
|
|
364
486
|
}
|
|
365
487
|
const modelReply=replies.find(x=>x?.id===2 && x?.result);
|
|
366
|
-
if(modelReply)
|
|
488
|
+
if(modelReply){
|
|
489
|
+
const items=normalizeCodexModels(modelReply);
|
|
490
|
+
finish({items, status:items.length ? 'reported' : 'unsupported'});
|
|
491
|
+
}
|
|
367
492
|
});
|
|
368
|
-
child.on('error',()=>finish([]));
|
|
369
|
-
child.on('exit',()=>{ if(!replies.find(x=>x?.id===2)) finish([]); });
|
|
493
|
+
child.on('error',()=>finish({items:[], status:'error'}));
|
|
494
|
+
child.on('exit',()=>{ if(!replies.find(x=>x?.id===2)) finish({items:[], status:'unsupported'}); });
|
|
370
495
|
try{
|
|
371
496
|
child.stdin.write(JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize',params:{clientInfo:{name:'project-autopilot',version:'10.2.0'}}})+'\n');
|
|
372
497
|
child.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'initialized',params:{}})+'\n');
|
|
373
498
|
child.stdin.write(JSON.stringify({jsonrpc:'2.0',id:2,method:'model/list',params:{includeHidden:false}})+'\n');
|
|
374
|
-
}catch{ finish([]); }
|
|
499
|
+
}catch{ finish({items:[], status:'unsupported'}); }
|
|
375
500
|
});
|
|
376
501
|
}
|
|
377
502
|
|
|
@@ -380,17 +505,30 @@ function discoverFromCodexAppServer(binary){
|
|
|
380
505
|
* Empty means no supported machine-readable discovery is available.
|
|
381
506
|
* Autopilot never invents a catalog.
|
|
382
507
|
* The return value is Promise<array|string> because Codex discovery is async.
|
|
508
|
+
* The array carries a non-enumerable `status` side field
|
|
509
|
+
* ("reported"|"timeout"|"unsupported"|"error") so callers can tell a slow
|
|
510
|
+
* model list from a CLI that genuinely has no machine-readable one.
|
|
383
511
|
*/
|
|
384
|
-
|
|
512
|
+
function catalogStatus(items, status){
|
|
513
|
+
Object.defineProperty(items, 'status', {value:status, enumerable:false, configurable:true});
|
|
514
|
+
return items;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function discoverModelCatalog(meta, {timeoutMs = modelDiscoveryTimeoutMs()} = {}){
|
|
385
518
|
const binary=meta.binary; const kind=meta.model_discovery;
|
|
386
|
-
const deadline = newDeadline();
|
|
519
|
+
const deadline = newDeadline(timeoutMs);
|
|
387
520
|
try{
|
|
388
|
-
|
|
389
|
-
if(kind==='
|
|
390
|
-
if(kind==='
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
521
|
+
let result;
|
|
522
|
+
if(kind==='opencode_models') result=await discoverFromOpenCode(meta, deadline);
|
|
523
|
+
else if(kind==='cursor_models'){
|
|
524
|
+
const cursor=await discoverFromCursor(binary, deadline);
|
|
525
|
+
result={items:cursor.items.map(id=>({id,label:id,reasoning:[],variants:[]})), status:cursor.status};
|
|
526
|
+
}
|
|
527
|
+
else if(kind==='codex_app_server') result=await discoverFromCodexAppServer(binary, deadline);
|
|
528
|
+
else if(kind==='antigravity_models') result=await discoverAntigravityModels(binary, deadline);
|
|
529
|
+
else result={items:[], status:'unsupported'};
|
|
530
|
+
return catalogStatus(result.items, result.status);
|
|
531
|
+
}catch{ return catalogStatus([], 'error'); }
|
|
394
532
|
}
|
|
395
533
|
|
|
396
534
|
export async function discoverModels(meta){
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// OpenCode 1.x / 2.x generation detection, cached per binary.
|
|
2
|
+
//
|
|
3
|
+
// 1.x (`opencode-ai`) exposes reasoning/effort through `run --variant` and
|
|
4
|
+
// lists per-model levels via `models --verbose`.
|
|
5
|
+
// 2.x (`@opencode/cli`) has no `run --variant`: the level is a model suffix,
|
|
6
|
+
// `--model provider/model#level`, and `models --verbose` is gone (levels come
|
|
7
|
+
// from `opencode api GET /api/model`). It also needs `--standalone` for an
|
|
8
|
+
// OPENCODE_CONFIG_CONTENT-defined agent (e.g. autopilot_readonly) to be seen.
|
|
9
|
+
//
|
|
10
|
+
// The probe is a real `run --help` launch, memoized per process so a mission
|
|
11
|
+
// pays for it once. Capability discovery normally fills this cache before any
|
|
12
|
+
// launch, so buildInvocation/verifyAgentInvocation just read it. A failed or
|
|
13
|
+
// unknown probe deliberately falls back to the 1.x path: that keeps the
|
|
14
|
+
// pre-2.x behaviour byte-for-byte identical when the help text cannot be read.
|
|
15
|
+
//
|
|
16
|
+
// The cache key is the adapter BINARY only (resolved path, else name), never
|
|
17
|
+
// the help argv: model discovery, capability discovery, the driver and the
|
|
18
|
+
// invocation verifier all describe the same installed CLI, so one probe must
|
|
19
|
+
// serve all of them even when a caller passes a partial adapter object
|
|
20
|
+
// (`{binary}`) and another the full manifest (with `help_args`).
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import {spawnAgentSync, resolveAgentBinary} from "./agent-process.mjs";
|
|
24
|
+
|
|
25
|
+
export const OPENCODE_HELP_ARGS = Object.freeze(["run", "--help"]);
|
|
26
|
+
export const OPENCODE_GENERATION_1 = "1";
|
|
27
|
+
export const OPENCODE_GENERATION_2 = "2";
|
|
28
|
+
|
|
29
|
+
const generationCache = new Map();
|
|
30
|
+
|
|
31
|
+
export function isOpenCodeAdapter(adapter = {}) {
|
|
32
|
+
if (!adapter || typeof adapter !== "object") return false;
|
|
33
|
+
if (adapter.driver === "opencode" || adapter.model_discovery === "opencode_models") return true;
|
|
34
|
+
// Manifest-shaped adapter without a driver/kind (tests and older callers).
|
|
35
|
+
return adapter.effort_flag === "--variant"
|
|
36
|
+
&& /(^|[\\/])opencode(?:\.(?:exe|cmd|bat))?$/i.test(String(adapter.binary || ""));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function openCodeHelpArgs(adapter = {}) {
|
|
40
|
+
return Array.isArray(adapter.help_args) && adapter.help_args.length
|
|
41
|
+
? adapter.help_args.map(String)
|
|
42
|
+
: [...OPENCODE_HELP_ARGS];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generationKey(adapter = {}) {
|
|
46
|
+
const binary = String(adapter?.binary || "opencode");
|
|
47
|
+
let resolved = binary;
|
|
48
|
+
try {
|
|
49
|
+
if (path.isAbsolute(binary) || /[\\/]/.test(binary)) {
|
|
50
|
+
resolved = fs.existsSync(binary) ? fs.realpathSync(binary) : path.resolve(binary);
|
|
51
|
+
} else {
|
|
52
|
+
resolved = resolveAgentBinary(binary) || binary;
|
|
53
|
+
if (resolved && fs.existsSync(resolved)) resolved = fs.realpathSync(resolved);
|
|
54
|
+
}
|
|
55
|
+
} catch { /* keep the caller-supplied name */ }
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function generationFromHelp(text) {
|
|
60
|
+
const value = String(text || "");
|
|
61
|
+
// 1.x help names the flag itself; 2.x help names it only inside the model
|
|
62
|
+
// value description. Check the real flag first so a 2.x help that happens
|
|
63
|
+
// to mention the word "variant" elsewhere is not misclassified.
|
|
64
|
+
if (/(?:^|\s)--variant(?:\s|=|,|$)/m.test(value)) return OPENCODE_GENERATION_1;
|
|
65
|
+
if (/--model\b[\s\S]{0,200}?#variant/.test(value) || /provider\/model#variant/.test(value)) {
|
|
66
|
+
return OPENCODE_GENERATION_2;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cachedOpenCodeGeneration(adapter = {}) {
|
|
72
|
+
return generationCache.get(generationKey(adapter));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Populate the cache from help text already collected by capability
|
|
76
|
+
// discovery, so model discovery and the invocation builder do not launch the
|
|
77
|
+
// same probe again.
|
|
78
|
+
export function rememberOpenCodeGeneration(adapter, helpText) {
|
|
79
|
+
const generation = generationFromHelp(helpText);
|
|
80
|
+
if (generation) generationCache.set(generationKey(adapter), generation);
|
|
81
|
+
return generation;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Synchronous probe for launch-time callers (applyAgentSelections,
|
|
85
|
+
// verifyAgentInvocation, the driver's buildInvocation). It is a LAST-RESORT
|
|
86
|
+
// fallback: discovery normally populates the same per-binary cache first.
|
|
87
|
+
// An unknown result is cached too so a broken/odd help output is not retried
|
|
88
|
+
// on every role in the same process.
|
|
89
|
+
export function detectOpenCodeGeneration(adapter = {}) {
|
|
90
|
+
const key = generationKey(adapter);
|
|
91
|
+
if (generationCache.has(key)) return generationCache.get(key);
|
|
92
|
+
let generation = null;
|
|
93
|
+
try {
|
|
94
|
+
const result = spawnAgentSync(adapter.binary || "opencode", openCodeHelpArgs(adapter), {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
|
+
input: "",
|
|
98
|
+
timeout: 5_000
|
|
99
|
+
});
|
|
100
|
+
generation = generationFromHelp(`${result?.stdout || ""}\n${result?.stderr || ""}`);
|
|
101
|
+
} catch { /* unknown -> 1.x path */ }
|
|
102
|
+
generationCache.set(key, generation);
|
|
103
|
+
return generation;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function openCodeUsesModelSuffix(adapter = {}) {
|
|
107
|
+
return isOpenCodeAdapter(adapter) && detectOpenCodeGeneration(adapter) === OPENCODE_GENERATION_2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// "provider/model#level" -> {model: "provider/model", reasoning: "level"}.
|
|
111
|
+
export function splitOpenCodeModelSuffix(value) {
|
|
112
|
+
const text = String(value || "");
|
|
113
|
+
const at = text.lastIndexOf("#");
|
|
114
|
+
if (at < 0) return {model: text, reasoning: null};
|
|
115
|
+
return {model: text.slice(0, at), reasoning: text.slice(at + 1) || null};
|
|
116
|
+
}
|
|
@@ -104,7 +104,9 @@ export const PER_ROLE_CAPS_DEFAULT = {
|
|
|
104
104
|
// Backlog #59(b): planner raised with the implementer (operator decision
|
|
105
105
|
// 2026-10-10; rarely reaches the limit, and a cut re-reads uncached).
|
|
106
106
|
planner: {maxTurns: 30, maxTokens: 200_000, extraSessions: 2},
|
|
107
|
-
|
|
107
|
+
// Operator decision 2026-10-28: 4, because with working hand-off notes two 30-turn sessions cost less than one long session and long full reviews were stopping missions at 2.
|
|
108
|
+
// Backlog #8: 250K so a full plan re-check finishes in one session on a large plan instead of splitting at ~135K.
|
|
109
|
+
plan_reviewer: {maxTurns: 30, maxTokens: 250_000, extraSessions: 4},
|
|
108
110
|
code_reviewer: {maxTurns: 22, maxTokens: 150_000, extraSessions: 2},
|
|
109
111
|
// Backlog #59(b): the verifier was cut at 22 in test mission 048 trial 3
|
|
110
112
|
// and re-ran in full (392K priced) — raised with the implementer.
|
|
@@ -114,11 +116,12 @@ export const PER_ROLE_CAPS_DEFAULT = {
|
|
|
114
116
|
// Per-role wrap-up token thresholds (mission 031 Phase 1 task 1.1; mission
|
|
115
117
|
// 044 Phase 1 task 1.3: per-turn values).
|
|
116
118
|
// Must be strictly below the role's maxTokens so wrap-up can fire before the cap.
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
+
// planner uses 180K (~one turn headroom below 200K), plan_reviewer uses 225K
|
|
120
|
+
// (~one turn headroom below its 250K cap, backlog #8); the other review roles
|
|
121
|
+
// use 135K (~one turn headroom below 150K).
|
|
119
122
|
export const PER_ROLE_WRAPUP_TOKENS_DEFAULT = {
|
|
120
123
|
planner: 180_000,
|
|
121
|
-
plan_reviewer:
|
|
124
|
+
plan_reviewer: 225_000,
|
|
122
125
|
code_reviewer: 135_000,
|
|
123
126
|
verifier: 135_000,
|
|
124
127
|
};
|
|
@@ -832,10 +835,11 @@ export function clearNoProgress(state, scope) {
|
|
|
832
835
|
// - anything else non-empty => `tasks ${taskRef}` for multi-token refs,
|
|
833
836
|
// else `task ${taskRef}`.
|
|
834
837
|
// - missing/empty taskRef => `${Prefix} task` (framework fallback).
|
|
835
|
-
// Prefix is "SpecKit" for framework "speckit"
|
|
836
|
-
// "Standalone".
|
|
838
|
+
// Prefix is "SpecKit" for framework "speckit", "GSD" for "gsd"
|
|
839
|
+
// (case-insensitive), else "Standalone".
|
|
837
840
|
export function noProgressDisplayLabel({taskId, taskRef, framework} = {}) {
|
|
838
|
-
const
|
|
841
|
+
const fw = String(framework ?? "").toLowerCase();
|
|
842
|
+
const prefix = fw === "speckit" ? "SpecKit" : fw === "gsd" ? "GSD" : "Standalone";
|
|
839
843
|
const id = taskId === null || taskId === undefined ? "" : String(taskId).trim();
|
|
840
844
|
if (id && id.toLowerCase() !== "null" && id.toLowerCase() !== "undefined") return `task ${id}`;
|
|
841
845
|
const ref = taskRef === null || taskRef === undefined ? "" : String(taskRef).trim();
|