@hizliemre/horse-code 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/{app-2GPGCDX6.js → app-4WN37LZ3.js} +282 -57
  2. package/dist/{chunk-7JMWPTJ5.js → chunk-27F44PBD.js} +221 -1363
  3. package/dist/{chunk-23CLQ2KO.js → chunk-2WXG35EM.js} +19 -15
  4. package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
  5. package/dist/{chunk-5ZV42XGJ.js → chunk-3ACDNDCG.js} +1 -1
  6. package/dist/{chunk-XEGQT5EN.js → chunk-4M6LXNG2.js} +1 -1
  7. package/dist/{chunk-6OSEQOYY.js → chunk-6S4WWQMN.js} +2 -2
  8. package/dist/{chunk-LLL7QWXB.js → chunk-BFIZMM4G.js} +6 -6
  9. package/dist/chunk-CYLPQWIF.js +214 -0
  10. package/dist/chunk-G45RWL7S.js +289 -0
  11. package/dist/{chunk-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
  12. package/dist/{chunk-KKWZBZYK.js → chunk-JR2JLRE3.js} +26 -4
  13. package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
  14. package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
  15. package/dist/chunk-YULQ4URQ.js +1220 -0
  16. package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
  17. package/dist/cli.js +586 -104
  18. package/dist/{fix-JOIXQFVP.js → fix-QCL5AITT.js} +10 -8
  19. package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
  20. package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
  21. package/dist/run-V5ZLZ3LS.js +274 -0
  22. package/dist/{save-skills-X7U3KCPU.js → save-skills-NPKTYNAF.js} +2 -1
  23. package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
  24. package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
  25. package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
  26. package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
  27. package/dist/{verify-6SC4I77M.js → verify-HWZBTK5X.js} +16 -12
  28. package/package.json +1 -1
  29. package/dist/chunk-MRZVA5JB.js +0 -163
package/dist/cli.js CHANGED
@@ -15,31 +15,25 @@ import {
15
15
  restoreTerminal,
16
16
  runJob,
17
17
  sttySane
18
- } from "./chunk-23CLQ2KO.js";
18
+ } from "./chunk-2WXG35EM.js";
19
19
  import "./chunk-M2RKCIGV.js";
20
20
  import {
21
- CODE_TEAM,
22
21
  CliProvider,
23
- DEFAULT_COUNCIL,
24
- DEFAULT_PROMPTS,
25
- DEFAULT_ROLE_SKILLS,
26
- PLAN_TEAM,
27
- REQUIRED_ROLES,
28
22
  RoleFitness,
29
- RoleRegistry,
30
23
  SHORT_CALL_MS,
31
- SPEC_TEAM,
32
24
  WorktreeManager,
33
25
  buildCouncilRegistry,
34
26
  buildTeamRegistry,
35
- checkProfile,
36
- cliCatalog,
37
27
  mainWorktreeRoot,
38
- runLogin,
39
28
  toSlug
40
- } from "./chunk-7JMWPTJ5.js";
29
+ } from "./chunk-27F44PBD.js";
41
30
  import "./chunk-QF4MP6BS.js";
42
- import "./chunk-UGESK765.js";
31
+ import {
32
+ MAX_CHUNK_CHARS,
33
+ discover,
34
+ readFinding
35
+ } from "./chunk-QJYVZPLG.js";
36
+ import "./chunk-UTHLEW5V.js";
43
37
  import "./chunk-ZSQ24YDJ.js";
44
38
  import {
45
39
  parseFrontmatter
@@ -48,53 +42,75 @@ import {
48
42
  UNSET_MODEL,
49
43
  loadConfig,
50
44
  saveSkillSource
51
- } from "./chunk-KKWZBZYK.js";
45
+ } from "./chunk-JR2JLRE3.js";
52
46
  import "./chunk-H2FDGPVW.js";
53
47
  import {
48
+ CODE_TEAM,
49
+ DEFAULT_COUNCIL,
50
+ DEFAULT_PROMPTS,
51
+ DEFAULT_ROLE_SKILLS,
52
+ PLAN_TEAM,
53
+ REQUIRED_ROLES,
54
+ RoleRegistry,
55
+ SPEC_TEAM,
56
+ ZAI_MODELS,
54
57
  buildBrief,
58
+ cliCatalog,
55
59
  describePlan,
60
+ modelsFor,
56
61
  planFor,
57
62
  runTraces,
58
63
  traceableFiles
59
- } from "./chunk-MRZVA5JB.js";
64
+ } from "./chunk-YULQ4URQ.js";
65
+ import {
66
+ CLI_KINDS,
67
+ checkProfile,
68
+ runLogin
69
+ } from "./chunk-G45RWL7S.js";
60
70
  import {
61
71
  defaultGitRunner
62
72
  } from "./chunk-LPQU436C.js";
63
73
  import {
64
74
  InjectionLog,
65
75
  memoryNote
66
- } from "./chunk-LLL7QWXB.js";
76
+ } from "./chunk-BFIZMM4G.js";
67
77
  import "./chunk-63E73TGI.js";
78
+ import {
79
+ ToolRegistry
80
+ } from "./chunk-CYLPQWIF.js";
81
+ import {
82
+ briefStatus
83
+ } from "./chunk-6S4WWQMN.js";
68
84
  import {
69
85
  Telemetry,
70
- ToolRegistry,
71
86
  clearPerfMarks,
72
87
  sampleMemory,
73
88
  setTelemetry,
74
89
  stripThinking,
75
90
  telemetry,
76
91
  writeHeapSnapshot
77
- } from "./chunk-AE36LLL2.js";
78
- import {
79
- briefStatus
80
- } from "./chunk-6OSEQOYY.js";
92
+ } from "./chunk-JLWQCA7B.js";
81
93
  import {
82
94
  discoverTraceRoot,
83
- setTraceRoot
84
- } from "./chunk-KAGKX2YT.js";
95
+ ensureGitignore,
96
+ localOnly,
97
+ setTraceRoot,
98
+ sharedDerived,
99
+ traceRootRel
100
+ } from "./chunk-ZPJP2VH5.js";
85
101
  import {
86
102
  buildProjectGraph,
87
103
  graphStatus,
88
104
  graphifyPython
89
- } from "./chunk-XEGQT5EN.js";
105
+ } from "./chunk-4M6LXNG2.js";
90
106
  import {
91
107
  sessionBase
92
108
  } from "./chunk-6W4UH2BQ.js";
93
109
 
94
110
  // src/cli.ts
95
111
  import { execFileSync } from "child_process";
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";
112
+ import { readFileSync as readFileSync5, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, realpathSync, rmSync, statSync as statSync2 } from "fs";
113
+ import { join as join12, dirname as dirname5 } from "path";
98
114
  import { pathToFileURL } from "url";
99
115
 
100
116
  // src/agents/cli-accounts.ts
@@ -181,6 +197,26 @@ var AccountPool = class {
181
197
  return kind ? this.accounts.filter((a) => a.kind === kind).length : this.accounts.length;
182
198
  }
183
199
  };
200
+ function modelsPanel(usage2, models, kinds, inUse = [], now = Date.now()) {
201
+ const used = new Set(inUse);
202
+ const rows = [];
203
+ for (const kind of kinds) {
204
+ const mine = usage2.filter((u) => u.account.kind === kind);
205
+ const who = mine.length ? mine.map(({ account, reading }) => {
206
+ const name = account.email ?? account.plan ?? account.name;
207
+ return `${name}${reading ? ` \xB7 ${Math.round(reading.spent * 100)}% used ${ageOf(reading.at, now)} ago` : ""}`;
208
+ }).join(" \xB7 ") : "_not connected \u2014 these will fail every call_";
209
+ rows.push(`**${kind}** \u2014 ${who}`);
210
+ rows.push(` ${models(kind).map((m) => `${used.has(m) ? "\u25CF" : "\xB7"} ${m}`).join(" ")}`);
211
+ }
212
+ const idle = kinds.filter((k) => usage2.some((u) => u.account.kind === k)).filter((k) => !models(k).some((m) => used.has(m)));
213
+ const note = idle.length ? `
214
+
215
+ _\u25CF = a role is running it. ${idle.join(" and ")} ${idle.length > 1 ? "are" : "is"} connected but in no chain \u2014 \`/roles adjust\` assigns them._` : "\n\n_\u25CF = a role is running it._";
216
+ return `**Models** \u2014 what your connected subscriptions serve
217
+
218
+ ${rows.join("\n")}${note}`;
219
+ }
184
220
  function ageOf(at, now) {
185
221
  const mins = Math.max(0, Math.round((now - at) / 6e4));
186
222
  if (mins < 60) return `${mins}m`;
@@ -207,9 +243,11 @@ function freshDir(kind, home, taken) {
207
243
  }
208
244
  }
209
245
  function nameFor(kind, status, dir) {
210
- return status.email ?? `${kind}-${dir.split("/").pop() ?? "profile"}`;
246
+ const base = dir.split("/").pop() ?? "profile";
247
+ return status.email ?? (base.startsWith(`${kind}-`) ? base : `${kind}-${base}`);
211
248
  }
212
249
  function withAmbient(kind, existing, ambient) {
250
+ if (kind === "zai") return [...existing];
213
251
  if (existing.some((a) => a.kind === kind)) return [...existing];
214
252
  if (!ambient.loggedIn) return [...existing];
215
253
  return [
@@ -228,11 +266,11 @@ function accountsIn(config) {
228
266
  return raw.filter((a) => {
229
267
  const e = a;
230
268
  if (typeof a !== "object" || a === null) return false;
231
- if (e.kind !== "claude" && e.kind !== "codex") return false;
269
+ if (!CLI_KINDS.includes(e.kind)) return false;
232
270
  return e.configDir === void 0 || typeof e.configDir === "string";
233
271
  });
234
272
  }
235
- function addProvider(kind, io) {
273
+ async function addProvider(kind, io) {
236
274
  const config = io.readConfig();
237
275
  const existing = accountsIn(config);
238
276
  const accounts = withAmbient(kind, existing, io.check(kind));
@@ -242,12 +280,26 @@ function addProvider(kind, io) {
242
280
  io.log(`It stays first in line, so runs keep spending it before anything added now.`);
243
281
  }
244
282
  const dir = freshDir(kind, io.home, accounts);
245
- io.log(`
283
+ if (kind === "zai") {
284
+ io.log(`
285
+ z.ai has no sign-in \u2014 an account is an API key, and the profile at ${dir} will hold it.`);
286
+ io.log(`Take the key from your z.ai dashboard; it is written to that profile alone and nowhere else.
287
+ `);
288
+ } else {
289
+ io.log(`
246
290
  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.
291
+ io.log(`Sign in with the account you want to add \u2014 not the one already connected.
248
292
  `);
249
- const r = io.login(kind, dir);
293
+ }
294
+ const r = await io.login(kind, dir);
250
295
  if (!r.ok) {
296
+ if (kind === "zai") {
297
+ io.log(`
298
+ z.ai did not accept that key${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
299
+ io.log(`The key is checked by making one real call, because \`claude auth status\` reports any token as connected \u2014 valid or not.`);
300
+ io.log(`Nothing needs cleaning up: running this again reuses ${dir}.`);
301
+ return 1;
302
+ }
251
303
  io.log(`
252
304
  ${kind} sign-in did not complete${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
253
305
  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.`);
@@ -277,13 +329,203 @@ That is the account already connected as "${already.name}". Nothing was changed.
277
329
  const mine = [...accounts, entry].filter((a) => a.kind === kind);
278
330
  io.log(`
279
331
  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.`);
332
+ io.log(`${mine.length} ${kind} account${mine.length === 1 ? "" : "s"} now connected. Runs take turns between them.`);
333
+ if (kind === "claude") io.log(`One nearly out of quota steps aside for the others.`);
334
+ else io.log(`${kind} reports no quota figures, so a spent account is discovered by being refused, not before.`);
335
+ if (kind === "zai" && mine.length > 1) {
336
+ io.log(`z.ai reports no account identity, so this cannot be checked: if that key is one already connected, remove the duplicate from ~/.horsecode/config.json.`);
337
+ }
281
338
  return 0;
282
339
  }
283
340
 
341
+ // src/agents/zai-profile.ts
342
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
343
+ import { join as join3 } from "path";
344
+ var ZAI_BASE_URL = "https://api.z.ai/api/anthropic";
345
+ function zaiSettings(token) {
346
+ return { env: { ANTHROPIC_BASE_URL: ZAI_BASE_URL, ANTHROPIC_AUTH_TOKEN: token } };
347
+ }
348
+ function settingsPath(dir) {
349
+ return join3(dir, "settings.json");
350
+ }
351
+ function writeZaiProfile(dir, token) {
352
+ mkdirSync2(dir, { recursive: true, mode: 448 });
353
+ writeFileSync2(settingsPath(dir), JSON.stringify(zaiSettings(token), null, 2) + "\n", { mode: 384 });
354
+ }
355
+ async function verifyZaiKey(token, model, timeoutMs = 3e4) {
356
+ const ctl = new AbortController();
357
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
358
+ try {
359
+ const res = await fetch(`${ZAI_BASE_URL}/v1/messages`, {
360
+ method: "POST",
361
+ signal: ctl.signal,
362
+ headers: {
363
+ "content-type": "application/json",
364
+ // Both spellings: z.ai accepts the bearer form, and `x-api-key` is what the Anthropic wire format
365
+ // names. Sending each costs nothing and removes a guess about which one this endpoint reads.
366
+ authorization: `Bearer ${token}`,
367
+ "x-api-key": token,
368
+ "anthropic-version": "2023-06-01"
369
+ },
370
+ body: JSON.stringify({ model, max_tokens: 16, messages: [{ role: "user", content: "Reply with: ok" }] })
371
+ });
372
+ const body = await res.json().catch(() => void 0);
373
+ if (!res.ok) return { ok: false, error: zaiErrorText(body) ?? `the endpoint answered HTTP ${res.status}` };
374
+ const served = body?.model;
375
+ return { ok: true, ...served ? { served } : {} };
376
+ } catch (e) {
377
+ const aborted = e instanceof Error && e.name === "AbortError";
378
+ return {
379
+ ok: false,
380
+ error: aborted ? `${ZAI_BASE_URL} did not answer within ${Math.round(timeoutMs / 1e3)}s` : e instanceof Error ? e.message : String(e)
381
+ };
382
+ } finally {
383
+ clearTimeout(timer);
384
+ }
385
+ }
386
+ function zaiErrorText(body) {
387
+ const msg = body?.error?.message;
388
+ return typeof msg === "string" && msg.trim() ? msg.trim() : void 0;
389
+ }
390
+
391
+ // src/agents/prompt.ts
392
+ import { spawnSync } from "child_process";
393
+ import { closeSync, openSync, readSync } from "fs";
394
+ function promptLine() {
395
+ return readTty(false);
396
+ }
397
+ function promptSecret() {
398
+ return readTty(true);
399
+ }
400
+ function confirm(question) {
401
+ process.stdout.write(`${question} [y/N] `);
402
+ const answer = promptLine();
403
+ process.stdout.write("\n");
404
+ return /^y(es)?$/i.test((answer ?? "").trim());
405
+ }
406
+ function chooseFrom(question, options) {
407
+ process.stdout.write(`${question}
408
+ `);
409
+ options.forEach((o, i) => process.stdout.write(` ${i + 1}) ${o}
410
+ `));
411
+ process.stdout.write(`Number (anything else cancels): `);
412
+ const line = promptLine();
413
+ process.stdout.write("\n");
414
+ const n = Number((line ?? "").trim());
415
+ return Number.isInteger(n) && n >= 1 && n <= options.length ? n - 1 : void 0;
416
+ }
417
+ function readTty(hidden) {
418
+ let fd;
419
+ try {
420
+ fd = openSync("/dev/tty", "r");
421
+ } catch {
422
+ return void 0;
423
+ }
424
+ const hushed = hidden && spawnSync("stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] }).status === 0;
425
+ try {
426
+ const buf = Buffer.alloc(1);
427
+ let out = "";
428
+ for (; ; ) {
429
+ let n = 0;
430
+ try {
431
+ n = readSync(fd, buf, 0, 1, null);
432
+ } catch {
433
+ break;
434
+ }
435
+ if (n === 0) break;
436
+ const ch = buf.toString("utf8");
437
+ if (ch === "\n" || ch === "\r") break;
438
+ out += ch;
439
+ }
440
+ return out.trim() || void 0;
441
+ } finally {
442
+ if (hushed) spawnSync("stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
443
+ closeSync(fd);
444
+ }
445
+ }
446
+
447
+ // src/agents/remove-provider.ts
448
+ function ownedProfile(home, dir) {
449
+ if (!dir) return false;
450
+ const root = `${home}/.horsecode/profiles/`;
451
+ return dir.startsWith(root) && dir.length > root.length && !dir.slice(root.length).includes("..");
452
+ }
453
+ function describeAccount(a) {
454
+ const who = a.email ?? a.name;
455
+ return `${a.kind} ${who}${a.plan ? ` (${a.plan})` : ""}${a.configDir ? "" : " \u2014 the signed-in default"}`;
456
+ }
457
+ function pickAccount(accounts, kind, name) {
458
+ const mine = accounts.filter((a) => a.kind === kind);
459
+ if (!mine.length) return { error: `no ${kind} account is connected` };
460
+ if (!name) return mine.length === 1 ? { account: mine[0] } : { choices: mine };
461
+ const found = mine.filter((a) => a.name === name || a.email === name);
462
+ if (!found.length) {
463
+ return {
464
+ error: `no ${kind} account called "${name}". Connected:
465
+ ` + mine.map((a) => ` ${a.name}${a.email && a.email !== a.name ? ` (${a.email})` : ""}`).join("\n")
466
+ };
467
+ }
468
+ if (found.length > 1) return { error: `"${name}" matches ${found.length} ${kind} accounts \u2014 remove one by its exact name` };
469
+ return { account: found[0] };
470
+ }
471
+ function removeProvider(kind, name, io) {
472
+ const config = io.readConfig();
473
+ const accounts = accountsIn(config);
474
+ const picked = pickAccount(accounts, kind, name);
475
+ if ("error" in picked) {
476
+ io.log(picked.error);
477
+ const others = accounts.filter((a) => a.kind !== kind);
478
+ if (!accounts.filter((a) => a.kind === kind).length && others.length) {
479
+ io.log(`
480
+ Connected accounts:
481
+ ${others.map((a) => ` ${describeAccount(a)}`).join("\n")}`);
482
+ }
483
+ return { code: 1 };
484
+ }
485
+ let account;
486
+ if ("account" in picked) {
487
+ account = picked.account;
488
+ } else {
489
+ const chosen = io.choose(`Which ${kind} account should be removed?`, picked.choices.map(describeAccount));
490
+ if (chosen === void 0) {
491
+ io.log("Nothing was removed.");
492
+ return { code: 1 };
493
+ }
494
+ account = picked.choices[chosen];
495
+ }
496
+ const kept = accounts.filter((a) => a !== account);
497
+ io.writeConfig({ ...config, accounts: kept });
498
+ if (io.usage) {
499
+ const readings = io.usage.load();
500
+ const { [readingKey(kind, account.name)]: _dropped, ...rest } = readings;
501
+ io.usage.save(rest);
502
+ }
503
+ io.log(`Removed ${describeAccount(account)}.`);
504
+ if (!account.configDir) {
505
+ io.log(`That was the account you are signed into ${kind} with, not a profile horse-code created.`);
506
+ io.log(`It is still signed in, and a run will find it again. To stop using it, sign out of ${kind} itself.`);
507
+ return { code: 0, removed: account };
508
+ }
509
+ if (!ownedProfile(io.home, account.configDir)) {
510
+ io.log(`Its profile at ${account.configDir} was not created by horse-code, so it is left alone.`);
511
+ return { code: 0, removed: account };
512
+ }
513
+ const holds = kind === "zai" ? "the API key" : `the ${kind} sign-in`;
514
+ if (!io.confirm(`Also delete ${account.configDir}, which holds ${holds}?`)) {
515
+ io.log(`Left ${account.configDir} in place. Delete it by hand to remove ${holds}.`);
516
+ return { code: 0, removed: account };
517
+ }
518
+ io.removeDir(account.configDir);
519
+ io.log(`Deleted ${account.configDir}.`);
520
+ return { code: 0, removed: account };
521
+ }
522
+ function removeUsage(given) {
523
+ return `remove-provider needs a CLI to disconnect: ${CLI_KINDS.map((k) => `\`hcode remove-provider ${k}\``).join(", ")}${given ? ` (got "${given}")` : ""}`;
524
+ }
525
+
284
526
  // src/skills/registry.ts
285
527
  import { readdir, readFile } from "fs/promises";
286
- import { join as join3 } from "path";
528
+ import { join as join4 } from "path";
287
529
  var SkillRegistry = class {
288
530
  skills = /* @__PURE__ */ new Map();
289
531
  register(skill) {
@@ -306,7 +548,7 @@ var SkillRegistry = class {
306
548
  if (!e.isDirectory()) continue;
307
549
  let raw;
308
550
  try {
309
- raw = await readFile(join3(dir, e.name, "SKILL.md"), "utf8");
551
+ raw = await readFile(join4(dir, e.name, "SKILL.md"), "utf8");
310
552
  } catch {
311
553
  continue;
312
554
  }
@@ -314,14 +556,14 @@ var SkillRegistry = class {
314
556
  if (!name || !description) {
315
557
  throw new Error(`skill ${e.name}: missing frontmatter (name/description)`);
316
558
  }
317
- this.register({ name, description, content: body, dir: join3(dir, e.name) });
559
+ this.register({ name, description, content: body, dir: join4(dir, e.name) });
318
560
  }
319
561
  }
320
562
  };
321
563
 
322
564
  // src/skills/builtin.ts
323
565
  import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
324
- import { dirname as dirname2, join as join4, resolve } from "path";
566
+ import { dirname as dirname2, join as join5, resolve } from "path";
325
567
  import { fileURLToPath } from "url";
326
568
  function candidates() {
327
569
  const here = dirname2(fileURLToPath(import.meta.url));
@@ -342,14 +584,14 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
342
584
  if (!e.isDirectory()) continue;
343
585
  let raw;
344
586
  try {
345
- raw = await readFile2(join4(dir, e.name, "SKILL.md"), "utf8");
587
+ raw = await readFile2(join5(dir, e.name, "SKILL.md"), "utf8");
346
588
  } catch {
347
589
  continue;
348
590
  }
349
591
  const { parseFrontmatter: parseFrontmatter2 } = await import("./frontmatter-UNIPNLLO.js");
350
592
  const { name, description, body } = parseFrontmatter2(raw);
351
593
  if (!name || !description) continue;
352
- found.push({ name, description, content: body, dir: join4(dir, e.name) });
594
+ found.push({ name, description, content: body, dir: join5(dir, e.name) });
353
595
  }
354
596
  if (!found.length) continue;
355
597
  for (const s of found) registry.register(s);
@@ -364,12 +606,12 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
364
606
  // src/skills/external.ts
365
607
  import { mkdir, rm, readFile as readFile3, writeFile, readdir as readdir3, cp } from "fs/promises";
366
608
  import { existsSync } from "fs";
367
- import { join as join5 } from "path";
609
+ import { join as join6 } from "path";
368
610
  import { spawn } from "child_process";
369
611
  import { tmpdir } from "os";
370
612
  var PROVENANCE = ".horsecode-source.json";
371
613
  function externalSkillsDir(home) {
372
- return join5(home, ".horsecode", "skills");
614
+ return join6(home, ".horsecode", "skills");
373
615
  }
374
616
  function validName(name) {
375
617
  return /^[A-Za-z0-9._-]+$/.test(name) && !name.includes("..");
@@ -413,7 +655,7 @@ async function resolveSha(src, deps) {
413
655
  }
414
656
  async function installedSource(home, name) {
415
657
  try {
416
- return JSON.parse(await readFile3(join5(externalSkillsDir(home), name, PROVENANCE), "utf8"));
658
+ return JSON.parse(await readFile3(join6(externalSkillsDir(home), name, PROVENANCE), "utf8"));
417
659
  } catch {
418
660
  return void 0;
419
661
  }
@@ -423,27 +665,27 @@ async function installSkillSource(home, src, deps = {}) {
423
665
  if (!validRepo(src.repo)) throw new Error(`skill source repo must be "owner/repo": ${src.repo}`);
424
666
  if (src.path?.includes("..")) throw new Error(`skill source path may not contain "..": ${src.path}`);
425
667
  const sha = await resolveSha(src, deps);
426
- const dest = join5(externalSkillsDir(home), src.name);
668
+ const dest = join6(externalSkillsDir(home), src.name);
427
669
  const current = await installedSource(home, src.name);
428
- if (current?.sha === sha && existsSync(join5(dest, "SKILL.md"))) {
670
+ if (current?.sha === sha && existsSync(join6(dest, "SKILL.md"))) {
429
671
  return { name: src.name, sha, changed: false };
430
672
  }
431
- const staging = join5(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
673
+ const staging = join6(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
432
674
  await rm(staging, { recursive: true, force: true });
433
675
  await mkdir(staging, { recursive: true });
434
676
  try {
435
677
  const url = `https://codeload.github.com/${src.repo}/tar.gz/${sha}`;
436
678
  const dl = await run("sh", ["-c", `curl -fsSL --max-time 120 ${JSON.stringify(url)} | tar xz -C ${JSON.stringify(staging)} --strip-components=1`]);
437
679
  if (dl.code !== 0) throw new Error(`download failed for ${src.repo}@${sha.slice(0, 8)}: ${dl.err.trim().slice(0, 200)}`);
438
- const from = src.path ? join5(staging, src.path) : staging;
439
- if (!existsSync(join5(from, "SKILL.md"))) {
680
+ const from = src.path ? join6(staging, src.path) : staging;
681
+ if (!existsSync(join6(from, "SKILL.md"))) {
440
682
  throw new Error(`${src.repo}${src.path ? `/${src.path}` : ""} has no SKILL.md`);
441
683
  }
442
684
  await rm(dest, { recursive: true, force: true });
443
685
  await mkdir(dest, { recursive: true });
444
686
  await cp(from, dest, { recursive: true });
445
687
  const record = { ...src, sha, installedAt: (deps.now ?? Date.now)() };
446
- await writeFile(join5(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
688
+ await writeFile(join6(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
447
689
  `, "utf8");
448
690
  return { name: src.name, sha, changed: true };
449
691
  } finally {
@@ -463,6 +705,137 @@ async function syncSkillSources(home, sources, deps = {}) {
463
705
  return { ok, failed };
464
706
  }
465
707
 
708
+ // src/engine/init-project.ts
709
+ import { existsSync as existsSync2, statSync, readdirSync } from "fs";
710
+ import { join as join7 } from "path";
711
+
712
+ // src/migrate/project-docs.ts
713
+ import { basename } from "path";
714
+ var DERIVED = [
715
+ { re: /^\.horsecode\//, why: "horse-code's own state \u2014 traces describe the code, so rules mined from them would be the code talking back to itself" },
716
+ { re: /^graphify-out\//, why: "generated by the graph build" },
717
+ { re: /(^|\/)(node_modules|vendor|third_party|\.git)\//, why: "not this project's writing" },
718
+ { re: /(^|\/)\.claude\/worktrees\//, why: "a nested checkout of this repository" }
719
+ ];
720
+ var HISTORY = [
721
+ { re: /(^|\/)archive(d)?\//i, why: "an archive of finished work" },
722
+ { re: /\d{4}-\d{2}-\d{2}/, why: "dated \u2014 a record of a moment, not a standing rule" },
723
+ { re: /(^|\/)(HANDOFF|RESUME|CHANGELOG|RELEASE[-_]NOTES)[^/]*$/i, why: "a record of what happened" },
724
+ { re: /(^|\/)chats?\//i, why: "a pasted conversation" },
725
+ { re: /(^|\/)chat\d*\.md$/i, why: "a pasted conversation" }
726
+ ];
727
+ function sortProjectDocs(files, known = []) {
728
+ const seen = new Set(known.map((k) => k.replace(/\\/g, "/")));
729
+ const candidates2 = [];
730
+ const skipped = [];
731
+ for (const f of files) {
732
+ const path = f.path.replace(/\\/g, "/");
733
+ if (!/\.mdx?$/i.test(path)) continue;
734
+ if (seen.has(path) || seen.has(basename(path))) continue;
735
+ const why = DERIVED.find((d) => d.re.test(path))?.why ?? HISTORY.find((h) => h.re.test(path))?.why;
736
+ if (why) skipped.push({ path, bytes: f.bytes, skipped: why });
737
+ else candidates2.push({ path, bytes: f.bytes });
738
+ }
739
+ const bySize = (a, b) => b.bytes - a.bytes;
740
+ return { candidates: candidates2.sort(bySize), skipped: skipped.sort(bySize) };
741
+ }
742
+ function totalBytes(docs) {
743
+ return docs.reduce((n, d) => n + d.bytes, 0);
744
+ }
745
+ function classifyCalls(bytes, chunk) {
746
+ return Math.max(1, Math.ceil(bytes / chunk));
747
+ }
748
+
749
+ // src/engine/init-project.ts
750
+ var UNCLAIMED_FILE_FLOOR = 50;
751
+ function countFiles(dir, cap = UNCLAIMED_FILE_FLOOR * 4) {
752
+ let n = 0;
753
+ const walk = (d) => {
754
+ if (n >= cap) return;
755
+ let entries;
756
+ try {
757
+ entries = readdirSync(d, { withFileTypes: true });
758
+ } catch {
759
+ return;
760
+ }
761
+ for (const e of entries) {
762
+ if (n >= cap) return;
763
+ if (e.isDirectory()) walk(join7(d, e.name));
764
+ else n++;
765
+ }
766
+ };
767
+ walk(dir);
768
+ return n;
769
+ }
770
+ async function initProject(cwd, isIgnored, untracked, knowledge) {
771
+ const changed = await ensureGitignore(cwd);
772
+ const unclaimed = [];
773
+ for (const rel of untracked()) {
774
+ const path = join7(cwd, rel);
775
+ let dir = false;
776
+ try {
777
+ dir = statSync(path).isDirectory();
778
+ } catch {
779
+ continue;
780
+ }
781
+ if (!dir || isIgnored(rel)) continue;
782
+ const files = countFiles(path);
783
+ if (files >= UNCLAIMED_FILE_FLOOR) unclaimed.push({ path: rel, files });
784
+ }
785
+ return {
786
+ changed,
787
+ kept: [`${traceRootRel().replace(/\\/g, "/")}/`, ...sharedDerived()].filter((p) => existsSync2(join7(cwd, p)) || p.endsWith("/")),
788
+ excluded: localOnly(),
789
+ unclaimed: unclaimed.sort((a, b) => b.files - a.files),
790
+ ...knowledge ? { knowledge } : {}
791
+ };
792
+ }
793
+ function hasKnowledge(k) {
794
+ return !!k && (k.named.length > 0 || k.docs.length > 0);
795
+ }
796
+ function size(bytes) {
797
+ return bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
798
+ }
799
+ function describeKnowledge(k, chunk = MAX_CHUNK_CHARS) {
800
+ const lines = ["", "**This project already says things about itself.**", ""];
801
+ if (k.named.length) {
802
+ lines.push(`Files a tool is known to write \u2014 ${size(totalBytes(k.named.map((n) => ({ path: n.label, bytes: n.bytes }))))}:`);
803
+ lines.push(...k.named.map((n) => ` \xB7 \`${n.label}\` (${size(n.bytes)})`));
804
+ }
805
+ if (k.docs.length) {
806
+ const bytes = totalBytes(k.docs);
807
+ lines.push("", `The project's other markdown \u2014 ${k.docs.length} file(s), ${size(bytes)}, about ${classifyCalls(bytes, chunk)} classification call(s):`);
808
+ lines.push(...k.docs.slice(0, 8).map((d) => ` \xB7 \`${d.path}\` (${size(d.bytes)})`));
809
+ if (k.docs.length > 8) lines.push(` \xB7 \u2026and ${k.docs.length - 8} more`);
810
+ }
811
+ if (k.skipped.length) {
812
+ const reasons = /* @__PURE__ */ new Map();
813
+ for (const sk of k.skipped) reasons.set(sk.skipped ?? "", (reasons.get(sk.skipped ?? "") ?? 0) + 1);
814
+ lines.push("", `Set aside \u2014 ${k.skipped.length} file(s), ${size(totalBytes(k.skipped))}:`);
815
+ for (const [why, n] of [...reasons].sort((a, b) => b[1] - a[1])) lines.push(` \xB7 ${n} \xD7 ${why}`);
816
+ }
817
+ return lines.join("\n");
818
+ }
819
+ function describeInit(report) {
820
+ const lines = [
821
+ report.changed ? "**Project set up for horse-code** \u2014 `.gitignore` updated." : "**Project is already set up** \u2014 `.gitignore` already says everything needed.",
822
+ "",
823
+ "Committed on purpose, because a clone would otherwise pay to produce it again:",
824
+ ...report.kept.map((p) => ` \xB7 \`${p}\``),
825
+ "",
826
+ "Kept out on purpose \u2014 derived from the source, keyed to this checkout, or unmergeable:",
827
+ ...report.excluded.map((p) => ` \xB7 \`${p}\``)
828
+ ];
829
+ if (report.unclaimed.length) {
830
+ lines.push(
831
+ "",
832
+ "Untracked and covered by no rule \u2014 `git add -A` would commit these. Yours to decide:",
833
+ ...report.unclaimed.map((u) => ` \xB7 \`${u.path}\` (${u.files >= UNCLAIMED_FILE_FLOOR * 4 ? `${u.files}+` : u.files} files)`)
834
+ );
835
+ }
836
+ return lines.join("\n");
837
+ }
838
+
466
839
  // src/permission/rules.ts
467
840
  import picomatch from "picomatch";
468
841
  var DANGEROUS_PATTERNS = [
@@ -733,8 +1106,8 @@ var ProposalQueue = class {
733
1106
  };
734
1107
 
735
1108
  // src/speckit/templates.ts
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";
1109
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1110
+ import { join as join8, dirname as dirname3 } from "path";
738
1111
  var TEMPLATE_FILES = {
739
1112
  spec: "spec-template.md",
740
1113
  plan: "plan-template.md",
@@ -757,10 +1130,10 @@ async function loadSpecKit(opts) {
757
1130
  );
758
1131
  }
759
1132
  const fetchFn = opts.fetch ?? globalThis.fetch;
760
- const cacheDir = join6(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
1133
+ const cacheDir = join8(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
761
1134
  const get = async (relPath) => {
762
- const cachePath = join6(cacheDir, relPath);
763
- if (existsSync2(cachePath)) return readFileSync2(cachePath, "utf8");
1135
+ const cachePath = join8(cacheDir, relPath);
1136
+ if (existsSync3(cachePath)) return readFileSync3(cachePath, "utf8");
764
1137
  const url = `${RAW_BASE}/${opts.version}/templates/${relPath}`;
765
1138
  const res = await fetchFn(url);
766
1139
  if (!res.ok) {
@@ -770,8 +1143,8 @@ Check your network or set specKit.version to a valid tag.`
770
1143
  );
771
1144
  }
772
1145
  const text = await res.text();
773
- mkdirSync2(dirname3(cachePath), { recursive: true });
774
- writeFileSync2(cachePath, text, "utf8");
1146
+ mkdirSync3(dirname3(cachePath), { recursive: true });
1147
+ writeFileSync3(cachePath, text, "utf8");
775
1148
  return text;
776
1149
  };
777
1150
  const templates = {};
@@ -787,7 +1160,7 @@ Check your network or set specKit.version to a valid tag.`
787
1160
 
788
1161
  // src/wiring.ts
789
1162
  import { homedir } from "os";
790
- import { join as join7 } from "path";
1163
+ import { join as join9 } from "path";
791
1164
  async function buildJobDeps(opts) {
792
1165
  const { config } = opts;
793
1166
  const roles = {};
@@ -801,7 +1174,12 @@ async function buildJobDeps(opts) {
801
1174
  const roleRegistry = new RoleRegistry(roles, DEFAULT_PROMPTS, opts.skillRegistry);
802
1175
  if (opts.accounts) {
803
1176
  const pool = opts.accounts;
804
- roleRegistry.setSourceWeights(() => ({ claude: pool.count("claude"), codex: pool.count("codex") }));
1177
+ roleRegistry.setSourceWeights(() => ({
1178
+ claude: pool.count("claude"),
1179
+ codex: pool.count("codex"),
1180
+ grok: pool.count("grok"),
1181
+ zai: pool.count("zai")
1182
+ }));
805
1183
  }
806
1184
  const fillModels = (r) => ({
807
1185
  ...r,
@@ -828,7 +1206,7 @@ async function buildJobDeps(opts) {
828
1206
  const queue = new ProposalQueue();
829
1207
  let kitPromise;
830
1208
  const specKit = () => kitPromise ??= loadSpecKit({ version: config.specKit.version, home: opts.home, fetch: opts.fetch });
831
- const fitness = new RoleFitness(join7(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
1209
+ const fitness = new RoleFitness(join9(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
832
1210
  roleRegistry.setFitness(fitness);
833
1211
  return {
834
1212
  provider: opts.provider,
@@ -1020,10 +1398,10 @@ function redactRecord(record) {
1020
1398
  }
1021
1399
 
1022
1400
  // src/obs/sink.ts
1023
- import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1024
- import { join as join8 } from "path";
1401
+ import { createWriteStream, mkdirSync as mkdirSync4 } from "fs";
1402
+ import { join as join10 } from "path";
1025
1403
  function telemetryDir(home) {
1026
- return join8(home, ".horsecode", "telemetry");
1404
+ return join10(home, ".horsecode", "telemetry");
1027
1405
  }
1028
1406
  var MAX_QUEUE = 1e4;
1029
1407
  var FileSink = class {
@@ -1032,9 +1410,9 @@ var FileSink = class {
1032
1410
  dropped = 0;
1033
1411
  path;
1034
1412
  constructor(home, runId) {
1035
- this.path = join8(telemetryDir(home), `${runId}.jsonl`);
1413
+ this.path = join10(telemetryDir(home), `${runId}.jsonl`);
1036
1414
  try {
1037
- mkdirSync3(telemetryDir(home), { recursive: true });
1415
+ mkdirSync4(telemetryDir(home), { recursive: true });
1038
1416
  this.stream = createWriteStream(this.path, { flags: "a" });
1039
1417
  this.stream.on("error", () => {
1040
1418
  this.stream = void 0;
@@ -1091,13 +1469,13 @@ var FileSink = class {
1091
1469
  };
1092
1470
 
1093
1471
  // src/cli-info.ts
1094
- import { readFileSync as readFileSync3 } from "fs";
1095
- import { dirname as dirname4, join as join9 } from "path";
1472
+ import { readFileSync as readFileSync4 } from "fs";
1473
+ import { dirname as dirname4, join as join11 } from "path";
1096
1474
  import { fileURLToPath as fileURLToPath2 } from "url";
1097
1475
  function hcodeVersion() {
1098
1476
  try {
1099
1477
  const here = dirname4(fileURLToPath2(import.meta.url));
1100
- const pkg = JSON.parse(readFileSync3(join9(here, "..", "package.json"), "utf8"));
1478
+ const pkg = JSON.parse(readFileSync4(join11(here, "..", "package.json"), "utf8"));
1101
1479
  return pkg.version ?? "unknown";
1102
1480
  } catch {
1103
1481
  return "unknown";
@@ -1124,7 +1502,9 @@ function usage() {
1124
1502
  " hcode start the REPL",
1125
1503
  ' hcode "<request>" run one request to completion, then report where the work landed',
1126
1504
  " 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",
1505
+ " hcode add-provider <cli> connect one more claude, codex, grok or zai subscription",
1506
+ " hcode remove-provider <cli> [name]",
1507
+ " disconnect one, and optionally delete the sign-in it holds",
1128
1508
  "",
1129
1509
  "OPTIONS",
1130
1510
  " -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
@@ -1152,6 +1532,18 @@ function nearestFlag(flag) {
1152
1532
 
1153
1533
  // src/cli.ts
1154
1534
  var HEAP_MB = 12288;
1535
+ async function connectZai(dir) {
1536
+ process.stdout.write("z.ai API key (not shown as you type): ");
1537
+ const token = promptSecret();
1538
+ process.stdout.write("\n");
1539
+ if (!token) return { ok: false, error: "no key was entered" };
1540
+ console.log(`Asking ${ZAI_MODELS[0]} one question, to see whether the key works\u2026`);
1541
+ const v = await verifyZaiKey(token, ZAI_MODELS[0]);
1542
+ if (!v.ok) return { ok: false, error: v.error };
1543
+ writeZaiProfile(dir, token);
1544
+ console.log(` answered by ${v.served ?? "the endpoint (which named no model)"}.`);
1545
+ return { ok: true };
1546
+ }
1155
1547
  function parseArgs(argv) {
1156
1548
  let fromBranch;
1157
1549
  let jobName;
@@ -1310,14 +1702,14 @@ async function main(argv) {
1310
1702
  read: read2,
1311
1703
  readFile: (p) => {
1312
1704
  try {
1313
- return readFileSync4(p, "utf8");
1705
+ return readFileSync5(p, "utf8");
1314
1706
  } catch {
1315
1707
  return void 0;
1316
1708
  }
1317
1709
  },
1318
1710
  writeFile: (p, c) => {
1319
- mkdirSync4(dirname5(p), { recursive: true });
1320
- writeFileSync3(p, c);
1711
+ mkdirSync5(dirname5(p), { recursive: true });
1712
+ writeFileSync4(p, c);
1321
1713
  },
1322
1714
  home: process.env.HOME ?? "",
1323
1715
  log: (s) => console.log(s)
@@ -1329,32 +1721,74 @@ async function main(argv) {
1329
1721
  }
1330
1722
  if (argv[0] === "add-provider") {
1331
1723
  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}")` : ""}`);
1724
+ if (!kind || !CLI_KINDS.includes(kind)) {
1725
+ console.error(`add-provider needs a CLI to connect: ${CLI_KINDS.map((k) => `\`hcode add-provider ${k}\``).join(", ")}${argv[1] ? ` (got "${argv[1]}")` : ""}`);
1334
1726
  process.exitCode = 1;
1335
1727
  return;
1336
1728
  }
1337
1729
  const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
1338
- process.exitCode = addProvider(kind, {
1730
+ process.exitCode = await addProvider(kind, {
1339
1731
  home: process.env.HOME ?? "",
1340
1732
  readConfig: () => {
1341
1733
  try {
1342
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
1734
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
1343
1735
  return typeof parsed === "object" && parsed !== null ? parsed : {};
1344
1736
  } catch {
1345
1737
  return {};
1346
1738
  }
1347
1739
  },
1348
1740
  writeConfig: (c) => {
1349
- mkdirSync4(dirname5(path), { recursive: true });
1350
- writeFileSync3(path, JSON.stringify(c, null, 2) + "\n");
1741
+ mkdirSync5(dirname5(path), { recursive: true });
1742
+ writeFileSync4(path, JSON.stringify(c, null, 2) + "\n");
1351
1743
  },
1352
- login: runLogin,
1744
+ /**
1745
+ * "Make this profile real" means two different things, and z.ai is the one that is not a sign-in.
1746
+ *
1747
+ * The other three hand the terminal to their own OAuth. z.ai has none — a profile is a settings file
1748
+ * holding an endpoint and a key — so here the step is: ask for the key, write it, and then SPEND one
1749
+ * call proving it works. That last part is not ceremony: `claude auth status` reports any token as
1750
+ * connected, valid or not, so without a real call a mistyped key would be filed as a working account
1751
+ * and fail every call of the next run.
1752
+ */
1753
+ login: (k, dir) => k === "zai" ? connectZai(dir ?? "") : runLogin(k, dir),
1353
1754
  check: checkProfile,
1354
1755
  log: (line) => console.log(line)
1355
1756
  });
1356
1757
  return;
1357
1758
  }
1759
+ if (argv[0] === "remove-provider") {
1760
+ const kind = argv[1];
1761
+ if (!kind || !CLI_KINDS.includes(kind)) {
1762
+ console.error(removeUsage(argv[1]));
1763
+ process.exitCode = 1;
1764
+ return;
1765
+ }
1766
+ const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
1767
+ const home2 = process.env.HOME ?? "";
1768
+ process.exitCode = removeProvider(kind, argv[2], {
1769
+ home: home2,
1770
+ readConfig: () => {
1771
+ try {
1772
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
1773
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1774
+ } catch {
1775
+ return {};
1776
+ }
1777
+ },
1778
+ writeConfig: (c) => {
1779
+ mkdirSync5(dirname5(path), { recursive: true });
1780
+ writeFileSync4(path, JSON.stringify(c, null, 2) + "\n");
1781
+ },
1782
+ removeDir: (dir) => {
1783
+ rmSync(dir, { recursive: true, force: true });
1784
+ },
1785
+ confirm,
1786
+ choose: chooseFrom,
1787
+ usage: fileUsageStore(home2),
1788
+ log: (line) => console.log(line)
1789
+ }).code;
1790
+ return;
1791
+ }
1358
1792
  const cwd = process.cwd();
1359
1793
  const config = loadConfig({
1360
1794
  cwd,
@@ -1362,7 +1796,7 @@ async function main(argv) {
1362
1796
  env: process.env,
1363
1797
  readFile: (p) => {
1364
1798
  try {
1365
- return readFileSync4(p, "utf8");
1799
+ return readFileSync5(p, "utf8");
1366
1800
  } catch {
1367
1801
  return void 0;
1368
1802
  }
@@ -1370,13 +1804,13 @@ async function main(argv) {
1370
1804
  });
1371
1805
  const home = process.env.HOME ?? "";
1372
1806
  if (!process.env.HC_HEAP_SET && !process.env.NODE_OPTIONS) {
1373
- const { spawnSync } = await import("child_process");
1807
+ const { spawnSync: spawnSync2 } = await import("child_process");
1374
1808
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1375
1809
  const ignore = () => {
1376
1810
  };
1377
1811
  const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
1378
1812
  for (const sig of SIGNALS) process.on(sig, ignore);
1379
- const r = spawnSync(process.execPath, [`--max-old-space-size=${HEAP_MB}`, ...process.argv.slice(1)], {
1813
+ const r = spawnSync2(process.execPath, [`--max-old-space-size=${HEAP_MB}`, ...process.argv.slice(1)], {
1380
1814
  stdio: "inherit",
1381
1815
  /**
1382
1816
  * `NODE_ENV=production` is set here because it must be set BEFORE React is imported.
@@ -1412,7 +1846,7 @@ async function main(argv) {
1412
1846
  process.once("SIGTERM", flush);
1413
1847
  }
1414
1848
  const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
1415
- const ambientLogins = ["claude", "codex"].filter((k) => !config.accounts.some((a) => a.kind === k)).map((kind) => ({ kind, status: checkProfile(kind) }));
1849
+ const ambientLogins = CLI_KINDS.filter((k) => k !== "zai" && !config.accounts.some((a) => a.kind === k)).map((kind) => ({ kind, status: checkProfile(kind) }));
1416
1850
  const accounts = new AccountPool(
1417
1851
  [
1418
1852
  ...config.accounts,
@@ -1427,13 +1861,43 @@ async function main(argv) {
1427
1861
  );
1428
1862
  const notSignedIn = ambientLogins.filter((a) => !a.status.loggedIn).map((a) => a.kind);
1429
1863
  const accountsNote = () => accountsLine(accounts.usage(), notSignedIn);
1864
+ const initProjectFn = async () => {
1865
+ const r = await defaultGitRunner(["ls-files", "--others", "--exclude-standard", "--directory"], cwd);
1866
+ const list = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean);
1867
+ const ignored = /* @__PURE__ */ new Set();
1868
+ for (const path of list) {
1869
+ if ((await defaultGitRunner(["check-ignore", "-q", path], cwd)).code === 0) ignored.add(path);
1870
+ }
1871
+ const all = await defaultGitRunner(["ls-files", "--cached", "--others", "--exclude-standard"], cwd);
1872
+ const md = all.stdout.split("\n").map((l) => l.trim()).filter((l) => /\.mdx?$/i.test(l));
1873
+ const withSize = md.map((path) => {
1874
+ try {
1875
+ return { path, bytes: statSync2(join12(cwd, path)).size };
1876
+ } catch {
1877
+ return { path, bytes: 0 };
1878
+ }
1879
+ });
1880
+ const found = await discover({ cwd, home });
1881
+ const named = found.filter((f) => f.kind === "rules" || f.kind === "mcp").map((f) => ({ label: f.label, bytes: f.bytes }));
1882
+ const { candidates: candidates2, skipped } = sortProjectDocs(withSize, found.map((f) => f.label));
1883
+ const knowledge = { named, docs: candidates2, skipped };
1884
+ const report = await initProject(cwd, (path) => ignored.has(path), () => list, knowledge);
1885
+ const canImport = hasKnowledge(knowledge);
1886
+ const extra = [];
1887
+ for (const d of candidates2) {
1888
+ const r2 = await readFinding(join12(cwd, d.path));
1889
+ if (r2?.text) extra.push({ kind: "rules", tool: "project document", path: join12(cwd, d.path), label: d.path, bytes: r2.bytes, own: true, text: r2.text });
1890
+ }
1891
+ return { text: describeInit(report) + (canImport ? describeKnowledge(knowledge) : ""), canImport, extra };
1892
+ };
1893
+ const modelsPanelNote = (inUse) => modelsPanel(accounts.usage(), modelsFor, CLI_KINDS, inUse);
1430
1894
  const raw = new CliProvider({ readOnly: false, accounts });
1431
1895
  const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
1432
1896
  const skillRegistry = new SkillRegistry();
1433
1897
  await registerBuiltinSkills(skillRegistry);
1434
1898
  await skillRegistry.loadFromDir(externalSkillsDir(home));
1435
- const skillsDir = join10(cwd, ".horsecode", "skills");
1436
- if (existsSync3(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
1899
+ const skillsDir = join12(cwd, ".horsecode", "skills");
1900
+ if (existsSync4(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
1437
1901
  const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
1438
1902
  if (worktreeHome !== cwd) {
1439
1903
  console.log(`\u2139\uFE0F Sessions will be opened in ${worktreeHome} \u2014 the repository's main checkout.`);
@@ -1465,7 +1929,7 @@ async function main(argv) {
1465
1929
  const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
1466
1930
  if (!args.prompt) {
1467
1931
  if (useTui) {
1468
- const { runTuiRepl } = await import("./app-2GPGCDX6.js");
1932
+ const { runTuiRepl } = await import("./app-4WN37LZ3.js");
1469
1933
  const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-G2Z6XC3O.js");
1470
1934
  const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
1471
1935
  const manualSources = config.modelSources.length > 0;
@@ -1516,8 +1980,8 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
1516
1980
  _Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
1517
1981
  };
1518
1982
  const reloadProjectSkills = async () => {
1519
- const dir = join10(cwd, ".horsecode", "skills");
1520
- if (existsSync3(dir)) await skillRegistry.loadFromDir(dir);
1983
+ const dir = join12(cwd, ".horsecode", "skills");
1984
+ if (existsSync4(dir)) await skillRegistry.loadFromDir(dir);
1521
1985
  };
1522
1986
  const updateSkills = async () => {
1523
1987
  if (!config.skillSources.length) {
@@ -1536,7 +2000,7 @@ _Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits;
1536
2000
  ${lines.join("\n")}${tail}`;
1537
2001
  };
1538
2002
  const graphStatusText = async () => {
1539
- const st = await graphStatus(cwd);
2003
+ const st = await graphStatus((await derivedWorkdir(false)).dir);
1540
2004
  if (!st.built) {
1541
2005
  const py = await graphifyPython();
1542
2006
  return py ? "No code graph yet. `/graph build` \u2014 AST parsing only, no tokens, a few seconds.\n\n_Without it every agent works blind: it can grep for a name but cannot tell what calls it._" : "No code graph, and graphify is not installed.\n\n`uv tool install graphifyy` (or `pipx install graphifyy`), then `/graph build`.\n\n_MIT, pure tree-sitter AST parsing \u2014 no API key, no tokens._";
@@ -1555,7 +2019,7 @@ ${lines.join("\n")}${tail}`;
1555
2019
 
1556
2020
  _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`graph_find\`, \`graph_context\`, \`graph_overview\`._`;
1557
2021
  };
1558
- const buildGraphText = async () => (await buildProjectGraph(cwd)).message;
2022
+ const buildGraphText = async () => (await buildProjectGraph((await derivedWorkdir(true)).dir)).message;
1559
2023
  const cleanWorktreesText = async (apply, branch) => {
1560
2024
  const { surveySessions, cleanSessions, describeSurvey, describeClean } = await import("./clean-YOQATBMZ.js");
1561
2025
  const target = branch?.trim() || fromBranch;
@@ -1565,31 +2029,42 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
1565
2029
  return describeClean(await cleanSessions(defaultGitRunner, cwd, target), target);
1566
2030
  };
1567
2031
  const gitFiles = async () => (await defaultGitRunner(["ls-files", "--cached", "--others", "--exclude-standard"], cwd)).stdout.split("\n").filter(Boolean);
1568
- const traceableDocs = async () => traceableFiles(cwd, { code: false });
1569
- const traceableFiles2 = async () => traceableFiles(cwd);
1570
- const tracerModel = () => {
2032
+ const traceableDocs = async (dir = cwd) => traceableFiles(dir, { code: false });
2033
+ const traceableFiles2 = async (dir = cwd) => traceableFiles(dir);
2034
+ const tracerChain = () => {
1571
2035
  for (const role of ["tracer", "architect", "senior-coder", "judge"]) {
1572
- const m = config.roles[role]?.models[0];
1573
- if (m && m !== "default") return m;
2036
+ const chain = (config.roles[role]?.models ?? []).filter((m) => m && m !== "default");
2037
+ if (chain.length) return chain;
1574
2038
  }
1575
- return config.model;
2039
+ return [config.model];
2040
+ };
2041
+ const derivedWorkdir = async (create) => {
2042
+ const inSession = sessionBase(cwd);
2043
+ if (inSession) return { dir: inSession };
2044
+ const standing = join12(cwd, ".horsecode", "worktrees", "traces", "base");
2045
+ if (!create) return { dir: existsSync4(standing) ? standing : cwd };
2046
+ const session = await manager.openFixed(fromBranch, "traces");
2047
+ return { dir: session.baseWorktree, session };
1576
2048
  };
1577
2049
  const planTracesFn = async () => {
1578
- const plan = await planFor(cwd, await traceableFiles2());
1579
- return { summary: describePlan(plan, tracerModel()), jobs: plan.jobs.length };
2050
+ const { dir } = await derivedWorkdir(false);
2051
+ const plan = await planFor(dir, await traceableFiles2(dir));
2052
+ return { summary: describePlan(plan, tracerChain()), jobs: plan.jobs.length };
1580
2053
  };
1581
2054
  const runTracesFn = async (onProgress, metered) => {
1582
- const files = await traceableFiles2();
1583
- const brief = await buildBrief({ cwd, provider: metered ?? provider, model: tracerModel(), files: await traceableDocs() });
1584
- const plan = await planFor(cwd, files);
2055
+ const { dir, session } = await derivedWorkdir(true);
2056
+ const files = await traceableFiles2(dir);
2057
+ const brief = await buildBrief({ cwd: dir, provider: metered ?? provider, models: tracerChain(), files: await traceableDocs(dir) });
2058
+ const plan = await planFor(dir, files);
1585
2059
  const res = await runTraces({
1586
- cwd,
2060
+ cwd: dir,
1587
2061
  provider: metered ?? provider,
1588
- model: tracerModel(),
2062
+ models: tracerChain(),
1589
2063
  plan,
1590
2064
  liveFiles: new Set(files),
1591
2065
  ...onProgress ? { onProgress } : {}
1592
2066
  });
2067
+ if (session) await manager.preserveSession(session, `docs(traces): ${res.written} file(s) described`);
1593
2068
  const bits = [`${brief.message}
1594
2069
 
1595
2070
  **Traces written: ${res.written}**`];
@@ -1597,9 +2072,13 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
1597
2072
  if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
1598
2073
  if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
1599
2074
  const failures = describeTraceFailures(res.failed);
2075
+ const landed = session ? `
2076
+
2077
+ _Written and committed on \`${session.baseBranch}\`, so your working tree is untouched._
2078
+ _Bring them in with \`git merge ${session.baseBranch}\`, or browse them at \`${session.baseWorktree}\`._` : "\n\n_Written in this session's worktree, on its own branch._";
1600
2079
  return `${bits.join(" \xB7 ")}${failures ? `
1601
2080
 
1602
- ${failures}` : ""}
2081
+ ${failures}` : ""}${landed}
1603
2082
 
1604
2083
  _Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
1605
2084
  };
@@ -1607,11 +2086,14 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1607
2086
  buildDeps,
1608
2087
  memStore,
1609
2088
  accountsNote,
2089
+ modelsPanel: modelsPanelNote,
2090
+ initProject: initProjectFn,
1610
2091
  listSkills,
1611
2092
  updateSkills,
1612
2093
  addSkill,
1613
2094
  reloadProjectSkills,
1614
2095
  graphStatus: graphStatusText,
2096
+ derivedDir: async () => (await derivedWorkdir(true)).dir,
1615
2097
  buildGraph: buildGraphText,
1616
2098
  cleanWorktrees: cleanWorktreesText,
1617
2099
  planTraces: planTracesFn,
@@ -1644,7 +2126,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1644
2126
  ...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
1645
2127
  };
1646
2128
  if (useTui) {
1647
- const { runTui } = await import("./app-2GPGCDX6.js");
2129
+ const { runTui } = await import("./app-4WN37LZ3.js");
1648
2130
  const res = await runTui({ buildDeps, job });
1649
2131
  console.log(renderResult(res));
1650
2132
  return;