opencode-harness-kit 0.2.1 → 0.3.3
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 +301 -0
- package/GLOBAL.md +50 -13
- package/MIGRATION.md +140 -0
- package/README.md +233 -116
- package/bin/oc-harness.mjs +474 -96
- package/dist/index.js +378 -155
- package/docs/KNOWN-LIMITS.md +189 -4
- package/docs/RESEARCH.md +80 -5
- package/docs/ROUTING.md +254 -0
- package/lanes.json +136 -0
- package/lanes.schema.json +55 -0
- package/package.json +12 -8
- package/profiles/beam-free.json +51 -0
- package/profiles/big-pickle.json +45 -0
- package/profiles/deepseek-v4.1-flash-free.json +49 -0
- package/profiles/fleet-all.json +139 -0
- package/profiles/fleet-safe.json +139 -0
- package/profiles/glm-5.3-flash-free.json +49 -0
- package/profiles/index.json +120 -37
- package/profiles/longcat-2.5-preview-free.json +14 -10
- package/profiles/mimo-v2.6-flash-free.json +44 -0
- package/profiles/minimax-m3.1-flash-free.json +51 -0
- package/profiles/nemotron-3-ultra-free.json +44 -0
- package/profiles/nemotron-3.5-lightning-free.json +44 -0
- package/profiles/nemotron-3.5-super-vl-free.json +52 -0
- package/profiles/schema.json +30 -11
- package/{agents → prompts}/build.md +2 -35
- package/prompts/doc.md +42 -0
- package/{agents → prompts}/locate.md +1 -16
- package/prompts/longform.md +34 -0
- package/{agents → prompts}/plan.md +2 -25
- package/{agents → prompts}/reviewer.md +2 -22
- package/prompts/schema.md +30 -0
- package/prompts/sweep.md +29 -0
- package/{agents → prompts}/verify.md +6 -29
- package/prompts/vision.md +33 -0
- package/retention.yml +75 -57
- package/skills/space-build/SKILL.md +16 -0
- package/skills/space-review/SKILL.md +8 -0
- package/src/config.js +94 -69
- package/src/effort.js +59 -22
- package/src/index.ts +15 -2
- package/src/lane-usage.js +142 -0
- package/src/profile.js +502 -212
- package/src/router.ts +148 -39
- package/src/telemetry.ts +98 -0
- package/examples/muse-spark-1.3-contributor-free.json +0 -41
- package/profiles/deepseek-v4-flash-free.json +0 -41
- package/profiles/muse-split-free.json +0 -64
- package/profiles/space-bunny-free.json +0 -42
- package/profiles/x-preview-f-free.json +0 -41
package/bin/oc-harness.mjs
CHANGED
|
@@ -25,17 +25,27 @@ import {
|
|
|
25
25
|
listProfileIds,
|
|
26
26
|
profileIdFromConfig,
|
|
27
27
|
profilePath,
|
|
28
|
+
inputCap,
|
|
28
29
|
pruneThreshold,
|
|
29
30
|
reservedTokens,
|
|
30
31
|
effectiveRetention,
|
|
32
|
+
modelsInPlay,
|
|
33
|
+
retentionFor,
|
|
31
34
|
roleSplitModels,
|
|
32
35
|
resolveDefaultVariant,
|
|
33
36
|
resolveRoles,
|
|
37
|
+
resolveRoleModel,
|
|
38
|
+
resolveLaneChain,
|
|
39
|
+
resolveLaneModel,
|
|
40
|
+
modelRetention,
|
|
41
|
+
loadLanes,
|
|
42
|
+
laneNames,
|
|
34
43
|
ProfileError,
|
|
35
44
|
parseJsonc,
|
|
36
45
|
} from "../src/profile.js"
|
|
37
46
|
import { generateConfig, globalInstructionsPath } from "../src/config.js"
|
|
38
47
|
import { planEffort } from "../src/effort.js"
|
|
48
|
+
import { findTelemetryFile, readLaneUsage, averageMs } from "../src/lane-usage.js"
|
|
39
49
|
|
|
40
50
|
const CONFIG_DIR =
|
|
41
51
|
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), ".config/opencode")
|
|
@@ -85,33 +95,58 @@ async function cmdList() {
|
|
|
85
95
|
console.log(`\n${bold("opencode-harness-kit")} ${stamp("profiles")}\n`)
|
|
86
96
|
const head = [
|
|
87
97
|
" " + "profile".padEnd(30),
|
|
88
|
-
"
|
|
98
|
+
"input".padStart(9),
|
|
89
99
|
"top".padStart(7),
|
|
100
|
+
"lanes".padStart(6),
|
|
90
101
|
"retention".padEnd(15),
|
|
91
102
|
].join(" ")
|
|
92
103
|
console.log(head)
|
|
93
|
-
console.log(" " + "-".repeat(
|
|
104
|
+
console.log(" " + "-".repeat(79))
|
|
94
105
|
|
|
95
106
|
for (const entry of idx.profiles ?? []) {
|
|
96
107
|
if (only && entry.retention !== only) continue
|
|
97
108
|
if (freeOnly && entry.free === false) continue
|
|
98
|
-
|
|
109
|
+
// The retention lookup is keyed on the model, not the profile id: a fleet
|
|
110
|
+
// profile's id is not a model, and looking it up by id silently found
|
|
111
|
+
// nothing, which is why the disagreement below never fired for one.
|
|
112
|
+
let model = entry.id
|
|
113
|
+
let lanes = 0
|
|
114
|
+
let effective = entry.retention
|
|
115
|
+
// The number that matters is the input cap the config can actually use:
|
|
116
|
+
// the narrowest lane model's on a fleet, the model's own on a
|
|
117
|
+
// single-model profile. The base model's context is a different number
|
|
118
|
+
// and it is printed by `show` and by the generated header.
|
|
119
|
+
let cap = entry.context ?? 0
|
|
120
|
+
try {
|
|
121
|
+
const loaded = loadProfile(entry.id)
|
|
122
|
+
model = loaded.model
|
|
123
|
+
lanes = Object.keys(loaded.roles ?? {}).length
|
|
124
|
+
// A fleet's retention is the worst of the models it resolves to, not the
|
|
125
|
+
// base model's. Comparing the base would report every fleet as
|
|
126
|
+
// disagreeing with retention.yml, which records the base model's terms.
|
|
127
|
+
effective = effectiveRetention(loaded).retention
|
|
128
|
+
cap = inputCap(loaded)
|
|
129
|
+
} catch {
|
|
130
|
+
model = entry.id
|
|
131
|
+
}
|
|
132
|
+
const r = retention[`opencode/${model}`]
|
|
99
133
|
const isDefault = entry.default
|
|
100
134
|
const name = (isDefault ? green("▸ ") : " ") + entry.id
|
|
101
135
|
const top = entry.topEffort ?? stamp("—")
|
|
102
136
|
const ret =
|
|
103
|
-
|
|
104
|
-
? green(
|
|
105
|
-
: yellow(
|
|
137
|
+
effective === "zero-retention"
|
|
138
|
+
? green(effective)
|
|
139
|
+
: yellow(effective)
|
|
106
140
|
const mismatch =
|
|
107
|
-
r && r.retention && r.retention !==
|
|
141
|
+
r && r.retention && r.retention !== effective && lanes === 0
|
|
108
142
|
? ` ${red(`(retention.yml says ${r.retention})`)}`
|
|
109
143
|
: ""
|
|
110
144
|
console.log(
|
|
111
145
|
[
|
|
112
146
|
" " + name.padEnd(32),
|
|
113
|
-
|
|
147
|
+
cap.toLocaleString("en-US").padStart(9),
|
|
114
148
|
String(top).padStart(7),
|
|
149
|
+
String(lanes).padStart(6),
|
|
115
150
|
ret.padEnd(15),
|
|
116
151
|
].join(" ") + mismatch,
|
|
117
152
|
)
|
|
@@ -126,6 +161,17 @@ async function cmdList() {
|
|
|
126
161
|
console.log(` ${yellow(entry.reason)}`)
|
|
127
162
|
}
|
|
128
163
|
}
|
|
164
|
+
// The routes that died. A profile is perishable, and a tier that turns over
|
|
165
|
+
// in days is the reason the fleet has chains instead of pins - so the ones
|
|
166
|
+
// that went are named, with what replaced them.
|
|
167
|
+
const gone = idx.withdrawn ?? []
|
|
168
|
+
if (gone.length) {
|
|
169
|
+
console.log("\n " + stamp("no longer on the tier"))
|
|
170
|
+
for (const entry of gone) {
|
|
171
|
+
console.log(` ${red("✗")} ${entry.id}`)
|
|
172
|
+
console.log(` ${stamp(entry.reason)}`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
129
175
|
console.log(
|
|
130
176
|
`\n ${stamp("install one:")} oc-harness use <profile>\n ${stamp("inspect:")} oc-harness show <profile>\n`,
|
|
131
177
|
)
|
|
@@ -141,7 +187,11 @@ function cmdShow(id) {
|
|
|
141
187
|
const row = (k, v) => console.log(` ${k.padEnd(20)} ${v}`)
|
|
142
188
|
row("provider/model", `${p.provider}/${p.model}`)
|
|
143
189
|
row("context", p.limits.context.toLocaleString("en-US"))
|
|
144
|
-
|
|
190
|
+
// The cap that actually governs: the narrowest lane model's on a fleet, the
|
|
191
|
+
// model's own on a single-model profile. Printing the base model's number
|
|
192
|
+
// next to a prune threshold derived from another one made the two
|
|
193
|
+
// irreconcilable on the screen.
|
|
194
|
+
row("input cap", inputCap(p).toLocaleString("en-US"))
|
|
145
195
|
row("output cap", p.limits.output.toLocaleString("en-US"))
|
|
146
196
|
row("effort levels", p.effort?.levels?.length ? p.effort.levels.join(" < ") : "(not tunable)")
|
|
147
197
|
row("default variant", resolveDefaultVariant(p) ?? "(none)")
|
|
@@ -157,14 +207,31 @@ function cmdShow(id) {
|
|
|
157
207
|
console.log(` ${"".padEnd(20)} ${stamp(`base ${p.model} is ${p.retention}, but ${eff.worst} makes the whole profile ${eff.retention}`)}`)
|
|
158
208
|
}
|
|
159
209
|
if (roleSplitModels(p).length) {
|
|
160
|
-
console.log(`\n ${bold("
|
|
210
|
+
console.log(`\n ${bold("lanes")}`)
|
|
161
211
|
for (const s of roleSplitModels(p)) {
|
|
162
212
|
const cap = s.limits ? ` out ${s.limits.output.toLocaleString("en-US")}` : ""
|
|
213
|
+
const chain = resolveLaneChain(p, s.role)
|
|
214
|
+
const via = chain.length > 1 ? ` via ${chain.join(" → ")}` : ""
|
|
163
215
|
console.log(` ${s.role.padEnd(20)} ${s.model}${cap}`)
|
|
216
|
+
if (via) console.log(` ${"".padEnd(20)} ${stamp(via.trim())}`)
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
console.log(`\n ${bold("lanes")}`)
|
|
220
|
+
for (const role of Object.keys(p.roles ?? {})) {
|
|
221
|
+
console.log(` ${role.padEnd(20)} ${p.model}`)
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
224
|
|
|
167
|
-
|
|
225
|
+
if (p.maxRetention && p.maxRetention !== p.retention) {
|
|
226
|
+
console.log(
|
|
227
|
+
`\n ${bold("retention ceiling")} ${p.maxRetention}`,
|
|
228
|
+
)
|
|
229
|
+
console.log(
|
|
230
|
+
` ${stamp("a lane may only resolve to a model at or below this")}`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`\n ${bold("lane effort")}`)
|
|
168
235
|
for (const [role, level] of Object.entries(plan.roles)) {
|
|
169
236
|
const want = p.roles?.[role]
|
|
170
237
|
const arrow = want && want !== level ? ` ${stamp(`(requested ${want})`)}` : ""
|
|
@@ -217,12 +284,69 @@ function cmdLink() {
|
|
|
217
284
|
/** Where the behavioural layer has to land for the config to mean anything. */
|
|
218
285
|
function installTargets() {
|
|
219
286
|
return [
|
|
220
|
-
{ src: join(ROOT, "
|
|
287
|
+
{ src: join(ROOT, "prompts"), dest: join(CONFIG_DIR, "prompts"), kind: "dir" },
|
|
221
288
|
{ src: join(ROOT, "skills"), dest: join(CONFIG_DIR, "skills"), kind: "dir" },
|
|
222
289
|
{ src: join(ROOT, "GLOBAL.md"), dest: join(CONFIG_DIR, "GLOBAL.md"), kind: "file" },
|
|
223
290
|
]
|
|
224
291
|
}
|
|
225
292
|
|
|
293
|
+
/**
|
|
294
|
+
* The directories opencode loads agent markdown files from.
|
|
295
|
+
*
|
|
296
|
+
* Measured on OpenCode 1.19, not read off the docs:
|
|
297
|
+
*
|
|
298
|
+
* - `$CONFIG_DIR/agents/` and `$CONFIG_DIR/agent/` are both read, and when
|
|
299
|
+
* the same name is in both the plural directory wins.
|
|
300
|
+
* - `<project>/.opencode/agents/` and `<project>/.opencode/agent/` are both
|
|
301
|
+
* read. A `./agent/` in the project root is not.
|
|
302
|
+
*
|
|
303
|
+
* A file in any of them replaces the config entry of the same name
|
|
304
|
+
* wholesale, so a shadow in the singular directory is exactly as dangerous as
|
|
305
|
+
* one in the plural one, and a checker that only looks at `agents/` misses
|
|
306
|
+
* half of them.
|
|
307
|
+
*/
|
|
308
|
+
function agentDirs() {
|
|
309
|
+
return [
|
|
310
|
+
{ dir: join(CONFIG_DIR, "agents"), scope: "config" },
|
|
311
|
+
{ dir: join(CONFIG_DIR, "agent"), scope: "config" },
|
|
312
|
+
{ dir: join(process.cwd(), ".opencode", "agents"), scope: "project" },
|
|
313
|
+
{ dir: join(process.cwd(), ".opencode", "agent"), scope: "project" },
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Remove the agent files an earlier version of this kit installed.
|
|
319
|
+
*
|
|
320
|
+
* They used to carry the model in their frontmatter, and an agent file of the
|
|
321
|
+
* same name replaces the config entry wholesale. A leftover `plan.md` from
|
|
322
|
+
* the old layout would therefore pin `plan` to whatever model it named -
|
|
323
|
+
* a model that may since have been withdrawn - and the config would look
|
|
324
|
+
* correct while the lane ran somewhere else. The prompts moved to prompts/,
|
|
325
|
+
* where a file cannot shadow a config entry.
|
|
326
|
+
*/
|
|
327
|
+
function removeLegacyAgentFiles() {
|
|
328
|
+
const removed = []
|
|
329
|
+
for (const { dir } of agentDirs()) {
|
|
330
|
+
if (!existsSync(dir)) continue
|
|
331
|
+
for (const name of readdirSync(dir)) {
|
|
332
|
+
if (!name.endsWith(".md")) continue
|
|
333
|
+
const full = join(dir, name)
|
|
334
|
+
let ours = false
|
|
335
|
+
try {
|
|
336
|
+
ours = readlinkSync(full).includes(join("space-harness", "agents"))
|
|
337
|
+
} catch {
|
|
338
|
+
ours = false
|
|
339
|
+
}
|
|
340
|
+
if (!ours) continue
|
|
341
|
+
try {
|
|
342
|
+
rmSync(full, { force: true })
|
|
343
|
+
removed.push(name)
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return removed
|
|
348
|
+
}
|
|
349
|
+
|
|
226
350
|
/**
|
|
227
351
|
* Replace `link` with `link` plus the agents, skills and GLOBAL.md.
|
|
228
352
|
*
|
|
@@ -239,6 +363,13 @@ async function cmdInstall() {
|
|
|
239
363
|
// The plugin link first: without it the guard is not enforcing anything.
|
|
240
364
|
cmdLink()
|
|
241
365
|
|
|
366
|
+
const legacy = removeLegacyAgentFiles()
|
|
367
|
+
if (legacy.length) {
|
|
368
|
+
console.log(
|
|
369
|
+
` ${green("✓")} ${stamp(`removed ${legacy.length} legacy agent file(s) that would have shadowed the config`)}`,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
242
373
|
for (const { src, dest, kind } of installTargets()) {
|
|
243
374
|
if (!existsSync(src)) {
|
|
244
375
|
console.log(` ${yellow("!")} ${stamp(`${kind} missing in repo: ${src}`)}`)
|
|
@@ -462,53 +593,88 @@ function cmdVerify() {
|
|
|
462
593
|
}
|
|
463
594
|
}
|
|
464
595
|
|
|
465
|
-
// 3. Every
|
|
466
|
-
// config
|
|
467
|
-
|
|
596
|
+
// 3. Every lane the profile enables must have its prompt installed, and the
|
|
597
|
+
// config's reference to it must resolve. A lane with a model and a
|
|
598
|
+
// permission but no prompt is a lane that runs on opencode's default
|
|
599
|
+
// instructions and nobody can tell.
|
|
600
|
+
const promptsDir = join(CONFIG_DIR, "prompts")
|
|
468
601
|
const roles = resolveRoles(p)
|
|
469
602
|
for (const [role, level] of Object.entries(roles)) {
|
|
470
603
|
if (level === undefined) continue
|
|
471
|
-
const file = join(
|
|
604
|
+
const file = join(promptsDir, `${role}.md`)
|
|
472
605
|
if (!existsSync(file)) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const msg = builtin
|
|
477
|
-
? `no agents/${role}.md - ${role} is a built-in agent, so it runs with opencode's default prompt, not this kit's`
|
|
478
|
-
: `no agents/${role}.md - ${role} is declared in the config but does not exist as an agent. Run: oc-harness install`
|
|
479
|
-
problems.push(msg)
|
|
606
|
+
problems.push(
|
|
607
|
+
`no prompts/${role}.md - the ${role} lane is declared in the config but has no instructions. Run: oc-harness install`,
|
|
608
|
+
)
|
|
480
609
|
continue
|
|
481
610
|
}
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
611
|
+
ok.push(`prompt ${role}.md installed`)
|
|
612
|
+
const ref = actual?.agent?.[role]?.prompt
|
|
613
|
+
if (ref) {
|
|
614
|
+
const target = ref.replace(/^\{file:/, "").replace(/\}$/, "")
|
|
615
|
+
const resolved = target.startsWith("~")
|
|
616
|
+
? join(homedir(), target.slice(1))
|
|
617
|
+
: target
|
|
618
|
+
if (!existsSync(resolved)) {
|
|
619
|
+
problems.push(
|
|
620
|
+
`agent.${role}.prompt points at ${target}, which does not exist. Run: oc-harness install`,
|
|
621
|
+
)
|
|
622
|
+
}
|
|
492
623
|
}
|
|
493
624
|
}
|
|
494
625
|
|
|
495
|
-
// 4. No installed agent may
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
626
|
+
// 4. No installed agent file may shadow a lane. An agent file of the same
|
|
627
|
+
// name replaces the config entry wholesale, and a file without a `model:`
|
|
628
|
+
// line makes the agent inherit the caller's model. Either way the lane
|
|
629
|
+
// stops running on the model the profile resolved for it, and the config
|
|
630
|
+
// keeps looking correct. Verified against OpenCode 1.19.
|
|
631
|
+
//
|
|
632
|
+
// The file's `model:` wins when the plugin is not there to re-pin it, and
|
|
633
|
+
// its `prompt` and `description` win even when it is. So a shadow is
|
|
634
|
+
// worth reporting wherever opencode would read one from.
|
|
635
|
+
for (const { dir, scope } of agentDirs()) {
|
|
636
|
+
if (!existsSync(dir)) continue
|
|
637
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
638
|
+
const where = `${scope}:${dir.replace(`${homedir()}/`, "~/")}`
|
|
639
|
+
const lane = name.replace(/\.md$/, "")
|
|
640
|
+
if (lane in (actual?.agent ?? {})) {
|
|
641
|
+
problems.push(
|
|
642
|
+
`agent file ${where}/${name} shadows the ${lane} lane: an agent file replaces the config entry, so the lane would not run on its own model. Run: oc-harness install`,
|
|
643
|
+
)
|
|
644
|
+
continue
|
|
645
|
+
}
|
|
646
|
+
const text = readFileSync(join(dir, name), "utf8")
|
|
501
647
|
const named = text.match(/opencode\/([a-z0-9.\-]+)/i)
|
|
502
|
-
if (named && named[1]
|
|
503
|
-
problems.push(
|
|
648
|
+
if (named && !modelsInPlay(p).includes(named[1])) {
|
|
649
|
+
problems.push(
|
|
650
|
+
`agent file ${where}/${name} mentions opencode/${named[1]}, which is not in the active profile`,
|
|
651
|
+
)
|
|
504
652
|
}
|
|
505
653
|
}
|
|
506
654
|
}
|
|
507
655
|
|
|
508
656
|
// 5. The plugin link has to exist or none of the guard is running.
|
|
509
657
|
const pluginLink = join(CONFIG_DIR, "plugins", "opencode-harness-kit.js")
|
|
510
|
-
if (existsSync(pluginLink))
|
|
511
|
-
|
|
658
|
+
if (existsSync(pluginLink)) {
|
|
659
|
+
ok.push("plugin linked")
|
|
660
|
+
// The bundle finds its profiles by walking up from itself, so a bundle
|
|
661
|
+
// that was copied out of the kit's tree loads and does nothing: no guard,
|
|
662
|
+
// no pruning, no lane failover, no telemetry, and no error anywhere. The
|
|
663
|
+
// symlink is what keeps this honest, and this check is what notices when
|
|
664
|
+
// somebody replaced it with a copy.
|
|
665
|
+
let resolved = pluginLink
|
|
666
|
+
try {
|
|
667
|
+
resolved = realpathSync(pluginLink)
|
|
668
|
+
} catch {}
|
|
669
|
+
const dataRoot = join(dirname(resolved), "..")
|
|
670
|
+
if (!existsSync(join(dataRoot, "profiles"))) {
|
|
671
|
+
problems.push(
|
|
672
|
+
`the plugin at ${resolved} cannot find its profiles (looked in ${dataRoot}). The guard, the pruning and the lane failover would all be silently absent. Run: oc-harness install`,
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
notes.push("no plugin link: the guard is not enforcing anything. Run: oc-harness link")
|
|
677
|
+
}
|
|
512
678
|
|
|
513
679
|
// 6. Schema check. `variant` at the top level is silently stripped by
|
|
514
680
|
// OpenCode, so emitting it looks like tiering and does nothing.
|
|
@@ -522,15 +688,30 @@ function cmdVerify() {
|
|
|
522
688
|
// is missing or whose frontmatter is broken is skipped silently by
|
|
523
689
|
// OpenCode, so half the plan -> build -> review -> finish loop can go
|
|
524
690
|
// missing without any error surfacing.
|
|
691
|
+
//
|
|
692
|
+
// The check covers the kit's OWN skills, and only the kit's own skills.
|
|
693
|
+
// OpenCode identifies a skill by its DIRECTORY name, not by the `name:`
|
|
694
|
+
// in its frontmatter, so a skill whose two differ loads perfectly well
|
|
695
|
+
// under its directory name. A checker that insists they match fails a
|
|
696
|
+
// perfectly good install over somebody else's naming choice - which is
|
|
697
|
+
// what happened on an install that carried 220-odd community skills.
|
|
698
|
+
//
|
|
699
|
+
// For the kit's own skills the two DO have to agree: the loop refers to
|
|
700
|
+
// them by name, and a skill whose frontmatter drifted away from its
|
|
701
|
+
// directory is one the loop will not find.
|
|
525
702
|
const skillsDir = join(CONFIG_DIR, "skills")
|
|
703
|
+
const ourSkills = kitSkillNames()
|
|
526
704
|
if (!existsSync(skillsDir)) {
|
|
527
705
|
notes.push("no skills directory: the skill loop is not installed. Run: oc-harness install")
|
|
528
706
|
} else {
|
|
707
|
+
const installed = new Set(readdirSync(skillsDir))
|
|
529
708
|
let valid = 0
|
|
530
|
-
for (const name of
|
|
709
|
+
for (const name of ourSkills) {
|
|
531
710
|
const skillFile = join(skillsDir, name, "SKILL.md")
|
|
532
711
|
if (!existsSync(skillFile)) {
|
|
533
|
-
problems.push(
|
|
712
|
+
problems.push(
|
|
713
|
+
`skills/${name} has no SKILL.md - the ${name} step of the loop will never load. Run: oc-harness install`,
|
|
714
|
+
)
|
|
534
715
|
continue
|
|
535
716
|
}
|
|
536
717
|
const front = stripFrontmatter(readFileSync(skillFile, "utf8"))
|
|
@@ -542,7 +723,19 @@ function cmdVerify() {
|
|
|
542
723
|
valid += 1
|
|
543
724
|
}
|
|
544
725
|
}
|
|
545
|
-
|
|
726
|
+
// A directory of the same name as one of the kit's would shadow it, and
|
|
727
|
+
// the installer refuses to replace a real directory - so the kit's skill
|
|
728
|
+
// would be missing and the loop would break at that step.
|
|
729
|
+
for (const name of installed) {
|
|
730
|
+
if (ourSkills.has(name)) continue
|
|
731
|
+
const foreign = join(skillsDir, name, "SKILL.md")
|
|
732
|
+
if (!existsSync(foreign)) {
|
|
733
|
+
problems.push(`skills/${name} has no SKILL.md - it will never load`)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const others = [...installed].filter((n) => !ourSkills.has(n)).length
|
|
737
|
+
if (valid) ok.push(`${valid} kit skill(s) valid`)
|
|
738
|
+
if (others) ok.push(`${others} other skill(s) present`)
|
|
546
739
|
}
|
|
547
740
|
|
|
548
741
|
console.log(`\n${bold("verify")} ${stamp(`profile ${p.id} -> ${CONFIG_FILE}`)}\n`)
|
|
@@ -557,6 +750,21 @@ function cmdVerify() {
|
|
|
557
750
|
console.log(`\n ${green("install is consistent with the profile")}\n`)
|
|
558
751
|
}
|
|
559
752
|
|
|
753
|
+
/**
|
|
754
|
+
* The names of the skills this kit installs, read from the repo.
|
|
755
|
+
*
|
|
756
|
+
* These are the ones the loop refers to by name, so their frontmatter has to
|
|
757
|
+
* agree with their directory. Everything else in the skills directory belongs
|
|
758
|
+
* to whoever put it there.
|
|
759
|
+
*
|
|
760
|
+
* @returns {Set<string>}
|
|
761
|
+
*/
|
|
762
|
+
function kitSkillNames() {
|
|
763
|
+
const dir = join(ROOT, "skills")
|
|
764
|
+
if (!existsSync(dir)) return new Set()
|
|
765
|
+
return new Set(readdirSync(dir).filter((n) => existsSync(join(dir, n, "SKILL.md"))))
|
|
766
|
+
}
|
|
767
|
+
|
|
560
768
|
/** Minimal `key: value` reader for agent frontmatter. */
|
|
561
769
|
function stripFrontmatter(text) {
|
|
562
770
|
const block = /^---\n([\s\S]*?)\n---/.exec(text)
|
|
@@ -580,6 +788,7 @@ function configDrift(p, actual) {
|
|
|
580
788
|
const roles = resolveRoles(p)
|
|
581
789
|
|
|
582
790
|
if (actual.model !== want) out.push(`model ${actual.model} -> ${want}`)
|
|
791
|
+
if (actual.small_model !== want) out.push(`small_model ${actual.small_model} -> ${want}`)
|
|
583
792
|
|
|
584
793
|
for (const [role, level] of Object.entries(roles)) {
|
|
585
794
|
if (level === undefined) continue
|
|
@@ -588,17 +797,22 @@ function configDrift(p, actual) {
|
|
|
588
797
|
out.push(`agent.${role} missing`)
|
|
589
798
|
continue
|
|
590
799
|
}
|
|
591
|
-
// A
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
// one command that exists to catch a moved
|
|
595
|
-
|
|
800
|
+
// A lane's expected model is the one its chain resolved to, not the
|
|
801
|
+
// profile's base. A fleet puts ten different models behind ten agents, so
|
|
802
|
+
// comparing every lane against the base would report drift forever and
|
|
803
|
+
// train the user to ignore the one command that exists to catch a moved
|
|
804
|
+
// lane.
|
|
805
|
+
const resolved = resolveRoleModel(p, role)
|
|
806
|
+
const wantRole = `${p.provider}/${resolved ? resolved.model : p.model}`
|
|
596
807
|
if (agent.model !== wantRole) {
|
|
597
808
|
out.push(`agent.${role}.model ${agent.model} -> ${wantRole}`)
|
|
598
809
|
}
|
|
599
810
|
if (agent.variant !== (level ?? undefined)) {
|
|
600
811
|
out.push(`agent.${role}.variant ${agent.variant ?? "(none)"} -> ${level ?? "(none)"}`)
|
|
601
812
|
}
|
|
813
|
+
if (!agent.prompt) {
|
|
814
|
+
out.push(`agent.${role}.prompt missing - the lane would run without instructions`)
|
|
815
|
+
}
|
|
602
816
|
}
|
|
603
817
|
|
|
604
818
|
const wantReserved = reservedTokens(p)
|
|
@@ -620,20 +834,33 @@ function configDrift(p, actual) {
|
|
|
620
834
|
async function cmdUse(id) {
|
|
621
835
|
if (!id) fail("usage: oc-harness use <profile> [--print] [--yes]")
|
|
622
836
|
const p = loadProfile(id)
|
|
623
|
-
|
|
837
|
+
|
|
838
|
+
// A split profile puts the user behind every model in play, not just the base
|
|
839
|
+
// one. `p.retention` describes the base only, so gating on it let a profile
|
|
840
|
+
// whose plan/build/reviewer run on a may-train endpoint install silently.
|
|
841
|
+
// Every check below is over the whole set.
|
|
842
|
+
const eff = effectiveRetention(p)
|
|
843
|
+
const unrecorded = modelsInPlay(p).filter((m) => {
|
|
844
|
+
const entry = retentionFor(m)
|
|
845
|
+
return !entry || entry.retention === "unknown"
|
|
846
|
+
})
|
|
624
847
|
|
|
625
848
|
// A profile may not become the default until a human has stated its
|
|
626
849
|
// retention terms. Capability metadata cannot answer this question.
|
|
627
|
-
if (
|
|
850
|
+
if (unrecorded.length) {
|
|
628
851
|
fail(
|
|
629
|
-
`retention for ${
|
|
852
|
+
`retention for ${unrecorded.join(", ")} is not recorded in retention.yml.\n` +
|
|
630
853
|
`Add an entry with retention: zero-retention | may-train before installing it.`,
|
|
631
854
|
)
|
|
632
855
|
}
|
|
633
|
-
if (
|
|
856
|
+
if (eff.retention === "may-train" && !has("--yes")) {
|
|
857
|
+
const trainOn = roleSplitModels(p)
|
|
858
|
+
.filter((s) => retentionFor(s.model)?.retention === "may-train")
|
|
859
|
+
.map((s) => `${s.role} runs on ${s.model}`)
|
|
634
860
|
const msg =
|
|
635
|
-
`${yellow("!")} ${
|
|
636
|
-
` ${
|
|
861
|
+
`${yellow("!")} ${eff.worst} may train on your prompts and completions.\n` +
|
|
862
|
+
(trainOn.length ? ` ${trainOn.join(", ")}.\n` : "") +
|
|
863
|
+
` ${retentionFor(eff.worst)?.note ?? ""}\n` +
|
|
637
864
|
` Re-run with --yes to accept.`
|
|
638
865
|
if (process.stdin.isTTY && !has("--print")) {
|
|
639
866
|
fail(msg + "\n")
|
|
@@ -682,11 +909,12 @@ function summary(p) {
|
|
|
682
909
|
const splits = roleSplitModels(p)
|
|
683
910
|
console.log(` ${stamp(`model ${p.provider}/${p.model}`)}`)
|
|
684
911
|
console.log(` ${stamp(`effort ${resolveDefaultVariant(p) ?? "(not tunable)"}`)}`)
|
|
685
|
-
console.log(` ${stamp(`
|
|
912
|
+
console.log(` ${stamp(`lanes ${Object.entries(resolveRoles(p)).map(([k, v]) => `${k}=${v ?? "-"}`).join(" ")}`)}`)
|
|
686
913
|
if (splits.length) {
|
|
687
|
-
// Without this the
|
|
688
|
-
// is too late to be told which
|
|
689
|
-
|
|
914
|
+
// Without this the fleet is invisible until someone opens the file, which
|
|
915
|
+
// is too late to be told which lane sends prompts to a provider that
|
|
916
|
+
// trains on them.
|
|
917
|
+
console.log(` ${stamp(`lane model ${splits.map((s) => `${s.role}=${s.model}`).join(" ")}`)}`)
|
|
690
918
|
console.log(` ${stamp(` ${splits.map((s) => s.limits ? `${s.role} out ${s.limits.output.toLocaleString("en-US")}` : null).filter(Boolean).join(" ")}`)}`)
|
|
691
919
|
}
|
|
692
920
|
console.log(` ${stamp(`retention ${eff.retention}${eff.retention === "may-train" ? " <-- prompts may train a model" : ""}`)}`)
|
|
@@ -818,6 +1046,20 @@ async function cmdRefresh(id) {
|
|
|
818
1046
|
)
|
|
819
1047
|
}
|
|
820
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* The model chain of every lane a profile enables, keyed by lane.
|
|
1051
|
+
* @param {ReturnType<typeof loadProfile>} p
|
|
1052
|
+
* @returns {Record<string, string[]>}
|
|
1053
|
+
*/
|
|
1054
|
+
function chainOf(p) {
|
|
1055
|
+
/** @type {Record<string, string[]>} */
|
|
1056
|
+
const out = {}
|
|
1057
|
+
for (const lane of Object.keys(p.roles ?? {})) {
|
|
1058
|
+
out[lane] = resolveLaneChain(p, lane)
|
|
1059
|
+
}
|
|
1060
|
+
return out
|
|
1061
|
+
}
|
|
1062
|
+
|
|
821
1063
|
/** Re-check every profile against the live provider catalog. */
|
|
822
1064
|
async function cmdDoctor() {
|
|
823
1065
|
const catalog = await fetchCatalog()
|
|
@@ -849,6 +1091,22 @@ async function cmdDoctor() {
|
|
|
849
1091
|
issues.push(red("retention.yml now says may-train"))
|
|
850
1092
|
}
|
|
851
1093
|
|
|
1094
|
+
// A fleet's lanes point at other models, and those are the routes that
|
|
1095
|
+
// get withdrawn. A lane whose whole chain has left the catalog is a lane
|
|
1096
|
+
// with no model at all, which no amount of `refresh` on the base profile
|
|
1097
|
+
// would reveal.
|
|
1098
|
+
for (const [lane, chain] of Object.entries(chainOf(p))) {
|
|
1099
|
+
const alive = chain.filter((rung) => catalog?.[p.provider]?.models?.[rung])
|
|
1100
|
+
if (!alive.length) {
|
|
1101
|
+
issues.push(red(`lane ${lane}: every model in its chain has left the catalog`))
|
|
1102
|
+
} else if (alive.length < chain.length) {
|
|
1103
|
+
const gone = chain.filter((rung) => !alive.includes(rung))
|
|
1104
|
+
issues.push(
|
|
1105
|
+
yellow(`lane ${lane}: ${gone.join(", ")} withdrawn, falls back to ${alive[0]}`),
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
852
1110
|
if (issues.length === 0) {
|
|
853
1111
|
console.log(` ${green("✓")} ${id.padEnd(32)} ${stamp("matches catalog")}`)
|
|
854
1112
|
} else {
|
|
@@ -891,6 +1149,93 @@ async function cmdDoctor() {
|
|
|
891
1149
|
}
|
|
892
1150
|
}
|
|
893
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* Which lanes the fleet actually used.
|
|
1154
|
+
*
|
|
1155
|
+
* A fleet is a hypothesis, not a fact: ten lanes is a guess about which jobs
|
|
1156
|
+
* need a different model. The only way to know which of them earn their place
|
|
1157
|
+
* is to count the calls. A lane that was never called is a description the
|
|
1158
|
+
* model reads on every turn and a free model doing nothing, so it is the first
|
|
1159
|
+
* thing to cut.
|
|
1160
|
+
*/
|
|
1161
|
+
function cmdLanes() {
|
|
1162
|
+
const activeId = profileIdFromConfig() ?? index().default
|
|
1163
|
+
let profile = null
|
|
1164
|
+
try {
|
|
1165
|
+
profile = loadProfile(activeId)
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
fail(`active profile "${activeId}" cannot be loaded: ${err.message}`)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// The plugin writes to the project root, so the file is found by walking up
|
|
1171
|
+
// from here. Reading `process.cwd()` alone reported "never called" for every
|
|
1172
|
+
// lane whenever the command was run from a subdirectory.
|
|
1173
|
+
const file = findTelemetryFile(process.cwd())
|
|
1174
|
+
const { lanes: stats, records, ignored } = readLaneUsage(file)
|
|
1175
|
+
|
|
1176
|
+
console.log(`\n${bold("lanes")} ${stamp(`${activeId} - ${records} lane request(s)`)}\n`)
|
|
1177
|
+
const lanes = Object.keys(profile.roles ?? {})
|
|
1178
|
+
if (!lanes.length) {
|
|
1179
|
+
console.log(` ${stamp("this profile enables no lanes")}\n`)
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
const head = [
|
|
1183
|
+
" " + "lane".padEnd(12),
|
|
1184
|
+
"reqs".padStart(7),
|
|
1185
|
+
"avg ms".padStart(9),
|
|
1186
|
+
"model".padEnd(32),
|
|
1187
|
+
].join(" ")
|
|
1188
|
+
console.log(head)
|
|
1189
|
+
console.log(" " + "-".repeat(64))
|
|
1190
|
+
|
|
1191
|
+
let unused = 0
|
|
1192
|
+
for (const lane of lanes) {
|
|
1193
|
+
const s = stats[lane]
|
|
1194
|
+
const model = resolveLaneModel(profile, lane).model
|
|
1195
|
+
if (!s) {
|
|
1196
|
+
unused++
|
|
1197
|
+
console.log(
|
|
1198
|
+
[
|
|
1199
|
+
" " + lane.padEnd(12),
|
|
1200
|
+
String(0).padStart(7),
|
|
1201
|
+
"-".padStart(9),
|
|
1202
|
+
(model ?? "-").padEnd(32),
|
|
1203
|
+
].join(" ") + yellow(" never used"),
|
|
1204
|
+
)
|
|
1205
|
+
continue
|
|
1206
|
+
}
|
|
1207
|
+
const avg = averageMs(s.ms)
|
|
1208
|
+
console.log(
|
|
1209
|
+
[
|
|
1210
|
+
" " + lane.padEnd(12),
|
|
1211
|
+
String(s.requests).padStart(7),
|
|
1212
|
+
String(avg ?? "-").padStart(9),
|
|
1213
|
+
([...s.models].join(", ") || model || "-").padEnd(32),
|
|
1214
|
+
].join(" "),
|
|
1215
|
+
)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
console.log("")
|
|
1219
|
+
if (unused) {
|
|
1220
|
+
console.log(
|
|
1221
|
+
` ${yellow(`${unused} lane(s) were never used`)} ${stamp("cut candidates, not defects")}`,
|
|
1222
|
+
)
|
|
1223
|
+
} else {
|
|
1224
|
+
console.log(` ${green("every lane was used at least once")}`)
|
|
1225
|
+
}
|
|
1226
|
+
console.log(
|
|
1227
|
+
` ${stamp("one request is one assistant message; avg ms covers the requests that finished")}`,
|
|
1228
|
+
)
|
|
1229
|
+
console.log(
|
|
1230
|
+
` ${stamp(`telemetry is read from ${file ?? "no .space/telemetry.jsonl found by walking up from here"}`)}\n`,
|
|
1231
|
+
)
|
|
1232
|
+
if (ignored) {
|
|
1233
|
+
console.log(
|
|
1234
|
+
` ${yellow(`${ignored} record(s) had no message id and were not counted`)} ${stamp("written by a kit version before the counter was fixed")}\n`,
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
894
1239
|
/**
|
|
895
1240
|
* Report agents in a project that the kit's effort tiering does not cover.
|
|
896
1241
|
*
|
|
@@ -903,13 +1248,16 @@ function cmdAgents() {
|
|
|
903
1248
|
// The global dir is where `oc-harness install` puts the kit's own agents, so
|
|
904
1249
|
// scanning only the project reported "no project agents found" forever and hid
|
|
905
1250
|
// the fact that none of them were installed at all.
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1251
|
+
//
|
|
1252
|
+
// opencode reads `agent/` and `agents/` alike, in the config dir and in
|
|
1253
|
+
// `.opencode/`, so all four are scanned. A file in the singular directory
|
|
1254
|
+
// shadows a lane exactly as one in the plural one does.
|
|
1255
|
+
const files = agentDirs().flatMap(({ dir }) =>
|
|
1256
|
+
existsSync(dir)
|
|
1257
|
+
? readdirSync(dir)
|
|
1258
|
+
.filter((f) => f.endsWith(".md"))
|
|
1259
|
+
.map((f) => join(dir, f))
|
|
1260
|
+
: [],
|
|
913
1261
|
)
|
|
914
1262
|
|
|
915
1263
|
// Never hardcode a profile here: the whole point is to check the one that is
|
|
@@ -926,48 +1274,74 @@ function cmdAgents() {
|
|
|
926
1274
|
}
|
|
927
1275
|
|
|
928
1276
|
const config = readConfigJson()
|
|
929
|
-
const configured = config.model ?? "(none)"
|
|
930
1277
|
const wantModel = `${current.provider}/${current.model}`
|
|
931
1278
|
|
|
932
1279
|
console.log(`\n${bold("agents")} ${stamp(`tiering against ${current.id}`)}\n`)
|
|
933
|
-
if (!files.length) {
|
|
934
|
-
console.log(` ${red("✗")} ${yellow("no agent markdown found")}`)
|
|
935
|
-
console.log(` ${stamp(`looked in ${dirs.join("\n ")}`)}`)
|
|
936
|
-
console.log(` ${stamp("run: oc-harness install")}\n`)
|
|
937
|
-
return
|
|
938
|
-
}
|
|
939
1280
|
|
|
1281
|
+
// The kit's own lanes live in the config, not in markdown: an agent file of
|
|
1282
|
+
// the same name replaces the config entry, and a file without a `model:`
|
|
1283
|
+
// line makes the agent inherit the caller's model.
|
|
940
1284
|
const roles = resolveRoles(current)
|
|
1285
|
+
const lanes = Object.keys(config.agent ?? {})
|
|
1286
|
+
if (!lanes.length) {
|
|
1287
|
+
console.log(` ${red("✗")} ${yellow("the config declares no agents")}`)
|
|
1288
|
+
console.log(` ${stamp("run: oc-harness use <profile>")}\n`)
|
|
1289
|
+
return
|
|
1290
|
+
}
|
|
941
1291
|
let untiered = 0
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const text = readFileSync(file, "utf8")
|
|
945
|
-
const name = file.split("/").pop().replace(/\.md$/, "")
|
|
946
|
-
const front = stripFrontmatter(text)
|
|
947
|
-
const hasModel = Boolean(front.model)
|
|
948
|
-
const hasVariant = Boolean(front.variant)
|
|
949
|
-
const isKitRole = name in roles
|
|
950
|
-
|
|
1292
|
+
for (const lane of lanes) {
|
|
1293
|
+
const a = config.agent[lane] ?? {}
|
|
951
1294
|
const notes = []
|
|
952
|
-
if (!
|
|
953
|
-
if (
|
|
954
|
-
notes.push(`model ${
|
|
1295
|
+
if (!a.model) notes.push("no model of its own, inherits the session model")
|
|
1296
|
+
else if (a.model !== wantModel && !modelsInPlay(current).some((m) => `${current.provider}/${m}` === a.model)) {
|
|
1297
|
+
notes.push(`model ${a.model} is not in the active profile`)
|
|
1298
|
+
}
|
|
1299
|
+
if (a.model && !a.variant) {
|
|
1300
|
+
notes.push("no variant, runs at the model default")
|
|
1301
|
+
}
|
|
1302
|
+
if (!a.prompt) notes.push("no prompt, runs on opencode's default instructions")
|
|
1303
|
+
else {
|
|
1304
|
+
const target = String(a.prompt).replace(/^\{file:/, "").replace(/\}$/, "")
|
|
1305
|
+
const resolved = target.startsWith("~") ? join(homedir(), target.slice(1)) : target
|
|
1306
|
+
if (!existsSync(resolved)) notes.push(`prompt ${target} is not installed`)
|
|
955
1307
|
}
|
|
956
|
-
if (
|
|
1308
|
+
if (lane in roles && roles[lane] === undefined) notes.push("not tiered by the profile")
|
|
957
1309
|
|
|
958
1310
|
if (!notes.length) {
|
|
959
|
-
console.log(
|
|
1311
|
+
console.log(
|
|
1312
|
+
` ${green("✓")} ${lane.padEnd(14)} ${stamp(`${a.model ?? "-"}${a.variant ? ` ${a.variant}` : ""}`)}`,
|
|
1313
|
+
)
|
|
960
1314
|
continue
|
|
961
1315
|
}
|
|
962
|
-
untiered
|
|
963
|
-
|
|
964
|
-
console.log(` ${yellow("!")} ${name.padEnd(14)} ${yellow("not tiered")}`)
|
|
1316
|
+
untiered++
|
|
1317
|
+
console.log(` ${yellow("!")} ${lane.padEnd(14)} ${yellow("not tiered")}`)
|
|
965
1318
|
for (const nn of notes) console.log(` ${nn}`)
|
|
966
1319
|
}
|
|
967
1320
|
|
|
1321
|
+
// A project agent declared in .opencode/agents/ with no model of its own
|
|
1322
|
+
// inherits the global model and runs at the global effort, which is usually
|
|
1323
|
+
// the most expensive tier. That is exactly the waste this kit exists to
|
|
1324
|
+
// remove, and it is invisible otherwise.
|
|
1325
|
+
for (const file of files) {
|
|
1326
|
+
const text = readFileSync(file, "utf8")
|
|
1327
|
+
const name = file.split("/").pop().replace(/\.md$/, "")
|
|
1328
|
+
const front = stripFrontmatter(text)
|
|
1329
|
+
if (name in config.agent) {
|
|
1330
|
+
console.log(
|
|
1331
|
+
` ${yellow("!")} ${name.padEnd(14)} ${yellow("shadows a lane in the config")}`,
|
|
1332
|
+
)
|
|
1333
|
+
console.log(` ${stamp("an agent file replaces the config entry, so the lane loses its model")}`)
|
|
1334
|
+
continue
|
|
1335
|
+
}
|
|
1336
|
+
if (!front.model) {
|
|
1337
|
+
untiered++
|
|
1338
|
+
console.log(` ${yellow("!")} ${name.padEnd(14)} ${yellow("not tiered")}`)
|
|
1339
|
+
console.log(` ${stamp("no model of its own, inherits the session model")}`)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
968
1343
|
console.log(
|
|
969
|
-
`\n ${untiered ? yellow(`${untiered} agent(s) run at the session effort`) : green("all agents tiered")}
|
|
970
|
-
`${stale ? `, ${stale} pinned to a model that is not active` : ""}`,
|
|
1344
|
+
`\n ${untiered ? yellow(`${untiered} agent(s) run at the session effort`) : green("all agents tiered")}`,
|
|
971
1345
|
)
|
|
972
1346
|
console.log(` ${stamp("add `model:` and `variant:` to the agent frontmatter, or map them in the profile")}\n`)
|
|
973
1347
|
}
|
|
@@ -1032,6 +1406,9 @@ try {
|
|
|
1032
1406
|
case "agents":
|
|
1033
1407
|
cmdAgents()
|
|
1034
1408
|
break
|
|
1409
|
+
case "lanes":
|
|
1410
|
+
cmdLanes()
|
|
1411
|
+
break
|
|
1035
1412
|
case "doctor":
|
|
1036
1413
|
await cmdDoctor()
|
|
1037
1414
|
break
|
|
@@ -1049,12 +1426,13 @@ try {
|
|
|
1049
1426
|
list show profiles with retention and caveats
|
|
1050
1427
|
init pick a free profile, then install + use + verify in one go
|
|
1051
1428
|
use <id> compile a profile into ${CONFIG_FILE}
|
|
1052
|
-
show <id> print the resolved
|
|
1429
|
+
show <id> print the resolved lane plan and derived numbers
|
|
1053
1430
|
refresh <id> update capability fields from models.dev
|
|
1054
|
-
install symlink the plugin,
|
|
1431
|
+
install symlink the plugin, prompts, skills and GLOBAL.md into the config dir
|
|
1055
1432
|
verify check the install still matches the profile and its rules are loaded
|
|
1056
1433
|
link symlink the built plugin into ~/.config/opencode/plugins
|
|
1057
1434
|
agents report agents the effort tiering does not reach
|
|
1435
|
+
lanes report which lanes the fleet actually used
|
|
1058
1436
|
doctor re-check profiles against models.dev and the installed config
|
|
1059
1437
|
baseline measure the current opencode install
|
|
1060
1438
|
run drive .space/tasks.md to done with the build agent
|