@hizliemre/horse-code 0.1.1 → 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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/{app-UGFQKMLX.js → app-2GPGCDX6.js} +136 -329
  3. package/dist/{chunk-2DGO2BUB.js → chunk-23CLQ2KO.js} +366 -600
  4. package/dist/{chunk-JWAEW7AJ.js → chunk-5ZV42XGJ.js} +2 -2
  5. package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
  6. package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
  7. package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
  8. package/dist/{chunk-3XVZXTB6.js → chunk-7JMWPTJ5.js} +1818 -384
  9. package/dist/{chunk-YILDXPSI.js → chunk-AE36LLL2.js} +49 -25
  10. package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
  11. package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
  12. package/dist/{chunk-TOPZL5SU.js → chunk-LLL7QWXB.js} +41 -9
  13. package/dist/{chunk-7TBYMFMG.js → chunk-LNW557IO.js} +2 -2
  14. package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
  15. package/dist/chunk-M2RKCIGV.js +11 -0
  16. package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
  17. package/dist/{chunk-HBSC2HT2.js → chunk-UGESK765.js} +1 -1
  18. package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
  19. package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
  20. package/dist/cli.js +414 -100
  21. package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
  22. package/dist/fix-JOIXQFVP.js +33 -0
  23. package/dist/git-QZTDZSJY.js +8 -0
  24. package/dist/{ongoing-OV5XROTU.js → ongoing-WHYXPW24.js} +4 -4
  25. package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
  26. package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
  27. package/dist/{save-skills-NSLBU33X.js → save-skills-X7U3KCPU.js} +1 -1
  28. package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
  29. package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
  30. package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
  31. package/dist/{triage-2J3T5PVQ.js → triage-FCYHD2AQ.js} +8 -9
  32. package/dist/{verify-WQ3GHION.js → verify-6SC4I77M.js} +18 -19
  33. package/package.json +1 -1
  34. package/dist/chunk-B67BK5GQ.js +0 -34
  35. package/dist/chunk-F2IALVBU.js +0 -212
  36. package/dist/chunk-O74BDQKS.js +0 -28
  37. package/dist/fix-HBBOTUWM.js +0 -34
  38. package/dist/git-VTSZALSR.js +0 -6
package/dist/cli.js CHANGED
@@ -2,25 +2,24 @@
2
2
  import {
3
3
  MAX_PARALLEL_TASKS,
4
4
  MemoryStore,
5
- OmniRouteProvider,
6
5
  Timings,
7
6
  autonomousAskHuman,
8
7
  defaultCmdRunner,
8
+ describeTally,
9
9
  detectPlatform,
10
10
  makeApprove,
11
11
  makeAskUser,
12
12
  makePRAdapter,
13
13
  nodeLineReader,
14
+ proposeSplit,
14
15
  restoreTerminal,
15
16
  runJob,
16
17
  sttySane
17
- } from "./chunk-2DGO2BUB.js";
18
- import {
19
- listOmniRouteModels
20
- } from "./chunk-O74BDQKS.js";
21
- import "./chunk-F2IALVBU.js";
18
+ } from "./chunk-23CLQ2KO.js";
19
+ import "./chunk-M2RKCIGV.js";
22
20
  import {
23
21
  CODE_TEAM,
22
+ CliProvider,
24
23
  DEFAULT_COUNCIL,
25
24
  DEFAULT_PROMPTS,
26
25
  DEFAULT_ROLE_SKILLS,
@@ -28,16 +27,20 @@ import {
28
27
  REQUIRED_ROLES,
29
28
  RoleFitness,
30
29
  RoleRegistry,
30
+ SHORT_CALL_MS,
31
31
  SPEC_TEAM,
32
32
  WorktreeManager,
33
33
  buildCouncilRegistry,
34
34
  buildTeamRegistry,
35
+ checkProfile,
36
+ cliCatalog,
35
37
  mainWorktreeRoot,
38
+ runLogin,
36
39
  toSlug
37
- } from "./chunk-3XVZXTB6.js";
40
+ } from "./chunk-7JMWPTJ5.js";
38
41
  import "./chunk-QF4MP6BS.js";
39
- import "./chunk-HBSC2HT2.js";
40
- import "./chunk-FGVJFMK5.js";
42
+ import "./chunk-UGESK765.js";
43
+ import "./chunk-ZSQ24YDJ.js";
41
44
  import {
42
45
  parseFrontmatter
43
46
  } from "./chunk-BY4DP7IE.js";
@@ -45,7 +48,7 @@ import {
45
48
  UNSET_MODEL,
46
49
  loadConfig,
47
50
  saveSkillSource
48
- } from "./chunk-4EWK7HWQ.js";
51
+ } from "./chunk-KKWZBZYK.js";
49
52
  import "./chunk-H2FDGPVW.js";
50
53
  import {
51
54
  buildBrief,
@@ -53,50 +56,234 @@ import {
53
56
  planFor,
54
57
  runTraces,
55
58
  traceableFiles
56
- } from "./chunk-NNTIACT4.js";
59
+ } from "./chunk-MRZVA5JB.js";
57
60
  import {
58
61
  defaultGitRunner
59
- } from "./chunk-IW2KBAVZ.js";
62
+ } from "./chunk-LPQU436C.js";
60
63
  import {
61
64
  InjectionLog,
62
65
  memoryNote
63
- } from "./chunk-TOPZL5SU.js";
64
- import "./chunk-2SVAHH5N.js";
66
+ } from "./chunk-LLL7QWXB.js";
67
+ import "./chunk-63E73TGI.js";
65
68
  import {
66
69
  Telemetry,
70
+ ToolRegistry,
67
71
  clearPerfMarks,
68
72
  sampleMemory,
69
73
  setTelemetry,
70
74
  stripThinking,
71
75
  telemetry,
72
76
  writeHeapSnapshot
73
- } from "./chunk-YILDXPSI.js";
77
+ } from "./chunk-AE36LLL2.js";
74
78
  import {
75
79
  briefStatus
76
- } from "./chunk-DTWKSZXY.js";
80
+ } from "./chunk-6OSEQOYY.js";
77
81
  import {
78
82
  discoverTraceRoot,
79
83
  setTraceRoot
80
- } from "./chunk-FFYBY2NA.js";
84
+ } from "./chunk-KAGKX2YT.js";
81
85
  import {
82
86
  buildProjectGraph,
83
87
  graphStatus,
84
88
  graphifyPython
85
- } from "./chunk-PGOYDOI4.js";
89
+ } from "./chunk-XEGQT5EN.js";
86
90
  import {
87
91
  sessionBase
88
- } from "./chunk-SSDLHWSF.js";
89
- import "./chunk-B67BK5GQ.js";
92
+ } from "./chunk-6W4UH2BQ.js";
90
93
 
91
94
  // src/cli.ts
92
95
  import { execFileSync } from "child_process";
93
- import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, realpathSync } from "fs";
94
- import { join as join8, dirname as dirname4 } from "path";
96
+ import { readFileSync as readFileSync4, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, realpathSync } from "fs";
97
+ import { join as join10, dirname as dirname5 } from "path";
95
98
  import { pathToFileURL } from "url";
96
99
 
100
+ // src/agents/cli-accounts.ts
101
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
102
+ import { dirname, join } from "path";
103
+ var SPENT = 0.95;
104
+ function readingKey(kind, name) {
105
+ return `${kind}:${name}`;
106
+ }
107
+ function fileUsageStore(home) {
108
+ const path = join(home, ".horsecode", "usage.json");
109
+ return {
110
+ load() {
111
+ try {
112
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
113
+ if (typeof parsed !== "object" || parsed === null) return {};
114
+ const out = {};
115
+ for (const [k, v] of Object.entries(parsed)) {
116
+ const r = v;
117
+ if (typeof r?.spent === "number" && typeof r?.at === "number") out[k] = { spent: r.spent, at: r.at };
118
+ }
119
+ return out;
120
+ } catch {
121
+ return {};
122
+ }
123
+ },
124
+ save(readings) {
125
+ try {
126
+ mkdirSync(dirname(path), { recursive: true });
127
+ writeFileSync(path, JSON.stringify(readings, null, 2) + "\n");
128
+ } catch {
129
+ }
130
+ }
131
+ };
132
+ }
133
+ var AccountPool = class {
134
+ accounts;
135
+ store;
136
+ readings;
137
+ constructor(accounts = [], store) {
138
+ this.accounts = accounts;
139
+ this.store = store;
140
+ this.readings = store?.load() ?? {};
141
+ }
142
+ /**
143
+ * The profile a call of this kind should run under, or undefined to run under the ambient login.
144
+ *
145
+ * Round-robin over the accounts that still have room, so two subscriptions drain together rather than one
146
+ * after the other. A spent one drops out of the rotation and rejoins the moment a reading says it can
147
+ * serve again.
148
+ */
149
+ pick(kind) {
150
+ const mine = this.accounts.filter((a) => a.kind === kind);
151
+ if (!mine.length) return void 0;
152
+ const live = mine.filter((a) => (this.readings[readingKey(kind, a.name)]?.spent ?? 0) < SPENT);
153
+ const pool = live.length ? live : mine;
154
+ const n = this.turn.get(kind) ?? 0;
155
+ this.turn.set(kind, n + 1);
156
+ return pool[n % pool.length];
157
+ }
158
+ /** Whose turn it is, per kind. Turns are per SOURCE: Claude's rotation says nothing about Codex's. */
159
+ turn = /* @__PURE__ */ new Map();
160
+ /**
161
+ * File a quota reading against a profile.
162
+ *
163
+ * The windows are taken at their worst: a seven-day limit that is nearly gone stops the profile just as
164
+ * surely as a five-hour one, and reading only the shorter window would keep sending calls to a
165
+ * subscription whose longer window ran out days ago.
166
+ */
167
+ record(kind, name, windows, now = Date.now()) {
168
+ const spent = Math.max(0, ...Object.values(windows));
169
+ this.readings = { ...this.readings, [readingKey(kind, name)]: { spent, at: now } };
170
+ this.store?.save(this.readings);
171
+ }
172
+ /** Every configured profile with its last reading, for a startup summary or a status line. */
173
+ usage() {
174
+ return this.accounts.map((a) => {
175
+ const r = this.readings[readingKey(a.kind, a.name)];
176
+ return { account: a, ...r ? { reading: r } : {} };
177
+ });
178
+ }
179
+ /** How many profiles are configured, in total or for one CLI. */
180
+ count(kind) {
181
+ return kind ? this.accounts.filter((a) => a.kind === kind).length : this.accounts.length;
182
+ }
183
+ };
184
+ function ageOf(at, now) {
185
+ const mins = Math.max(0, Math.round((now - at) / 6e4));
186
+ if (mins < 60) return `${mins}m`;
187
+ const hours = Math.round(mins / 60);
188
+ return hours < 48 ? `${hours}h` : `${Math.round(hours / 24)}d`;
189
+ }
190
+ function accountsLine(usage2, notSignedIn = [], now = Date.now()) {
191
+ const parts = usage2.map(({ account, reading }) => {
192
+ const who = `${account.email ?? account.plan ?? account.name}${account.email && account.plan ? ` (${account.plan})` : ""}`;
193
+ const left = reading ? ` ${Math.round(reading.spent * 100)}% used ${ageOf(reading.at, now)} ago` : "";
194
+ return `${account.kind} ${who}${left}`;
195
+ });
196
+ for (const kind of notSignedIn) parts.push(`${kind} NOT signed in \u2014 every ${kind} call will fail`);
197
+ return parts.length ? parts.join(" \xB7 ") : void 0;
198
+ }
199
+
200
+ // src/agents/add-provider.ts
201
+ import { join as join2 } from "path";
202
+ function freshDir(kind, home, taken) {
203
+ const used = new Set(taken.map((a) => a.configDir));
204
+ for (let n = 2; ; n++) {
205
+ const dir = join2(home, ".horsecode", "profiles", `${kind}-${n}`);
206
+ if (!used.has(dir)) return dir;
207
+ }
208
+ }
209
+ function nameFor(kind, status, dir) {
210
+ return status.email ?? `${kind}-${dir.split("/").pop() ?? "profile"}`;
211
+ }
212
+ function withAmbient(kind, existing, ambient) {
213
+ if (existing.some((a) => a.kind === kind)) return [...existing];
214
+ if (!ambient.loggedIn) return [...existing];
215
+ return [
216
+ ...existing,
217
+ {
218
+ kind,
219
+ name: ambient.email ?? `${kind}-default`,
220
+ ...ambient.email ? { email: ambient.email } : {},
221
+ ...ambient.plan ? { plan: ambient.plan } : {}
222
+ }
223
+ ];
224
+ }
225
+ function accountsIn(config) {
226
+ const raw = config.accounts;
227
+ if (!Array.isArray(raw)) return [];
228
+ return raw.filter((a) => {
229
+ const e = a;
230
+ if (typeof a !== "object" || a === null) return false;
231
+ if (e.kind !== "claude" && e.kind !== "codex") return false;
232
+ return e.configDir === void 0 || typeof e.configDir === "string";
233
+ });
234
+ }
235
+ function addProvider(kind, io) {
236
+ const config = io.readConfig();
237
+ const existing = accountsIn(config);
238
+ const accounts = withAmbient(kind, existing, io.check(kind));
239
+ if (accounts.length > existing.length) {
240
+ const a = accounts.at(-1);
241
+ io.log(`Recorded the ${kind} account you are already signed in as: ${a.email ?? a.name}${a.plan ? ` (${a.plan})` : ""}`);
242
+ io.log(`It stays first in line, so runs keep spending it before anything added now.`);
243
+ }
244
+ const dir = freshDir(kind, io.home, accounts);
245
+ io.log(`
246
+ Opening ${kind}'s own sign-in, for a NEW profile at ${dir}.`);
247
+ io.log(`Sign in with the account you want to add \u2014 not the one already connected.
248
+ `);
249
+ const r = io.login(kind, dir);
250
+ if (!r.ok) {
251
+ io.log(`
252
+ ${kind} sign-in did not complete${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
253
+ io.log(`If a browser page opened, its address holds the reason \u2014 an \`error=\` there is the account being refused, not this command failing.`);
254
+ io.log(`Nothing needs cleaning up: ${dir} holds no session, and running this again reuses it.`);
255
+ return 1;
256
+ }
257
+ const status = io.check(kind, dir);
258
+ if (!status.loggedIn) {
259
+ io.log(`
260
+ ${kind} still reports no session in ${dir}. Nothing was changed \u2014 run this again to retry.`);
261
+ return 1;
262
+ }
263
+ const already = status.email ? accounts.find((a) => a.kind === kind && a.email === status.email) : void 0;
264
+ if (already) {
265
+ io.log(`
266
+ That is the account already connected as "${already.name}". Nothing was changed.`);
267
+ return 1;
268
+ }
269
+ const entry = {
270
+ kind,
271
+ name: nameFor(kind, status, dir),
272
+ configDir: dir,
273
+ ...status.email ? { email: status.email } : {},
274
+ ...status.plan ? { plan: status.plan } : {}
275
+ };
276
+ io.writeConfig({ ...config, accounts: [...accounts, entry] });
277
+ const mine = [...accounts, entry].filter((a) => a.kind === kind);
278
+ io.log(`
279
+ Connected ${entry.email ?? entry.name}${entry.plan ? ` (${entry.plan})` : ""}.`);
280
+ io.log(`${mine.length} ${kind} account${mine.length === 1 ? "" : "s"} now connected. Runs use them in order, spilling to the next when one is nearly out.`);
281
+ return 0;
282
+ }
283
+
97
284
  // src/skills/registry.ts
98
285
  import { readdir, readFile } from "fs/promises";
99
- import { join } from "path";
286
+ import { join as join3 } from "path";
100
287
  var SkillRegistry = class {
101
288
  skills = /* @__PURE__ */ new Map();
102
289
  register(skill) {
@@ -119,7 +306,7 @@ var SkillRegistry = class {
119
306
  if (!e.isDirectory()) continue;
120
307
  let raw;
121
308
  try {
122
- raw = await readFile(join(dir, e.name, "SKILL.md"), "utf8");
309
+ raw = await readFile(join3(dir, e.name, "SKILL.md"), "utf8");
123
310
  } catch {
124
311
  continue;
125
312
  }
@@ -127,17 +314,17 @@ var SkillRegistry = class {
127
314
  if (!name || !description) {
128
315
  throw new Error(`skill ${e.name}: missing frontmatter (name/description)`);
129
316
  }
130
- this.register({ name, description, content: body, dir: join(dir, e.name) });
317
+ this.register({ name, description, content: body, dir: join3(dir, e.name) });
131
318
  }
132
319
  }
133
320
  };
134
321
 
135
322
  // src/skills/builtin.ts
136
323
  import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
137
- import { dirname, join as join2, resolve } from "path";
324
+ import { dirname as dirname2, join as join4, resolve } from "path";
138
325
  import { fileURLToPath } from "url";
139
326
  function candidates() {
140
- const here = dirname(fileURLToPath(import.meta.url));
327
+ const here = dirname2(fileURLToPath(import.meta.url));
141
328
  return [
142
329
  resolve(here, "..", "skills"),
143
330
  // bundled: dist/… → <repo>/skills
@@ -155,14 +342,14 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
155
342
  if (!e.isDirectory()) continue;
156
343
  let raw;
157
344
  try {
158
- raw = await readFile2(join2(dir, e.name, "SKILL.md"), "utf8");
345
+ raw = await readFile2(join4(dir, e.name, "SKILL.md"), "utf8");
159
346
  } catch {
160
347
  continue;
161
348
  }
162
349
  const { parseFrontmatter: parseFrontmatter2 } = await import("./frontmatter-UNIPNLLO.js");
163
350
  const { name, description, body } = parseFrontmatter2(raw);
164
351
  if (!name || !description) continue;
165
- found.push({ name, description, content: body, dir: join2(dir, e.name) });
352
+ found.push({ name, description, content: body, dir: join4(dir, e.name) });
166
353
  }
167
354
  if (!found.length) continue;
168
355
  for (const s of found) registry.register(s);
@@ -177,12 +364,12 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
177
364
  // src/skills/external.ts
178
365
  import { mkdir, rm, readFile as readFile3, writeFile, readdir as readdir3, cp } from "fs/promises";
179
366
  import { existsSync } from "fs";
180
- import { join as join3 } from "path";
367
+ import { join as join5 } from "path";
181
368
  import { spawn } from "child_process";
182
369
  import { tmpdir } from "os";
183
370
  var PROVENANCE = ".horsecode-source.json";
184
371
  function externalSkillsDir(home) {
185
- return join3(home, ".horsecode", "skills");
372
+ return join5(home, ".horsecode", "skills");
186
373
  }
187
374
  function validName(name) {
188
375
  return /^[A-Za-z0-9._-]+$/.test(name) && !name.includes("..");
@@ -226,7 +413,7 @@ async function resolveSha(src, deps) {
226
413
  }
227
414
  async function installedSource(home, name) {
228
415
  try {
229
- return JSON.parse(await readFile3(join3(externalSkillsDir(home), name, PROVENANCE), "utf8"));
416
+ return JSON.parse(await readFile3(join5(externalSkillsDir(home), name, PROVENANCE), "utf8"));
230
417
  } catch {
231
418
  return void 0;
232
419
  }
@@ -236,27 +423,27 @@ async function installSkillSource(home, src, deps = {}) {
236
423
  if (!validRepo(src.repo)) throw new Error(`skill source repo must be "owner/repo": ${src.repo}`);
237
424
  if (src.path?.includes("..")) throw new Error(`skill source path may not contain "..": ${src.path}`);
238
425
  const sha = await resolveSha(src, deps);
239
- const dest = join3(externalSkillsDir(home), src.name);
426
+ const dest = join5(externalSkillsDir(home), src.name);
240
427
  const current = await installedSource(home, src.name);
241
- if (current?.sha === sha && existsSync(join3(dest, "SKILL.md"))) {
428
+ if (current?.sha === sha && existsSync(join5(dest, "SKILL.md"))) {
242
429
  return { name: src.name, sha, changed: false };
243
430
  }
244
- const staging = join3(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
431
+ const staging = join5(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
245
432
  await rm(staging, { recursive: true, force: true });
246
433
  await mkdir(staging, { recursive: true });
247
434
  try {
248
435
  const url = `https://codeload.github.com/${src.repo}/tar.gz/${sha}`;
249
436
  const dl = await run("sh", ["-c", `curl -fsSL --max-time 120 ${JSON.stringify(url)} | tar xz -C ${JSON.stringify(staging)} --strip-components=1`]);
250
437
  if (dl.code !== 0) throw new Error(`download failed for ${src.repo}@${sha.slice(0, 8)}: ${dl.err.trim().slice(0, 200)}`);
251
- const from = src.path ? join3(staging, src.path) : staging;
252
- if (!existsSync(join3(from, "SKILL.md"))) {
438
+ const from = src.path ? join5(staging, src.path) : staging;
439
+ if (!existsSync(join5(from, "SKILL.md"))) {
253
440
  throw new Error(`${src.repo}${src.path ? `/${src.path}` : ""} has no SKILL.md`);
254
441
  }
255
442
  await rm(dest, { recursive: true, force: true });
256
443
  await mkdir(dest, { recursive: true });
257
444
  await cp(from, dest, { recursive: true });
258
445
  const record = { ...src, sha, installedAt: (deps.now ?? Date.now)() };
259
- await writeFile(join3(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
446
+ await writeFile(join5(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
260
447
  `, "utf8");
261
448
  return { name: src.name, sha, changed: true };
262
449
  } finally {
@@ -546,8 +733,8 @@ var ProposalQueue = class {
546
733
  };
547
734
 
548
735
  // src/speckit/templates.ts
549
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
550
- import { join as join4, dirname as dirname2 } from "path";
736
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
737
+ import { join as join6, dirname as dirname3 } from "path";
551
738
  var TEMPLATE_FILES = {
552
739
  spec: "spec-template.md",
553
740
  plan: "plan-template.md",
@@ -570,10 +757,10 @@ async function loadSpecKit(opts) {
570
757
  );
571
758
  }
572
759
  const fetchFn = opts.fetch ?? globalThis.fetch;
573
- const cacheDir = join4(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
760
+ const cacheDir = join6(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
574
761
  const get = async (relPath) => {
575
- const cachePath = join4(cacheDir, relPath);
576
- if (existsSync2(cachePath)) return readFileSync(cachePath, "utf8");
762
+ const cachePath = join6(cacheDir, relPath);
763
+ if (existsSync2(cachePath)) return readFileSync2(cachePath, "utf8");
577
764
  const url = `${RAW_BASE}/${opts.version}/templates/${relPath}`;
578
765
  const res = await fetchFn(url);
579
766
  if (!res.ok) {
@@ -583,8 +770,8 @@ Check your network or set specKit.version to a valid tag.`
583
770
  );
584
771
  }
585
772
  const text = await res.text();
586
- mkdirSync(dirname2(cachePath), { recursive: true });
587
- writeFileSync(cachePath, text, "utf8");
773
+ mkdirSync2(dirname3(cachePath), { recursive: true });
774
+ writeFileSync2(cachePath, text, "utf8");
588
775
  return text;
589
776
  };
590
777
  const templates = {};
@@ -600,7 +787,7 @@ Check your network or set specKit.version to a valid tag.`
600
787
 
601
788
  // src/wiring.ts
602
789
  import { homedir } from "os";
603
- import { join as join5 } from "path";
790
+ import { join as join7 } from "path";
604
791
  async function buildJobDeps(opts) {
605
792
  const { config } = opts;
606
793
  const roles = {};
@@ -612,6 +799,10 @@ async function buildJobDeps(opts) {
612
799
  roles[name] = { ...cfg ?? { models: sessionChain }, ...skills.length ? { skills } : { skills: [] } };
613
800
  }
614
801
  const roleRegistry = new RoleRegistry(roles, DEFAULT_PROMPTS, opts.skillRegistry);
802
+ if (opts.accounts) {
803
+ const pool = opts.accounts;
804
+ roleRegistry.setSourceWeights(() => ({ claude: pool.count("claude"), codex: pool.count("codex") }));
805
+ }
615
806
  const fillModels = (r) => ({
616
807
  ...r,
617
808
  models: r.models.length > 0 ? r.models : config.roles[r.name]?.models ?? sessionChain
@@ -637,10 +828,27 @@ async function buildJobDeps(opts) {
637
828
  const queue = new ProposalQueue();
638
829
  let kitPromise;
639
830
  const specKit = () => kitPromise ??= loadSpecKit({ version: config.specKit.version, home: opts.home, fetch: opts.fetch });
640
- const fitness = new RoleFitness(join5(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
831
+ const fitness = new RoleFitness(join7(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
641
832
  roleRegistry.setFitness(fitness);
642
833
  return {
643
834
  provider: opts.provider,
835
+ /**
836
+ * Implementers run as the CLI, because with a CLI provider nothing else can write.
837
+ *
838
+ * A CLI answers with TEXT. It never emits a tool call — it ran its tools itself, in its own process — so
839
+ * an implementer driven through this process's loop would receive prose, execute nothing, and report
840
+ * `no_changes` for every task on the board. This is not a choice between two working paths.
841
+ *
842
+ * It belongs HERE and not inside `runJob`, because it is a property of the provider that was chosen. Set
843
+ * in the pipeline it would override every caller that injected one: the job tests hand in a mock and
844
+ * would have spawned the real binary instead, which is exactly how it was first written and exactly how
845
+ * the tests caught it.
846
+ *
847
+ * `claude` is the fallback, not the decision — `cliFor` reads each role's own chain head, so a role on a
848
+ * `gpt-*` model runs Codex.
849
+ */
850
+ ...opts.delegateTo === null ? {} : { delegateTo: opts.delegateTo ?? "claude" },
851
+ ...opts.accounts ? { accounts: opts.accounts } : {},
644
852
  roleRegistry,
645
853
  fitness,
646
854
  skillRegistry: opts.skillRegistry,
@@ -665,7 +873,30 @@ async function buildJobDeps(opts) {
665
873
  timings: new Timings(),
666
874
  rounds: 3,
667
875
  maxParallel: opts.config.maxParallel ?? MAX_PARALLEL_TASKS,
668
- askHuman: opts.askHuman
876
+ askHuman: opts.askHuman,
877
+ /**
878
+ * Cutting a stuck card up, asked of the role that owns breakdowns in the first place.
879
+ *
880
+ * Read-only tools: this is a planning question about a card that already exists, and a splitter with a
881
+ * writer's toolset would be tempted to start doing the work it is describing.
882
+ */
883
+ splitCard: async (card) => {
884
+ const resolved = roleRegistry.resolve("project-manager");
885
+ return proposeSplit({
886
+ provider: opts.provider,
887
+ ...resolved,
888
+ tools: new ToolRegistry(),
889
+ // a planning question about a card that exists; it needs no tools
890
+ messages: [],
891
+ permission,
892
+ approve: opts.approve,
893
+ cwd: process.cwd(),
894
+ signal: opts.signal,
895
+ // It reads a card and answers; anything longer is exploring, which is not what was asked.
896
+ maxTurns: 3,
897
+ perAttemptMs: SHORT_CALL_MS
898
+ }, card);
899
+ }
669
900
  };
670
901
  }
671
902
 
@@ -717,6 +948,7 @@ async function* stream(inner, tel, req, signal) {
717
948
  let promptTokens;
718
949
  let completionTokens;
719
950
  let cachedTokens;
951
+ let cacheWriteTokens;
720
952
  let finish;
721
953
  let failure;
722
954
  let toolCalls = 0;
@@ -727,6 +959,7 @@ async function* stream(inner, tel, req, signal) {
727
959
  promptTokens = ev.promptTokens;
728
960
  completionTokens = ev.completionTokens;
729
961
  cachedTokens = ev.cachedTokens;
962
+ cacheWriteTokens = ev.cacheWriteTokens;
730
963
  } else if (ev.type === "tool-call") toolCalls++;
731
964
  else if (ev.type === "text-delta") textChars += ev.text.length;
732
965
  else if (ev.type === "done") finish = ev.finishReason;
@@ -745,6 +978,14 @@ async function* stream(inner, tel, req, signal) {
745
978
  "gen_ai.usage.input_tokens": promptTokens,
746
979
  "gen_ai.usage.output_tokens": completionTokens,
747
980
  "gen_ai.usage.cached_tokens": cachedTokens,
981
+ /**
982
+ * What the cache COST, beside what it saved.
983
+ *
984
+ * A read is billed at a fraction and a write at a premium, so a run that reports only reads reads as
985
+ * pure profit — the first turn of every agent, which fills the cache, would cost nothing on paper.
986
+ * The true prompt spend is fresh + read + write, and this is the third.
987
+ */
988
+ "gen_ai.usage.cache_write_tokens": cacheWriteTokens,
748
989
  "gen_ai.response.finish_reason": finish,
749
990
  "hc.duration_ms": Date.now() - started,
750
991
  "hc.tools_requested": toolCalls,
@@ -779,10 +1020,10 @@ function redactRecord(record) {
779
1020
  }
780
1021
 
781
1022
  // src/obs/sink.ts
782
- import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
783
- import { join as join6 } from "path";
1023
+ import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1024
+ import { join as join8 } from "path";
784
1025
  function telemetryDir(home) {
785
- return join6(home, ".horsecode", "telemetry");
1026
+ return join8(home, ".horsecode", "telemetry");
786
1027
  }
787
1028
  var MAX_QUEUE = 1e4;
788
1029
  var FileSink = class {
@@ -791,9 +1032,9 @@ var FileSink = class {
791
1032
  dropped = 0;
792
1033
  path;
793
1034
  constructor(home, runId) {
794
- this.path = join6(telemetryDir(home), `${runId}.jsonl`);
1035
+ this.path = join8(telemetryDir(home), `${runId}.jsonl`);
795
1036
  try {
796
- mkdirSync2(telemetryDir(home), { recursive: true });
1037
+ mkdirSync3(telemetryDir(home), { recursive: true });
797
1038
  this.stream = createWriteStream(this.path, { flags: "a" });
798
1039
  this.stream.on("error", () => {
799
1040
  this.stream = void 0;
@@ -825,16 +1066,18 @@ var FileSink = class {
825
1066
  return this.dropped;
826
1067
  }
827
1068
  /**
828
- * A stream that failed to open never calls `end`'s callback, and this awaited it forever.
1069
+ * `flush()` is awaited as a run finishes, so whatever happens here decides whether the run can end.
829
1070
  *
830
- * `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick. So
1071
+ * `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick, so
831
1072
  * the constructor's `catch` does not run, `stream` is set, and whether the error handler has cleared it by
832
- * the time anything flushes is a race with the event loop. Lose that race and `end(cb)` is called on a
833
- * broken stream, the callback is never invoked, and the promise has nothing left to resolve it.
1073
+ * the time anything flushes is a race with the event loop. Losing that race means `end(cb)` is called on a
1074
+ * stream that never opened.
834
1075
  *
835
- * The observer then outlives what it observes: `flush()` is awaited as a run finishes, so a machine that
836
- * cannot write the log cannot finish the run either. The `error` listener is the second exit, and either
837
- * one is enough.
1076
+ * The `error` listener is a second exit for that case. It was added believing the callback would otherwise
1077
+ * never fire and hang the run an honest guess that has since been TESTED and did not hold: with the
1078
+ * listener removed, an EISDIR stream still resolves (test/obs/telemetry.test.ts). It stays because it
1079
+ * costs one line and closes a path nothing else covers, not because a hang was ever reproduced through it.
1080
+ * The 3h42m CI hang this was written during had a different cause, in the tests themselves.
838
1081
  */
839
1082
  async flush() {
840
1083
  const s = this.stream;
@@ -848,13 +1091,13 @@ var FileSink = class {
848
1091
  };
849
1092
 
850
1093
  // src/cli-info.ts
851
- import { readFileSync as readFileSync2 } from "fs";
852
- import { dirname as dirname3, join as join7 } from "path";
1094
+ import { readFileSync as readFileSync3 } from "fs";
1095
+ import { dirname as dirname4, join as join9 } from "path";
853
1096
  import { fileURLToPath as fileURLToPath2 } from "url";
854
1097
  function hcodeVersion() {
855
1098
  try {
856
- const here = dirname3(fileURLToPath2(import.meta.url));
857
- const pkg = JSON.parse(readFileSync2(join7(here, "..", "package.json"), "utf8"));
1099
+ const here = dirname4(fileURLToPath2(import.meta.url));
1100
+ const pkg = JSON.parse(readFileSync3(join9(here, "..", "package.json"), "utf8"));
858
1101
  return pkg.version ?? "unknown";
859
1102
  } catch {
860
1103
  return "unknown";
@@ -881,6 +1124,7 @@ function usage() {
881
1124
  " hcode start the REPL",
882
1125
  ' hcode "<request>" run one request to completion, then report where the work landed',
883
1126
  " hcode init write .horsecode/ for this project and ask for the essentials",
1127
+ " hcode add-provider <cli> connect one more claude or codex subscription, through that CLI's own sign-in",
884
1128
  "",
885
1129
  "OPTIONS",
886
1130
  " -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
@@ -943,7 +1187,7 @@ function parseArgs(argv) {
943
1187
  };
944
1188
  }
945
1189
  function whereItLanded(path) {
946
- const base = sessionBase(dirname4(path));
1190
+ const base = sessionBase(dirname5(path));
947
1191
  if (base === void 0) {
948
1192
  return "Written directly in your working tree: no branch, no worktree, nothing to merge. Review it and commit it when you are happy with it.";
949
1193
  }
@@ -959,12 +1203,8 @@ function whereItLanded(path) {
959
1203
  return `Written on ${branch ? `branch \`${branch}\`` : "its own branch"}, in the worktree at \`${base}\`. Review it there, then merge it in.`;
960
1204
  }
961
1205
  function describeDelivery(d) {
962
- if (d.mergedInto) {
963
- return `Merged into \`${d.mergedInto}\` \u2014 the files are in your working copy.`;
964
- }
965
1206
  return [
966
1207
  `**The work is on branch \`${d.branch}\`** \u2014 not in your working copy yet.`,
967
- d.notMerged ? `Not merged: ${d.notMerged}.` : "",
968
1208
  "",
969
1209
  "To bring it in:",
970
1210
  "```",
@@ -973,6 +1213,37 @@ function describeDelivery(d) {
973
1213
  `Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
974
1214
  ].filter((l) => l !== void 0).join("\n");
975
1215
  }
1216
+ function describeOutcome(w) {
1217
+ if (w.status === "completed") return w.pr ? `PR: ${w.pr.url}` : "all tasks merged";
1218
+ const failed = w.failed.length;
1219
+ const blocked = w.skipped.length;
1220
+ const total = w.waves.flat().length || failed + blocked;
1221
+ return describeTally({ merged: Math.max(0, total - failed - blocked), failed, blocked, unfinished: 0 });
1222
+ }
1223
+ function describeTraceFailures(failed) {
1224
+ if (!failed.length) return "";
1225
+ const byError = /* @__PURE__ */ new Map();
1226
+ for (const f of failed) byError.set(f.error, [...byError.get(f.error) ?? [], f.file]);
1227
+ if (byError.size === 1) {
1228
+ const [error, files] = [...byError][0];
1229
+ const shown = files.slice(0, 3).map((f) => `- \`${f}\``);
1230
+ if (files.length > shown.length) shown.push(`- \u2026and ${files.length - shown.length} more`);
1231
+ return `\u26A0\uFE0F ${failed.length} failed, all for the same reason \u2014 ${error}
1232
+ ${shown.join("\n")}`;
1233
+ }
1234
+ const rows = [...byError].sort((a, b) => b[1].length - a[1].length).slice(0, 5).map(([error, files]) => `- ${files.length}\xD7 ${error} (e.g. \`${files[0]}\`)`);
1235
+ const rest = byError.size - rows.length;
1236
+ if (rest > 0) rows.push(`- \u2026and ${rest} further cause(s)`);
1237
+ return `\u26A0\uFE0F ${failed.length} failed:
1238
+ ${rows.join("\n")}`;
1239
+ }
1240
+ async function poolWithConfigured(catalog, configured, probe) {
1241
+ const known = new Set(catalog);
1242
+ const orphaned = configured.filter((m) => !known.has(m));
1243
+ if (!orphaned.length) return catalog;
1244
+ const alive = await Promise.all(orphaned.map(async (m) => await probe(m) ? m : void 0));
1245
+ return [...catalog, ...alive.filter((m) => m !== void 0)];
1246
+ }
976
1247
  function renderResult(res) {
977
1248
  if (res.kind === "chat") return stripThinking(res.response);
978
1249
  if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
@@ -984,7 +1255,7 @@ function renderResult(res) {
984
1255
 
985
1256
  _${whereItLanded(res.path)}_` : `The constitution phase finished without writing \`${res.path}\` \u2014 nothing was changed.`;
986
1257
  }
987
- const outcome = res.wave.status === "completed" ? res.wave.pr ? `PR: ${res.wave.pr.url}` : "all tasks completed" : `Partial: ${res.wave.failed.length} failed, ${res.wave.skipped.length} skipped`;
1258
+ const outcome = describeOutcome(res.wave);
988
1259
  const rev = res.revision ? `
989
1260
  revision: ${res.revision.status}` : "";
990
1261
  return `${stripThinking(res.report)}
@@ -1039,14 +1310,14 @@ async function main(argv) {
1039
1310
  read: read2,
1040
1311
  readFile: (p) => {
1041
1312
  try {
1042
- return readFileSync3(p, "utf8");
1313
+ return readFileSync4(p, "utf8");
1043
1314
  } catch {
1044
1315
  return void 0;
1045
1316
  }
1046
1317
  },
1047
1318
  writeFile: (p, c) => {
1048
- mkdirSync3(dirname4(p), { recursive: true });
1049
- writeFileSync2(p, c);
1319
+ mkdirSync4(dirname5(p), { recursive: true });
1320
+ writeFileSync3(p, c);
1050
1321
  },
1051
1322
  home: process.env.HOME ?? "",
1052
1323
  log: (s) => console.log(s)
@@ -1056,6 +1327,34 @@ async function main(argv) {
1056
1327
  }
1057
1328
  return;
1058
1329
  }
1330
+ if (argv[0] === "add-provider") {
1331
+ const kind = argv[1];
1332
+ if (kind !== "claude" && kind !== "codex") {
1333
+ console.error(`add-provider needs a CLI to connect: \`hcode add-provider claude\` or \`hcode add-provider codex\`${kind ? ` (got "${kind}")` : ""}`);
1334
+ process.exitCode = 1;
1335
+ return;
1336
+ }
1337
+ const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
1338
+ process.exitCode = addProvider(kind, {
1339
+ home: process.env.HOME ?? "",
1340
+ readConfig: () => {
1341
+ try {
1342
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
1343
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1344
+ } catch {
1345
+ return {};
1346
+ }
1347
+ },
1348
+ writeConfig: (c) => {
1349
+ mkdirSync4(dirname5(path), { recursive: true });
1350
+ writeFileSync3(path, JSON.stringify(c, null, 2) + "\n");
1351
+ },
1352
+ login: runLogin,
1353
+ check: checkProfile,
1354
+ log: (line) => console.log(line)
1355
+ });
1356
+ return;
1357
+ }
1059
1358
  const cwd = process.cwd();
1060
1359
  const config = loadConfig({
1061
1360
  cwd,
@@ -1063,7 +1362,7 @@ async function main(argv) {
1063
1362
  env: process.env,
1064
1363
  readFile: (p) => {
1065
1364
  try {
1066
- return readFileSync3(p, "utf8");
1365
+ return readFileSync4(p, "utf8");
1067
1366
  } catch {
1068
1367
  return void 0;
1069
1368
  }
@@ -1113,12 +1412,27 @@ async function main(argv) {
1113
1412
  process.once("SIGTERM", flush);
1114
1413
  }
1115
1414
  const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
1116
- const raw = new OmniRouteProvider({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1415
+ const ambientLogins = ["claude", "codex"].filter((k) => !config.accounts.some((a) => a.kind === k)).map((kind) => ({ kind, status: checkProfile(kind) }));
1416
+ const accounts = new AccountPool(
1417
+ [
1418
+ ...config.accounts,
1419
+ ...ambientLogins.filter((a) => a.status.loggedIn).map(({ kind, status }) => ({
1420
+ kind,
1421
+ name: status.email ?? `${kind}-default`,
1422
+ ...status.email ? { email: status.email } : {},
1423
+ ...status.plan ? { plan: status.plan } : {}
1424
+ }))
1425
+ ],
1426
+ fileUsageStore(home)
1427
+ );
1428
+ const notSignedIn = ambientLogins.filter((a) => !a.status.loggedIn).map((a) => a.kind);
1429
+ const accountsNote = () => accountsLine(accounts.usage(), notSignedIn);
1430
+ const raw = new CliProvider({ readOnly: false, accounts });
1117
1431
  const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
1118
1432
  const skillRegistry = new SkillRegistry();
1119
1433
  await registerBuiltinSkills(skillRegistry);
1120
1434
  await skillRegistry.loadFromDir(externalSkillsDir(home));
1121
- const skillsDir = join8(cwd, ".horsecode", "skills");
1435
+ const skillsDir = join10(cwd, ".horsecode", "skills");
1122
1436
  if (existsSync3(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
1123
1437
  const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
1124
1438
  if (worktreeHome !== cwd) {
@@ -1140,6 +1454,7 @@ async function main(argv) {
1140
1454
  manager,
1141
1455
  prAdapter,
1142
1456
  rules,
1457
+ accounts,
1143
1458
  // Autonomous by default: a task that exhausts the escalation ladder is auto-retried (then abandoned) so
1144
1459
  // an unattended run finishes on its own instead of blocking on an interactive per-failure human prompt.
1145
1460
  askHuman: autonomousAskHuman(),
@@ -1150,25 +1465,12 @@ async function main(argv) {
1150
1465
  const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
1151
1466
  if (!args.prompt) {
1152
1467
  if (useTui) {
1153
- const { runTuiRepl } = await import("./app-UGFQKMLX.js");
1154
- const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-5URG7C4J.js");
1468
+ const { runTuiRepl } = await import("./app-2GPGCDX6.js");
1469
+ const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-G2Z6XC3O.js");
1155
1470
  const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
1156
1471
  const manualSources = config.modelSources.length > 0;
1157
1472
  const sourcesRef = { current: manualSources ? config.modelSources : loadSourceCache(home, config.baseUrl) ?? [] };
1158
1473
  const configuredModels = () => [...new Set(Object.values(config.roles).flatMap((r) => r.models ?? []))].filter((m) => m && m !== "default");
1159
- const listModels = async () => {
1160
- const catalog = await listOmniRouteModels({ baseUrl: config.baseUrl, apiKey: config.apiKey, sources: sourcesRef.current });
1161
- const known = new Set(catalog);
1162
- return [...catalog, ...configuredModels().filter((m) => !known.has(m))];
1163
- };
1164
- const refreshSources = async () => {
1165
- const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1166
- const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
1167
- sourcesRef.current = found;
1168
- saveSourceCache(home, config.baseUrl, found);
1169
- return found;
1170
- };
1171
- const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
1172
1474
  const probeModel = async (model) => {
1173
1475
  try {
1174
1476
  const res = await fetch(`${config.baseUrl.replace(/\/$/, "")}/api/v1/chat/completions`, {
@@ -1182,6 +1484,16 @@ async function main(argv) {
1182
1484
  return false;
1183
1485
  }
1184
1486
  };
1487
+ const routableModel = makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1488
+ const listModels = async () => cliCatalog();
1489
+ const refreshSources = async () => {
1490
+ const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1491
+ const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
1492
+ sourcesRef.current = found;
1493
+ saveSourceCache(home, config.baseUrl, found);
1494
+ return found;
1495
+ };
1496
+ const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
1185
1497
  const effectiveRoleSkills = (skill) => Object.keys(DEFAULT_ROLE_SKILLS).concat(Object.keys(config.roles)).filter((r, i, a) => a.indexOf(r) === i).filter((r) => (config.roles[r]?.skills ?? DEFAULT_ROLE_SKILLS[r] ?? []).includes(skill));
1186
1498
  const listSkills = () => skillRegistry.list().map((s) => ({
1187
1499
  ...s,
@@ -1204,7 +1516,7 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
1204
1516
  _Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
1205
1517
  };
1206
1518
  const reloadProjectSkills = async () => {
1207
- const dir = join8(cwd, ".horsecode", "skills");
1519
+ const dir = join10(cwd, ".horsecode", "skills");
1208
1520
  if (existsSync3(dir)) await skillRegistry.loadFromDir(dir);
1209
1521
  };
1210
1522
  const updateSkills = async () => {
@@ -1284,19 +1596,17 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
1284
1596
  if (res.upToDate) bits.push(`${res.upToDate} already current`);
1285
1597
  if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
1286
1598
  if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
1287
- if (res.failed.length) {
1288
- bits.push(`
1599
+ const failures = describeTraceFailures(res.failed);
1600
+ return `${bits.join(" \xB7 ")}${failures ? `
1289
1601
 
1290
- \u26A0\uFE0F ${res.failed.length} failed:
1291
- ${res.failed.slice(0, 5).map((f) => `- \`${f.file}\` \u2014 ${f.error}`).join("\n")}`);
1292
- }
1293
- return `${bits.join(" \xB7 ")}
1602
+ ${failures}` : ""}
1294
1603
 
1295
1604
  _Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
1296
1605
  };
1297
1606
  await runTuiRepl({
1298
1607
  buildDeps,
1299
1608
  memStore,
1609
+ accountsNote,
1300
1610
  listSkills,
1301
1611
  updateSkills,
1302
1612
  addSkill,
@@ -1316,7 +1626,8 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1316
1626
  ...sink ? { telemetryPath: sink.path } : {},
1317
1627
  refreshSources,
1318
1628
  sourcesInfo,
1319
- probeModel
1629
+ probeModel,
1630
+ routableModel
1320
1631
  });
1321
1632
  return;
1322
1633
  }
@@ -1333,7 +1644,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1333
1644
  ...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
1334
1645
  };
1335
1646
  if (useTui) {
1336
- const { runTui } = await import("./app-UGFQKMLX.js");
1647
+ const { runTui } = await import("./app-2GPGCDX6.js");
1337
1648
  const res = await runTui({ buildDeps, job });
1338
1649
  console.log(renderResult(res));
1339
1650
  return;
@@ -1381,8 +1692,11 @@ if (isMainModule()) {
1381
1692
  }
1382
1693
  export {
1383
1694
  describeDelivery,
1695
+ describeOutcome,
1696
+ describeTraceFailures,
1384
1697
  main,
1385
1698
  parseArgs,
1699
+ poolWithConfigured,
1386
1700
  renderResult,
1387
1701
  shouldUseTui,
1388
1702
  whereItLanded