@hizliemre/horse-code 0.2.0 → 0.3.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.
@@ -10,7 +10,7 @@ import {
10
10
  restoreTerminal,
11
11
  runJob,
12
12
  sttySane
13
- } from "./chunk-23CLQ2KO.js";
13
+ } from "./chunk-XYZVZPAY.js";
14
14
  import "./chunk-M2RKCIGV.js";
15
15
  import {
16
16
  DURABLE_ROLES,
@@ -20,6 +20,7 @@ import {
20
20
  adjustRoleModels,
21
21
  asChoice,
22
22
  capabilityScore,
23
+ cliFor,
23
24
  describeUnfinished,
24
25
  effortFor,
25
26
  filterModelsForRole,
@@ -31,13 +32,14 @@ import {
31
32
  strongestPrimary,
32
33
  toSlug,
33
34
  unfinishedSessions
34
- } from "./chunk-7JMWPTJ5.js";
35
+ } from "./chunk-UEWVVN5L.js";
35
36
  import "./chunk-QF4MP6BS.js";
36
37
  import "./chunk-UGESK765.js";
37
38
  import "./chunk-ZSQ24YDJ.js";
38
39
  import {
39
40
  saveRoleSkills
40
- } from "./chunk-KKWZBZYK.js";
41
+ } from "./chunk-EAF22QIG.js";
42
+ import "./chunk-G45RWL7S.js";
41
43
  import {
42
44
  objectField,
43
45
  patchConfig
@@ -356,7 +358,7 @@ async function connectAllMcp(specs) {
356
358
 
357
359
  // src/tui/app.tsx
358
360
  import { homedir as homedir2 } from "os";
359
- import { join as join6 } from "path";
361
+ import { join as join7 } from "path";
360
362
  import { basename } from "path";
361
363
 
362
364
  // src/tui/labels.ts
@@ -1737,6 +1739,7 @@ var COMMANDS = [
1737
1739
  { name: "/memories", desc: "List remembered facts (/forget N to remove)" },
1738
1740
  { name: "/forget", desc: "Forget a remembered fact (/forget N)" },
1739
1741
  { name: "/mcp", desc: "Connected MCP servers (/mcp add <url|command> installs one and verifies it)" },
1742
+ { name: "/models", desc: "Models each connected subscription serves, and which ones a role is running" },
1740
1743
  { name: "/sources", desc: "Show your connected model sources (/sources refresh re-detects)" },
1741
1744
  { name: "/migrate", desc: "Bring a project from Claude Code / Codex / Cursor into horse-code (rules, memory, skills)" },
1742
1745
  { name: "/continue-from-claude", desc: "Continue work started in a Claude Code worktree (/continue-from-claude <name>) \u2014 its branch becomes the base" },
@@ -1748,6 +1751,7 @@ var COMMANDS = [
1748
1751
  { name: "/monitor", desc: "Where the run's time is going (/monitor enable shows the panel, disable hides it, log shows the file, heap writes a snapshot)" },
1749
1752
  { name: "/watch", desc: "Watch any command \u2014 each line it prints becomes an event (/watch <cmd>, /watch stop N)" },
1750
1753
  { name: "/paste", desc: "Put the clipboard's image into the input (same as Ctrl+V, for terminals that swallow it)" },
1754
+ { name: "/start-smoke-test", desc: "Walk the finished feature's manual verification one step at a time, and record what you saw into its guide" },
1751
1755
  { name: "/help", desc: "List the available commands" },
1752
1756
  { name: "/clear", desc: "Clear the conversation" },
1753
1757
  { name: "/exit", desc: "Quit horse-code" }
@@ -1957,7 +1961,7 @@ function rankFiles(files, query, limit = 8) {
1957
1961
  }
1958
1962
 
1959
1963
  // src/tui/components.tsx
1960
- import { join as join2 } from "path";
1964
+ import { join as join3 } from "path";
1961
1965
  import { homedir } from "os";
1962
1966
 
1963
1967
  // src/tui/paste.ts
@@ -2288,7 +2292,130 @@ function shortName(command) {
2288
2292
  return first.split("/").pop()?.slice(0, 24) || "watch";
2289
2293
  }
2290
2294
 
2295
+ // src/engine/smoke-test.ts
2296
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
2297
+ import { join as join2 } from "path";
2298
+ function splitInstructions(paragraph) {
2299
+ return paragraph.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 12);
2300
+ }
2301
+ function parseGuide(path, markdown) {
2302
+ const lines = markdown.split("\n");
2303
+ const title = lines.find((l) => l.startsWith("# "))?.slice(2).trim() ?? "Manual verification";
2304
+ const preconditions = [];
2305
+ const scenarios = [];
2306
+ const steps = [];
2307
+ let section = "other";
2308
+ let scenario = "";
2309
+ let buffer = [];
2310
+ const flush = () => {
2311
+ if (!scenario) {
2312
+ buffer = [];
2313
+ return;
2314
+ }
2315
+ const text = buffer.join(" ").trim();
2316
+ for (const instruction of splitInstructions(text)) {
2317
+ steps.push({ n: steps.length + 1, scenario, scenarioIndex: Math.max(0, scenarios.length - 1), instruction });
2318
+ }
2319
+ buffer = [];
2320
+ };
2321
+ for (const line of lines) {
2322
+ const h2 = /^##\s+(?!#)(.*)$/.exec(line);
2323
+ const h3 = /^###\s+(.*)$/.exec(line);
2324
+ if (h2) {
2325
+ flush();
2326
+ scenario = "";
2327
+ section = /ön koşul|precondition/i.test(h2[1]) ? "pre" : /senaryo|scenario/i.test(h2[1]) ? "scenario" : "other";
2328
+ continue;
2329
+ }
2330
+ if (h3) {
2331
+ flush();
2332
+ if (section === "scenario") {
2333
+ scenario = h3[1].trim();
2334
+ scenarios.push(scenario);
2335
+ }
2336
+ continue;
2337
+ }
2338
+ const text = line.trim();
2339
+ if (!text) {
2340
+ flush();
2341
+ continue;
2342
+ }
2343
+ if (section === "pre") {
2344
+ const item = /^\d+\.\s+(.*)$/.exec(text)?.[1] ?? /^[-*]\s+(.*)$/.exec(text)?.[1];
2345
+ if (item) preconditions.push(item);
2346
+ continue;
2347
+ }
2348
+ if (section === "scenario" && scenario) buffer.push(text);
2349
+ }
2350
+ flush();
2351
+ return { path, title, preconditions, steps, scenarios };
2352
+ }
2353
+ function renderStep(step, total) {
2354
+ return `**Ad\u0131m ${step.n}/${total}** \u2014 _${step.scenario}_
2355
+
2356
+ ${step.instruction}`;
2357
+ }
2358
+ function recordOutcome(markdown, scenarioIndex, status, evidence) {
2359
+ const want = scenarioIndex + 1;
2360
+ return markdown.split("\n").map((line) => {
2361
+ if (!line.startsWith("|")) return line;
2362
+ const cells = line.split("|");
2363
+ if (cells.length < 4) return line;
2364
+ if (Number(/^\s*(\d+)/.exec(cells[1] ?? "")?.[1]) !== want) return line;
2365
+ cells[2] = ` ${status} `;
2366
+ cells[3] = ` ${evidence.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim()} `;
2367
+ return cells.join("|");
2368
+ }).join("\n");
2369
+ }
2370
+ function scenarioOutcome(results) {
2371
+ const status = results.some((r) => r.status === "ba\u015Far\u0131s\u0131z") ? "ba\u015Far\u0131s\u0131z" : results.every((r) => r.status === "\xE7al\u0131\u015Ft\u0131r\u0131lmad\u0131") ? "\xE7al\u0131\u015Ft\u0131r\u0131lmad\u0131" : "ge\xE7ti";
2372
+ const evidence = results.filter((r) => r.evidence.trim()).map((r) => `${r.step.n}. ${r.evidence.trim()}`).join(" ");
2373
+ return { status, evidence: evidence || "g\xF6zlem kaydedilmedi" };
2374
+ }
2375
+ function findGuide(projectRoot) {
2376
+ const roots = [projectRoot, ...worktreeRoots(projectRoot)];
2377
+ let best;
2378
+ for (const root of roots) {
2379
+ let names = [];
2380
+ try {
2381
+ names = readdirSync(join2(root, "specs"));
2382
+ } catch {
2383
+ continue;
2384
+ }
2385
+ for (const slug of names) {
2386
+ if (!/^\d{3}-/.test(slug)) continue;
2387
+ const path = join2(root, "specs", slug, "quickstart.md");
2388
+ if (!existsSync(path)) continue;
2389
+ if (!best || slug > best.slug) best = { root, slug, path };
2390
+ }
2391
+ }
2392
+ return best;
2393
+ }
2394
+ function worktreeRoots(projectRoot) {
2395
+ const dir = join2(projectRoot, ".horsecode", "worktrees");
2396
+ try {
2397
+ return readdirSync(dir).map((s) => join2(dir, s, "base")).filter((p) => existsSync(p));
2398
+ } catch {
2399
+ return [];
2400
+ }
2401
+ }
2402
+ function progressLine(done, total) {
2403
+ const width = 24;
2404
+ const filled = total ? Math.round(done / total * width) : 0;
2405
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)} ${done}/${total}`;
2406
+ }
2407
+ function loadGuide(projectRoot) {
2408
+ const found = findGuide(projectRoot);
2409
+ if (!found) {
2410
+ return { error: "No `specs/NNN-\u2026/quickstart.md` in this project or any of its session worktrees \u2014 a feature run writes one when it finishes." };
2411
+ }
2412
+ const guide = parseGuide(found.path, readFileSync2(found.path, "utf8"));
2413
+ if (!guide.steps.length) return { error: `${found.path} has no scenarios to walk \u2014 it may not be a verification guide.` };
2414
+ return { ...guide, slug: found.slug };
2415
+ }
2416
+
2291
2417
  // src/tui/components.tsx
2418
+ import { readFileSync as readFileSync3, writeFileSync } from "fs";
2292
2419
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2293
2420
  function parsePending(raw) {
2294
2421
  const t = raw.replace(/^\s+/, "");
@@ -3042,7 +3169,7 @@ function ViewportLines({ lines, height }) {
3042
3169
  lines.map((line, i) => /* @__PURE__ */ jsx4(Text4, { children: line.length === 0 ? " " : line.map((s, j) => /* @__PURE__ */ jsx4(Text4, { color: s.color, backgroundColor: s.backgroundColor, bold: s.bold, italic: s.italic, dimColor: s.dim, children: s.text }, j)) }, i))
3043
3170
  ] });
3044
3171
  }
3045
- function App({ controller, fullscreen = false, model, coachModel, refinerModel, listModels, setModel, setRoleModel, listRoles, adjustRoles, listSessions, resumeSession, listPins, addPin, removePin, listMemories, addMemory, removeMemory, listMcp, sourcesInfo, refreshSources, listSkills, updateSkills, addSkill, graphStatus: graphStatus2, buildGraph, planTraces, runTraces, cleanWorktrees, migrate, continueFromClaude, addMcp, answerByTheWay, telemetryPath, parallel, setParallel, permMode, setPermMode, cancelJob, onExit }) {
3172
+ function App({ controller, fullscreen = false, model, coachModel, refinerModel, listModels, setModel, setRoleModel, listRoles, adjustRoles, modelsPanel, listSessions, resumeSession, listPins, addPin, removePin, listMemories, addMemory, removeMemory, listMcp, sourcesInfo, refreshSources, listSkills, updateSkills, addSkill, graphStatus: graphStatus2, buildGraph, planTraces, runTraces, cleanWorktrees, migrate, continueFromClaude, addMcp, answerByTheWay, telemetryPath, parallel, setParallel, permMode, setPermMode, cancelJob, onExit }) {
3046
3173
  const [state, setState] = useState3(controller.getState());
3047
3174
  useEffect3(() => controller.subscribe(() => setState(controller.getState())), [controller]);
3048
3175
  const { stdout } = useStdout();
@@ -3255,10 +3382,10 @@ _Type \`/resume N\` to continue one._`);
3255
3382
  }
3256
3383
  try {
3257
3384
  const { writeFile: writeFile3, mkdir: mkdir3 } = await import("fs/promises");
3258
- const dir = join2(homedir(), ".horsecode", "pastes");
3385
+ const dir = join3(homedir(), ".horsecode", "pastes");
3259
3386
  await mkdir3(dir, { recursive: true });
3260
3387
  const id = ++imageIdRef.current;
3261
- const file = join2(dir, `paste-${process.pid}-${id}.png`);
3388
+ const file = join3(dir, `paste-${process.pid}-${id}.png`);
3262
3389
  await writeFile3(file, Buffer.from(uri.slice(uri.indexOf(",") + 1), "base64"));
3263
3390
  imageMapRef.current.set(id, file);
3264
3391
  const tok = imageToken(id);
@@ -3498,6 +3625,14 @@ ${rows.join("\n")}
3498
3625
 
3499
3626
  _\`/mcp add <url|command>\` to install another._`);
3500
3627
  };
3628
+ const doModels = () => {
3629
+ if (!modelsPanel) {
3630
+ controller.note("Models are not available.");
3631
+ return;
3632
+ }
3633
+ const inUse = [...new Set((listRoles?.() ?? []).flatMap((r) => r.models))];
3634
+ controller.note(modelsPanel(inUse));
3635
+ };
3501
3636
  const doSources = (arg) => {
3502
3637
  const info = sourcesInfo?.();
3503
3638
  if (arg.trim().toLowerCase() === "refresh") {
@@ -3561,6 +3696,56 @@ _\`/sources refresh\` to re-probe your connected subscriptions._`);
3561
3696
  }
3562
3697
  );
3563
3698
  };
3699
+ const doSmokeTest = async () => {
3700
+ const guide = loadGuide(process.cwd());
3701
+ if ("error" in guide) {
3702
+ controller.note(guide.error);
3703
+ return;
3704
+ }
3705
+ controller.note(
3706
+ `\u{1F9EA} **${guide.title}**
3707
+
3708
+ \`${guide.slug}\` \xB7 ${guide.steps.length} ad\u0131m \xB7 ${guide.scenarios.length} senaryo`
3709
+ );
3710
+ if (guide.preconditions.length) {
3711
+ controller.note(`**Ba\u015Flamadan \xF6nce**
3712
+ ${guide.preconditions.map((p) => `- ${p}`).join("\n")}`);
3713
+ }
3714
+ controller.note(
3715
+ "Her ad\u0131mda ne g\xF6rd\xFC\u011F\xFCn\xFC yap\u0131\u015Ft\u0131r \u2014 API yan\u0131t\u0131, DB sat\u0131r\u0131, log kayd\u0131. horse-code veritaban\u0131na ve Loki'ye kendisi bakmaz; kan\u0131t senin g\xF6zlemin olarak kaydedilir."
3716
+ );
3717
+ if (await controller.ask("Ortam haz\u0131r m\u0131?", { options: ["Ba\u015Fla", "\u0130ptal"] }) !== "Ba\u015Fla") {
3718
+ controller.note("B\u0131rak\u0131ld\u0131 \u2014 rehberde hi\xE7bir \u015Fey de\u011Fi\u015Fmedi.");
3719
+ return;
3720
+ }
3721
+ let markdown = readFileSync3(guide.path, "utf8");
3722
+ let batch = [];
3723
+ const flush = (index) => {
3724
+ if (!batch.length) return;
3725
+ const { status, evidence } = scenarioOutcome(batch);
3726
+ markdown = recordOutcome(markdown, index, status, evidence);
3727
+ writeFileSync(guide.path, markdown);
3728
+ controller.note(`\u{1F4DD} _${guide.scenarios[index]}_ \u2192 **${status}**`);
3729
+ batch = [];
3730
+ };
3731
+ for (const [i, step] of guide.steps.entries()) {
3732
+ const previous = guide.steps[i - 1];
3733
+ if (previous && previous.scenarioIndex !== step.scenarioIndex) flush(previous.scenarioIndex);
3734
+ controller.note(renderStep(step, guide.steps.length));
3735
+ const answer = await controller.ask("Sonu\xE7?", { options: ["ge\xE7ti", "ba\u015Far\u0131s\u0131z", "atla", "durdur"] });
3736
+ if (answer === "durdur") {
3737
+ flush(step.scenarioIndex);
3738
+ controller.note(`Durduruldu \u2014 ${step.n - 1}/${guide.steps.length} ad\u0131m kaydedildi.`);
3739
+ return;
3740
+ }
3741
+ const status = answer === "ge\xE7ti" ? "ge\xE7ti" : answer === "ba\u015Far\u0131s\u0131z" ? "ba\u015Far\u0131s\u0131z" : "\xE7al\u0131\u015Ft\u0131r\u0131lmad\u0131";
3742
+ const evidence = status === "\xE7al\u0131\u015Ft\u0131r\u0131lmad\u0131" ? "" : await controller.ask("Ne g\xF6zlemledin? (API yan\u0131t\u0131 / DB sat\u0131r\u0131 / log)");
3743
+ batch.push({ step, status, evidence });
3744
+ controller.note(progressLine(step.n, guide.steps.length));
3745
+ }
3746
+ flush(guide.steps[guide.steps.length - 1].scenarioIndex);
3747
+ controller.note(`\u2705 Bitti \u2014 ${guide.path} g\xFCncellendi.`);
3748
+ };
3564
3749
  const doGraph = (arg) => {
3565
3750
  if (!graphStatus2 || !buildGraph) {
3566
3751
  controller.note("The project graph is not available.");
@@ -3724,8 +3909,10 @@ _\`/skills add <github-url>\` to install one \xB7 \`/skills update\` re-installs
3724
3909
  else if (c.name === "/remember") doRemember("");
3725
3910
  else if (c.name === "/forget") doForget("");
3726
3911
  else if (c.name === "/mcp") doMcp("");
3912
+ else if (c.name === "/models") doModels();
3727
3913
  else if (c.name === "/sources") doSources("");
3728
3914
  else if (c.name === "/skills") doSkills("");
3915
+ else if (c.name === "/start-smoke-test") void doSmokeTest();
3729
3916
  else if (c.name === "/graph") doGraph("");
3730
3917
  else if (c.name === "/clean-worktrees") doCleanWorktrees("");
3731
3918
  else if (c.name === "/paste") pasteImage();
@@ -4015,9 +4202,9 @@ _\`/skills add <github-url>\` to install one \xB7 \`/skills update\` re-installs
4015
4202
  const cw = Math.max(1, size.cols - 4);
4016
4203
  const inputH = draft.split("\n").reduce((n, l) => n + Math.max(1, Math.ceil((l.length + 3) / cw)), 0);
4017
4204
  const running = mode === "running";
4018
- const progressLine = running && !state.pending;
4205
+ const progressLine2 = running && !state.pending;
4019
4206
  const doneLine = !!state.meta && !state.meta.running && !state.pending;
4020
- const showStatus = progressLine || !!state.pending || doneLine;
4207
+ const showStatus = progressLine2 || !!state.pending || doneLine;
4021
4208
  const choiceOptionsForFit = state.pending?.options ?? [];
4022
4209
  const pendingBodyMax = state.pending ? Math.max(
4023
4210
  1,
@@ -4028,7 +4215,7 @@ _\`/skills add <github-url>\` to install one \xB7 \`/skills update\` re-installs
4028
4215
  flattenMarkdown(parsePending(state.pending.question).body, pendingBodyWidth(size.cols)).length,
4029
4216
  pendingBodyMax
4030
4217
  ) : 0;
4031
- const statusH = (progressLine || doneLine ? 1 : 0) + pendingLines;
4218
+ const statusH = (progressLine2 || doneLine ? 1 : 0) + pendingLines;
4032
4219
  const inputMarginTop = showStatus ? 0 : 1;
4033
4220
  const choiceOptions = state.pending?.options ?? [];
4034
4221
  const choiceActive = choiceOptions.length > 0 && !choiceDismissed;
@@ -4075,7 +4262,7 @@ _\`/skills add <github-url>\` to install one \xB7 \`/skills update\` re-installs
4075
4262
  /* @__PURE__ */ jsx4(Box4, { paddingLeft: CHAT_INDENT, children: /* @__PURE__ */ jsx4(ViewportLines, { lines: windowed, height: viewportH }) }),
4076
4263
  /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: clamped > 0 ? ` \u2193 ${clamped} more \xB7 \u2193/PgDn to jump to bottom` : " " }),
4077
4264
  showStatus ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
4078
- progressLine ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(ProgressView, { phase: state.phase, detail: state.detail, refinerModel: refinerModel?.(), meta: state.meta, cols: size.cols, live: state.liveActivity }) }) : null,
4265
+ progressLine2 ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(ProgressView, { phase: state.phase, detail: state.detail, refinerModel: refinerModel?.(), meta: state.meta, cols: size.cols, live: state.liveActivity }) }) : null,
4079
4266
  doneLine ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${donePhrase(state.phase)} for ${fmtDuration(state.meta?.durationMs ?? 0)}${state.meta && state.meta.calls > 0 ? ` \xB7 \u2191${fmtTokens(state.meta.promptTokens)} \u2193${fmtTokens(state.meta.completionTokens)} \xB7 ${state.meta.calls} call${state.meta.calls === 1 ? "" : "s"}` : ""}` }) }) : null,
4080
4267
  state.pending ? /* @__PURE__ */ jsx4(PendingQuestion, { text: state.pending.question, cols: size.cols, maxLines: pendingBodyMax }) : null
4081
4268
  ] }) : null,
@@ -4268,6 +4455,15 @@ _\`/skills add <github-url>\` to install one \xB7 \`/skills update\` re-installs
4268
4455
  doMcp(trimmed.slice("/mcp".length).trim());
4269
4456
  return;
4270
4457
  }
4458
+ if (cmd === "/start-smoke-test") {
4459
+ setScroll(0);
4460
+ setDraft("");
4461
+ setDraftCursor(0);
4462
+ usedPastes();
4463
+ controller.echoCommand(trimmed);
4464
+ void doSmokeTest();
4465
+ return;
4466
+ }
4271
4467
  if (cmd.startsWith("/graph ")) {
4272
4468
  setScroll(0);
4273
4469
  setDraft("");
@@ -4379,7 +4575,7 @@ async function saveMode(home, mode) {
4379
4575
  // src/session/store.ts
4380
4576
  import { mkdir, readFile as readFile2, readdir } from "fs/promises";
4381
4577
  import { createHash } from "crypto";
4382
- import { join as join3 } from "path";
4578
+ import { join as join4 } from "path";
4383
4579
  var SessionStore = class {
4384
4580
  activeId;
4385
4581
  dir;
@@ -4387,7 +4583,7 @@ var SessionStore = class {
4387
4583
  constructor(opts) {
4388
4584
  this.now = opts.now ?? (() => Date.now());
4389
4585
  const hash = createHash("sha256").update(opts.cwd).digest("hex").slice(0, 16);
4390
- this.dir = join3(opts.home, ".horsecode", "projects", hash, "sessions");
4586
+ this.dir = join4(opts.home, ".horsecode", "projects", hash, "sessions");
4391
4587
  this.activeId = `s${this.now()}`;
4392
4588
  }
4393
4589
  /** The id this store currently writes to (a fresh one, or a resumed session's id after setActive). */
@@ -4399,7 +4595,7 @@ var SessionStore = class {
4399
4595
  this.activeId = id;
4400
4596
  }
4401
4597
  file(id) {
4402
- return join3(this.dir, `${id}.json`);
4598
+ return join4(this.dir, `${id}.json`);
4403
4599
  }
4404
4600
  /** Overwrite the active session with the current messages (no-op on an empty transcript). */
4405
4601
  async save(messages) {
@@ -4426,7 +4622,7 @@ var SessionStore = class {
4426
4622
  for (const n of names) {
4427
4623
  if (!n.endsWith(".json")) continue;
4428
4624
  try {
4429
- const d = JSON.parse(await readFile2(join3(this.dir, n), "utf8"));
4625
+ const d = JSON.parse(await readFile2(join4(this.dir, n), "utf8"));
4430
4626
  out.push({ id: d.id, title: d.title, updatedAt: d.updatedAt, count: d.count });
4431
4627
  } catch {
4432
4628
  }
@@ -4452,7 +4648,7 @@ function titleOf(messages) {
4452
4648
  // src/session/pins.ts
4453
4649
  import { mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
4454
4650
  import { createHash as createHash2 } from "crypto";
4455
- import { join as join4, dirname } from "path";
4651
+ import { join as join5, dirname } from "path";
4456
4652
  var MAX_PINS = 20;
4457
4653
  var MAX_PIN_CHARS = 500;
4458
4654
  var PinStore = class {
@@ -4460,7 +4656,7 @@ var PinStore = class {
4460
4656
  cache;
4461
4657
  constructor(opts) {
4462
4658
  const hash = createHash2("sha256").update(opts.cwd).digest("hex").slice(0, 16);
4463
- this.file = join4(opts.home, ".horsecode", "projects", hash, "pins.json");
4659
+ this.file = join5(opts.home, ".horsecode", "projects", hash, "pins.json");
4464
4660
  }
4465
4661
  /** Load pins from disk (memoized). Missing/corrupt → empty. */
4466
4662
  async load() {
@@ -4650,7 +4846,7 @@ var ModelHealth = class {
4650
4846
  }
4651
4847
  const live = all.filter((m) => !dead.has(m));
4652
4848
  const configured = new Set(this.port.registries().flatMap((r) => r.knownModels()));
4653
- const curated = live.filter((m) => configured.has(m));
4849
+ const curated = live.filter((m) => configured.has(m) || cliFor(m) !== void 0);
4654
4850
  telemetry().event("decision.pool", {
4655
4851
  "hc.decision": "pool",
4656
4852
  "hc.pool.catalog": all.length,
@@ -4995,7 +5191,7 @@ First explain your key choices in a few short sentences, including which skills
4995
5191
  // src/engine/scan-repo.ts
4996
5192
  import { spawn as spawn2 } from "child_process";
4997
5193
  import { readFile as readFile4 } from "fs/promises";
4998
- import { join as join5 } from "path";
5194
+ import { join as join6 } from "path";
4999
5195
  var MANIFESTS = ["package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "build.gradle", "composer.json"];
5000
5196
  var MAX_FILES = 2e4;
5001
5197
  function gitFiles(cwd) {
@@ -5015,7 +5211,7 @@ async function scanRepo(cwd) {
5015
5211
  for (const name of MANIFESTS) {
5016
5212
  if (!files.includes(name)) continue;
5017
5213
  try {
5018
- manifests[name] = await readFile4(join5(cwd, name), "utf8");
5214
+ manifests[name] = await readFile4(join6(cwd, name), "utf8");
5019
5215
  } catch {
5020
5216
  }
5021
5217
  }
@@ -5192,7 +5388,7 @@ function startupSummary(f) {
5192
5388
 
5193
5389
  // src/tui/app.tsx
5194
5390
  import { execFileSync } from "child_process";
5195
- import { existsSync } from "fs";
5391
+ import { existsSync as existsSync2 } from "fs";
5196
5392
  import { jsx as jsx5 } from "react/jsx-runtime";
5197
5393
  async function runTui(opts) {
5198
5394
  const controller = new TuiController();
@@ -5242,7 +5438,7 @@ async function runTuiRepl(opts) {
5242
5438
  const chain = regFor(r).chain(r);
5243
5439
  return { name: r, model: chain[0] ?? "", models: chain, council: reviewNames.has(r), decider: councilNames.includes(r) };
5244
5440
  });
5245
- const fitness = deps0.fitness ?? new RoleFitness(join6(homedir2(), ".horsecode", "model-fitness.json"));
5441
+ const fitness = deps0.fitness ?? new RoleFitness(join7(homedir2(), ".horsecode", "model-fitness.json"));
5246
5442
  for (const r of [deps0.roleRegistry, deps0.teamRegistries.spec, deps0.teamRegistries.plan, deps0.teamRegistries.code, deps0.councilRegistry]) {
5247
5443
  r.setFitness(fitness);
5248
5444
  }
@@ -5373,7 +5569,7 @@ Question: ${question}` }
5373
5569
  };
5374
5570
  const addMcp = async (input) => {
5375
5571
  const { parseCommand, parseConfigBlock, extractFromPage, verify } = await import("./install-O34KMWJB.js");
5376
- const { saveMcpServer } = await import("./save-skills-X7U3KCPU.js");
5572
+ const { saveMcpServer } = await import("./save-skills-ZW5GY6KV.js");
5377
5573
  let cand = parseCommand(input) ?? parseConfigBlock(input);
5378
5574
  if (!cand) {
5379
5575
  const url = input.trim();
@@ -5671,7 +5867,7 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5671
5867
  rules: rulesFromMemory().length,
5672
5868
  memory: { total: all.length, rules: kindOf("rule"), lessons: kindOf("lesson"), facts: kindOf("fact") },
5673
5869
  skills: opts.listSkills?.().length ?? 0,
5674
- constitution: existsSync(join6(process.cwd(), ".specify", "memory", "constitution.md")),
5870
+ constitution: existsSync2(join7(process.cwd(), ".specify", "memory", "constitution.md")),
5675
5871
  graph: { built: false, nodes: 0 },
5676
5872
  traceRoot: traceRootRel(),
5677
5873
  ...(() => {
@@ -5754,7 +5950,7 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5754
5950
  process.cwd(),
5755
5951
  index,
5756
5952
  adoption,
5757
- async (f) => readFile5(join6(process.cwd(), f), "utf8").catch(() => void 0)
5953
+ async (f) => readFile5(join7(process.cwd(), f), "utf8").catch(() => void 0)
5758
5954
  );
5759
5955
  if (r.added) {
5760
5956
  index = r.index;
@@ -5880,6 +6076,7 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5880
6076
  setRoleModel: applyChainPersisted,
5881
6077
  listRoles,
5882
6078
  adjustRoles,
6079
+ modelsPanel: opts.modelsPanel,
5883
6080
  listSkills: opts.listSkills,
5884
6081
  updateSkills: opts.updateSkills,
5885
6082
  addSkill: opts.addSkill,
@@ -1,3 +1,6 @@
1
+ import {
2
+ CLI_KINDS
3
+ } from "./chunk-G45RWL7S.js";
1
4
  import {
2
5
  arrayField,
3
6
  objectField,
@@ -61,14 +64,33 @@ var fileSchema = z.object({
61
64
  council: z.object({ members: z.array(reviewerSchema) }).optional(),
62
65
  specKit: z.object({ version: z.string() }).optional(),
63
66
  modelSources: z.array(z.string()).optional(),
64
- // Logged-in profile directories, in spill order. A path each, never a credential.
67
+ /**
68
+ * Logged-in profile directories, in spill order. A path each, never a credential.
69
+ *
70
+ * Two things here were wrong in a way that could not be seen from this file, and both were found by
71
+ * connecting a real account.
72
+ *
73
+ * The kinds were spelled out — `["claude", "codex"]` — and every entry of a kind added since was
74
+ * rejected. `CLI_KINDS` is where they are declared, so this reads them rather than repeating them, and a
75
+ * fifth subscription cannot be half-added again.
76
+ *
77
+ * `configDir` was REQUIRED, and the signed-in default is precisely the entry that has none: `withAmbient`
78
+ * records it without a directory, deliberately, because Claude Code keeps that session in the Keychain
79
+ * and naming a directory switches it to file credentials. So the one entry written to stop a second
80
+ * account quietly retiring the first was itself unloadable.
81
+ *
82
+ * `.catch([])` bounds what a bad row can cost. The loader reads `parsed.success ? parsed.data : {}`, so
83
+ * a single rejected entry did not merely drop that account — it discarded the WHOLE global config.
84
+ * Measured on a live one: the file held 64 role chains and an API key, and with one z.ai entry present
85
+ * `loadConfig` returned zero roles and no key, silently, for every session since it was connected.
86
+ */
65
87
  accounts: z.array(z.object({
66
- kind: z.enum(["claude", "codex"]),
88
+ kind: z.enum(CLI_KINDS),
67
89
  name: z.string(),
68
- configDir: z.string(),
90
+ configDir: z.string().optional(),
69
91
  email: z.string().optional(),
70
92
  plan: z.string().optional()
71
- })).optional(),
93
+ })).catch([]).optional(),
72
94
  traceDir: z.string().optional(),
73
95
  // where /graph trace writes; empty = .horsecode/traces
74
96
  mainBranch: z.string().optional(),