@hizliemre/horse-code 0.1.1 → 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.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Implementation tasks run in parallel, each in its own worktree, and escalate thr
33
33
 
34
34
  Three constraints drove most of the design:
35
35
 
36
- **One session, one worktree.** A run never writes to the checkout you are working in. Every file an agent writes is committed as a `wip(…)` checkpoint, so a bad change is recoverable and the review always sees the whole diff — an unstaged file is a hole in the evidence.
36
+ **One session, one worktree.** A run never writes to the checkout you are working in — enforced, not intended: `merge`, `commit`, `reset`, `checkout` and the rest are refused outright at the project root, so a finished run hands you a branch and the command to bring it in, and the decision stays yours. Every file an agent writes is committed as a `wip(…)` checkpoint, so a bad change is recoverable and the review always sees the whole diff — an unstaged file is a hole in the evidence.
37
37
 
38
38
  **Every message an agent reads is a decision point.** A tool that answers "unknown tool: `view_file`" costs a full model turn to say nothing. So error messages name what exists, suggest the near miss, and say what to do next. Much of this repository is that: the difference between a syscall name and an answer.
39
39
 
@@ -11,8 +11,8 @@ import {
11
11
  restoreTerminal,
12
12
  runJob,
13
13
  sttySane
14
- } from "./chunk-2DGO2BUB.js";
15
- import "./chunk-F2IALVBU.js";
14
+ } from "./chunk-K2VERI5Q.js";
15
+ import "./chunk-3UYA3KUG.js";
16
16
  import {
17
17
  CODE_TEAM,
18
18
  DEFAULT_COUNCIL,
@@ -24,9 +24,9 @@ import {
24
24
  describeUnfinished,
25
25
  toSlug,
26
26
  unfinishedSessions
27
- } from "./chunk-3XVZXTB6.js";
27
+ } from "./chunk-RPVAIS3P.js";
28
28
  import "./chunk-QF4MP6BS.js";
29
- import "./chunk-HBSC2HT2.js";
29
+ import "./chunk-EQX7BQYN.js";
30
30
  import "./chunk-FGVJFMK5.js";
31
31
  import {
32
32
  saveRoleSkills
@@ -41,7 +41,7 @@ import {
41
41
  memoryNote,
42
42
  memoryState,
43
43
  walkFiles
44
- } from "./chunk-TOPZL5SU.js";
44
+ } from "./chunk-DRZSUQ7Q.js";
45
45
  import "./chunk-2SVAHH5N.js";
46
46
  import {
47
47
  estimateFreezeSeconds,
@@ -51,7 +51,7 @@ import {
51
51
  relTime,
52
52
  stripThinking,
53
53
  writeHeapSnapshot
54
- } from "./chunk-YILDXPSI.js";
54
+ } from "./chunk-VPAWRRHL.js";
55
55
  import "./chunk-DTWKSZXY.js";
56
56
  import {
57
57
  traceRootRel
@@ -5904,7 +5904,7 @@ _${saved ? `Saved to your config \u2014 future sessions start with these. ` : ""
5904
5904
  const cwd = process.cwd();
5905
5905
  const main = await recordedMainBranch(cwd) ?? await detectMainBranch(cwd, defaultGitRunner);
5906
5906
  if (!main) return;
5907
- const { ongoingWork, chooseOngoing } = await import("./ongoing-OV5XROTU.js");
5907
+ const { ongoingWork, chooseOngoing } = await import("./ongoing-XP6WXNI7.js");
5908
5908
  const open = await ongoingWork(defaultGitRunner, cwd, main);
5909
5909
  if (!open.length) return;
5910
5910
  const picked = await chooseOngoing(deps, read, open[0]?.language, open);
@@ -2,16 +2,16 @@ import {
2
2
  buildSkillTool,
3
3
  editFileTool,
4
4
  writeFileTool
5
- } from "./chunk-3XVZXTB6.js";
5
+ } from "./chunk-RPVAIS3P.js";
6
6
  import {
7
7
  globTool,
8
8
  grepTool,
9
9
  readFileTool
10
- } from "./chunk-TOPZL5SU.js";
10
+ } from "./chunk-DRZSUQ7Q.js";
11
11
  import {
12
12
  ToolRegistry,
13
13
  runStructuredRole
14
- } from "./chunk-YILDXPSI.js";
14
+ } from "./chunk-VPAWRRHL.js";
15
15
 
16
16
  // src/engine/language.ts
17
17
  function respondIn(language) {
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-2SVAHH5N.js";
6
6
  import {
7
7
  telemetry
8
- } from "./chunk-YILDXPSI.js";
8
+ } from "./chunk-VPAWRRHL.js";
9
9
  import {
10
10
  readBriefSync
11
11
  } from "./chunk-DTWKSZXY.js";
@@ -83,6 +83,23 @@ async function sameNameElsewhere(cwd, asked) {
83
83
  if (!hits.length) return "";
84
84
  return hits.length === 1 ? ` There is one file named \`${base}\` in this project, at \`${hits[0]}\` \u2014 read that if it is what you meant.` : ` Files named \`${base}\` in this project: ${hits.map((h) => `\`${h}\``).join(", ")}.`;
85
85
  }
86
+ var MAX_SIBLINGS = 12;
87
+ async function whatIsInThatDirectory(cwd, asked) {
88
+ const slash = asked.lastIndexOf("/");
89
+ if (slash <= 0) return "";
90
+ const dir = asked.slice(0, slash);
91
+ try {
92
+ const { readdir: readdir2 } = await import("fs/promises");
93
+ const entries = await readdir2(resolve(cwd, dir), { withFileTypes: true });
94
+ const names = entries.filter((e) => !e.name.startsWith(".")).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
95
+ if (!names.length) return ` \`${dir}/\` exists but is empty.`;
96
+ const shown = names.slice(0, MAX_SIBLINGS).map((n) => `\`${n}\``).join(", ");
97
+ const rest = names.length - Math.min(names.length, MAX_SIBLINGS);
98
+ return ` \`${dir}/\` does exist, and holds: ${shown}${rest > 0 ? `, and ${rest} more` : ""}.`;
99
+ } catch {
100
+ return "";
101
+ }
102
+ }
86
103
  function numbered(lines, startLine) {
87
104
  const width = String(startLine + lines.length - 1).length;
88
105
  return lines.map((l, i) => `${String(startLine + i).padStart(width, " ")} ${l}`).join("\n");
@@ -132,8 +149,12 @@ var readFileTool = {
132
149
  isError: true
133
150
  };
134
151
  }
135
- const elsewhere = said.includes("ENOENT") ? await sameNameElsewhere(ctx.cwd, args.path) : "";
136
- return { content: `read_file error: ${said}${elsewhere}`, isError: true };
152
+ let hint = "";
153
+ if (said.includes("ENOENT")) {
154
+ hint = await sameNameElsewhere(ctx.cwd, args.path);
155
+ if (!hint) hint = await whatIsInThatDirectory(ctx.cwd, args.path);
156
+ }
157
+ return { content: `read_file error: ${said}${hint}`, isError: true };
137
158
  }
138
159
  const all = raw.split("\n");
139
160
  if (args.offset === void 0 && args.limit === void 0 && raw.length <= MAX_READ_CHARS) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ToolRegistry,
3
3
  runStructuredRole
4
- } from "./chunk-YILDXPSI.js";
4
+ } from "./chunk-VPAWRRHL.js";
5
5
 
6
6
  // src/engine/user-language.ts
7
7
  import { z } from "zod";
@@ -4,7 +4,7 @@ import {
4
4
  normalizeQuestion,
5
5
  respondIn,
6
6
  writerRegistry
7
- } from "./chunk-F2IALVBU.js";
7
+ } from "./chunk-3UYA3KUG.js";
8
8
  import {
9
9
  Board,
10
10
  LONG_CALL_MS,
@@ -36,14 +36,14 @@ import {
36
36
  squashTask,
37
37
  worktreeState,
38
38
  writeFileTool
39
- } from "./chunk-3XVZXTB6.js";
39
+ } from "./chunk-RPVAIS3P.js";
40
40
  import {
41
41
  resolveMainBranch
42
42
  } from "./chunk-QF4MP6BS.js";
43
43
  import {
44
44
  askInUserLanguage,
45
45
  inUserLanguage
46
- } from "./chunk-HBSC2HT2.js";
46
+ } from "./chunk-EQX7BQYN.js";
47
47
  import {
48
48
  clearCheckpoint,
49
49
  isContinuePrompt,
@@ -71,7 +71,7 @@ import {
71
71
  relationStrength,
72
72
  supersedes,
73
73
  verifyAnchors
74
- } from "./chunk-TOPZL5SU.js";
74
+ } from "./chunk-DRZSUQ7Q.js";
75
75
  import {
76
76
  ToolRegistry,
77
77
  handedOver,
@@ -80,7 +80,7 @@ import {
80
80
  sanitizeForJson,
81
81
  stripThinking,
82
82
  telemetry
83
- } from "./chunk-YILDXPSI.js";
83
+ } from "./chunk-VPAWRRHL.js";
84
84
  import {
85
85
  TRACE_INDEX,
86
86
  mergeTraceIndexes,
@@ -372,7 +372,52 @@ var AnthropicDecoder = class {
372
372
  }
373
373
  };
374
374
 
375
+ // src/providers/transport.ts
376
+ var SAYS = {
377
+ ECONNREFUSED: (w) => `nothing is listening at ${w} \u2014 connection refused`,
378
+ ENOTFOUND: (w) => `${w} could not be resolved \u2014 no such host`,
379
+ EAI_AGAIN: (w) => `${w} could not be resolved right now \u2014 DNS is not answering`,
380
+ ECONNRESET: (w) => `${w} closed the connection`,
381
+ EPIPE: (w) => `${w} closed the connection while it was being written to`,
382
+ ETIMEDOUT: (w) => `${w} did not accept a connection in time`,
383
+ EHOSTUNREACH: (w) => `${w} is unreachable from this machine`,
384
+ ENETUNREACH: (w) => `${w} is unreachable \u2014 no route`,
385
+ CERT_HAS_EXPIRED: (w) => `${w} presented an expired TLS certificate`,
386
+ DEPTH_ZERO_SELF_SIGNED_CERT: (w) => `${w} presented a self-signed TLS certificate`,
387
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: (w) => `${w} presented a certificate that could not be verified`
388
+ };
389
+ function causeCode(e) {
390
+ const queue = [e];
391
+ for (let i = 0; i < queue.length && i < 32; i++) {
392
+ const cur = queue[i];
393
+ if (typeof cur !== "object" || cur === null) continue;
394
+ const code = cur.code;
395
+ if (typeof code === "string" && code) return code;
396
+ const errors = cur.errors;
397
+ if (Array.isArray(errors)) queue.push(...errors);
398
+ const cause = cur.cause;
399
+ if (cause !== void 0) queue.push(cause);
400
+ }
401
+ return void 0;
402
+ }
403
+ function origin(url) {
404
+ try {
405
+ return new URL(url).origin;
406
+ } catch {
407
+ return url;
408
+ }
409
+ }
410
+ function transportMessage(e, url) {
411
+ const said = e instanceof Error ? e.message : String(e);
412
+ const code = causeCode(e);
413
+ if (code === void 0) return said;
414
+ const where = origin(url);
415
+ const say = SAYS[code];
416
+ return say ? `${say(where)} (${code})` : `${where} could not be reached (${code})`;
417
+ }
418
+
375
419
  // src/providers/omniroute.ts
420
+ var UNPRODUCTIVE_BUDGET = 3;
376
421
  async function* withIdleTimeout(source, idleMs, onIdle) {
377
422
  const it = source[Symbol.asyncIterator]();
378
423
  for (; ; ) {
@@ -457,6 +502,11 @@ var OmniRouteProvider = class {
457
502
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
458
503
  const idleAc = new AbortController();
459
504
  const combined = AbortSignal.any([signal, idleAc.signal]);
505
+ let silent = false;
506
+ const firstByte = setTimeout(() => {
507
+ silent = true;
508
+ idleAc.abort();
509
+ }, this.idleMs);
460
510
  let res;
461
511
  try {
462
512
  res = await this.fetchFn(`${this.baseUrl}${native ? "/v1/messages" : "/api/v1/chat/completions"}`, {
@@ -467,7 +517,17 @@ var OmniRouteProvider = class {
467
517
  body: JSON.stringify(sanitizeForJson(native ? toAnthropicBody(req) : toOpenAIBody(req))),
468
518
  signal: combined
469
519
  });
520
+ clearTimeout(firstByte);
470
521
  } catch (e) {
522
+ clearTimeout(firstByte);
523
+ if (silent && !isCallerAbort(signal)) {
524
+ yield {
525
+ type: "error",
526
+ retryable: true,
527
+ message: `the model sent nothing at all for ${Math.round(this.idleMs / 1e3)}s \u2014 not even a response header`
528
+ };
529
+ return;
530
+ }
471
531
  if (isCallerAbort(signal)) {
472
532
  yield { type: "error", message: "cancelled", retryable: false };
473
533
  return;
@@ -476,7 +536,7 @@ var OmniRouteProvider = class {
476
536
  yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
477
537
  return;
478
538
  }
479
- yield { type: "error", message: e instanceof Error ? e.message : String(e), retryable: true };
539
+ yield { type: "error", message: transportMessage(e, this.baseUrl), retryable: true };
480
540
  return;
481
541
  }
482
542
  if (!res.ok) {
@@ -504,9 +564,17 @@ var OmniRouteProvider = class {
504
564
  let sawText = false;
505
565
  let usage;
506
566
  const billed = {};
567
+ let producedAt = Date.now();
568
+ const budget = this.idleMs * UNPRODUCTIVE_BUDGET;
569
+ const noProduction = () => Date.now() - producedAt > budget;
507
570
  try {
508
571
  for await (const line of withIdleTimeout(parseSSE(stream), this.idleMs, () => idleAc.abort())) {
572
+ if (noProduction()) {
573
+ idleAc.abort();
574
+ throw new Error(`omniroute: the stream stayed open for ${Math.round(budget / 1e3)}s without the model producing anything \u2014 aborted`);
575
+ }
509
576
  if (line.kind === "comment") {
577
+ producedAt = Date.now();
510
578
  const m = line.value.match(/^x-omniroute-tokens-(in|out)\s*=\s*(\d+)/i);
511
579
  if (m) billed[m[1] === "in" ? "in" : "out"] = Number(m[2]);
512
580
  continue;
@@ -517,7 +585,13 @@ var OmniRouteProvider = class {
517
585
  } catch {
518
586
  continue;
519
587
  }
588
+ const c = chunk;
589
+ if (c.usage !== void 0 || (c.choices ?? []).some((ch) => ch.finish_reason || Object.keys(ch.delta ?? {}).length > 0)) {
590
+ producedAt = Date.now();
591
+ }
520
592
  if (decoder) {
593
+ const type = chunk.type;
594
+ if (type !== void 0 && type !== "ping") producedAt = Date.now();
521
595
  for (const ev of decoder.push(chunk)) yield ev;
522
596
  continue;
523
597
  }
@@ -560,7 +634,7 @@ var OmniRouteProvider = class {
560
634
  yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
561
635
  return;
562
636
  }
563
- yield { type: "error", message: e instanceof Error ? e.message : String(e), retryable: true };
637
+ yield { type: "error", message: transportMessage(e, this.baseUrl), retryable: true };
564
638
  return;
565
639
  }
566
640
  const cut = sawText ? void 0 : [...toolCalls.values()].find((a) => !argumentsComplete(a.arguments));
@@ -3500,7 +3574,7 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3500
3574
  emitPhase("verify");
3501
3575
  const cwd = await documentWorkdir(process.cwd(), prompt, ensureWorktree, r.title);
3502
3576
  laneCheckpoint(cwd, "verify", resume, prompt, r);
3503
- const { runVerify, describeVerify, currentBranchOf } = await import("./verify-WQ3GHION.js");
3577
+ const { runVerify, describeVerify, currentBranchOf } = await import("./verify-3R7DGUSI.js");
3504
3578
  const branch = await currentBranchOf(cwd);
3505
3579
  const res = await runVerify({
3506
3580
  deps,
@@ -3543,11 +3617,11 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3543
3617
  if (!resume && !hasPreservedWork && routeIntent(r.intent) === "pipeline") {
3544
3618
  const cwd = workingIn?.() ?? process.cwd();
3545
3619
  emitPhase("sizing");
3546
- const { sizeRequest } = await import("./triage-2J3T5PVQ.js");
3620
+ const { sizeRequest } = await import("./triage-ES5OHZOS.js");
3547
3621
  const size = await sizeRequest(deps, cwd, r.refinedPrompt);
3548
3622
  let small = size.verdict === "small";
3549
3623
  if (size.verdict === "unsure") {
3550
- const { describeSizeDoubt } = await import("./triage-2J3T5PVQ.js");
3624
+ const { describeSizeDoubt } = await import("./triage-ES5OHZOS.js");
3551
3625
  const answer = await askInUserLanguage(
3552
3626
  deps,
3553
3627
  askUser,
@@ -3565,8 +3639,8 @@ Which is it?`,
3565
3639
  if (small) {
3566
3640
  emitPhase("small change");
3567
3641
  emit({ kind: "note", text: `\u26A1 Small change \u2014 ${size.reason}. No branch, no spec, no plan.` });
3568
- const { runSmallChange, describeSmallChange } = await import("./fix-HBBOTUWM.js");
3569
- const { currentBranchOf } = await import("./verify-WQ3GHION.js");
3642
+ const { runSmallChange, describeSmallChange } = await import("./fix-CMARU6JR.js");
3643
+ const { currentBranchOf } = await import("./verify-3R7DGUSI.js");
3570
3644
  const res = await runSmallChange(deps, cwd, r.title, r.refinedPrompt, size);
3571
3645
  return {
3572
3646
  intent: r.intent,
@@ -4370,14 +4444,7 @@ async function runJob(deps, opts) {
4370
4444
  }
4371
4445
  }
4372
4446
  if (!wave.pr && reviewable) {
4373
- const landed = await deps.manager.deliverLocally(session, opts.fromBranch);
4374
- if (landed.ok) {
4375
- wave.delivery.mergedInto = opts.fromBranch;
4376
- emit({ kind: "note", text: `\u{1F4E6} Merged into \`${opts.fromBranch}\` \u2014 the files are in your working copy.` });
4377
- } else {
4378
- wave.delivery.notMerged = landed.why;
4379
- emit({ kind: "note", text: `\u{1F4E6} Not merged (${landed.why}) \u2014 the work is on \`${wave.delivery.branch}\`.` });
4380
- }
4447
+ emit({ kind: "note", text: `\u{1F4E6} The work is on \`${wave.delivery.branch}\` \u2014 merge it when you are ready.` });
4381
4448
  }
4382
4449
  await curate(deps, up.refinedPrompt ?? opts.prompt, board.list(), deferredAll, session.baseWorktree);
4383
4450
  emit({ kind: "phase", phase: "report" });
@@ -2,7 +2,7 @@ import {
2
2
  Board,
3
3
  refreshAfterChange,
4
4
  runTaskCycle
5
- } from "./chunk-3XVZXTB6.js";
5
+ } from "./chunk-RPVAIS3P.js";
6
6
  import {
7
7
  defaultGitRunner
8
8
  } from "./chunk-IW2KBAVZ.js";
@@ -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,9 +34,9 @@ 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
@@ -60,7 +60,7 @@ import {
60
60
  import {
61
61
  InjectionLog,
62
62
  memoryNote
63
- } from "./chunk-TOPZL5SU.js";
63
+ } from "./chunk-DRZSUQ7Q.js";
64
64
  import "./chunk-2SVAHH5N.js";
65
65
  import {
66
66
  Telemetry,
@@ -70,7 +70,7 @@ import {
70
70
  stripThinking,
71
71
  telemetry,
72
72
  writeHeapSnapshot
73
- } from "./chunk-YILDXPSI.js";
73
+ } from "./chunk-VPAWRRHL.js";
74
74
  import {
75
75
  briefStatus
76
76
  } from "./chunk-DTWKSZXY.js";
@@ -825,16 +825,18 @@ var FileSink = class {
825
825
  return this.dropped;
826
826
  }
827
827
  /**
828
- * 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.
829
829
  *
830
- * `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
831
831
  * the constructor's `catch` does not run, `stream` is set, and whether the error handler has cleared it by
832
- * the time anything flushes is a race with the event loop. Lose that race and `end(cb)` is called on a
833
- * 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.
834
834
  *
835
- * The observer then outlives what it observes: `flush()` is awaited as a run finishes, so a machine that
836
- * cannot write the log cannot finish the run either. The `error` listener is the second exit, and either
837
- * 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.
838
840
  */
839
841
  async flush() {
840
842
  const s = this.stream;
@@ -959,12 +961,8 @@ function whereItLanded(path) {
959
961
  return `Written on ${branch ? `branch \`${branch}\`` : "its own branch"}, in the worktree at \`${base}\`. Review it there, then merge it in.`;
960
962
  }
961
963
  function describeDelivery(d) {
962
- if (d.mergedInto) {
963
- return `Merged into \`${d.mergedInto}\` \u2014 the files are in your working copy.`;
964
- }
965
964
  return [
966
965
  `**The work is on branch \`${d.branch}\`** \u2014 not in your working copy yet.`,
967
- d.notMerged ? `Not merged: ${d.notMerged}.` : "",
968
966
  "",
969
967
  "To bring it in:",
970
968
  "```",
@@ -973,6 +971,36 @@ function describeDelivery(d) {
973
971
  `Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
974
972
  ].filter((l) => l !== void 0).join("\n");
975
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
+ }
976
1004
  function renderResult(res) {
977
1005
  if (res.kind === "chat") return stripThinking(res.response);
978
1006
  if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
@@ -984,7 +1012,7 @@ function renderResult(res) {
984
1012
 
985
1013
  _${whereItLanded(res.path)}_` : `The constitution phase finished without writing \`${res.path}\` \u2014 nothing was changed.`;
986
1014
  }
987
- 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);
988
1016
  const rev = res.revision ? `
989
1017
  revision: ${res.revision.status}` : "";
990
1018
  return `${stripThinking(res.report)}
@@ -1150,7 +1178,7 @@ async function main(argv) {
1150
1178
  const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
1151
1179
  if (!args.prompt) {
1152
1180
  if (useTui) {
1153
- const { runTuiRepl } = await import("./app-UGFQKMLX.js");
1181
+ const { runTuiRepl } = await import("./app-LAJN3TWC.js");
1154
1182
  const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-5URG7C4J.js");
1155
1183
  const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
1156
1184
  const manualSources = config.modelSources.length > 0;
@@ -1284,13 +1312,10 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
1284
1312
  if (res.upToDate) bits.push(`${res.upToDate} already current`);
1285
1313
  if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
1286
1314
  if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
1287
- if (res.failed.length) {
1288
- bits.push(`
1315
+ const failures = describeTraceFailures(res.failed);
1316
+ return `${bits.join(" \xB7 ")}${failures ? `
1289
1317
 
1290
- \u26A0\uFE0F ${res.failed.length} failed:
1291
- ${res.failed.slice(0, 5).map((f) => `- \`${f.file}\` \u2014 ${f.error}`).join("\n")}`);
1292
- }
1293
- return `${bits.join(" \xB7 ")}
1318
+ ${failures}` : ""}
1294
1319
 
1295
1320
  _Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
1296
1321
  };
@@ -1333,7 +1358,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
1333
1358
  ...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
1334
1359
  };
1335
1360
  if (useTui) {
1336
- const { runTui } = await import("./app-UGFQKMLX.js");
1361
+ const { runTui } = await import("./app-LAJN3TWC.js");
1337
1362
  const res = await runTui({ buildDeps, job });
1338
1363
  console.log(renderResult(res));
1339
1364
  return;
@@ -1380,7 +1405,10 @@ if (isMainModule()) {
1380
1405
  });
1381
1406
  }
1382
1407
  export {
1408
+ DELIVERED_SHARE,
1383
1409
  describeDelivery,
1410
+ describeOutcome,
1411
+ describeTraceFailures,
1384
1412
  main,
1385
1413
  parseArgs,
1386
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
@@ -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.1",
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>",