@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
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-K2VERI5Q.js";
18
- import {
19
- listOmniRouteModels
20
- } from "./chunk-O74BDQKS.js";
21
- import "./chunk-3UYA3KUG.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-RPVAIS3P.js";
40
+ } from "./chunk-7JMWPTJ5.js";
38
41
  import "./chunk-QF4MP6BS.js";
39
- import "./chunk-EQX7BQYN.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-DRZSUQ7Q.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-VPAWRRHL.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;
@@ -850,13 +1091,13 @@ var FileSink = class {
850
1091
  };
851
1092
 
852
1093
  // src/cli-info.ts
853
- import { readFileSync as readFileSync2 } from "fs";
854
- 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";
855
1096
  import { fileURLToPath as fileURLToPath2 } from "url";
856
1097
  function hcodeVersion() {
857
1098
  try {
858
- const here = dirname3(fileURLToPath2(import.meta.url));
859
- 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"));
860
1101
  return pkg.version ?? "unknown";
861
1102
  } catch {
862
1103
  return "unknown";
@@ -883,6 +1124,7 @@ function usage() {
883
1124
  " hcode start the REPL",
884
1125
  ' hcode "<request>" run one request to completion, then report where the work landed',
885
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",
886
1128
  "",
887
1129
  "OPTIONS",
888
1130
  " -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
@@ -945,7 +1187,7 @@ function parseArgs(argv) {
945
1187
  };
946
1188
  }
947
1189
  function whereItLanded(path) {
948
- const base = sessionBase(dirname4(path));
1190
+ const base = sessionBase(dirname5(path));
949
1191
  if (base === void 0) {
950
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.";
951
1193
  }
@@ -971,18 +1213,12 @@ function describeDelivery(d) {
971
1213
  `Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
972
1214
  ].filter((l) => l !== void 0).join("\n");
973
1215
  }
974
- var DELIVERED_SHARE = 0.5;
975
1216
  function describeOutcome(w) {
976
1217
  if (w.status === "completed") return w.pr ? `PR: ${w.pr.url}` : "all tasks merged";
977
- const stuck = w.failed.length + w.skipped.length;
978
- const total = w.waves.flat().length || stuck;
979
- const merged = Math.max(0, total - stuck);
980
- const parts = [
981
- w.failed.length ? `${w.failed.length} failed` : "",
982
- w.skipped.length ? `${w.skipped.length} blocked behind them` : ""
983
- ].filter(Boolean).join(", ");
984
- const head = `${merged} of ${total} tasks merged`;
985
- return merged / total < DELIVERED_SHARE ? `\u26A0\uFE0F ${head} \u2014 ${parts}. Most of the plan did not land; the feature is not built.` : `${head} \u2014 ${parts}.`;
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 });
986
1222
  }
987
1223
  function describeTraceFailures(failed) {
988
1224
  if (!failed.length) return "";
@@ -1001,6 +1237,13 @@ ${shown.join("\n")}`;
1001
1237
  return `\u26A0\uFE0F ${failed.length} failed:
1002
1238
  ${rows.join("\n")}`;
1003
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
+ }
1004
1247
  function renderResult(res) {
1005
1248
  if (res.kind === "chat") return stripThinking(res.response);
1006
1249
  if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
@@ -1067,14 +1310,14 @@ async function main(argv) {
1067
1310
  read: read2,
1068
1311
  readFile: (p) => {
1069
1312
  try {
1070
- return readFileSync3(p, "utf8");
1313
+ return readFileSync4(p, "utf8");
1071
1314
  } catch {
1072
1315
  return void 0;
1073
1316
  }
1074
1317
  },
1075
1318
  writeFile: (p, c) => {
1076
- mkdirSync3(dirname4(p), { recursive: true });
1077
- writeFileSync2(p, c);
1319
+ mkdirSync4(dirname5(p), { recursive: true });
1320
+ writeFileSync3(p, c);
1078
1321
  },
1079
1322
  home: process.env.HOME ?? "",
1080
1323
  log: (s) => console.log(s)
@@ -1084,6 +1327,34 @@ async function main(argv) {
1084
1327
  }
1085
1328
  return;
1086
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
+ }
1087
1358
  const cwd = process.cwd();
1088
1359
  const config = loadConfig({
1089
1360
  cwd,
@@ -1091,7 +1362,7 @@ async function main(argv) {
1091
1362
  env: process.env,
1092
1363
  readFile: (p) => {
1093
1364
  try {
1094
- return readFileSync3(p, "utf8");
1365
+ return readFileSync4(p, "utf8");
1095
1366
  } catch {
1096
1367
  return void 0;
1097
1368
  }
@@ -1141,12 +1412,27 @@ async function main(argv) {
1141
1412
  process.once("SIGTERM", flush);
1142
1413
  }
1143
1414
  const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
1144
- 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 });
1145
1431
  const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
1146
1432
  const skillRegistry = new SkillRegistry();
1147
1433
  await registerBuiltinSkills(skillRegistry);
1148
1434
  await skillRegistry.loadFromDir(externalSkillsDir(home));
1149
- const skillsDir = join8(cwd, ".horsecode", "skills");
1435
+ const skillsDir = join10(cwd, ".horsecode", "skills");
1150
1436
  if (existsSync3(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
1151
1437
  const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
1152
1438
  if (worktreeHome !== cwd) {
@@ -1168,6 +1454,7 @@ async function main(argv) {
1168
1454
  manager,
1169
1455
  prAdapter,
1170
1456
  rules,
1457
+ accounts,
1171
1458
  // Autonomous by default: a task that exhausts the escalation ladder is auto-retried (then abandoned) so
1172
1459
  // an unattended run finishes on its own instead of blocking on an interactive per-failure human prompt.
1173
1460
  askHuman: autonomousAskHuman(),
@@ -1178,25 +1465,12 @@ async function main(argv) {
1178
1465
  const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
1179
1466
  if (!args.prompt) {
1180
1467
  if (useTui) {
1181
- const { runTuiRepl } = await import("./app-LAJN3TWC.js");
1182
- 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");
1183
1470
  const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
1184
1471
  const manualSources = config.modelSources.length > 0;
1185
1472
  const sourcesRef = { current: manualSources ? config.modelSources : loadSourceCache(home, config.baseUrl) ?? [] };
1186
1473
  const configuredModels = () => [...new Set(Object.values(config.roles).flatMap((r) => r.models ?? []))].filter((m) => m && m !== "default");
1187
- const listModels = async () => {
1188
- const catalog = await listOmniRouteModels({ baseUrl: config.baseUrl, apiKey: config.apiKey, sources: sourcesRef.current });
1189
- const known = new Set(catalog);
1190
- return [...catalog, ...configuredModels().filter((m) => !known.has(m))];
1191
- };
1192
- const refreshSources = async () => {
1193
- const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
1194
- const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
1195
- sourcesRef.current = found;
1196
- saveSourceCache(home, config.baseUrl, found);
1197
- return found;
1198
- };
1199
- const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
1200
1474
  const probeModel = async (model) => {
1201
1475
  try {
1202
1476
  const res = await fetch(`${config.baseUrl.replace(/\/$/, "")}/api/v1/chat/completions`, {
@@ -1210,6 +1484,16 @@ async function main(argv) {
1210
1484
  return false;
1211
1485
  }
1212
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 });
1213
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));
1214
1498
  const listSkills = () => skillRegistry.list().map((s) => ({
1215
1499
  ...s,
@@ -1232,7 +1516,7 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
1232
1516
  _Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
1233
1517
  };
1234
1518
  const reloadProjectSkills = async () => {
1235
- const dir = join8(cwd, ".horsecode", "skills");
1519
+ const dir = join10(cwd, ".horsecode", "skills");
1236
1520
  if (existsSync3(dir)) await skillRegistry.loadFromDir(dir);
1237
1521
  };
1238
1522
  const updateSkills = async () => {
@@ -1322,6 +1606,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1322
1606
  await runTuiRepl({
1323
1607
  buildDeps,
1324
1608
  memStore,
1609
+ accountsNote,
1325
1610
  listSkills,
1326
1611
  updateSkills,
1327
1612
  addSkill,
@@ -1341,7 +1626,8 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1341
1626
  ...sink ? { telemetryPath: sink.path } : {},
1342
1627
  refreshSources,
1343
1628
  sourcesInfo,
1344
- probeModel
1629
+ probeModel,
1630
+ routableModel
1345
1631
  });
1346
1632
  return;
1347
1633
  }
@@ -1358,7 +1644,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1358
1644
  ...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
1359
1645
  };
1360
1646
  if (useTui) {
1361
- const { runTui } = await import("./app-LAJN3TWC.js");
1647
+ const { runTui } = await import("./app-2GPGCDX6.js");
1362
1648
  const res = await runTui({ buildDeps, job });
1363
1649
  console.log(renderResult(res));
1364
1650
  return;
@@ -1405,12 +1691,12 @@ if (isMainModule()) {
1405
1691
  });
1406
1692
  }
1407
1693
  export {
1408
- DELIVERED_SHARE,
1409
1694
  describeDelivery,
1410
1695
  describeOutcome,
1411
1696
  describeTraceFailures,
1412
1697
  main,
1413
1698
  parseArgs,
1699
+ poolWithConfigured,
1414
1700
  renderResult,
1415
1701
  shouldUseTui,
1416
1702
  whereItLanded