@hizliemre/horse-code 0.1.0 → 0.1.2

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.
@@ -22,7 +22,7 @@ import {
22
22
  readFileTool,
23
23
  reinforceTouched,
24
24
  reinforceUsed
25
- } from "./chunk-TOPZL5SU.js";
25
+ } from "./chunk-DRZSUQ7Q.js";
26
26
  import {
27
27
  ToolRegistry,
28
28
  handedOver,
@@ -30,7 +30,7 @@ import {
30
30
  runToCompletion,
31
31
  telemetry,
32
32
  truncateSafe
33
- } from "./chunk-YILDXPSI.js";
33
+ } from "./chunk-VPAWRRHL.js";
34
34
  import {
35
35
  loadTraceIndex,
36
36
  pruneTraces,
@@ -262,6 +262,43 @@ function sessionName(now, taken) {
262
262
  }
263
263
  return `${day}_${Date.now()}`;
264
264
  }
265
+ var FORBIDDEN_AT_ROOT = /* @__PURE__ */ new Set([
266
+ "merge",
267
+ "rebase",
268
+ "cherry-pick",
269
+ "revert",
270
+ "reset",
271
+ "checkout",
272
+ "switch",
273
+ "restore",
274
+ "commit",
275
+ "am",
276
+ "apply",
277
+ "stash",
278
+ "pull",
279
+ "clean"
280
+ ]);
281
+ var TAKES_A_VALUE = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
282
+ function gitVerb(args) {
283
+ for (let i = 0; i < args.length; i++) {
284
+ const a = args[i];
285
+ if (a === void 0) continue;
286
+ if (!a.startsWith("-")) return a;
287
+ if (TAKES_A_VALUE.has(a)) i++;
288
+ }
289
+ return void 0;
290
+ }
291
+ function guardRoot(run, repoRoot) {
292
+ return async (args, cwd) => {
293
+ const verb = gitVerb(args);
294
+ if (cwd === repoRoot && verb !== void 0 && FORBIDDEN_AT_ROOT.has(verb)) {
295
+ throw new Error(
296
+ `refusing to run \`git ${verb}\` in the project checkout (${repoRoot}). A session's work stays on its own branch and in its own worktree; bringing it in is the user's decision, taken in their own time.`
297
+ );
298
+ }
299
+ return run(args, cwd);
300
+ };
301
+ }
265
302
  var WorktreeManager = class {
266
303
  repoRoot;
267
304
  /** The project checkout this manager was built for — where per-project settings and the remote live. */
@@ -281,12 +318,23 @@ var WorktreeManager = class {
281
318
  */
282
319
  worktreeHome;
283
320
  git;
321
+ /**
322
+ * The unguarded runner, for the one case that legitimately needs a forbidden verb: turning a directory
323
+ * that is not a repository into one.
324
+ *
325
+ * `git worktree add` needs a commit to branch from, and an empty repository has none — so a first commit
326
+ * is not delivery, it is the precondition for ever leaving the root alone again. It runs only when there
327
+ * is no HEAD, so there is no branch to disturb and no work to overwrite. Named rather than flagged, so
328
+ * grepping for it finds every use.
329
+ */
330
+ rawGit;
284
331
  /** Injectable clock: a session's NAME is the day it opened, so a test has to be able to say which day. */
285
332
  now;
286
333
  constructor(deps) {
287
334
  this.repoRoot = deps.repoRoot;
288
335
  this.worktreeHome = deps.worktreeHome ?? deps.repoRoot;
289
- this.git = deps.runGit ?? defaultGitRunner;
336
+ this.rawGit = deps.runGit ?? defaultGitRunner;
337
+ this.git = guardRoot(this.rawGit, deps.repoRoot);
290
338
  this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
291
339
  }
292
340
  /** Runs git; nonzero exit → throws a clear error. Returns output (stdout). */
@@ -318,7 +366,8 @@ var WorktreeManager = class {
318
366
  await this.ensureRepo();
319
367
  const head = await this.git(["rev-parse", "--verify", "--quiet", "HEAD"], this.repoRoot);
320
368
  if (head.code === 0) return;
321
- await this.run(["commit", "--allow-empty", "-m", "hc: initial commit"], this.repoRoot);
369
+ const r = await this.rawGit(["commit", "--allow-empty", "-m", "hc: initial commit"], this.repoRoot);
370
+ if (r.code !== 0) throw new Error(`git commit --allow-empty failed (${r.code}): ${(r.stderr || r.stdout).trim()}`);
322
371
  }
323
372
  /**
324
373
  * The ref to base the session's worktree on. Uses `fromBranch` when it resolves; otherwise falls back to
@@ -602,39 +651,6 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
602
651
  if (check.code !== 0) return;
603
652
  await this.run(["push", remote, session.baseBranch], session.baseWorktree);
604
653
  }
605
- /**
606
- * Lands the finished work on the branch the job started from, in the main working copy.
607
- *
608
- * Without this, a project with no git remote gets nothing: `push` is a no-op and a pull request has
609
- * nowhere to go, so every completed task sits on `hc/<job>/base` — invisible from the repository root.
610
- * A user who watched thirty tasks succeed then finds an empty directory and cannot run the project.
611
- *
612
- * A pull request is delivery when there is a remote to open it against. When there is not, merging is.
613
- *
614
- * Refuses rather than forces. A dirty working copy or a checkout on some other branch means the user has
615
- * something in progress, and overwriting that to deliver would be a worse failure than not delivering:
616
- * the branch still exists and the caller reports how to merge it by hand.
617
- */
618
- async deliverLocally(session, targetBranch) {
619
- const dirty = await this.git(["status", "--porcelain"], this.repoRoot);
620
- if (dirty.code !== 0) return { ok: false, why: "the repository could not be read" };
621
- if (dirty.stdout.split("\n").some((l) => l.trim() && !l.startsWith("??"))) {
622
- return { ok: false, why: "the working copy has uncommitted changes" };
623
- }
624
- const head = await this.git(["symbolic-ref", "--short", "HEAD"], this.repoRoot);
625
- const current = head.stdout.trim();
626
- if (head.code !== 0 || !current) return { ok: false, why: "the repository is not on a branch" };
627
- if (current !== targetBranch) return { ok: false, why: `the repository is on \`${current}\`, not \`${targetBranch}\`` };
628
- const count = await this.git(["rev-list", "--count", `${targetBranch}..${session.baseBranch}`], this.repoRoot);
629
- const commits = Number(count.stdout.trim()) || 0;
630
- if (!commits) return { ok: true, commits: 0 };
631
- const merged = await this.git(
632
- ["merge", "--no-ff", "-m", `hc: ${session.jobSlug}`, session.baseBranch],
633
- this.repoRoot
634
- );
635
- if (merged.code !== 0) return { ok: false, why: "the merge did not apply cleanly" };
636
- return { ok: true, commits };
637
- }
638
654
  async openPR(session, adapter, input) {
639
655
  const res = await adapter.createPR({
640
656
  branch: session.baseBranch,
@@ -2148,7 +2164,7 @@ function destroysWork(command) {
2148
2164
  for (const seg of command.split(/&&|\|\||;|\|/)) {
2149
2165
  const m = /^\s*git\s+(.*)$/.exec(seg.trim());
2150
2166
  if (!m) continue;
2151
- const rest = (m[1] ?? "").replace(/^(?:-\S+\s+)*/, "").trim();
2167
+ const rest = (m[1] ?? "").replace(/^(?:(?:-[cC]|--(?:git-dir|work-tree|namespace|exec-path|config-env))\s+\S+|-\S+)\s+/g, "").trim();
2152
2168
  const hit = DESTROYS_WORK.find((d) => d.re.test(rest));
2153
2169
  if (hit) return hit.what;
2154
2170
  }
@@ -1241,6 +1241,23 @@ var ToolRegistry = class {
1241
1241
  };
1242
1242
 
1243
1243
  // src/agent/structured.ts
1244
+ function valueAt(args, path) {
1245
+ let cur = args;
1246
+ for (const key of path) {
1247
+ if (typeof cur !== "object" || cur === null) return void 0;
1248
+ cur = cur[key];
1249
+ }
1250
+ return cur;
1251
+ }
1252
+ function whatWasWrong(issues, args) {
1253
+ return issues.map((i) => {
1254
+ const where = i.path.length ? i.path.join(".") : void 0;
1255
+ const got = valueAt(args, i.path);
1256
+ const shown = got === void 0 ? "nothing" : JSON.stringify(got);
1257
+ const head = where ? `${where}: ${i.message}` : i.message;
1258
+ return `${head} \u2014 got ${shown.length > 120 ? `${shown.slice(0, 120)}\u2026` : shown}`;
1259
+ }).join("; ");
1260
+ }
1244
1261
  function buildSubmitTool(schema) {
1245
1262
  let box;
1246
1263
  const tool = {
@@ -1251,10 +1268,7 @@ function buildSubmitTool(schema) {
1251
1268
  run: async (rawArgs) => {
1252
1269
  const parsed = schema.safeParse(rawArgs);
1253
1270
  if (!parsed.success) {
1254
- return {
1255
- content: `submit: invalid output: ${parsed.error.issues.map((i) => i.message).join("; ")}`,
1256
- isError: true
1257
- };
1271
+ return { content: `submit: invalid output: ${whatWasWrong(parsed.error.issues, rawArgs)}`, isError: true };
1258
1272
  }
1259
1273
  box = { value: parsed.data };
1260
1274
  return { content: "received", isError: false };
@@ -5,11 +5,11 @@ import {
5
5
  memoryHints,
6
6
  readFileTool,
7
7
  reinforceUsed
8
- } from "./chunk-TOPZL5SU.js";
8
+ } from "./chunk-DRZSUQ7Q.js";
9
9
  import {
10
10
  ToolRegistry,
11
11
  runStructuredRole
12
- } from "./chunk-YILDXPSI.js";
12
+ } from "./chunk-VPAWRRHL.js";
13
13
 
14
14
  // src/engine/triage.ts
15
15
  import { z } from "zod";
package/dist/cli.js CHANGED
@@ -14,11 +14,11 @@ import {
14
14
  restoreTerminal,
15
15
  runJob,
16
16
  sttySane
17
- } from "./chunk-2DGO2BUB.js";
17
+ } from "./chunk-K2VERI5Q.js";
18
18
  import {
19
19
  listOmniRouteModels
20
20
  } from "./chunk-O74BDQKS.js";
21
- import "./chunk-F2IALVBU.js";
21
+ import "./chunk-3UYA3KUG.js";
22
22
  import {
23
23
  CODE_TEAM,
24
24
  DEFAULT_COUNCIL,
@@ -34,16 +34,18 @@ import {
34
34
  buildTeamRegistry,
35
35
  mainWorktreeRoot,
36
36
  toSlug
37
- } from "./chunk-3XVZXTB6.js";
37
+ } from "./chunk-RPVAIS3P.js";
38
38
  import "./chunk-QF4MP6BS.js";
39
- import "./chunk-HBSC2HT2.js";
39
+ import "./chunk-EQX7BQYN.js";
40
40
  import "./chunk-FGVJFMK5.js";
41
41
  import {
42
42
  parseFrontmatter
43
43
  } from "./chunk-BY4DP7IE.js";
44
44
  import {
45
+ UNSET_MODEL,
46
+ loadConfig,
45
47
  saveSkillSource
46
- } from "./chunk-DKVIN43T.js";
48
+ } from "./chunk-4EWK7HWQ.js";
47
49
  import "./chunk-H2FDGPVW.js";
48
50
  import {
49
51
  buildBrief,
@@ -58,7 +60,7 @@ import {
58
60
  import {
59
61
  InjectionLog,
60
62
  memoryNote
61
- } from "./chunk-TOPZL5SU.js";
63
+ } from "./chunk-DRZSUQ7Q.js";
62
64
  import "./chunk-2SVAHH5N.js";
63
65
  import {
64
66
  Telemetry,
@@ -68,7 +70,7 @@ import {
68
70
  stripThinking,
69
71
  telemetry,
70
72
  writeHeapSnapshot
71
- } from "./chunk-YILDXPSI.js";
73
+ } from "./chunk-VPAWRRHL.js";
72
74
  import {
73
75
  briefStatus
74
76
  } from "./chunk-DTWKSZXY.js";
@@ -92,114 +94,6 @@ import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync
92
94
  import { join as join8, dirname as dirname4 } from "path";
93
95
  import { pathToFileURL } from "url";
94
96
 
95
- // src/config/config.ts
96
- import { z } from "zod";
97
- var UNSET_MODEL = "default";
98
- var DEFAULT_CONFIG = {
99
- baseUrl: "http://localhost:20128",
100
- model: UNSET_MODEL,
101
- // acceptEdits: auto-approve file writes/edits (the pipeline builds in an isolated worktree → reviewed as a
102
- // PR), still prompt for shell/exec. Keeps the automated build flowing without an approval per file.
103
- mode: "acceptEdits",
104
- allowlist: [],
105
- roles: {},
106
- specKit: { version: "v0.13.2" },
107
- mcp: {},
108
- modelSources: [],
109
- traceDir: "",
110
- skillSources: [],
111
- maxParallel: 8,
112
- telemetry: true
113
- };
114
- var reviewerSchema = z.object({ name: z.string(), perspective: z.string(), models: z.array(z.string()) });
115
- var fileSchema = z.object({
116
- apiKey: z.string().optional(),
117
- baseUrl: z.string().optional(),
118
- model: z.string().optional(),
119
- mode: z.enum(["ask", "acceptEdits", "auto"]).optional(),
120
- allowlist: z.array(z.string()).optional(),
121
- roles: z.record(
122
- z.string(),
123
- z.object({
124
- models: z.array(z.string()),
125
- systemPrompt: z.string().optional(),
126
- skills: z.array(z.string()).optional(),
127
- // Anthropic models only — see RoleConfig.effort. Unknown keys are stripped, so without this line a
128
- // level written into the config would be silently discarded on the way in.
129
- effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional()
130
- })
131
- ).optional(),
132
- // The review team's finder lenses, one set per stage (any omitted set falls back to the built-in default).
133
- team: z.object({
134
- spec: z.array(reviewerSchema).optional(),
135
- plan: z.array(reviewerSchema).optional(),
136
- code: z.array(reviewerSchema).optional()
137
- }).optional(),
138
- council: z.object({ members: z.array(reviewerSchema) }).optional(),
139
- specKit: z.object({ version: z.string() }).optional(),
140
- modelSources: z.array(z.string()).optional(),
141
- traceDir: z.string().optional(),
142
- // where /graph trace writes; empty = .horsecode/traces
143
- mainBranch: z.string().optional(),
144
- // the branch a resumed session syncs from; asked once, then remembered
145
- // Bounded: below 1 nothing runs; above 32 the git merge lock, not the models, becomes the limit.
146
- maxParallel: z.number().int().min(1).max(32).optional(),
147
- telemetry: z.boolean().optional(),
148
- skillSources: z.array(z.object({
149
- name: z.string(),
150
- repo: z.string(),
151
- path: z.string().optional(),
152
- ref: z.string().optional()
153
- })).optional(),
154
- mcp: z.record(
155
- z.string(),
156
- z.union([
157
- z.object({ command: z.array(z.string()).min(1), env: z.record(z.string(), z.string()).optional(), readOnly: z.boolean().optional() }),
158
- z.object({ url: z.string(), headers: z.record(z.string(), z.string()).optional(), readOnly: z.boolean().optional() })
159
- ])
160
- ).optional()
161
- }).partial();
162
- function parseFile(raw) {
163
- if (!raw) return {};
164
- try {
165
- const parsed = fileSchema.safeParse(JSON.parse(raw));
166
- return parsed.success ? parsed.data : {};
167
- } catch {
168
- return {};
169
- }
170
- }
171
- function loadConfig(opts) {
172
- const global = parseFile(opts.readFile(`${opts.home}/.horsecode/config.json`));
173
- const project = parseFile(opts.readFile(`${opts.cwd}/.horsecode/config.json`));
174
- const { apiKey: _leak, ...projectSafe } = project;
175
- const merged = {
176
- ...DEFAULT_CONFIG,
177
- ...global,
178
- ...projectSafe
179
- };
180
- merged.allowlist = projectSafe.allowlist ?? global.allowlist ?? [];
181
- merged.roles = { ...global.roles ?? {}, ...projectSafe.roles ?? {} };
182
- merged.mcp = { ...global.mcp ?? {}, ...projectSafe.mcp ?? {} };
183
- merged.modelSources = projectSafe.modelSources ?? global.modelSources ?? [];
184
- merged.maxParallel = projectSafe.maxParallel ?? global.maxParallel ?? DEFAULT_CONFIG.maxParallel;
185
- merged.telemetry = projectSafe.telemetry ?? global.telemetry ?? DEFAULT_CONFIG.telemetry;
186
- const byName = new Map((global.skillSources ?? []).map((s) => [s.name, s]));
187
- for (const s of projectSafe.skillSources ?? []) byName.set(s.name, s);
188
- merged.skillSources = [...byName.values()];
189
- merged.specKit = projectSafe.specKit ?? global.specKit ?? DEFAULT_CONFIG.specKit;
190
- const team = {
191
- spec: projectSafe.team?.spec ?? global.team?.spec,
192
- plan: projectSafe.team?.plan ?? global.team?.plan,
193
- code: projectSafe.team?.code ?? global.team?.code
194
- };
195
- merged.team = team.spec || team.plan || team.code ? team : void 0;
196
- const councilMembers = projectSafe.council?.members ?? global.council?.members;
197
- merged.council = councilMembers ? { members: councilMembers } : void 0;
198
- if (opts.env.OMNIROUTE_API_KEY) merged.apiKey = opts.env.OMNIROUTE_API_KEY;
199
- if (opts.env.OMNIROUTE_BASE_URL) merged.baseUrl = opts.env.OMNIROUTE_BASE_URL;
200
- return merged;
201
- }
202
-
203
97
  // src/skills/registry.ts
204
98
  import { readdir, readFile } from "fs/promises";
205
99
  import { join } from "path";
@@ -931,16 +825,18 @@ var FileSink = class {
931
825
  return this.dropped;
932
826
  }
933
827
  /**
934
- * A stream that failed to open never calls `end`'s callback, and this awaited it forever.
828
+ * `flush()` is awaited as a run finishes, so whatever happens here decides whether the run can end.
935
829
  *
936
- * `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick. So
830
+ * `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick, so
937
831
  * the constructor's `catch` does not run, `stream` is set, and whether the error handler has cleared it by
938
- * the time anything flushes is a race with the event loop. Lose that race and `end(cb)` is called on a
939
- * broken stream, the callback is never invoked, and the promise has nothing left to resolve it.
832
+ * the time anything flushes is a race with the event loop. Losing that race means `end(cb)` is called on a
833
+ * stream that never opened.
940
834
  *
941
- * The observer then outlives what it observes: `flush()` is awaited as a run finishes, so a machine that
942
- * cannot write the log cannot finish the run either. The `error` listener is the second exit, and either
943
- * one is enough.
835
+ * The `error` listener is a second exit for that case. It was added believing the callback would otherwise
836
+ * never fire and hang the run an honest guess that has since been TESTED and did not hold: with the
837
+ * listener removed, an EISDIR stream still resolves (test/obs/telemetry.test.ts). It stays because it
838
+ * costs one line and closes a path nothing else covers, not because a hang was ever reproduced through it.
839
+ * The 3h42m CI hang this was written during had a different cause, in the tests themselves.
944
840
  */
945
841
  async flush() {
946
842
  const s = this.stream;
@@ -1065,12 +961,8 @@ function whereItLanded(path) {
1065
961
  return `Written on ${branch ? `branch \`${branch}\`` : "its own branch"}, in the worktree at \`${base}\`. Review it there, then merge it in.`;
1066
962
  }
1067
963
  function describeDelivery(d) {
1068
- if (d.mergedInto) {
1069
- return `Merged into \`${d.mergedInto}\` \u2014 the files are in your working copy.`;
1070
- }
1071
964
  return [
1072
965
  `**The work is on branch \`${d.branch}\`** \u2014 not in your working copy yet.`,
1073
- d.notMerged ? `Not merged: ${d.notMerged}.` : "",
1074
966
  "",
1075
967
  "To bring it in:",
1076
968
  "```",
@@ -1079,6 +971,36 @@ function describeDelivery(d) {
1079
971
  `Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
1080
972
  ].filter((l) => l !== void 0).join("\n");
1081
973
  }
974
+ var DELIVERED_SHARE = 0.5;
975
+ function describeOutcome(w) {
976
+ if (w.status === "completed") return w.pr ? `PR: ${w.pr.url}` : "all tasks merged";
977
+ const stuck = w.failed.length + w.skipped.length;
978
+ const total = w.waves.flat().length || stuck;
979
+ const merged = Math.max(0, total - stuck);
980
+ const parts = [
981
+ w.failed.length ? `${w.failed.length} failed` : "",
982
+ w.skipped.length ? `${w.skipped.length} blocked behind them` : ""
983
+ ].filter(Boolean).join(", ");
984
+ const head = `${merged} of ${total} tasks merged`;
985
+ return merged / total < DELIVERED_SHARE ? `\u26A0\uFE0F ${head} \u2014 ${parts}. Most of the plan did not land; the feature is not built.` : `${head} \u2014 ${parts}.`;
986
+ }
987
+ function describeTraceFailures(failed) {
988
+ if (!failed.length) return "";
989
+ const byError = /* @__PURE__ */ new Map();
990
+ for (const f of failed) byError.set(f.error, [...byError.get(f.error) ?? [], f.file]);
991
+ if (byError.size === 1) {
992
+ const [error, files] = [...byError][0];
993
+ const shown = files.slice(0, 3).map((f) => `- \`${f}\``);
994
+ if (files.length > shown.length) shown.push(`- \u2026and ${files.length - shown.length} more`);
995
+ return `\u26A0\uFE0F ${failed.length} failed, all for the same reason \u2014 ${error}
996
+ ${shown.join("\n")}`;
997
+ }
998
+ const rows = [...byError].sort((a, b) => b[1].length - a[1].length).slice(0, 5).map(([error, files]) => `- ${files.length}\xD7 ${error} (e.g. \`${files[0]}\`)`);
999
+ const rest = byError.size - rows.length;
1000
+ if (rest > 0) rows.push(`- \u2026and ${rest} further cause(s)`);
1001
+ return `\u26A0\uFE0F ${failed.length} failed:
1002
+ ${rows.join("\n")}`;
1003
+ }
1082
1004
  function renderResult(res) {
1083
1005
  if (res.kind === "chat") return stripThinking(res.response);
1084
1006
  if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
@@ -1090,7 +1012,7 @@ function renderResult(res) {
1090
1012
 
1091
1013
  _${whereItLanded(res.path)}_` : `The constitution phase finished without writing \`${res.path}\` \u2014 nothing was changed.`;
1092
1014
  }
1093
- const outcome = res.wave.status === "completed" ? res.wave.pr ? `PR: ${res.wave.pr.url}` : "all tasks completed" : `Partial: ${res.wave.failed.length} failed, ${res.wave.skipped.length} skipped`;
1015
+ const outcome = describeOutcome(res.wave);
1094
1016
  const rev = res.revision ? `
1095
1017
  revision: ${res.revision.status}` : "";
1096
1018
  return `${stripThinking(res.report)}
@@ -1256,7 +1178,7 @@ async function main(argv) {
1256
1178
  const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
1257
1179
  if (!args.prompt) {
1258
1180
  if (useTui) {
1259
- const { runTuiRepl } = await import("./app-SB2L34JW.js");
1181
+ const { runTuiRepl } = await import("./app-LAJN3TWC.js");
1260
1182
  const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-5URG7C4J.js");
1261
1183
  const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
1262
1184
  const manualSources = config.modelSources.length > 0;
@@ -1390,13 +1312,10 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
1390
1312
  if (res.upToDate) bits.push(`${res.upToDate} already current`);
1391
1313
  if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
1392
1314
  if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
1393
- if (res.failed.length) {
1394
- bits.push(`
1315
+ const failures = describeTraceFailures(res.failed);
1316
+ return `${bits.join(" \xB7 ")}${failures ? `
1395
1317
 
1396
- \u26A0\uFE0F ${res.failed.length} failed:
1397
- ${res.failed.slice(0, 5).map((f) => `- \`${f.file}\` \u2014 ${f.error}`).join("\n")}`);
1398
- }
1399
- return `${bits.join(" \xB7 ")}
1318
+ ${failures}` : ""}
1400
1319
 
1401
1320
  _Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
1402
1321
  };
@@ -1439,7 +1358,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1439
1358
  ...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
1440
1359
  };
1441
1360
  if (useTui) {
1442
- const { runTui } = await import("./app-SB2L34JW.js");
1361
+ const { runTui } = await import("./app-LAJN3TWC.js");
1443
1362
  const res = await runTui({ buildDeps, job });
1444
1363
  console.log(renderResult(res));
1445
1364
  return;
@@ -1486,7 +1405,10 @@ if (isMainModule()) {
1486
1405
  });
1487
1406
  }
1488
1407
  export {
1408
+ DELIVERED_SHARE,
1489
1409
  describeDelivery,
1410
+ describeOutcome,
1411
+ describeTraceFailures,
1490
1412
  main,
1491
1413
  parseArgs,
1492
1414
  renderResult,
@@ -8,14 +8,14 @@ import {
8
8
  dirtyPaths,
9
9
  runFix,
10
10
  runSmallChange
11
- } from "./chunk-JWAEW7AJ.js";
12
- import "./chunk-3XVZXTB6.js";
11
+ } from "./chunk-KOWMHL23.js";
12
+ import "./chunk-RPVAIS3P.js";
13
13
  import "./chunk-FGVJFMK5.js";
14
14
  import "./chunk-NNTIACT4.js";
15
15
  import "./chunk-IW2KBAVZ.js";
16
- import "./chunk-TOPZL5SU.js";
16
+ import "./chunk-DRZSUQ7Q.js";
17
17
  import "./chunk-2SVAHH5N.js";
18
- import "./chunk-YILDXPSI.js";
18
+ import "./chunk-VPAWRRHL.js";
19
19
  import "./chunk-DTWKSZXY.js";
20
20
  import "./chunk-FFYBY2NA.js";
21
21
  import "./chunk-PGOYDOI4.js";
@@ -3,12 +3,12 @@ import {
3
3
  } from "./chunk-YBWTCXUS.js";
4
4
  import {
5
5
  askInUserLanguage
6
- } from "./chunk-HBSC2HT2.js";
6
+ } from "./chunk-EQX7BQYN.js";
7
7
  import {
8
8
  checkpointMtime,
9
9
  readCheckpoint
10
10
  } from "./chunk-FGVJFMK5.js";
11
- import "./chunk-YILDXPSI.js";
11
+ import "./chunk-VPAWRRHL.js";
12
12
  import "./chunk-B67BK5GQ.js";
13
13
 
14
14
  // src/engine/ongoing.ts
@@ -3,7 +3,7 @@ import {
3
3
  saveMcpServer,
4
4
  saveRoleSkills,
5
5
  saveSkillSource
6
- } from "./chunk-DKVIN43T.js";
6
+ } from "./chunk-4EWK7HWQ.js";
7
7
  import "./chunk-H2FDGPVW.js";
8
8
  export {
9
9
  removeSkillSource,
@@ -8,10 +8,10 @@ import {
8
8
  describeSizeDoubt,
9
9
  sizeRequest,
10
10
  triageFinding
11
- } from "./chunk-7TBYMFMG.js";
12
- import "./chunk-TOPZL5SU.js";
11
+ } from "./chunk-WYQBRCKY.js";
12
+ import "./chunk-DRZSUQ7Q.js";
13
13
  import "./chunk-2SVAHH5N.js";
14
- import "./chunk-YILDXPSI.js";
14
+ import "./chunk-VPAWRRHL.js";
15
15
  import "./chunk-DTWKSZXY.js";
16
16
  import "./chunk-FFYBY2NA.js";
17
17
  import "./chunk-PGOYDOI4.js";
@@ -3,11 +3,11 @@ import {
3
3
  describeFix,
4
4
  dirtyPaths,
5
5
  runFix
6
- } from "./chunk-JWAEW7AJ.js";
6
+ } from "./chunk-KOWMHL23.js";
7
7
  import {
8
8
  buildAskUserTool,
9
9
  respondIn
10
- } from "./chunk-F2IALVBU.js";
10
+ } from "./chunk-3UYA3KUG.js";
11
11
  import {
12
12
  buildRememberTool,
13
13
  buildSkillTool,
@@ -22,11 +22,11 @@ import {
22
22
  specsDir,
23
23
  verifyPaths,
24
24
  writeFileTool
25
- } from "./chunk-3XVZXTB6.js";
25
+ } from "./chunk-RPVAIS3P.js";
26
26
  import {
27
27
  askInUserLanguage,
28
28
  inUserLanguage
29
- } from "./chunk-HBSC2HT2.js";
29
+ } from "./chunk-EQX7BQYN.js";
30
30
  import "./chunk-FGVJFMK5.js";
31
31
  import "./chunk-NNTIACT4.js";
32
32
  import {
@@ -35,7 +35,7 @@ import {
35
35
  import {
36
36
  describeEscalation,
37
37
  triageFinding
38
- } from "./chunk-7TBYMFMG.js";
38
+ } from "./chunk-WYQBRCKY.js";
39
39
  import {
40
40
  BATCH_TOOLS_NOTE,
41
41
  contextTools,
@@ -45,13 +45,13 @@ import {
45
45
  projectToolsNote,
46
46
  readFileTool,
47
47
  reinforceUsed
48
- } from "./chunk-TOPZL5SU.js";
48
+ } from "./chunk-DRZSUQ7Q.js";
49
49
  import "./chunk-2SVAHH5N.js";
50
50
  import {
51
51
  ToolRegistry,
52
52
  handedOver,
53
53
  runToCompletion
54
- } from "./chunk-YILDXPSI.js";
54
+ } from "./chunk-VPAWRRHL.js";
55
55
  import "./chunk-DTWKSZXY.js";
56
56
  import "./chunk-FFYBY2NA.js";
57
57
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hizliemre/horse-code",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Terminal coding agent: one sentence to reviewed, committed code — in its own git worktree",
5
5
  "license": "MIT",
6
6
  "author": "Emre Hızlı <hizliemre@gmail.com>",
@@ -32,7 +32,8 @@
32
32
  "node": ">=20"
33
33
  },
34
34
  "files": [
35
- "dist"
35
+ "dist",
36
+ "skills"
36
37
  ],
37
38
  "scripts": {
38
39
  "build": "tsup",