@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.
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
 
@@ -108,6 +108,27 @@ Facts an agent learns — a command that only works from a subdirectory, a file
108
108
 
109
109
  Each role is asked, at the close of its turn, whether anything cost it more than one attempt. That question is the difference between a run that learns and one that rediscovers.
110
110
 
111
+ ## Skills
112
+
113
+ Six skills ship with the package. Each is a verbatim copy of its upstream, under `skills/`, in the same
114
+ format a project uses for its own — the only difference is who supplies them, and a project may replace any
115
+ of them by defining a skill of the same name in `.horsecode/skills/`.
116
+
117
+ | | |
118
+ |---|---|
119
+ | `brainstorming` | intent and requirements before implementation — bound to the brainstormer |
120
+ | `writing-plans` | what makes an individual task executable — bound to the task list |
121
+ | `test-driven-development` | inlined into every role that writes code |
122
+ | `frontend-design` | design direction, inlined into the design roles |
123
+ | `systematic-debugging` | fetched on demand, when something is stuck |
124
+ | `ui-ux-pro-max` | 161 palettes, 57 font pairings, 25 chart types across 10 stacks — fetched on demand |
125
+
126
+ A skill can also be **referenced** rather than copied, for the ones that are large, script-driven, or
127
+ maintained upstream. [`impeccable`](https://github.com/pbakaus/impeccable) ships that way: declared by
128
+ default, installed by `/skills update` into `~/.horsecode/skills/`, and updatable from its source. Startup
129
+ never waits on the network — installing is an explicit act. An explicitly stated `skillSources` list, an
130
+ empty one included, replaces the shipped default entirely.
131
+
111
132
  ## Tools an agent has
112
133
 
113
134
  `read_file` `write_file` `edit_file` `grep` `glob` `shell` `git` (read-only) `git_write` `web_fetch` `ask_user` `remember_fact` `propose_memory` `skill` `find_tool` `find_unfinished`, plus the graph tools (`graph_overview`, `graph_find`, `graph_context`, `graph_impact`, `graph_trace`) and every tool exposed by a connected MCP server.
@@ -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,13 +24,13 @@ 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
33
- } from "./chunk-DKVIN43T.js";
33
+ } from "./chunk-4EWK7HWQ.js";
34
34
  import {
35
35
  objectField,
36
36
  patchConfig
@@ -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
@@ -5570,7 +5570,7 @@ Question: ${question}` }
5570
5570
  };
5571
5571
  const addMcp = async (input) => {
5572
5572
  const { parseCommand, parseConfigBlock, extractFromPage, verify } = await import("./install-O34KMWJB.js");
5573
- const { saveMcpServer } = await import("./save-skills-OHYGVTQ4.js");
5573
+ const { saveMcpServer } = await import("./save-skills-NSLBU33X.js");
5574
5574
  let cand = parseCommand(input) ?? parseConfigBlock(input);
5575
5575
  if (!cand) {
5576
5576
  const url = input.trim();
@@ -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) {
@@ -0,0 +1,177 @@
1
+ import {
2
+ arrayField,
3
+ objectField,
4
+ patchConfig
5
+ } from "./chunk-H2FDGPVW.js";
6
+
7
+ // src/config/config.ts
8
+ import { z } from "zod";
9
+ var UNSET_MODEL = "default";
10
+ var DEFAULT_CONFIG = {
11
+ baseUrl: "http://localhost:20128",
12
+ model: UNSET_MODEL,
13
+ // acceptEdits: auto-approve file writes/edits (the pipeline builds in an isolated worktree → reviewed as a
14
+ // PR), still prompt for shell/exec. Keeps the automated build flowing without an approval per file.
15
+ mode: "acceptEdits",
16
+ allowlist: [],
17
+ roles: {},
18
+ specKit: { version: "v0.13.2" },
19
+ mcp: {},
20
+ modelSources: [],
21
+ traceDir: "",
22
+ /**
23
+ * Shipped as a REFERENCE, not a copy, for the reasons skills/README.md gives for exactly this shape: it is
24
+ * 3.3 MB across 154 files, it carries its own scripts, and it is maintained upstream. Vendoring it would
25
+ * freeze it at the commit it was taken on and turn every upstream fix into a manual merge — and it would
26
+ * multiply the published package by ten for a skill most runs never open.
27
+ *
28
+ * A default source is not an install. Startup stays offline; `/skills update` is the explicit act that
29
+ * fetches it, and a user who removes this entry from their own config is not overruled — the two configs
30
+ * merge by name.
31
+ */
32
+ skillSources: [{ name: "impeccable", repo: "pbakaus/impeccable", path: ".agents/skills/impeccable" }],
33
+ maxParallel: 8,
34
+ telemetry: true
35
+ };
36
+ var reviewerSchema = z.object({ name: z.string(), perspective: z.string(), models: z.array(z.string()) });
37
+ var fileSchema = z.object({
38
+ apiKey: z.string().optional(),
39
+ baseUrl: z.string().optional(),
40
+ model: z.string().optional(),
41
+ mode: z.enum(["ask", "acceptEdits", "auto"]).optional(),
42
+ allowlist: z.array(z.string()).optional(),
43
+ roles: z.record(
44
+ z.string(),
45
+ z.object({
46
+ models: z.array(z.string()),
47
+ systemPrompt: z.string().optional(),
48
+ skills: z.array(z.string()).optional(),
49
+ // Anthropic models only — see RoleConfig.effort. Unknown keys are stripped, so without this line a
50
+ // level written into the config would be silently discarded on the way in.
51
+ effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional()
52
+ })
53
+ ).optional(),
54
+ // The review team's finder lenses, one set per stage (any omitted set falls back to the built-in default).
55
+ team: z.object({
56
+ spec: z.array(reviewerSchema).optional(),
57
+ plan: z.array(reviewerSchema).optional(),
58
+ code: z.array(reviewerSchema).optional()
59
+ }).optional(),
60
+ council: z.object({ members: z.array(reviewerSchema) }).optional(),
61
+ specKit: z.object({ version: z.string() }).optional(),
62
+ modelSources: z.array(z.string()).optional(),
63
+ traceDir: z.string().optional(),
64
+ // where /graph trace writes; empty = .horsecode/traces
65
+ mainBranch: z.string().optional(),
66
+ // the branch a resumed session syncs from; asked once, then remembered
67
+ // Bounded: below 1 nothing runs; above 32 the git merge lock, not the models, becomes the limit.
68
+ maxParallel: z.number().int().min(1).max(32).optional(),
69
+ telemetry: z.boolean().optional(),
70
+ skillSources: z.array(z.object({
71
+ name: z.string(),
72
+ repo: z.string(),
73
+ path: z.string().optional(),
74
+ ref: z.string().optional()
75
+ })).optional(),
76
+ mcp: z.record(
77
+ z.string(),
78
+ z.union([
79
+ z.object({ command: z.array(z.string()).min(1), env: z.record(z.string(), z.string()).optional(), readOnly: z.boolean().optional() }),
80
+ z.object({ url: z.string(), headers: z.record(z.string(), z.string()).optional(), readOnly: z.boolean().optional() })
81
+ ])
82
+ ).optional()
83
+ }).partial();
84
+ function parseFile(raw) {
85
+ if (!raw) return {};
86
+ try {
87
+ const parsed = fileSchema.safeParse(JSON.parse(raw));
88
+ return parsed.success ? parsed.data : {};
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+ function loadConfig(opts) {
94
+ const global = parseFile(opts.readFile(`${opts.home}/.horsecode/config.json`));
95
+ const project = parseFile(opts.readFile(`${opts.cwd}/.horsecode/config.json`));
96
+ const { apiKey: _leak, ...projectSafe } = project;
97
+ const merged = {
98
+ ...DEFAULT_CONFIG,
99
+ ...global,
100
+ ...projectSafe
101
+ };
102
+ merged.allowlist = projectSafe.allowlist ?? global.allowlist ?? [];
103
+ merged.roles = { ...global.roles ?? {}, ...projectSafe.roles ?? {} };
104
+ merged.mcp = { ...global.mcp ?? {}, ...projectSafe.mcp ?? {} };
105
+ merged.modelSources = projectSafe.modelSources ?? global.modelSources ?? [];
106
+ merged.maxParallel = projectSafe.maxParallel ?? global.maxParallel ?? DEFAULT_CONFIG.maxParallel;
107
+ merged.telemetry = projectSafe.telemetry ?? global.telemetry ?? DEFAULT_CONFIG.telemetry;
108
+ const spoken = global.skillSources !== void 0 || projectSafe.skillSources !== void 0;
109
+ const byName = new Map((spoken ? [] : DEFAULT_CONFIG.skillSources).map((s) => [s.name, s]));
110
+ for (const s of global.skillSources ?? []) byName.set(s.name, s);
111
+ for (const s of projectSafe.skillSources ?? []) byName.set(s.name, s);
112
+ merged.skillSources = [...byName.values()];
113
+ merged.specKit = projectSafe.specKit ?? global.specKit ?? DEFAULT_CONFIG.specKit;
114
+ const team = {
115
+ spec: projectSafe.team?.spec ?? global.team?.spec,
116
+ plan: projectSafe.team?.plan ?? global.team?.plan,
117
+ code: projectSafe.team?.code ?? global.team?.code
118
+ };
119
+ merged.team = team.spec || team.plan || team.code ? team : void 0;
120
+ const councilMembers = projectSafe.council?.members ?? global.council?.members;
121
+ merged.council = councilMembers ? { members: councilMembers } : void 0;
122
+ if (opts.env.OMNIROUTE_API_KEY) merged.apiKey = opts.env.OMNIROUTE_API_KEY;
123
+ if (opts.env.OMNIROUTE_BASE_URL) merged.baseUrl = opts.env.OMNIROUTE_BASE_URL;
124
+ return merged;
125
+ }
126
+
127
+ // src/config/save-skills.ts
128
+ async function saveSkillSource(home, src) {
129
+ return patchConfig(home, (current) => {
130
+ if (current["skillSources"] === void 0) current = { ...current, skillSources: [...DEFAULT_CONFIG.skillSources] };
131
+ const sources = arrayField(current, "skillSources").filter(
132
+ (s) => !(typeof s === "object" && s !== null && s.name === src.name)
133
+ );
134
+ return { ...current, skillSources: [...sources, src] };
135
+ });
136
+ }
137
+ async function removeSkillSource(home, name) {
138
+ let found = false;
139
+ const ok = await patchConfig(home, (current) => {
140
+ const sources = arrayField(current, "skillSources");
141
+ const kept = sources.filter((s) => {
142
+ const match = typeof s === "object" && s !== null && s.name === name;
143
+ if (match) found = true;
144
+ return !match;
145
+ });
146
+ return found ? { ...current, skillSources: kept } : void 0;
147
+ });
148
+ return ok && found;
149
+ }
150
+ async function saveRoleSkills(home, assignments) {
151
+ const names = Object.keys(assignments);
152
+ if (!names.length) return 0;
153
+ const ok = await patchConfig(home, (current) => {
154
+ const roles = objectField(current, "roles");
155
+ for (const role of names) {
156
+ const prev = typeof roles[role] === "object" && roles[role] !== null ? roles[role] : {};
157
+ roles[role] = { ...prev, skills: [...assignments[role]] };
158
+ }
159
+ return { ...current, roles };
160
+ });
161
+ return ok ? names.length : 0;
162
+ }
163
+ async function saveMcpServer(home, name, spec) {
164
+ return patchConfig(home, (current) => ({
165
+ ...current,
166
+ mcp: { ...objectField(current, "mcp"), [name]: spec }
167
+ }));
168
+ }
169
+
170
+ export {
171
+ UNSET_MODEL,
172
+ loadConfig,
173
+ saveSkillSource,
174
+ removeSkillSource,
175
+ saveRoleSkills,
176
+ saveMcpServer
177
+ };
@@ -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";