@hizliemre/horse-code 0.1.2 → 0.2.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.
Files changed (37) hide show
  1. package/dist/{app-LAJN3TWC.js → app-2GPGCDX6.js} +136 -329
  2. package/dist/{chunk-K2VERI5Q.js → chunk-23CLQ2KO.js} +365 -666
  3. package/dist/{chunk-KOWMHL23.js → chunk-5ZV42XGJ.js} +2 -2
  4. package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
  5. package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
  6. package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
  7. package/dist/{chunk-RPVAIS3P.js → chunk-7JMWPTJ5.js} +1724 -306
  8. package/dist/{chunk-VPAWRRHL.js → chunk-AE36LLL2.js} +31 -21
  9. package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
  10. package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
  11. package/dist/{chunk-DRZSUQ7Q.js → chunk-LLL7QWXB.js} +18 -7
  12. package/dist/{chunk-WYQBRCKY.js → chunk-LNW557IO.js} +2 -2
  13. package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
  14. package/dist/chunk-M2RKCIGV.js +11 -0
  15. package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
  16. package/dist/{chunk-EQX7BQYN.js → chunk-UGESK765.js} +1 -1
  17. package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
  18. package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
  19. package/dist/cli.js +379 -93
  20. package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
  21. package/dist/fix-JOIXQFVP.js +33 -0
  22. package/dist/git-QZTDZSJY.js +8 -0
  23. package/dist/{ongoing-XP6WXNI7.js → ongoing-WHYXPW24.js} +4 -4
  24. package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
  25. package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
  26. package/dist/{save-skills-NSLBU33X.js → save-skills-X7U3KCPU.js} +1 -1
  27. package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
  28. package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
  29. package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
  30. package/dist/{triage-ES5OHZOS.js → triage-FCYHD2AQ.js} +8 -9
  31. package/dist/{verify-3R7DGUSI.js → verify-6SC4I77M.js} +18 -19
  32. package/package.json +1 -1
  33. package/dist/chunk-3UYA3KUG.js +0 -212
  34. package/dist/chunk-B67BK5GQ.js +0 -34
  35. package/dist/chunk-O74BDQKS.js +0 -28
  36. package/dist/fix-CMARU6JR.js +0 -34
  37. package/dist/git-VTSZALSR.js +0 -6
@@ -4,45 +4,52 @@ import {
4
4
  import {
5
5
  MAX_PARALLEL_TASKS,
6
6
  MemoryStore,
7
- isAnthropicModel,
8
7
  isUnknownModelError,
9
8
  makeAskUser,
10
9
  restoreOnExit,
11
10
  restoreTerminal,
12
11
  runJob,
13
12
  sttySane
14
- } from "./chunk-K2VERI5Q.js";
15
- import "./chunk-3UYA3KUG.js";
13
+ } from "./chunk-23CLQ2KO.js";
14
+ import "./chunk-M2RKCIGV.js";
16
15
  import {
17
- CODE_TEAM,
18
- DEFAULT_COUNCIL,
19
- PLAN_TEAM,
16
+ DURABLE_ROLES,
20
17
  REQUIRED_ROLES,
18
+ ROLE_PROFILES,
21
19
  RoleFitness,
22
- SPEC_TEAM,
20
+ adjustRoleModels,
23
21
  asChoice,
22
+ capabilityScore,
24
23
  describeUnfinished,
24
+ effortFor,
25
+ filterModelsForRole,
26
+ isKnownModel,
27
+ modelBand,
28
+ mostCapable,
29
+ newestPrimary,
30
+ sourceOf,
31
+ strongestPrimary,
25
32
  toSlug,
26
33
  unfinishedSessions
27
- } from "./chunk-RPVAIS3P.js";
34
+ } from "./chunk-7JMWPTJ5.js";
28
35
  import "./chunk-QF4MP6BS.js";
29
- import "./chunk-EQX7BQYN.js";
30
- import "./chunk-FGVJFMK5.js";
36
+ import "./chunk-UGESK765.js";
37
+ import "./chunk-ZSQ24YDJ.js";
31
38
  import {
32
39
  saveRoleSkills
33
- } from "./chunk-4EWK7HWQ.js";
40
+ } from "./chunk-KKWZBZYK.js";
34
41
  import {
35
42
  objectField,
36
43
  patchConfig
37
44
  } from "./chunk-H2FDGPVW.js";
38
- import "./chunk-NNTIACT4.js";
39
- import "./chunk-IW2KBAVZ.js";
45
+ import "./chunk-MRZVA5JB.js";
46
+ import "./chunk-LPQU436C.js";
40
47
  import {
41
48
  memoryNote,
42
49
  memoryState,
43
50
  walkFiles
44
- } from "./chunk-DRZSUQ7Q.js";
45
- import "./chunk-2SVAHH5N.js";
51
+ } from "./chunk-LLL7QWXB.js";
52
+ import "./chunk-63E73TGI.js";
46
53
  import {
47
54
  estimateFreezeSeconds,
48
55
  fmtDuration,
@@ -50,319 +57,24 @@ import {
50
57
  redactSecrets,
51
58
  relTime,
52
59
  stripThinking,
60
+ telemetry,
53
61
  writeHeapSnapshot
54
- } from "./chunk-VPAWRRHL.js";
55
- import "./chunk-DTWKSZXY.js";
62
+ } from "./chunk-AE36LLL2.js";
63
+ import "./chunk-6OSEQOYY.js";
56
64
  import {
57
65
  traceRootRel
58
- } from "./chunk-FFYBY2NA.js";
66
+ } from "./chunk-KAGKX2YT.js";
59
67
  import {
60
68
  buildProjectGraph,
61
69
  graphStatus
62
- } from "./chunk-PGOYDOI4.js";
63
- import "./chunk-SSDLHWSF.js";
70
+ } from "./chunk-XEGQT5EN.js";
64
71
  import {
65
72
  writeAtomic
66
- } from "./chunk-B67BK5GQ.js";
73
+ } from "./chunk-6W4UH2BQ.js";
67
74
 
68
75
  // src/tui/app.tsx
69
76
  import { render } from "ink";
70
77
 
71
- // src/tui/role-models.ts
72
- var WEAK_RE = /\b(flash|mini|nano|haiku|lite|small|turbo|fast|\d{1,2}b)\b/i;
73
- var FLAGSHIP_ROLES = ["judge", "principal-coder"];
74
- var COUNCIL_ROLES = DEFAULT_COUNCIL.map((c) => c.name);
75
- var SPEC_LENS_ROLES = SPEC_TEAM.map((c) => c.name);
76
- var PLAN_LENS_ROLES = PLAN_TEAM.map((c) => c.name);
77
- var CODE_LENS_ROLES = CODE_TEAM.map((c) => c.name);
78
- var STRONG_ROLES = [
79
- "brainstormer",
80
- "analyst",
81
- "planner",
82
- "architect",
83
- "senior-coder",
84
- "senior-designer",
85
- ...COUNCIL_ROLES,
86
- ...PLAN_LENS_ROLES,
87
- ...CODE_LENS_ROLES
88
- ];
89
- var MID_ROLES = ["coach", "coder", "designer", "code-reviewer", "operational", "memory-keeper", "task-auditor", ...SPEC_LENS_ROLES];
90
- var FAST_ROLES = ["refiner", "router", "project-manager", "team-lead"];
91
- var CAPABLE_ROLES = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES]);
92
- var ROLE_PROFILES = {
93
- tracer: "Writes the per-file reference note every other agent reads before changing unfamiliar code \u2014 high volume, but its output is a COMMITTED FILE, not a turn in a conversation: a shallow note is believed by every agent that opens that file, forever, and nothing later corrects it. Give it the MOST capable non-[flagship] model in the catalogue, not merely one that qualifies as [strong]. Volume is not a reason to go cheaper here.",
94
- refiner: "Classifies intent and rewrites the prompt every turn \u2014 highest call volume, trivial task \u2192 a fast, cheap model.",
95
- router: "Picks coder-vs-designer for a task \u2014 tiny and frequent \u2192 fast, cheap.",
96
- "project-manager": "Turns a task list into board items \u2014 light and structured \u2192 fast, cheap.",
97
- "task-auditor": "The only check on the task breakdown before hours of implementation are spent executing it \u2014 reads the plan against the task list and finds what was dropped. Low volume, and everything downstream depends on it \u2192 a capable model, never the cheapest.",
98
- "team-lead": "Coordinates implementation waves \u2014 light orchestration \u2192 fast, cheap.",
99
- coach: "Your main interactive assistant, used constantly all session (highest interaction volume) \u2192 a capable but EFFICIENT model, never the costly flagship.",
100
- brainstormer: "Turns a raw request into a decided design before the spec: explores the repo, weighs 2-3 approaches, gets the user to choose. Low volume, sets the direction for everything downstream \u2192 a strong reasoning model.",
101
- analyst: "Authors the spec and constitution \u2192 a strong reasoning model (Opus-tier).",
102
- planner: "Designs the implementation plan \u2192 a strong reasoning model (Opus-tier).",
103
- architect: "Diagnoses stuck tasks and produces recovery plans \u2014 serious design work \u2192 a strong model.",
104
- judge: "Critiques specs/plans and makes the final review call \u2014 low volume, high stakes \u2192 the most capable flagship model.",
105
- coder: "Writes the bulk of the implementation \u2014 very high work volume \u2192 a good high-throughput coding model (Sonnet-tier), NOT the flagship (wasteful at this volume).",
106
- "senior-coder": "Reviews and revises above the coder \u2014 must be MORE capable than the coder (Opus-tier).",
107
- "principal-coder": "Final code decision-maker \u2014 low volume, high stakes \u2192 the flagship is appropriate.",
108
- designer: "Builds UI \u2014 high volume \u2192 a capable coding/design model, not the flagship.",
109
- "senior-designer": "Senior UI reviewer \u2014 more capable than the designer.",
110
- "code-reviewer": "Reviews diffs \u2014 moderate volume \u2192 a solid capable model.",
111
- "memory-keeper": "Decides what a finished job taught the project and writes it to durable memory \u2014 low volume, but a bad memory poisons every later run \u2192 a capable, efficient model, never the cheapest.",
112
- operational: "Handles version control: writes conventional commit messages and (later) drives merges/conflicts \u2014 high volume \u2192 a capable, efficient model."
113
- };
114
- for (const [stage, lenses, heft] of [
115
- ["spec", SPEC_TEAM, "a capable, efficient model (a spec is a short business-level doc)"],
116
- ["plan", PLAN_TEAM, "a strong model (technical design judgment)"],
117
- ["code", CODE_TEAM, "a strong model (reads real implementations)"]
118
- ]) {
119
- for (const l of lenses) ROLE_PROFILES[l.name] = `${stage.toUpperCase()}-review lens \u2014 ${l.perspective}. Low volume, quality-critical \u2192 ${heft}.`;
120
- }
121
- for (const c of DEFAULT_COUNCIL) {
122
- ROLE_PROFILES[c.name] = `Review COUNCIL decider \u2014 ${c.perspective} Casts the binding pass/revise vote on contested work \u2192 a strong model.`;
123
- }
124
- var ROLE_ADVICE = ROLE_PROFILES;
125
- function filterModelsForRole(role, all, exclude = []) {
126
- const advice = ROLE_ADVICE[role];
127
- const excluded = new Set(exclude);
128
- const avail = all.filter((m) => !excluded.has(m));
129
- if (CAPABLE_ROLES.has(role)) {
130
- const strong = avail.filter((m) => !WEAK_RE.test(m));
131
- if (strong.length === 0) return { models: avail.length ? avail : all, note: advice ? `${advice} (No strong models detected \u2014 showing all.)` : void 0 };
132
- return { models: strong, note: `${advice ?? ""} Showing ${strong.length} of ${avail.length} models (fast/weak models hidden for this role).`.trim() };
133
- }
134
- if (FAST_ROLES.includes(role)) {
135
- const fast = avail.filter((m) => WEAK_RE.test(m));
136
- if (fast.length === 0) return { models: avail.length ? avail : all, note: advice };
137
- return { models: fast, note: `${advice ?? ""} Showing ${fast.length} of ${avail.length} fast/cheap models.`.trim() };
138
- }
139
- return { models: avail.length ? avail : all };
140
- }
141
- function effortFor(role, model) {
142
- if (!isAnthropicModel(model)) return void 0;
143
- if (FLAGSHIP_ROLES.includes(role)) return "max";
144
- if (STRONG_ROLES.includes(role)) return "xhigh";
145
- if (FAST_ROLES.includes(role)) return "low";
146
- if (MID_ROLES.includes(role)) return "high";
147
- return void 0;
148
- }
149
- var effortBump = (s) => /-(ultra|max|xhigh)/.test(s) ? 4 : /-high/.test(s) ? 3 : /-medium/.test(s) ? 2 : /-low/.test(s) ? 1 : 0;
150
- var versionBump = (s, family) => {
151
- if (family) {
152
- const m = s.match(new RegExp(`${family}[-_. ]?(\\d+)(?:[-.](\\d+))?`));
153
- if (m) {
154
- const major = Number(m[1]);
155
- const minor = m[2] === void 0 ? 0 : Number(m[2]);
156
- if (major < 100) return major + (minor < 10 ? minor / 10 : minor / 100);
157
- }
158
- }
159
- const g = s.match(/(\d)[-.](\d)\b/);
160
- return g ? Number(g[1]) + Number(g[2]) / 10 : 0;
161
- };
162
- var KNOWN_FAMILY_RE = /(fable|mythos|opus|sonnet|haiku|claude|codex|gpt-|\bo\d\b|gemini|deepseek|llama|qwen|kimi|glm|mistral|grok|nova|command-r|phi-\d)/i;
163
- var NON_TEXT_RE = /\b(image|imagen|vision|video|veo|tts|audio|speech|voice|embed|embedding|rerank|ocr|computer-use|realtime|moderation)\b/i;
164
- function isKnownModel(model) {
165
- return KNOWN_FAMILY_RE.test(model) && !NON_TEXT_RE.test(model);
166
- }
167
- var UNRANKED_SCORE = 50;
168
- function capabilityScore(model) {
169
- const s = model.toLowerCase();
170
- if (WEAK_RE.test(s)) return 20 + effortBump(s);
171
- if (/fable|mythos/.test(s)) return 100;
172
- if (/opus/.test(s)) return 88 + versionBump(s, "opus");
173
- if (/codex|gpt-5|\bo3\b/.test(s)) return 82 + effortBump(s) + versionBump(s, "gpt") / 100;
174
- if (/sonnet/.test(s)) return 78 + versionBump(s, "sonnet");
175
- if (/gemini/.test(s) && /pro/.test(s)) return 76 + versionBump(s, "gemini") + effortBump(s);
176
- if (/gpt-4/.test(s)) return 65;
177
- if (/deepseek/.test(s)) return 55;
178
- return UNRANKED_SCORE;
179
- }
180
- function mostCapable(models) {
181
- return [...models].sort((a, b) => capabilityScore(b) - capabilityScore(a))[0] ?? "";
182
- }
183
- function modelBand(model) {
184
- if (WEAK_RE.test(model)) return "fast";
185
- const s = capabilityScore(model);
186
- if (s >= 95) return "flagship";
187
- if (s >= 84) return "strong";
188
- if (s <= UNRANKED_SCORE) return "fast";
189
- return "mid";
190
- }
191
- function baseModel(model) {
192
- const segs = model.toLowerCase().split("/");
193
- let s = segs[segs.length - 1];
194
- s = s.replace(/-(ultra|max|xhigh|high|medium|low|free|thinking|preview)\b/g, "");
195
- s = s.replace(/-\d{6,8}\b/g, "");
196
- return s.replace(/-+$/, "");
197
- }
198
- function modelFamily(model) {
199
- return baseModel(model).replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
200
- }
201
- function latestFirst(models) {
202
- const best = /* @__PURE__ */ new Map();
203
- for (const m of models) {
204
- const key = modelFamily(m);
205
- const cur = best.get(key);
206
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key, m);
207
- }
208
- const isLatest = (m) => best.get(modelFamily(m)) === m;
209
- return [...models.filter(isLatest), ...models.filter((m) => !isLatest(m))];
210
- }
211
- function versionlessId(model) {
212
- const cut = model.lastIndexOf("/");
213
- const prefix = cut >= 0 ? model.slice(0, cut + 1) : "";
214
- const name = model.slice(cut + 1).toLowerCase().replace(/-\d{6,8}\b/g, "").replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
215
- return prefix + name;
216
- }
217
- var DURABLE_ROLES = ["tracer"];
218
- function strongestPrimary(chain, pool) {
219
- const head = chain[0];
220
- if (!head) return chain;
221
- let best = head;
222
- for (const m of pool) {
223
- if (modelBand(m) === "flagship" || !isKnownModel(m)) continue;
224
- if (capabilityScore(m) > capabilityScore(best)) best = m;
225
- }
226
- if (best === head) return chain;
227
- const at = chain.indexOf(best);
228
- if (at > 0) {
229
- const next = [...chain];
230
- next[at] = head;
231
- next[0] = best;
232
- return next;
233
- }
234
- return [best, ...chain.slice(1)];
235
- }
236
- function newestPrimary(chain, pool) {
237
- const head = chain[0];
238
- if (!head) return chain;
239
- const key = versionlessId(head);
240
- let best = head;
241
- for (const m of pool) {
242
- if (versionlessId(m) !== key) continue;
243
- if (capabilityScore(m) > capabilityScore(best)) best = m;
244
- }
245
- if (best === head) return chain;
246
- const at = chain.indexOf(best);
247
- if (at > 0) {
248
- const next = [...chain];
249
- next[at] = head;
250
- next[0] = best;
251
- return next;
252
- }
253
- return [best, ...chain.slice(1)];
254
- }
255
- function dedupBest(models) {
256
- const best = /* @__PURE__ */ new Map();
257
- for (const m of models) {
258
- const key = baseModel(m);
259
- const cur = best.get(key);
260
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key, m);
261
- }
262
- return [...best.values()].sort((a, b) => capabilityScore(b) - capabilityScore(a));
263
- }
264
- function sourceOf(model) {
265
- const s = model.toLowerCase().replace(/^no-think\//, "");
266
- const p = s.split("/")[0];
267
- return p === "cc" ? "claude" : p === "cx" ? "codex" : p;
268
- }
269
- function interleaveBySource(pool) {
270
- const bySource = /* @__PURE__ */ new Map();
271
- for (const m of pool) {
272
- const s = sourceOf(m);
273
- const q = bySource.get(s);
274
- if (q) q.push(m);
275
- else bySource.set(s, [m]);
276
- }
277
- const queues = [...bySource.values()];
278
- const out = [];
279
- for (let more = true; more; ) {
280
- more = false;
281
- for (const q of queues) {
282
- const m = q.shift();
283
- if (m !== void 0) {
284
- out.push(m);
285
- more = true;
286
- }
287
- }
288
- }
289
- return out;
290
- }
291
- var BAND_ORDER = { fast: 0, mid: 1, strong: 2, flagship: 3 };
292
- function bandDistance(primary, candidate) {
293
- const p = BAND_ORDER[modelBand(primary)];
294
- const c = BAND_ORDER[modelBand(candidate)];
295
- return Math.abs(c - p) * 2 + (c < p ? 1 : 0);
296
- }
297
- function pickFallbacks(primary, pool, n) {
298
- const chosen = [];
299
- const usedModels = /* @__PURE__ */ new Set([baseModel(primary)]);
300
- const usedSources = /* @__PURE__ */ new Set([sourceOf(primary)]);
301
- const byHeft = pool.map((m, i) => ({ m, i })).sort((a, b) => bandDistance(primary, a.m) - bandDistance(primary, b.m) || a.i - b.i).map((x) => x.m);
302
- for (const m of byHeft) {
303
- if (chosen.length >= n) break;
304
- if (usedModels.has(baseModel(m)) || usedSources.has(sourceOf(m))) continue;
305
- chosen.push(m);
306
- usedModels.add(baseModel(m));
307
- usedSources.add(sourceOf(m));
308
- }
309
- for (const m of byHeft) {
310
- if (chosen.length >= n) break;
311
- if (usedModels.has(baseModel(m))) continue;
312
- chosen.push(m);
313
- usedModels.add(baseModel(m));
314
- }
315
- return chosen;
316
- }
317
- var FALLBACK_COUNT = 2;
318
- function adjustRoleModels(roles, models, unfit) {
319
- if (models.length === 0) return [];
320
- const recognised = models.filter(isKnownModel);
321
- const pick = recognised.length ? recognised : models;
322
- const capable = dedupBest(pick.filter((m) => !WEAK_RE.test(m)));
323
- const fast = dedupBest(pick.filter((m) => WEAK_RE.test(m)));
324
- const capablePool = capable.length ? capable : fast;
325
- const fastPool = fast.length ? fast : capable;
326
- const primaryPool = latestFirst(capablePool);
327
- const primaryFast = latestFirst(fastPool);
328
- const nonFlagship = primaryPool.filter((m) => modelBand(m) !== "flagship");
329
- const strongPool = primaryPool.filter((m) => modelBand(m) === "strong");
330
- const midPool = primaryPool.filter((m) => modelBand(m) === "mid");
331
- const wanted = new Set(roles);
332
- const forRole = (role, pool) => {
333
- if (!unfit) return pool;
334
- const fit = pool.filter((m) => !unfit(role, m));
335
- return fit.length ? fit : pool;
336
- };
337
- const known = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES, ...FAST_ROLES]);
338
- const primary = /* @__PURE__ */ new Map();
339
- const flagSrc = primaryPool;
340
- FLAGSHIP_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
341
- const src = forRole(r, flagSrc);
342
- primary.set(r, src[i % src.length]);
343
- });
344
- const strongSrc = interleaveBySource(strongPool.length ? strongPool : nonFlagship.length ? nonFlagship : primaryPool);
345
- STRONG_ROLES.filter((r) => wanted.has(r)).concat(roles.filter((r) => !known.has(r))).forEach((r, i) => {
346
- const src = forRole(r, strongSrc);
347
- primary.set(r, src[i % src.length]);
348
- });
349
- const midSrc = interleaveBySource(midPool.length ? midPool : nonFlagship.length ? nonFlagship : primaryPool);
350
- MID_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
351
- const src = forRole(r, midSrc);
352
- primary.set(r, src[i % src.length]);
353
- });
354
- FAST_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
355
- const src = forRole(r, primaryFast);
356
- primary.set(r, src[i % src.length]);
357
- });
358
- return roles.map((role) => {
359
- const head = primary.get(role) ?? primaryPool[0];
360
- const capForFb = MID_ROLES.includes(role) ? capablePool.filter((m) => modelBand(m) !== "flagship") : capablePool;
361
- const pool = FAST_ROLES.includes(role) ? [...fastPool, ...capForFb] : [...capForFb, ...fastPool];
362
- return { role, models: newestPrimary([head, ...pickFallbacks(head, forRole(role, pool), FALLBACK_COUNT)], models) };
363
- });
364
- }
365
-
366
78
  // src/engine/role-tuner.ts
367
79
  function describeCatalog(models) {
368
80
  const bySource = /* @__PURE__ */ new Map();
@@ -4798,6 +4510,8 @@ var ModelHealth = class {
4798
4510
  fitness;
4799
4511
  listModels;
4800
4512
  probe;
4513
+ /** Routability, not recovery — see ModelHealthOpts.routable. */
4514
+ routable;
4801
4515
  note;
4802
4516
  now;
4803
4517
  /** Serializes healing: a whole review team failing at once must not trigger 14 concurrent re-assignments. */
@@ -4873,6 +4587,7 @@ var ModelHealth = class {
4873
4587
  this.port = opts.port;
4874
4588
  this.listModels = opts.listModels;
4875
4589
  this.probe = opts.probe;
4590
+ this.routable = opts.routable;
4876
4591
  this.note = opts.note ?? (() => {
4877
4592
  });
4878
4593
  this.now = opts.now ?? (() => Date.now());
@@ -4893,7 +4608,38 @@ var ModelHealth = class {
4893
4608
  mark(model, reason) {
4894
4609
  for (const r of this.port.registries()) r.markExhausted(model, reason, this.now());
4895
4610
  }
4896
- /** Models that may be assigned right now: the catalog minus everything quarantined. */
4611
+ /**
4612
+ * Models that may be assigned right now: the catalog, minus everything quarantined, minus what no role has
4613
+ * ever been configured with.
4614
+ *
4615
+ * The catalog is what the gateway LISTS, which is not what it will route to. Measured across a whole run:
4616
+ * `opencode-go/muse-spark-1.2-contributor-xhigh` is in the catalog of 726 and answers every request with
4617
+ * "not available in the active live catalog". It was in nobody's chain — `/roles adjust` had already
4618
+ * dropped it — and re-assignment handed it out anyway, to twenty-three parallel slots at once. It took the
4619
+ * ladders of twenty-one tasks and, through their dependencies, ninety-seven more.
4620
+ *
4621
+ * The configured set is the curated one: every model in it was chosen for a role and has been reached at
4622
+ * least once. Preferring it costs nothing, because the wider catalog is still there when the curated pool
4623
+ * is spent — which is the only situation that ever needed the breadth.
4624
+ *
4625
+ * That fallback is the door the herd came back through, so nothing walks through it unproven. Measured on
4626
+ * the run after the preference was added: the curated pool emptied for about a minute, the catalog came
4627
+ * back whole, and `muse-spark-1.2-contributor-xhigh` — ranked top by name alone — was handed to every role
4628
+ * being re-assigned at the same instant. 204 calls left before the first answer could bench it. Its eight
4629
+ * siblings each cost exactly ONE call, which is the bench working; the difference is only that they were
4630
+ * not the head of the ranking.
4631
+ *
4632
+ * A probe is the cheapest thing that can tell "listed" from "routable", it runs only on this rare path, and
4633
+ * `poolWithConfigured` already proves the same pattern at startup. It must be the ROUTABILITY probe (see
4634
+ * `routable`): asking the recovery probe instead rejects everything the moment the fleet is busy, which is
4635
+ * exactly when this path runs.
4636
+ *
4637
+ * And a probe that clears nothing does not get to strand every role. Measured live: "probed 78 catalog
4638
+ * model(s) and dropped 78", then `No healthy model left to reassign code-data-integrity` — the lens kept
4639
+ * its spent chain and returned UNVERIFIED, blocking the review. Handing back the unproven catalog risks
4640
+ * one wasted call per model, which the bench ends and the ladder no longer charges for; handing back
4641
+ * nothing leaves a role with no way to run at all. The first is recoverable, so it wins.
4642
+ */
4897
4643
  async healthyModels() {
4898
4644
  const dead = new Set(this.quarantined().map((q) => q.model));
4899
4645
  let all;
@@ -4902,7 +4648,46 @@ var ModelHealth = class {
4902
4648
  } catch {
4903
4649
  return [];
4904
4650
  }
4905
- return all.filter((m) => !dead.has(m));
4651
+ const live = all.filter((m) => !dead.has(m));
4652
+ const configured = new Set(this.port.registries().flatMap((r) => r.knownModels()));
4653
+ const curated = live.filter((m) => configured.has(m));
4654
+ telemetry().event("decision.pool", {
4655
+ "hc.decision": "pool",
4656
+ "hc.pool.catalog": all.length,
4657
+ "hc.pool.live": live.length,
4658
+ "hc.pool.configured": configured.size,
4659
+ "hc.pool.curated": curated.length,
4660
+ "hc.pool.source": curated.length ? "curated" : "fallback"
4661
+ });
4662
+ if (curated.length) return curated;
4663
+ const routable = this.routable;
4664
+ if (!routable || !live.length) {
4665
+ telemetry().event("decision.pool", {
4666
+ "hc.decision": "pool",
4667
+ "hc.pool.source": "unscreened",
4668
+ "hc.pool.live": live.length,
4669
+ "hc.pool.reason": routable ? "no live models" : "no routability probe"
4670
+ });
4671
+ return live;
4672
+ }
4673
+ const checked = await Promise.all(live.map(async (m) => ({ m, ok: await routable(m).catch(() => false) })));
4674
+ const usable = checked.filter((c) => c.ok).map((c) => c.m);
4675
+ if (!usable.length) {
4676
+ this.note(`\u{1F50E} The configured models are all spent, and none of the ${checked.length} catalog model(s) answered a probe \u2014 offering them anyway rather than leaving roles with nothing.`);
4677
+ return live;
4678
+ }
4679
+ const refused = checked.length - usable.length;
4680
+ telemetry().event("decision.pool", {
4681
+ "hc.decision": "pool",
4682
+ "hc.pool.source": "probed",
4683
+ "hc.pool.probed": checked.length,
4684
+ "hc.pool.refused": refused,
4685
+ "hc.pool.usable": usable.length
4686
+ });
4687
+ if (refused) {
4688
+ this.note(`\u{1F50E} The configured models are all spent \u2014 probed ${checked.length} catalog model(s) and dropped ${refused} the gateway would not route to.`);
4689
+ }
4690
+ return usable;
4906
4691
  }
4907
4692
  /**
4908
4693
  * Re-probes the quarantine and releases whatever answers again. A 429 is a temporary limit, not a permanent
@@ -4970,6 +4755,12 @@ var ModelHealth = class {
4970
4755
  const reg = this.port.registryFor(role);
4971
4756
  const before = [...reg.rawChain(role)];
4972
4757
  reg.setRoleModel(role, chain);
4758
+ telemetry().event("decision.rechain", {
4759
+ "hc.decision": "rechain",
4760
+ "hc.role": role,
4761
+ "hc.chain": chain.join(","),
4762
+ "hc.chain.before": before.join(",")
4763
+ });
4973
4764
  out.push({ role, chain, before });
4974
4765
  }
4975
4766
  return out;
@@ -5373,6 +5164,12 @@ function startupSummary(f) {
5373
5164
  `- Rules: ${f.rules} active${f.rules ? " (`/memories` to read them)" : ""}`,
5374
5165
  `- Memory: ${plural(m.total, "entry", "entries")}${kinds ? ` \u2014 ${kinds}` : ""}`,
5375
5166
  `- Skills: ${plural(f.skills, "available", "available")}`,
5167
+ /**
5168
+ * The one line about what the work will be CHARGED to, so it sits with the rest of what the session has
5169
+ * to work with. It was previously printed with `console.log` before this panel rendered — which the TUI
5170
+ * then cleared, so a person with two working logins was shown nothing at all.
5171
+ */
5172
+ ...f.accounts ? [`- Accounts: ${f.accounts}`] : [],
5376
5173
  /**
5377
5174
  * "traces" is named precisely, because the word is taken.
5378
5175
  *
@@ -5460,6 +5257,12 @@ async function runTuiRepl(opts) {
5460
5257
  // STRICTER than source discovery, which counts 429 as "routed": a rate-limited model is exactly what was
5461
5258
  // quarantined, so only a real answer may release it.
5462
5259
  ...opts.probeModel ? { probe: opts.probeModel } : {},
5260
+ /**
5261
+ * …and the 429-tolerant one for the other question. `healthyModels` asks whether the gateway will ROUTE
5262
+ * to a model, not whether a benched one has recovered, and it asks precisely when the fleet is busiest.
5263
+ * Given the strict probe it dropped all 78 catalog models at once — see ModelHealthOpts.routable.
5264
+ */
5265
+ ...opts.routableModel ? { routable: opts.routableModel } : {},
5463
5266
  note: (m) => controller.note(m)
5464
5267
  });
5465
5268
  health.watch();
@@ -5570,7 +5373,7 @@ Question: ${question}` }
5570
5373
  };
5571
5374
  const addMcp = async (input) => {
5572
5375
  const { parseCommand, parseConfigBlock, extractFromPage, verify } = await import("./install-O34KMWJB.js");
5573
- const { saveMcpServer } = await import("./save-skills-NSLBU33X.js");
5376
+ const { saveMcpServer } = await import("./save-skills-X7U3KCPU.js");
5574
5377
  let cand = parseCommand(input) ?? parseConfigBlock(input);
5575
5378
  if (!cand) {
5576
5379
  const url = input.trim();
@@ -5619,7 +5422,7 @@ _Restart to connect it._`;
5619
5422
  const baseRef = { current: opts.jobBase.fromBranch };
5620
5423
  const continueFromClaude = async (arg) => {
5621
5424
  const { adoptClaudeWorktree, describeAdoption, AdoptError, listClaudeWorktrees } = await import("./worktree-F7TWLWLN.js");
5622
- const { defaultGitRunner } = await import("./git-VTSZALSR.js");
5425
+ const { defaultGitRunner } = await import("./git-QZTDZSJY.js");
5623
5426
  const git = defaultGitRunner;
5624
5427
  try {
5625
5428
  const w = await adoptClaudeWorktree(git, process.cwd(), arg, baseRef.current);
@@ -5642,7 +5445,7 @@ _This project has no worktrees under \`.claude/worktrees\`._`}`);
5642
5445
  const migrate = async () => {
5643
5446
  if (!opts.memStore) return "Migration needs the memory store, which is not available in this session.";
5644
5447
  const progressLines = /* @__PURE__ */ new Map();
5645
- const { runMigration, describeResult } = await import("./run-LQOZ5I7Z.js");
5448
+ const { runMigration, describeResult } = await import("./run-P6ZYL5JL.js");
5646
5449
  const r = await runMigration({
5647
5450
  cwd: process.cwd(),
5648
5451
  home: homedir2(),
@@ -5871,6 +5674,10 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5871
5674
  constitution: existsSync(join6(process.cwd(), ".specify", "memory", "constitution.md")),
5872
5675
  graph: { built: false, nodes: 0 },
5873
5676
  traceRoot: traceRootRel(),
5677
+ ...(() => {
5678
+ const a = opts.accountsNote?.();
5679
+ return a ? { accounts: a } : {};
5680
+ })(),
5874
5681
  ...unfinished.length ? { unfinished } : {},
5875
5682
  ...startupExtra
5876
5683
  };
@@ -5899,12 +5706,12 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5899
5706
  paintSummary();
5900
5707
  void (async () => {
5901
5708
  try {
5902
- const { defaultGitRunner } = await import("./git-VTSZALSR.js");
5709
+ const { defaultGitRunner } = await import("./git-QZTDZSJY.js");
5903
5710
  const { recordedMainBranch, detectMainBranch } = await import("./main-branch-KGWUINYQ.js");
5904
5711
  const cwd = process.cwd();
5905
5712
  const main = await recordedMainBranch(cwd) ?? await detectMainBranch(cwd, defaultGitRunner);
5906
5713
  if (!main) return;
5907
- const { ongoingWork, chooseOngoing } = await import("./ongoing-XP6WXNI7.js");
5714
+ const { ongoingWork, chooseOngoing } = await import("./ongoing-WHYXPW24.js");
5908
5715
  const open = await ongoingWork(defaultGitRunner, cwd, main);
5909
5716
  if (!open.length) return;
5910
5717
  const picked = await chooseOngoing(deps, read, open[0]?.language, open);
@@ -5923,11 +5730,11 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5923
5730
  })();
5924
5731
  void (async () => {
5925
5732
  try {
5926
- const { loadTraceIndex } = await import("./trace-ZMB7LT7W.js");
5733
+ const { loadTraceIndex } = await import("./trace-X6TU3AG6.js");
5927
5734
  let index = await loadTraceIndex(process.cwd());
5928
5735
  startupExtra.traces = Object.keys(index.traces).length;
5929
5736
  paintSummary();
5930
- const { graphStatus: graphStatus2 } = await import("./project-graph-IOPCSZUA.js");
5737
+ const { graphStatus: graphStatus2 } = await import("./project-graph-5HNPRFQG.js");
5931
5738
  const g = await graphStatus2(process.cwd());
5932
5739
  startupExtra.graph = { built: g.built, nodes: g.nodes, stale: g.stale };
5933
5740
  if (!g.built || g.stale) {
@@ -5936,11 +5743,11 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5936
5743
  const after = await graphStatus2(process.cwd());
5937
5744
  startupExtra.graph = { built: after.built, nodes: after.nodes, stale: after.stale };
5938
5745
  }
5939
- const g0 = await import("./project-graph-IOPCSZUA.js").then((m) => m.loadGraphSync(process.cwd()));
5746
+ const g0 = await import("./project-graph-5HNPRFQG.js").then((m) => m.loadGraphSync(process.cwd()));
5940
5747
  if (g0) {
5941
5748
  const files = /* @__PURE__ */ new Set();
5942
5749
  for (const n of g0.nodes) if (n.source_file) files.add(n.source_file);
5943
- const { adoptDocs, indexAdoption, describeAdoption } = await import("./trace-adopt-C6TUWFJL.js");
5750
+ const { adoptDocs, indexAdoption, describeAdoption } = await import("./trace-adopt-URECQWJV.js");
5944
5751
  const adoption = await adoptDocs(process.cwd(), files);
5945
5752
  const { readFile: readFile5 } = await import("fs/promises");
5946
5753
  const r = await indexAdoption(
@@ -5956,8 +5763,8 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5956
5763
  }
5957
5764
  startupExtra.traces = Object.keys(index.traces).length;
5958
5765
  paintSummary();
5959
- const { traceableFiles } = await import("./trace-run-F23MFTY4.js");
5960
- const { traceCoverage } = await import("./trace-ZMB7LT7W.js");
5766
+ const { traceableFiles } = await import("./trace-run-7U4WJZ3V.js");
5767
+ const { traceCoverage } = await import("./trace-X6TU3AG6.js");
5961
5768
  startupExtra.coverage = await traceCoverage(process.cwd(), await traceableFiles(process.cwd()), index);
5962
5769
  paintSummary();
5963
5770
  } catch {