@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.
- package/dist/{app-2GPGCDX6.js → app-5FXHE7GX.js} +223 -26
- package/dist/{chunk-KKWZBZYK.js → chunk-EAF22QIG.js} +26 -4
- package/dist/chunk-G45RWL7S.js +289 -0
- package/dist/{chunk-7JMWPTJ5.js → chunk-UEWVVN5L.js} +53 -265
- package/dist/{chunk-23CLQ2KO.js → chunk-XYZVZPAY.js} +4 -4
- package/dist/{chunk-5ZV42XGJ.js → chunk-YPZP7LYL.js} +1 -1
- package/dist/cli.js +360 -66
- package/dist/{fix-JOIXQFVP.js → fix-ONLA45HD.js} +3 -2
- package/dist/{save-skills-X7U3KCPU.js → save-skills-ZW5GY6KV.js} +2 -1
- package/dist/{verify-6SC4I77M.js → verify-LC57A6H2.js} +3 -2
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
restoreTerminal,
|
|
11
11
|
runJob,
|
|
12
12
|
sttySane
|
|
13
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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 =
|
|
3385
|
+
const dir = join3(homedir(), ".horsecode", "pastes");
|
|
3259
3386
|
await mkdir3(dir, { recursive: true });
|
|
3260
3387
|
const id = ++imageIdRef.current;
|
|
3261
|
-
const file =
|
|
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
|
|
4205
|
+
const progressLine2 = running && !state.pending;
|
|
4019
4206
|
const doneLine = !!state.meta && !state.meta.running && !state.pending;
|
|
4020
|
-
const showStatus =
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(),
|