@hizliemre/horse-code 0.1.2 → 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.
Files changed (38) hide show
  1. package/dist/{app-LAJN3TWC.js → app-5FXHE7GX.js} +354 -350
  2. package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
  3. package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
  4. package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
  5. package/dist/{chunk-VPAWRRHL.js → chunk-AE36LLL2.js} +31 -21
  6. package/dist/{chunk-4EWK7HWQ.js → chunk-EAF22QIG.js} +32 -0
  7. package/dist/chunk-G45RWL7S.js +289 -0
  8. package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
  9. package/dist/{chunk-DRZSUQ7Q.js → chunk-LLL7QWXB.js} +18 -7
  10. package/dist/{chunk-WYQBRCKY.js → chunk-LNW557IO.js} +2 -2
  11. package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
  12. package/dist/chunk-M2RKCIGV.js +11 -0
  13. package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
  14. package/dist/{chunk-RPVAIS3P.js → chunk-UEWVVN5L.js} +1511 -305
  15. package/dist/{chunk-EQX7BQYN.js → chunk-UGESK765.js} +1 -1
  16. package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
  17. package/dist/{chunk-K2VERI5Q.js → chunk-XYZVZPAY.js} +365 -666
  18. package/dist/{chunk-KOWMHL23.js → chunk-YPZP7LYL.js} +2 -2
  19. package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
  20. package/dist/cli.js +675 -95
  21. package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
  22. package/dist/fix-ONLA45HD.js +34 -0
  23. package/dist/git-QZTDZSJY.js +8 -0
  24. package/dist/{ongoing-XP6WXNI7.js → ongoing-WHYXPW24.js} +4 -4
  25. package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
  26. package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
  27. package/dist/{save-skills-NSLBU33X.js → save-skills-ZW5GY6KV.js} +2 -1
  28. package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
  29. package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
  30. package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
  31. package/dist/{triage-ES5OHZOS.js → triage-FCYHD2AQ.js} +8 -9
  32. package/dist/{verify-3R7DGUSI.js → verify-LC57A6H2.js} +19 -19
  33. package/package.json +1 -1
  34. package/dist/chunk-3UYA3KUG.js +0 -212
  35. package/dist/chunk-B67BK5GQ.js +0 -34
  36. package/dist/chunk-O74BDQKS.js +0 -28
  37. package/dist/fix-CMARU6JR.js +0 -34
  38. package/dist/git-VTSZALSR.js +0 -6
@@ -1,15 +1,12 @@
1
1
  import {
2
- buildAskUserTool,
3
- extractChoicesFrom,
4
- normalizeQuestion,
5
- respondIn,
6
- writerRegistry
7
- } from "./chunk-3UYA3KUG.js";
2
+ respondIn
3
+ } from "./chunk-M2RKCIGV.js";
8
4
  import {
9
5
  Board,
10
6
  LONG_CALL_MS,
11
7
  WHAT_IT_COST,
12
8
  applySkills,
9
+ buildAskUserTool,
13
10
  buildRememberTool,
14
11
  buildSkillTool,
15
12
  changedByMerge,
@@ -21,8 +18,10 @@ import {
21
18
  describeInherited,
22
19
  describeTopUp,
23
20
  editFileTool,
21
+ extractChoicesFrom,
24
22
  hasWorkAgainst,
25
23
  nextFeatureSlug,
24
+ normalizeQuestion,
26
25
  placedSkills,
27
26
  readOnlyRegistry,
28
27
  refreshAfterChange,
@@ -34,25 +33,27 @@ import {
34
33
  runReviewer,
35
34
  scaffoldFeature,
36
35
  squashTask,
36
+ subjectOf,
37
37
  worktreeState,
38
- writeFileTool
39
- } from "./chunk-RPVAIS3P.js";
38
+ writeFileTool,
39
+ writerRegistry
40
+ } from "./chunk-UEWVVN5L.js";
40
41
  import {
41
42
  resolveMainBranch
42
43
  } from "./chunk-QF4MP6BS.js";
43
44
  import {
44
45
  askInUserLanguage,
45
46
  inUserLanguage
46
- } from "./chunk-EQX7BQYN.js";
47
+ } from "./chunk-UGESK765.js";
47
48
  import {
48
49
  clearCheckpoint,
49
50
  isContinuePrompt,
50
51
  readCheckpoint,
51
52
  writeCheckpoint
52
- } from "./chunk-FGVJFMK5.js";
53
+ } from "./chunk-ZSQ24YDJ.js";
53
54
  import {
54
55
  defaultGitRunner
55
- } from "./chunk-IW2KBAVZ.js";
56
+ } from "./chunk-LPQU436C.js";
56
57
  import {
57
58
  BATCH_TOOLS_NOTE,
58
59
  SHORT_TTL_MS,
@@ -71,607 +72,32 @@ import {
71
72
  relationStrength,
72
73
  supersedes,
73
74
  verifyAnchors
74
- } from "./chunk-DRZSUQ7Q.js";
75
+ } from "./chunk-LLL7QWXB.js";
75
76
  import {
76
77
  ToolRegistry,
77
78
  handedOver,
78
79
  runStructuredRole,
79
80
  runToCompletion,
80
- sanitizeForJson,
81
81
  stripThinking,
82
82
  telemetry
83
- } from "./chunk-VPAWRRHL.js";
83
+ } from "./chunk-AE36LLL2.js";
84
84
  import {
85
85
  TRACE_INDEX,
86
86
  mergeTraceIndexes,
87
87
  parseTraceIndex,
88
88
  serializeTraceIndex,
89
89
  traceRootRel
90
- } from "./chunk-FFYBY2NA.js";
90
+ } from "./chunk-KAGKX2YT.js";
91
91
  import {
92
92
  loadGraphSync
93
- } from "./chunk-PGOYDOI4.js";
93
+ } from "./chunk-XEGQT5EN.js";
94
94
  import {
95
95
  inLinkedWorktree,
96
96
  sessionBase,
97
97
  stateRoot,
98
- writableStateRoot
99
- } from "./chunk-SSDLHWSF.js";
100
- import {
98
+ writableStateRoot,
101
99
  writeAtomic
102
- } from "./chunk-B67BK5GQ.js";
103
-
104
- // src/providers/sse.ts
105
- function* handleLine(line) {
106
- const trimmed = line.trim();
107
- if (trimmed.startsWith("data:")) {
108
- const payload = trimmed.slice(5).trim();
109
- if (payload === "[DONE]") return true;
110
- if (payload) yield { kind: "data", value: payload };
111
- return false;
112
- }
113
- if (trimmed.startsWith(":")) {
114
- const value = trimmed.slice(1).trim();
115
- if (value) yield { kind: "comment", value };
116
- }
117
- return false;
118
- }
119
- async function* parseSSE(body) {
120
- const reader = body.getReader();
121
- const decoder = new TextDecoder();
122
- let buffer = "";
123
- try {
124
- while (true) {
125
- const { value, done } = await reader.read();
126
- if (done) break;
127
- buffer += decoder.decode(value, { stream: true });
128
- let nl;
129
- while ((nl = buffer.indexOf("\n")) !== -1) {
130
- const line = buffer.slice(0, nl);
131
- buffer = buffer.slice(nl + 1);
132
- const isDone = yield* handleLine(line);
133
- if (isDone) return;
134
- }
135
- }
136
- if (buffer) {
137
- yield* handleLine(buffer);
138
- }
139
- } finally {
140
- reader.releaseLock();
141
- }
142
- }
143
-
144
- // src/providers/openai.ts
145
- function objectArgs(args) {
146
- if (!args || !args.trim()) return "{}";
147
- try {
148
- const v = JSON.parse(args);
149
- return v && typeof v === "object" && !Array.isArray(v) ? args : "{}";
150
- } catch {
151
- return "{}";
152
- }
153
- }
154
- function toOpenAIMessages(messages) {
155
- return messages.map((m) => {
156
- if (m.role === "assistant" && m.toolCalls?.length) {
157
- return {
158
- role: "assistant",
159
- content: m.content || null,
160
- tool_calls: m.toolCalls.map((tc) => ({
161
- id: tc.id,
162
- type: "function",
163
- function: { name: tc.name, arguments: objectArgs(tc.arguments) }
164
- }))
165
- };
166
- }
167
- if (m.role === "tool") {
168
- return { role: "tool", tool_call_id: m.toolCallId, content: m.content };
169
- }
170
- if (m.images?.length) {
171
- const parts = [];
172
- if (m.content) parts.push({ type: "text", text: m.content });
173
- for (const url of m.images) parts.push({ type: "image_url", image_url: { url } });
174
- const withImg = { role: m.role, content: parts };
175
- if (m.name) withImg.name = m.name;
176
- return withImg;
177
- }
178
- const base = { role: m.role, content: m.content };
179
- if (m.name) base.name = m.name;
180
- return base;
181
- });
182
- }
183
- function toOpenAITools(tools) {
184
- if (!tools.length) return void 0;
185
- return tools.map((t) => ({
186
- type: "function",
187
- function: { name: t.name, description: t.description, parameters: t.parameters }
188
- }));
189
- }
190
- function toOpenAIBody(req) {
191
- const body = {
192
- model: req.model,
193
- messages: toOpenAIMessages(req.messages),
194
- stream: true,
195
- // Ask the backend to append a final chunk carrying token usage (streaming otherwise omits it).
196
- stream_options: { include_usage: true }
197
- };
198
- const tools = toOpenAITools(req.tools);
199
- if (tools) {
200
- body.tools = tools;
201
- body.tool_choice = "auto";
202
- body.parallel_tool_calls = true;
203
- }
204
- return body;
205
- }
206
- function mapFinishReason(reason) {
207
- if (reason === "tool_calls") return "tool_calls";
208
- if (reason === "length") return "length";
209
- return "stop";
210
- }
211
-
212
- // src/providers/anthropic.ts
213
- var MAX_OUTPUT_TOKENS = 64e3;
214
- function isAnthropicModel(model) {
215
- return /(^|\/)(claude|fable|mythos)/i.test(model) || /claude/i.test(model);
216
- }
217
- function imageSource(uri) {
218
- const m = /^data:([^;,]+);base64,(.*)$/s.exec(uri);
219
- return m?.[1] && m[2] ? { type: "base64", media_type: m[1], data: m[2] } : void 0;
220
- }
221
- function toolInput(args) {
222
- if (!args?.trim()) return {};
223
- try {
224
- const v = JSON.parse(args);
225
- return v && typeof v === "object" && !Array.isArray(v) ? v : {};
226
- } catch {
227
- return {};
228
- }
229
- }
230
- function toAnthropicMessages(messages) {
231
- const system = [];
232
- const turns = [];
233
- const push = (role, blocks) => {
234
- if (!blocks.length) return;
235
- const last = turns[turns.length - 1];
236
- if (last && last.role === role) last.content.push(...blocks);
237
- else turns.push({ role, content: blocks });
238
- };
239
- for (const m of messages) {
240
- if (m.role === "system") {
241
- if (m.content.trim()) system.push(m.content);
242
- continue;
243
- }
244
- if (m.role === "tool") {
245
- push("user", [{
246
- type: "tool_result",
247
- tool_use_id: m.toolCallId ?? "",
248
- content: m.content || "(no output)"
249
- }]);
250
- continue;
251
- }
252
- const blocks = [];
253
- for (const uri of m.images ?? []) {
254
- const source = imageSource(uri);
255
- if (source) blocks.push({ type: "image", source });
256
- }
257
- if (m.content.trim()) blocks.push({ type: "text", text: m.content });
258
- for (const tc of m.toolCalls ?? []) {
259
- blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: toolInput(tc.arguments) });
260
- }
261
- push(m.role === "assistant" ? "assistant" : "user", blocks);
262
- }
263
- return { system: system.join("\n\n"), turns };
264
- }
265
- function toAnthropicBody(req) {
266
- const { system, turns } = toAnthropicMessages(req.messages);
267
- const body = {
268
- model: req.model,
269
- max_tokens: MAX_OUTPUT_TOKENS,
270
- stream: true,
271
- messages: turns
272
- };
273
- if (system) body.system = system;
274
- if (req.tools.length) {
275
- body.tools = req.tools.map((t) => ({
276
- name: t.name,
277
- description: t.description,
278
- input_schema: t.parameters
279
- }));
280
- }
281
- if (req.effort) body.output_config = { effort: req.effort };
282
- return body;
283
- }
284
- function mapStopReason(reason) {
285
- if (reason === "tool_use") return "tool_calls";
286
- if (reason === "max_tokens") return "length";
287
- return "stop";
288
- }
289
- var PROGRESS_EVERY = 64;
290
- function pathOf(partial) {
291
- const m = /"(?:path|file|filename|target)"\s*:\s*"((?:[^"\\]|\\.)*)/.exec(partial);
292
- return m?.[1] ? m[1].replace(/\\(.)/g, "$1") : void 0;
293
- }
294
- var AnthropicDecoder = class {
295
- blocks = /* @__PURE__ */ new Map();
296
- stop = "stop";
297
- inTokens = 0;
298
- outTokens = 0;
299
- cachedTokens = 0;
300
- sawUsage = false;
301
- push(chunk) {
302
- const c = chunk;
303
- const out = [];
304
- switch (c.type) {
305
- case "message_start": {
306
- const u = c.message?.usage;
307
- if (u) {
308
- this.sawUsage = true;
309
- this.inTokens = u.input_tokens ?? 0;
310
- this.cachedTokens = u.cache_read_input_tokens ?? 0;
311
- }
312
- break;
313
- }
314
- case "content_block_start": {
315
- const b = c.content_block;
316
- if (b && c.index !== void 0) {
317
- this.blocks.set(c.index, {
318
- type: b.type ?? "text",
319
- id: b.id ?? "",
320
- name: b.name ?? "",
321
- args: "",
322
- emitted: 0
323
- });
324
- }
325
- break;
326
- }
327
- case "content_block_delta": {
328
- const b = c.index === void 0 ? void 0 : this.blocks.get(c.index);
329
- if (c.delta?.type === "text_delta" && c.delta.text) {
330
- out.push({ type: "text-delta", text: c.delta.text });
331
- } else if (c.delta?.type === "input_json_delta" && b) {
332
- b.args += c.delta.partial_json ?? "";
333
- if (b.name && b.args.length - b.emitted >= PROGRESS_EVERY) {
334
- b.emitted = b.args.length;
335
- out.push({ type: "tool-progress", name: b.name, chars: b.args.length, path: pathOf(b.args) });
336
- }
337
- }
338
- break;
339
- }
340
- case "content_block_stop": {
341
- const b = c.index === void 0 ? void 0 : this.blocks.get(c.index);
342
- if (b?.type === "tool_use") {
343
- out.push({ type: "tool-call", toolCall: { id: b.id, name: b.name, arguments: b.args || "{}" } });
344
- }
345
- if (c.index !== void 0) this.blocks.delete(c.index);
346
- break;
347
- }
348
- case "message_delta": {
349
- if (c.delta?.stop_reason) this.stop = mapStopReason(c.delta.stop_reason);
350
- if (c.usage) {
351
- this.sawUsage = true;
352
- this.outTokens = c.usage.output_tokens ?? this.outTokens;
353
- if (!this.inTokens) this.inTokens = c.usage.input_tokens ?? 0;
354
- }
355
- break;
356
- }
357
- case "error": {
358
- out.push({ type: "error", message: c.error?.message ?? "anthropic: stream error", retryable: true });
359
- break;
360
- }
361
- default:
362
- break;
363
- }
364
- return out;
365
- }
366
- /** Usage as the transport should report it, or undefined when the stream never said. */
367
- usage() {
368
- return this.sawUsage ? { promptTokens: this.inTokens, completionTokens: this.outTokens, cachedTokens: this.cachedTokens } : void 0;
369
- }
370
- finishReason() {
371
- return this.stop;
372
- }
373
- };
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
-
419
- // src/providers/omniroute.ts
420
- var UNPRODUCTIVE_BUDGET = 3;
421
- async function* withIdleTimeout(source, idleMs, onIdle) {
422
- const it = source[Symbol.asyncIterator]();
423
- for (; ; ) {
424
- let timer;
425
- const idle = new Promise((resolve) => {
426
- timer = setTimeout(() => resolve("idle"), idleMs);
427
- });
428
- const next = it.next();
429
- next.catch(() => {
430
- });
431
- const winner = await Promise.race([next, idle]);
432
- if (timer) clearTimeout(timer);
433
- if (winner === "idle") {
434
- onIdle?.();
435
- void Promise.resolve(it.return?.(void 0)).catch(() => {
436
- });
437
- throw new Error(`omniroute: stream stalled (no data for ${Math.round(idleMs / 1e3)}s) \u2014 aborted`);
438
- }
439
- if (winner.done) return;
440
- yield winner.value;
441
- }
442
- }
443
- async function readErrorMessage(res) {
444
- let body;
445
- try {
446
- body = await res.json();
447
- } catch {
448
- return `omniroute ${res.status}`;
449
- }
450
- const err = body.error;
451
- if (typeof err === "string") return err;
452
- if (err && typeof err === "object" && "message" in err) {
453
- return String(err.message);
454
- }
455
- return `omniroute ${res.status}`;
456
- }
457
- function isRetryableStatus(status) {
458
- return status === 429 || status === 404 || status >= 500;
459
- }
460
- function isCapabilityError(message) {
461
- return /long[- ]context|not (yet )?available for this subscription|context[- ](length|window)|too many tokens|maximum context|unsupported|not supported/i.test(message);
462
- }
463
- function isUnknownModelError(message) {
464
- return /unable to determine provider for model|unknown model|model not found|no such model|invalid model/i.test(message);
465
- }
466
- function pathOf2(args) {
467
- return args.match(/"path"\s*:\s*"([^"\\]+)"/)?.[1];
468
- }
469
- function argumentsComplete(args) {
470
- if (!args.trim()) return true;
471
- try {
472
- JSON.parse(args);
473
- return true;
474
- } catch {
475
- return false;
476
- }
477
- }
478
- function isCallerAbort(signal) {
479
- return signal.aborted && signal.reason?.name !== "TimeoutError";
480
- }
481
- function isDeadline(signal) {
482
- return signal.aborted && signal.reason?.name === "TimeoutError";
483
- }
484
- var OmniRouteProvider = class {
485
- apiKey;
486
- baseUrl;
487
- fetchFn;
488
- idleMs;
489
- constructor(opts) {
490
- this.apiKey = opts.apiKey;
491
- this.baseUrl = opts.baseUrl.replace(/\/$/, "");
492
- this.fetchFn = opts.fetch ?? globalThis.fetch;
493
- this.idleMs = opts.idleTimeoutMs ?? 12e4;
494
- }
495
- async *chat(req, signal) {
496
- const native = isAnthropicModel(req.model);
497
- const headers = {
498
- "Content-Type": "application/json",
499
- Accept: "text/event-stream"
500
- };
501
- if (native) headers["anthropic-version"] = "2023-06-01";
502
- if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
503
- const idleAc = new AbortController();
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);
510
- let res;
511
- try {
512
- res = await this.fetchFn(`${this.baseUrl}${native ? "/v1/messages" : "/api/v1/chat/completions"}`, {
513
- method: "POST",
514
- headers,
515
- // Sanitised at the socket, not at each of the dozens of places that build a prompt — see
516
- // src/core/surrogates.ts for the four-hour run this cost.
517
- body: JSON.stringify(sanitizeForJson(native ? toAnthropicBody(req) : toOpenAIBody(req))),
518
- signal: combined
519
- });
520
- clearTimeout(firstByte);
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
- }
531
- if (isCallerAbort(signal)) {
532
- yield { type: "error", message: "cancelled", retryable: false };
533
- return;
534
- }
535
- if (isDeadline(signal)) {
536
- yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
537
- return;
538
- }
539
- yield { type: "error", message: transportMessage(e, this.baseUrl), retryable: true };
540
- return;
541
- }
542
- if (!res.ok) {
543
- const message = await readErrorMessage(res);
544
- const capability = isCapabilityError(message);
545
- const unknownModel = isUnknownModelError(message);
546
- yield {
547
- type: "error",
548
- message,
549
- retryable: isRetryableStatus(res.status) || capability || unknownModel,
550
- ...capability && { capability: true },
551
- ...unknownModel && { noBench: true }
552
- };
553
- return;
554
- }
555
- const stream = res.body;
556
- if (!stream) {
557
- yield { type: "error", message: "omniroute: empty response body" };
558
- return;
559
- }
560
- const decoder = native ? new AnthropicDecoder() : void 0;
561
- const toolCalls = /* @__PURE__ */ new Map();
562
- const lastProgress = /* @__PURE__ */ new Map();
563
- let finishReason = "stop";
564
- let sawText = false;
565
- let usage;
566
- const billed = {};
567
- let producedAt = Date.now();
568
- const budget = this.idleMs * UNPRODUCTIVE_BUDGET;
569
- const noProduction = () => Date.now() - producedAt > budget;
570
- try {
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
- }
576
- if (line.kind === "comment") {
577
- producedAt = Date.now();
578
- const m = line.value.match(/^x-omniroute-tokens-(in|out)\s*=\s*(\d+)/i);
579
- if (m) billed[m[1] === "in" ? "in" : "out"] = Number(m[2]);
580
- continue;
581
- }
582
- let chunk;
583
- try {
584
- chunk = JSON.parse(line.value);
585
- } catch {
586
- continue;
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
- }
592
- if (decoder) {
593
- const type = chunk.type;
594
- if (type !== void 0 && type !== "ping") producedAt = Date.now();
595
- for (const ev of decoder.push(chunk)) yield ev;
596
- continue;
597
- }
598
- const u = chunk.usage;
599
- if (u) usage = {
600
- promptTokens: u.prompt_tokens ?? 0,
601
- completionTokens: u.completion_tokens ?? 0,
602
- cachedTokens: u.prompt_tokens_details?.cached_tokens ?? 0
603
- };
604
- const choice = chunk?.choices?.[0];
605
- if (!choice) continue;
606
- const delta = choice.delta ?? {};
607
- if (typeof delta.content === "string" && delta.content.length) {
608
- sawText = true;
609
- yield { type: "text-delta", text: delta.content };
610
- }
611
- const deltaCalls = delta.tool_calls;
612
- if (Array.isArray(deltaCalls)) {
613
- for (const tc of deltaCalls) {
614
- const idx = tc.index ?? 0;
615
- const acc = toolCalls.get(idx) ?? { id: "", name: "", arguments: "" };
616
- if (tc.id) acc.id = tc.id;
617
- if (tc.function?.name) acc.name = tc.function.name;
618
- if (tc.function?.arguments) acc.arguments += tc.function.arguments;
619
- toolCalls.set(idx, acc);
620
- if (acc.name && acc.arguments.length - (lastProgress.get(idx) ?? 0) >= 64) {
621
- lastProgress.set(idx, acc.arguments.length);
622
- yield { type: "tool-progress", name: acc.name, chars: acc.arguments.length, path: pathOf2(acc.arguments) };
623
- }
624
- }
625
- }
626
- if (choice.finish_reason) finishReason = mapFinishReason(choice.finish_reason);
627
- }
628
- } catch (e) {
629
- if (isCallerAbort(signal)) {
630
- yield { type: "error", message: "cancelled", retryable: false };
631
- return;
632
- }
633
- if (isDeadline(signal)) {
634
- yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
635
- return;
636
- }
637
- yield { type: "error", message: transportMessage(e, this.baseUrl), retryable: true };
638
- return;
639
- }
640
- const cut = sawText ? void 0 : [...toolCalls.values()].find((a) => !argumentsComplete(a.arguments));
641
- if (cut) {
642
- yield {
643
- type: "error",
644
- message: `the stream ended in the middle of ${cut.name || "a tool call"}'s arguments`,
645
- retryable: true
646
- };
647
- return;
648
- }
649
- for (const acc of toolCalls.values()) {
650
- const toolCall = { id: acc.id, name: acc.name, arguments: acc.arguments };
651
- yield { type: "tool-call", toolCall };
652
- }
653
- if (decoder) {
654
- finishReason = decoder.finishReason();
655
- usage = decoder.usage() ?? usage;
656
- }
657
- if (billed.in !== void 0 || billed.out !== void 0) {
658
- yield { type: "usage", promptTokens: billed.in ?? 0, completionTokens: billed.out ?? 0 };
659
- } else if (usage) {
660
- yield { type: "usage", promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, cachedTokens: usage.cachedTokens };
661
- } else {
662
- const inHeader = res.headers.get("X-OmniRoute-Tokens-In");
663
- const outHeader = res.headers.get("X-OmniRoute-Tokens-Out");
664
- if (inHeader !== null || outHeader !== null) {
665
- yield {
666
- type: "usage",
667
- promptTokens: Number(inHeader) || 0,
668
- completionTokens: Number(outHeader) || 0
669
- };
670
- }
671
- }
672
- yield { type: "done", finishReason };
673
- }
674
- };
100
+ } from "./chunk-6W4UH2BQ.js";
675
101
 
676
102
  // src/adapters/pr.ts
677
103
  import { spawn } from "child_process";
@@ -986,8 +412,12 @@ function makePRAdapter(opts) {
986
412
  // src/engine/council.ts
987
413
  import { z } from "zod";
988
414
  var ArchitectPlanSchema = z.object({
989
- rootCause: z.string(),
990
- plan: z.array(z.string())
415
+ rootCause: z.string().describe(
416
+ "Why the earlier attempts failed \u2014 the underlying reason, not a summary of what they tried. One paragraph, naming the file, symbol or assumption that is actually wrong."
417
+ ),
418
+ plan: z.array(z.string()).describe(
419
+ 'The steps to fix it, as an ARRAY OF STRINGS \u2014 one instruction per element, in the order they should be carried out. Each names what to change and where, e.g. "add IOrderDeliveryPipe to src/domain/Suppliers/Abstractions/". Not prose, not a single string, not a document: the senior implementer executes these one at a time.'
420
+ )
991
421
  });
992
422
  async function runEscalationCouncil(deps, board, taskId, cwd, family) {
993
423
  const task = board.get(taskId);
@@ -1055,6 +485,127 @@ Identify the root cause and produce a concrete plan.`
1055
485
  return v;
1056
486
  }
1057
487
 
488
+ // src/core/failures.ts
489
+ function isProviderOutage(message) {
490
+ return /no active credentials for provider:?\s*[\w.-]+/i.test(message) || /provider\s+'?[\w.-]+'?\s+is not configured/i.test(message) || /all\s+[\w.-]+\s+accounts have exhausted their quota/i.test(message) || /shared egress ip quota exhausted/i.test(message);
491
+ }
492
+ function isUnknownModelError(message) {
493
+ return /unable to determine provider for model|unknown model|model not found|no such model|invalid model/i.test(message);
494
+ }
495
+ function isCatalogRejection(message) {
496
+ return /not (?:currently )?available in the [^.]{0,40}catalog/i.test(message);
497
+ }
498
+
499
+ // src/engine/split-card.ts
500
+ import { z as z2 } from "zod";
501
+ var SPLIT_AFTER_ATTEMPTS = 5;
502
+ var MIN_PIECES = 2;
503
+ var MAX_PIECES = 4;
504
+ function failureSubjects(card) {
505
+ const seen = [];
506
+ for (const h of card.stageHistory) {
507
+ if (h.action !== "reviewed:fail") continue;
508
+ const s = subjectOf(String(h.note ?? ""));
509
+ if (s && !seen.includes(s)) seen.push(s);
510
+ }
511
+ return seen;
512
+ }
513
+ function shouldSplit(card, after = SPLIT_AFTER_ATTEMPTS) {
514
+ return !wasSplit(card) && reviewFailures(card) >= after;
515
+ }
516
+ function wasSplit(card) {
517
+ return card.stageHistory.some((h) => h.action === "split:into");
518
+ }
519
+ function reviewFailures(card) {
520
+ return card.stageHistory.filter((h) => h.action === "reviewed:fail").length;
521
+ }
522
+ var PiecesSchema = z2.object({
523
+ pieces: z2.array(z2.object({
524
+ title: z2.string().describe("What this piece delivers, in the same voice as the original card's title."),
525
+ acceptance: z2.array(z2.string()).default([]).describe(
526
+ "What must be OBSERVABLY true when this piece is done \u2014 checkable against the worktree."
527
+ ),
528
+ files: z2.array(z2.string()).default([]).describe(
529
+ "Repo-relative files this piece creates or modifies. Two pieces that write the same file are not independent, so say so honestly rather than spreading one file across several."
530
+ )
531
+ }))
532
+ });
533
+ function splitRequest(card) {
534
+ const subjects = failureSubjects(card);
535
+ const notes = card.reviewNotes.slice(0, 12).map((n) => `- ${n}`).join("\n");
536
+ return (
537
+ // `attempts` is reset each run; the lifetime count is the number that means anything to a reader.
538
+ `This task has failed review ${reviewFailures(card)} times and is being CUT UP rather than attempted again.
539
+
540
+ Task: ${card.title}
541
+ ` + (card.acceptance.length ? `
542
+ It must still deliver:
543
+ ${card.acceptance.map((a) => `- ${a}`).join("\n")}
544
+ ` : "") + (subjects.length ? `
545
+ Its review failures have named these areas, which is the evidence of where the seams are:
546
+ ${subjects.map((s) => `- ${s}`).join("\n")}
547
+ ` : "") + (notes ? `
548
+ The findings still open:
549
+ ${notes}
550
+ ` : "") + `
551
+ Cut it into ${MIN_PIECES}-${MAX_PIECES} pieces that together deliver exactly what the original did \u2014 no more, no less. Cut along the seams above: a second concern, a second layer, a second story. Each piece must be something one reviewer can judge whole, and must leave the repository different on its own. Do not invent work the original did not ask for, and do not leave any of its acceptance criteria unclaimed.
552
+ Return {pieces} via submit.`
553
+ );
554
+ }
555
+ async function proposeSplit(opts, card) {
556
+ const { pieces } = await runStructuredRole(
557
+ { ...opts, messages: [...opts.messages, { role: "user", content: splitRequest(card) }] },
558
+ PiecesSchema
559
+ );
560
+ return pieces.slice(0, MAX_PIECES);
561
+ }
562
+ function applySplit(board, parentId, pieces) {
563
+ if (pieces.length < MIN_PIECES) return [];
564
+ const parent = board.get(parentId);
565
+ if (!parent) return [];
566
+ const ids = [];
567
+ let previous;
568
+ for (const [i, piece] of pieces.entries()) {
569
+ const id = freeId(board, parentId, i);
570
+ board.addCard({
571
+ id,
572
+ title: piece.title,
573
+ // The first piece inherits what the parent waited for; each later one waits for the piece before it.
574
+ deps: previous ? [previous] : [...parent.deps],
575
+ acceptance: piece.acceptance,
576
+ files: piece.files
577
+ });
578
+ board.appendStage(id, { role: "team-lead", action: "split:from", note: `${parentId} \u2014 ${parent.title}` });
579
+ ids.push(id);
580
+ previous = id;
581
+ }
582
+ const last = ids[ids.length - 1];
583
+ for (const c of board.list()) {
584
+ if (c.id === parentId || ids.includes(c.id) || !c.deps.includes(parentId)) continue;
585
+ board.removeDep(c.id, parentId);
586
+ board.addDep(c.id, last);
587
+ }
588
+ board.appendStage(parentId, {
589
+ role: "team-lead",
590
+ action: "split:into",
591
+ // The LIFETIME count, for the same reason the brief uses it: `attempts` is reset each run, so the record
592
+ // of why this card was cut would have read "0 attempts" on the very run that cut it. It did.
593
+ note: `${reviewFailures(parent)} review failures over ${failureSubjects(parent).length} areas \u2192 ${ids.join(", ")}`
594
+ });
595
+ board.move(parentId, "ABANDONED", "team-lead");
596
+ return ids;
597
+ }
598
+ function freeId(board, parentId, index) {
599
+ for (let n = index; n < index + 26; n++) {
600
+ const id = `${parentId}${String.fromCharCode(97 + n)}`;
601
+ if (!board.get(id)) return id;
602
+ }
603
+ return `${parentId}-${index}`;
604
+ }
605
+ function humanAbandoned(card) {
606
+ return card.column === "ABANDONED" && card.stageHistory.some((h) => h.action === "human:abandon");
607
+ }
608
+
1058
609
  // src/engine/escalation.ts
1059
610
  function autonomousAskHuman(maxRetries = 2) {
1060
611
  const retries = /* @__PURE__ */ new Map();
@@ -1082,16 +633,47 @@ function noChangeStreak(task) {
1082
633
  function tierOf(attempts, rounds) {
1083
634
  return attempts < rounds ? 0 : attempts < 2 * rounds ? 1 : 2;
1084
635
  }
636
+ function isFleetFailure(message) {
637
+ return isCatalogRejection(message) || isProviderOutage(message) || isUnknownModelError(message) || /capacity is (?:temporarily unavailable|busy)|retry shortly/i.test(message) || /did not accept a connection in time|upstream request failed/i.test(message) || /is an image-generation model|only available hosted/i.test(message) || /not (?:yet )?available for this subscription/i.test(message);
638
+ }
639
+ var FLEET_PATIENCE = 3;
640
+ function consecutiveFleetFailures(board, taskId) {
641
+ const h = board.get(taskId)?.stageHistory ?? [];
642
+ let n = 0;
643
+ for (let i = h.length - 1; i >= 0; i--) {
644
+ const { action, note } = h[i];
645
+ if (action === "attempt-error") {
646
+ if (!isFleetFailure(note ?? "")) break;
647
+ n += 1;
648
+ continue;
649
+ }
650
+ if (action.startsWith("\u2192")) continue;
651
+ break;
652
+ }
653
+ return n;
654
+ }
1085
655
  function attemptError(board, taskId, role, e) {
1086
656
  const msg = e instanceof Error ? e.message : String(e);
657
+ const fleet = isFleetFailure(msg);
1087
658
  board.appendStage(taskId, { role, action: "attempt-error", note: msg });
1088
- board.addReviewNote(taskId, `The previous attempt did not finish (${msg}). Complete the task within the turn budget.`);
659
+ board.addReviewNote(taskId, fleet ? `The previous attempt never reached a model (${msg}). Nothing is known about the work yet \u2014 start it fresh.` : `The previous attempt did not finish (${msg}). Complete the task within the turn budget.`);
1089
660
  board.move(taskId, "TODO", role);
1090
- return { verdict: "fail", notes: [msg] };
661
+ if (!fleet) return { verdict: "fail", notes: [msg] };
662
+ return consecutiveFleetFailures(board, taskId) >= FLEET_PATIENCE ? { verdict: "fail", notes: [msg], fleetDown: true } : { verdict: "fail", notes: [msg], fleetFailure: true };
1091
663
  }
1092
664
  async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1093
665
  const task = board.get(taskId);
1094
666
  if (!task) throw new Error(`runTaskWithEscalation: unknown task: ${taskId}`);
667
+ if (shouldSplit(task) && deps.splitCard) {
668
+ const pieces = await deps.splitCard(task);
669
+ const ids = applySplit(board, taskId, pieces);
670
+ if (ids.length) {
671
+ const note = `${reviewFailures(task)} review failures over ${failureSubjects(task).length} areas \u2192 split into ${ids.join(", ")}`;
672
+ deps.note?.(`\u2702\uFE0F **${taskId}** ${note}`);
673
+ return { verdict: "fail", notes: [note], split: ids };
674
+ }
675
+ board.appendStage(taskId, { role: "team-lead", action: "split:declined", note: "nothing separable found" });
676
+ }
1095
677
  const noChangeCount = noChangeStreak(task);
1096
678
  if (noChangeCount >= 3) {
1097
679
  const notes = ["Three attempts produced no file changes. The task needs a human decision before another agent is dispatched."];
@@ -1134,6 +716,8 @@ async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1134
716
  v2 = attemptError(board, taskId, role, e);
1135
717
  }
1136
718
  if (v2.verdict === "pass") return v2;
719
+ if (v2.fleetDown) return v2;
720
+ if (v2.fleetFailure) continue;
1137
721
  if (v2.noProgress) {
1138
722
  const tierStart = Math.floor(attempts / deps.rounds) * deps.rounds;
1139
723
  const target = attempts === tierStart ? attempts + 1 : tierStart + deps.rounds;
@@ -1179,13 +763,13 @@ async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1179
763
  }
1180
764
 
1181
765
  // src/engine/pr-summary.ts
1182
- import { z as z2 } from "zod";
766
+ import { z as z3 } from "zod";
1183
767
  var MAX_EXAMPLES = 12;
1184
768
  var MERGED_PREFIX = /^Merged PR \d+:\s*/;
1185
769
  var PR_SUMMARY_MAX_TURNS = 3;
1186
- var PRSummarySchema = z2.object({
1187
- title: z2.string().min(1),
1188
- body: z2.string().min(1)
770
+ var PRSummarySchema = z3.object({
771
+ title: z3.string().min(1),
772
+ body: z3.string().min(1)
1189
773
  });
1190
774
  async function recentSubjects(git, cwd, base) {
1191
775
  const read = async (args) => {
@@ -1262,19 +846,19 @@ async function prSummary(deps, opts) {
1262
846
  }
1263
847
 
1264
848
  // src/engine/revision.ts
1265
- import { z as z3 } from "zod";
849
+ import { z as z4 } from "zod";
1266
850
  var REVISION_CARD = "__revision__";
1267
- var PrincipalReviewSchema = z3.object({
1268
- decision: z3.enum(["approve", "request-changes"]).describe(
851
+ var PrincipalReviewSchema = z4.object({
852
+ decision: z4.enum(["approve", "request-changes"]).describe(
1269
853
  "`request-changes` only for what must change before merge; each comment is then applied by another agent and answered on the pull request. `approve` when nothing left would block the merge."
1270
854
  ),
1271
- comments: z3.array(z3.string())
855
+ comments: z4.array(z4.string())
1272
856
  });
1273
- var PrincipalFinalSchema = z3.object({
1274
- decision: z3.enum(["accept", "ask-human"]).describe(
857
+ var PrincipalFinalSchema = z4.object({
858
+ decision: z4.enum(["accept", "ask-human"]).describe(
1275
859
  "The rounds are over and findings remain. `accept`: they can merge as they stand. `ask-human`: only a person can settle it \u2014 this stops the run, so use it when the remaining finding is a real decision, not when it is merely unfinished."
1276
860
  ),
1277
- question: z3.string()
861
+ question: z4.string()
1278
862
  });
1279
863
  async function worktreeState2(deps, base) {
1280
864
  const git = deps.git ?? defaultGitRunner;
@@ -1518,6 +1102,10 @@ async function runWaveTask(deps, session, board, taskId, slot = 0) {
1518
1102
  return { status: "task-failed", task: tw };
1519
1103
  }
1520
1104
  deps.signal.throwIfAborted();
1105
+ if (v.verdict === "fail" && v.fleetDown) {
1106
+ board.appendStage(taskId, { role: "team-lead", action: "fleet-down", note: v.notes[0]?.slice(0, 200) });
1107
+ return { status: "fleet-down", task: tw };
1108
+ }
1521
1109
  if (v.verdict === "fail") {
1522
1110
  board.appendStage(taskId, { role: "team-lead", action: "task-failed" });
1523
1111
  board.move(taskId, "ABANDONED", "team-lead");
@@ -1792,7 +1380,7 @@ ${card.reviewNotes.map((n) => `- ${n}`).join("\n")}` : "";
1792
1380
  }
1793
1381
 
1794
1382
  // src/engine/team-lead.ts
1795
- import { z as z4 } from "zod";
1383
+ import { z as z5 } from "zod";
1796
1384
 
1797
1385
  // src/engine/waves.ts
1798
1386
  function computeWaves(board) {
@@ -1884,10 +1472,10 @@ function describeWaves(s) {
1884
1472
  }
1885
1473
 
1886
1474
  // src/engine/team-lead.ts
1887
- var fix = z4.object({ task: z4.string(), needs: z4.string(), why: z4.string() });
1888
- var DepAuditSchema = z4.object({
1889
- missing: z4.array(fix).default([]),
1890
- spurious: z4.array(fix).default([])
1475
+ var fix = z5.object({ task: z5.string(), needs: z5.string(), why: z5.string() });
1476
+ var DepAuditSchema = z5.object({
1477
+ missing: z5.array(fix).default([]),
1478
+ spurious: z5.array(fix).default([])
1891
1479
  });
1892
1480
  var AUDIT_TIMEOUT_MS = 9e4;
1893
1481
  var MAX_AUDIT_CARDS = 40;
@@ -2018,11 +1606,12 @@ async function runReady(deps, session, board) {
2018
1606
  return h.slice(since + 1).filter((e) => e.action === "conflict:resolve-failed").length;
2019
1607
  };
2020
1608
  const restartsSoFar = (cardId) => (board.get(cardId)?.stageHistory ?? []).filter((e) => e.action === "restarted").length;
2021
- const pending = new Set(cards.filter((c) => !done.has(c.id)).map((c) => c.id));
1609
+ const pending = new Set(cards.filter((c) => !done.has(c.id) && !humanAbandoned(c)).map((c) => c.id));
2022
1610
  for (const id of pending) {
2023
1611
  const c = board.get(id);
2024
1612
  if (!c || c.attempts === 0) continue;
2025
1613
  board.resetAttempts(id);
1614
+ if (c.column === "ABANDONED") board.reopen(id);
2026
1615
  }
2027
1616
  const busy = /* @__PURE__ */ new Set();
2028
1617
  const running = /* @__PURE__ */ new Map();
@@ -2037,7 +1626,7 @@ async function runReady(deps, session, board) {
2037
1626
  board.appendStage(id, {
2038
1627
  role: "team-lead",
2039
1628
  action: "parked",
2040
- note: reason === "waiting" ? `waiting for ${on}` : reason === "conflict" ? "merge conflicted" : "ladder exhausted"
1629
+ note: reason === "waiting" ? `waiting for ${on}` : reason === "conflict" ? "merge conflicted" : reason === "fleet" ? "no model could be reached" : "ladder exhausted"
2041
1630
  });
2042
1631
  board.move(id, "PARKED", "team-lead");
2043
1632
  };
@@ -2045,7 +1634,7 @@ async function runReady(deps, session, board) {
2045
1634
  let woken = 0;
2046
1635
  for (const [id, p] of [...parked]) {
2047
1636
  if (p.wakes >= MAX_WAKES) continue;
2048
- const ready = p.reason === "waiting" ? p.on !== void 0 && done.has(p.on) : merged.length > p.mergedAt;
1637
+ const ready = p.reason === "waiting" ? p.on !== void 0 && done.has(p.on) : p.reason === "fleet" ? true : merged.length > p.mergedAt;
2049
1638
  if (!ready) continue;
2050
1639
  parked.delete(id);
2051
1640
  pending.add(id);
@@ -2069,7 +1658,27 @@ async function runReady(deps, session, board) {
2069
1658
  }
2070
1659
  }
2071
1660
  };
1661
+ const known = /* @__PURE__ */ new Set([...pending, ...done]);
1662
+ const absorbNewCards = () => {
1663
+ for (const c of board.list()) {
1664
+ if (wasSplit(c) || humanAbandoned(c)) {
1665
+ pending.delete(c.id);
1666
+ parked.delete(c.id);
1667
+ known.add(c.id);
1668
+ continue;
1669
+ }
1670
+ if (c.id === REVISION_CARD || known.has(c.id)) continue;
1671
+ known.add(c.id);
1672
+ if (c.column === "MERGED") {
1673
+ done.add(c.id);
1674
+ continue;
1675
+ }
1676
+ pending.add(c.id);
1677
+ deps.note?.(`\u2795 **${c.id}** ${c.title}`);
1678
+ }
1679
+ };
2072
1680
  while (pending.size > 0 || running.size > 0 || parked.size > 0) {
1681
+ absorbNewCards();
2073
1682
  parkUnreachable();
2074
1683
  const limit = ceiling();
2075
1684
  for (const id of [...pending]) {
@@ -2142,7 +1751,12 @@ async function runReady(deps, session, board) {
2142
1751
  });
2143
1752
  park(id, "conflict", void 0, mergedAtStart);
2144
1753
  }
2145
- } else park(id, res.status === "conflict" ? "conflict" : "exhausted", void 0, mergedAtStart);
1754
+ } else park(
1755
+ id,
1756
+ res.status === "conflict" ? "conflict" : res.status === "fleet-down" ? "fleet" : "exhausted",
1757
+ void 0,
1758
+ mergedAtStart
1759
+ );
2146
1760
  } finally {
2147
1761
  for (const f of files) busy.delete(f);
2148
1762
  free.push(slot);
@@ -2180,7 +1794,7 @@ async function runReady(deps, session, board) {
2180
1794
  }
2181
1795
  if (rewrote > 0) continue;
2182
1796
  for (const [id, p] of parked) {
2183
- (p.reason === "waiting" ? skipped : failed).push(id);
1797
+ (p.reason === "waiting" || p.reason === "fleet" ? skipped : failed).push(id);
2184
1798
  board.appendStage(id, {
2185
1799
  role: "team-lead",
2186
1800
  action: "abandoned",
@@ -2974,6 +2588,31 @@ ${USAGE_FILE}
2974
2588
  }
2975
2589
  };
2976
2590
 
2591
+ // src/engine/tally.ts
2592
+ var DELIVERED_SHARE = 0.5;
2593
+ function describeTally(t) {
2594
+ const total = t.merged + t.failed + t.blocked + t.unfinished;
2595
+ if (!total) return "no tasks were planned";
2596
+ const parts = [
2597
+ t.failed ? `${t.failed} failed` : "",
2598
+ t.blocked ? `${t.blocked} blocked behind them` : "",
2599
+ t.unfinished ? `${t.unfinished} still open` : ""
2600
+ ].filter(Boolean).join(", ");
2601
+ const head = `${t.merged} of ${total} tasks merged`;
2602
+ if (t.merged === total) return head;
2603
+ return t.merged / total < DELIVERED_SHARE ? `\u26A0\uFE0F ${head} \u2014 ${parts}. Most of the plan did not land; the feature is not built.` : `${head} \u2014 ${parts}.`;
2604
+ }
2605
+ function tallyBoard(cards, revisionCardId = "__revision__") {
2606
+ const t = { merged: 0, failed: 0, blocked: 0, unfinished: 0 };
2607
+ for (const c of cards) {
2608
+ if (c.id === revisionCardId) continue;
2609
+ if (c.column === "MERGED") t.merged++;
2610
+ else if (c.column === "ABANDONED") (c.attempts ?? 0) > 0 ? t.failed++ : t.blocked++;
2611
+ else t.unfinished++;
2612
+ }
2613
+ return t;
2614
+ }
2615
+
2977
2616
  // src/engine/turn-effect.ts
2978
2617
  import { readFile as readFile3, mkdir as mkdir2 } from "fs/promises";
2979
2618
  import { existsSync as existsSync2 } from "fs";
@@ -3080,9 +2719,9 @@ import { existsSync as existsSync6 } from "fs";
3080
2719
  import { relative as relative3, dirname as dirname4 } from "path";
3081
2720
 
3082
2721
  // src/engine/refiner.ts
3083
- import { z as z5 } from "zod";
3084
- var RefinerSchema = z5.object({
3085
- refinedPrompt: z5.string().describe("The refined instruction, ALWAYS in English \u2014 translate from the user's language if needed; never output the user's original language here."),
2722
+ import { z as z6 } from "zod";
2723
+ var RefinerSchema = z6.object({
2724
+ refinedPrompt: z6.string().describe("The refined instruction, ALWAYS in English \u2014 translate from the user's language if needed; never output the user's original language here."),
3086
2725
  /**
3087
2726
  * The choice that decides what a run COSTS, described where it is made.
3088
2727
  *
@@ -3094,13 +2733,13 @@ var RefinerSchema = z5.object({
3094
2733
  * needs no specification. The prompt's own words for `verify` describe that request exactly; they were just
3095
2734
  * nowhere near the field being filled in.
3096
2735
  */
3097
- intent: z5.enum(["chat", "feature", "bugfix", "govern", "undo", "verify"]).describe(
2736
+ intent: z6.enum(["chat", "feature", "bugfix", "govern", "undo", "verify"]).describe(
3098
2737
  "What the request PRODUCES, not what it mentions. `verify`: a record of what EXISTING software DID \u2014 running scenarios, querying the database or logs for evidence, writing or extending a test report. Anything whose output is findings rather than changed behaviour is verify, including a follow-up that only adds more evidence to a report already being written. `feature`: new or changed behaviour in the product. `bugfix`: existing behaviour is wrong and must be corrected. `govern`: the output is a governing document (constitution, conventions), no source changes. `undo`: reverse what the previous turn did. `chat`: a question or conversation, nothing to build. A verify request never needs a specification or a plan: if the answer is 'run it and write down what happened', it is verify."
3099
2738
  ),
3100
2739
  // The natural language the user wrote in (English name, e.g. "Turkish") → the coach replies in it.
3101
- language: z5.string().default("English"),
2740
+ language: z6.string().default("English"),
3102
2741
  // A concise 2-5 word English kebab-case summary → used as the worktree/branch name (e.g. "add-login-page").
3103
- title: z5.string().default("task")
2742
+ title: z6.string().default("task")
3104
2743
  });
3105
2744
  async function runRefiner(deps, prompt, history = []) {
3106
2745
  const resolved = deps.roleRegistry.resolve("refiner");
@@ -3419,7 +3058,11 @@ ${carryOver.map((c) => `- ${c}`).join("\n")}` : "";
3419
3058
  This codebase ALREADY EXISTS. The template below is written for a project being created from scratch, so its Setup and Foundational phases describe work that was done years ago here, and its Polish phase lists things that are not tasks. Take the template's FORMAT \u2014 ids, [P] markers, file paths, phases \u2014 and let the plan decide what is actually in it.
3420
3059
  Every task must leave the repository DIFFERENT \u2014 code, a test, a document. Looking at something is not a task: an implementer reads the code, checks the versions and finds its way around as part of doing the work, so "verify X", "inspect Y", "confirm Z" belong inside the task that needs the answer, not beside it. If the only thing a task would deliver is knowing something, it is not on the list.
3421
3060
  Nor is running a command: linting, formatting, building and typechecking are how a task is known to be FINISHED. Put them in the acceptance criteria of the tasks that changed the code \u2014 the implementer runs them there anyway \u2014 instead of giving each one a task of its own.
3422
- And one file is usually one task. Tasks that write the same file cannot run at the same time, so splitting a single file across several of them buys nothing and pays for a full implement-and-review round each time. Split by what is genuinely independent, not by what is separately describable \u2014 the template's "one entity per task" examples are about creating new files, not changing existing ones.
3061
+ Size a task to a coherent piece of BEHAVIOUR someone can review as a whole, not to a file. ONE entity with its configuration, its migration and its tests is one task: nobody can review those apart and none of them is deliverable alone.
3062
+ But ONE entity, not the data model: a task nobody can hold in their head at once comes back from review again and again, and each return costs another full round of the team. Measured on one board, by review calls spent: 2 files 35, 3 files 45, 6 files 40, 19 files 116 across six attempts.
3063
+ Do not count files. What costs is BREADTH: how many separate decisions a reviewer must hold at once. A card that adds one entity end to end is narrow even across several files; a card that touches four files belonging to four different concerns is broad, and it was a four-file card that cost the most per file on that board. Split along the seam a reviewer would look for anyway \u2014 a second concern, a second layer, a second story \u2014 not at a file boundary.
3064
+ Every task carries a full implement-and-review round whatever it holds, so splitting finer does not divide that cost \u2014 but a task nobody can review in one sitting does not avoid it either, it pays it repeatedly.
3065
+ Never split one file across two tasks, and that includes across PHASES \u2014 creating a file empty in Setup and filling it in later is one task, not two. Split only where the parts are genuinely independent: they can be reviewed and merged on their own, or they must run at the same time in different worktrees. "They are separately describable" is not independence, and the template's "one entity per task" examples are about creating new files, not about sizing work in a repository that already exists.
3423
3066
  Follow this template:
3424
3067
 
3425
3068
  ${template}
@@ -3436,10 +3079,10 @@ async function runTasks(p, paths, carryOver) {
3436
3079
  // src/speckit/clarify.ts
3437
3080
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3438
3081
  import { relative as relative2 } from "path";
3439
- import { z as z6 } from "zod";
3440
- var ClarifyStepSchema = z6.object({
3082
+ import { z as z7 } from "zod";
3083
+ var ClarifyStepSchema = z7.object({
3441
3084
  // The single most important clarifying question, or null when the spec is sufficiently clear.
3442
- nextQuestion: z6.string().nullable()
3085
+ nextQuestion: z7.string().nullable()
3443
3086
  });
3444
3087
  async function runClarify(p, paths, maxRounds = 5) {
3445
3088
  const specRel = relative2(p.workdir, paths.spec);
@@ -3574,7 +3217,7 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3574
3217
  emitPhase("verify");
3575
3218
  const cwd = await documentWorkdir(process.cwd(), prompt, ensureWorktree, r.title);
3576
3219
  laneCheckpoint(cwd, "verify", resume, prompt, r);
3577
- const { runVerify, describeVerify, currentBranchOf } = await import("./verify-3R7DGUSI.js");
3220
+ const { runVerify, describeVerify, currentBranchOf } = await import("./verify-LC57A6H2.js");
3578
3221
  const branch = await currentBranchOf(cwd);
3579
3222
  const res = await runVerify({
3580
3223
  deps,
@@ -3617,11 +3260,11 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3617
3260
  if (!resume && !hasPreservedWork && routeIntent(r.intent) === "pipeline") {
3618
3261
  const cwd = workingIn?.() ?? process.cwd();
3619
3262
  emitPhase("sizing");
3620
- const { sizeRequest } = await import("./triage-ES5OHZOS.js");
3263
+ const { sizeRequest } = await import("./triage-FCYHD2AQ.js");
3621
3264
  const size = await sizeRequest(deps, cwd, r.refinedPrompt);
3622
3265
  let small = size.verdict === "small";
3623
3266
  if (size.verdict === "unsure") {
3624
- const { describeSizeDoubt } = await import("./triage-ES5OHZOS.js");
3267
+ const { describeSizeDoubt } = await import("./triage-FCYHD2AQ.js");
3625
3268
  const answer = await askInUserLanguage(
3626
3269
  deps,
3627
3270
  askUser,
@@ -3639,8 +3282,8 @@ Which is it?`,
3639
3282
  if (small) {
3640
3283
  emitPhase("small change");
3641
3284
  emit({ kind: "note", text: `\u26A1 Small change \u2014 ${size.reason}. No branch, no spec, no plan.` });
3642
- const { runSmallChange, describeSmallChange } = await import("./fix-CMARU6JR.js");
3643
- const { currentBranchOf } = await import("./verify-3R7DGUSI.js");
3285
+ const { runSmallChange, describeSmallChange } = await import("./fix-ONLA45HD.js");
3286
+ const { currentBranchOf } = await import("./verify-LC57A6H2.js");
3644
3287
  const res = await runSmallChange(deps, cwd, r.title, r.refinedPrompt, size);
3645
3288
  return {
3646
3289
  intent: r.intent,
@@ -3824,17 +3467,17 @@ The last attempt left conflict markers in: ${conflicted.join(", ")}`;
3824
3467
  }
3825
3468
 
3826
3469
  // src/engine/project-manager.ts
3827
- import { z as z7 } from "zod";
3828
- var taskSchema = z7.object({
3829
- id: z7.string(),
3830
- title: z7.string(),
3831
- deps: z7.array(z7.string()),
3470
+ import { z as z8 } from "zod";
3471
+ var taskSchema = z8.object({
3472
+ id: z8.string(),
3473
+ title: z8.string(),
3474
+ deps: z8.array(z8.string()),
3832
3475
  /**
3833
3476
  * Acceptance criteria: what must be OBSERVABLY true when this task is done — each one checkable against the
3834
3477
  * worktree (a file exists and exports X, a command succeeds, a behavior is covered by a test). Without them
3835
3478
  * "done" is whatever the implementer says it is; with them, completion is verified rather than asserted.
3836
3479
  */
3837
- acceptance: z7.array(z7.string()).default([]).describe(
3480
+ acceptance: z8.array(z8.string()).default([]).describe(
3838
3481
  'What must be OBSERVABLY true when this task is done \u2014 each one checkable against the worktree (a file exists and exports X, a command succeeds, a behaviour is covered by a test). Without them "done" is whatever the implementer says it is.'
3839
3482
  ),
3840
3483
  /**
@@ -3845,11 +3488,11 @@ var taskSchema = z7.object({
3845
3488
  * `deps` alone can be trusted to say — a missed dependency does not fail loudly, it surfaces hours later
3846
3489
  * as a merge conflict.
3847
3490
  */
3848
- files: z7.array(z7.string()).default([]).describe(
3491
+ files: z8.array(z8.string()).default([]).describe(
3849
3492
  "Repo-relative files this task will CREATE or MODIFY, including its test file. Do not list files it only reads. This decides what may run in parallel: two tasks that write the same file are not independent whatever their `deps` say, and a miss does not fail loudly \u2014 it surfaces hours later as a merge conflict."
3850
3493
  )
3851
3494
  });
3852
- var TasksSchema = z7.object({ tasks: z7.array(taskSchema) }).superRefine((val, ctx) => {
3495
+ var TasksSchema = z8.object({ tasks: z8.array(taskSchema) }).superRefine((val, ctx) => {
3853
3496
  const ids = /* @__PURE__ */ new Set();
3854
3497
  for (const t of val.tasks) {
3855
3498
  if (ids.has(t.id)) ctx.addIssue({ code: "custom", message: `duplicate task id: ${t.id}` });
@@ -3871,13 +3514,29 @@ async function runProjectManager(opts) {
3871
3514
  }
3872
3515
 
3873
3516
  // src/engine/task-audit.ts
3874
- import { z as z8 } from "zod";
3875
- var CoverageSchema = z8.object({
3876
- missing: z8.array(z8.string()).default([]).describe(
3517
+ import { z as z9 } from "zod";
3518
+ var CoverageSchema = z9.object({
3519
+ missing: z9.array(z9.string()).default([]).describe(
3877
3520
  "Plan requirements that NO task covers. Quote each one from the plan, so it can be found again."
3878
3521
  ),
3879
- weak: z8.array(z8.object({ task: z8.string(), issue: z8.string() })).default([]).describe(
3522
+ weak: z9.array(z9.object({ task: z9.string(), issue: z9.string() })).default([]).describe(
3880
3523
  "Tasks whose acceptance criteria do not actually establish what the task claims to deliver \u2014 the work could be marked done without the requirement being met."
3524
+ ),
3525
+ /**
3526
+ * The question this audit was not asking, and the one that cost a 16-hour run.
3527
+ *
3528
+ * Coverage was checked in one direction only: is every requirement covered? Measured live on a 124-task
3529
+ * breakdown, the expensive gap was the other direction. `T001 — Backend: Supplier entity model` invented a
3530
+ * `Supplier` entity and a `SupplierContext` that the spec never described; the spec asks for
3531
+ * `SupplierRelationship`. The implementer built what the task said and the code reviewer rejected it for
3532
+ * not matching the spec — six times, across two roles, until the task was abandoned. 117 further tasks
3533
+ * were parked behind it and never attempted. Four of 124 landed.
3534
+ *
3535
+ * A task nothing asked for is not merely wasted work: it deadlocks, because the two halves of the
3536
+ * pipeline are reading different documents and each is right about its own.
3537
+ */
3538
+ fabricated: z9.array(z9.object({ task: z9.string(), issue: z9.string() })).default([]).describe(
3539
+ "Tasks that deliver something the plan does not ask for \u2014 an entity, a module or a behaviour that appears in the task and nowhere in the plan. Name what the task invents and what the plan says instead. A task that merely IMPLEMENTS a plan requirement in a reasonable way is not fabricated."
3881
3540
  )
3882
3541
  });
3883
3542
  var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
@@ -3982,9 +3641,9 @@ function structuralFindings(board) {
3982
3641
  }
3983
3642
  return out;
3984
3643
  }
3985
- async function auditBreakdown(opts, board, planText) {
3644
+ async function auditBreakdown(opts, board, planText, alwaysAsk = false) {
3986
3645
  const findings = structuralFindings(board);
3987
- if (findings.length > 0 || !opts) return { findings, asked: false };
3646
+ if (findings.length > 0 && !alwaysAsk || !opts) return { findings, asked: false };
3988
3647
  const cards = board.list().map((c) => `- ${c.id}: "${c.title}"
3989
3648
  writes: ${c.files.join(", ") || "(none)"}
3990
3649
  done when: ${c.acceptance.join("; ")}`).join("\n");
@@ -3997,19 +3656,29 @@ ${planText}
3997
3656
  The tasks it was broken into:
3998
3657
  ${cards}
3999
3658
 
4000
- Two questions, and only these:
3659
+ Three questions, and only these:
4001
3660
  1. missing \u2014 is there anything the plan REQUIRES that no task delivers? Quote the plan. Do not list work the plan does not ask for, however sensible it would be.
4002
3661
  2. weak \u2014 is there a task whose acceptance criteria would still be satisfied by an implementation that does not do what the task says?
3662
+ 3. fabricated \u2014 the reverse of 1: is there a task that delivers something the plan never asks for? An entity, module or behaviour named in the task and nowhere in the plan. Name what the task invents AND what the plan says instead. Implementing a plan requirement in a reasonable way is not fabrication; inventing the requirement is.
4003
3663
 
4004
- Both lists are usually empty on a good breakdown. Return {missing, weak} via submit.`
3664
+ All three lists are usually empty on a good breakdown. Return {missing, weak, fabricated} via submit.`
4005
3665
  };
4006
3666
  try {
4007
3667
  const out = await runStructuredRole({ ...opts, messages: [...opts.messages, msg] }, CoverageSchema);
4008
3668
  return {
4009
3669
  asked: true,
3670
+ // Structural findings are carried through: on the second pass they are still open and still true, and
3671
+ // dropping them here would report a repaired board as clean.
4010
3672
  findings: [
3673
+ ...findings,
4011
3674
  ...out.missing.map((m) => ({ issue: `the plan requires this and no task delivers it: ${m}` })),
4012
- ...out.weak.filter((w) => board.get(w.task)).map((w) => ({ task: w.task, issue: w.issue }))
3675
+ ...out.weak.filter((w) => board.get(w.task)).map((w) => ({ task: w.task, issue: w.issue })),
3676
+ // Filtered against the board like `weak`: an auditor naming a task that does not exist has answered
3677
+ // about something else, and acting on it would repair a card nobody planned.
3678
+ ...out.fabricated.filter((f) => board.get(f.task)).map((f) => ({
3679
+ task: f.task,
3680
+ issue: `the plan does not ask for this: ${f.issue}`
3681
+ }))
4013
3682
  ]
4014
3683
  };
4015
3684
  } catch (e) {
@@ -4030,19 +3699,19 @@ function snapshotBoard(board) {
4030
3699
  }
4031
3700
 
4032
3701
  // src/engine/memory-consolidate.ts
4033
- import { z as z9 } from "zod";
3702
+ import { z as z10 } from "zod";
4034
3703
  var MAX_LEARNED = 5;
4035
3704
  var MAX_EVIDENCE_CHARS = 6e3;
4036
3705
  var MAX_EXISTING_SHOWN = 40;
4037
3706
  var EXTRACTED_CONFIDENCE = 0.75;
4038
- var LearnedSchema = z9.object({
4039
- memories: z9.array(z9.object({
4040
- text: z9.string(),
4041
- kind: z9.enum(["fact", "lesson"]),
4042
- audience: z9.array(z9.string()).optional().describe(
3707
+ var LearnedSchema = z10.object({
3708
+ memories: z10.array(z10.object({
3709
+ text: z10.string(),
3710
+ kind: z10.enum(["fact", "lesson"]),
3711
+ audience: z10.array(z10.string()).optional().describe(
4043
3712
  "Roles this is FOR, if it is genuinely role-specific. Omit it for anything the whole project should know \u2014 a narrow audience on a general fact hides it from everyone else."
4044
3713
  ),
4045
- importance: z9.number().min(0).max(1).optional().describe(
3714
+ importance: z10.number().min(0).max(1).optional().describe(
4046
3715
  "0 to 1. Around 0.9 for something that would cause real damage if forgotten (a hard project rule, a trap that has already cost a run); around 0.5 for a useful convention; below 0.3 for detail that is cheap to rediscover. This orders what survives when the store is trimmed."
4047
3716
  )
4048
3717
  }))
@@ -4160,7 +3829,7 @@ async function loadBoard(path2) {
4160
3829
  }
4161
3830
 
4162
3831
  // src/engine/job.ts
4163
- import { existsSync as existsSync7 } from "fs";
3832
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
4164
3833
  import { rm as rm2, readFile as readFile5 } from "fs/promises";
4165
3834
  import { dirname as dirname6, join as join6 } from "path";
4166
3835
  function moveNote(card, to, actor) {
@@ -4241,7 +3910,7 @@ async function gateBreakdown(deps, workdir, tasksPath, planPath, emit) {
4241
3910
  emit({ kind: "note", text: `\u26A0\uFE0F The repaired breakdown did not come back \u2014 continuing with the original.` });
4242
3911
  return board;
4243
3912
  }
4244
- const left = await auditBreakdown(auditOpts(deps, workdir), board, planText);
3913
+ const left = await auditBreakdown(auditOpts(deps, workdir), board, planText, true);
4245
3914
  if (left.findings.length) {
4246
3915
  emit({ kind: "note", text: `\u{1F9FE} ${left.findings.length} of those are still open after the repair \u2014 continuing anyway.` });
4247
3916
  }
@@ -4287,6 +3956,15 @@ Give the user a short final report (what happened in each task).` }],
4287
3956
  const msg = await runToCompletion(opts);
4288
3957
  return stripThinking(msg.content);
4289
3958
  }
3959
+ function tallyOnDisk(sessionRoot) {
3960
+ try {
3961
+ const raw = JSON.parse(readFileSync3(join6(sessionRoot, "board.json"), "utf8"));
3962
+ const cards = Array.isArray(raw) ? raw : raw.cards;
3963
+ return Array.isArray(cards) && cards.length ? tallyBoard(cards) : void 0;
3964
+ } catch {
3965
+ return void 0;
3966
+ }
3967
+ }
4290
3968
  async function runJob(deps, opts) {
4291
3969
  const onEvent = opts.onEvent;
4292
3970
  const emit = onEvent ? (ev) => {
@@ -4389,7 +4067,7 @@ async function runJob(deps, opts) {
4389
4067
  board = await loadBoard(boardPath);
4390
4068
  const interrupted = board.list().filter((c) => c.id !== REVISION_CARD && (c.column === "IN-PROGRESS" || c.column === "REVIEW"));
4391
4069
  for (const c of interrupted) board.reopen(c.id);
4392
- const neverTried = board.list().filter((c) => c.id !== REVISION_CARD && c.column === "ABANDONED" && (c.attempts ?? 0) === 0);
4070
+ const neverTried = board.list().filter((c) => c.id !== REVISION_CARD && c.column === "ABANDONED" && (c.attempts ?? 0) === 0 && !humanAbandoned(c));
4393
4071
  const rev = board.get(REVISION_CARD);
4394
4072
  if (rev && rev.column !== "TODO") board.move(REVISION_CARD, "TODO", "team-lead");
4395
4073
  for (const c of neverTried) board.reopen(c.id);
@@ -4407,7 +4085,12 @@ async function runJob(deps, opts) {
4407
4085
  };
4408
4086
  board.onMove = (card, _from, to, actor) => emit({ kind: "note", text: moveNote(card, to, actor) });
4409
4087
  emit({ kind: "phase", phase: "waves" });
4410
- const wave = await runWaves(deps, session, board, { base: opts.fromBranch, prTitle: opts.prTitle, request: opts.prompt });
4088
+ const wave = await runWaves(
4089
+ { ...deps, askUser: opts.askUser },
4090
+ session,
4091
+ board,
4092
+ { base: opts.fromBranch, prTitle: opts.prTitle, request: opts.prompt }
4093
+ );
4411
4094
  emit({ kind: "phase", phase: "waves-done", detail: wave.status });
4412
4095
  let revision;
4413
4096
  let deferredAll = [];
@@ -4446,9 +4129,21 @@ async function runJob(deps, opts) {
4446
4129
  if (!wave.pr && reviewable) {
4447
4130
  emit({ kind: "note", text: `\u{1F4E6} The work is on \`${wave.delivery.branch}\` \u2014 merge it when you are ready.` });
4448
4131
  }
4449
- await curate(deps, up.refinedPrompt ?? opts.prompt, board.list(), deferredAll, session.baseWorktree);
4132
+ const afterDelivery = async (what, run) => {
4133
+ try {
4134
+ await run();
4135
+ } catch (e) {
4136
+ if (deps.signal.aborted) throw e;
4137
+ emit({ kind: "note", text: `\u26A0\uFE0F ${what} could not run (${e instanceof Error ? e.message : String(e)}). The delivered work is unaffected.` });
4138
+ }
4139
+ };
4140
+ const live = session;
4141
+ await afterDelivery("Memory curation", () => curate(deps, up.refinedPrompt ?? opts.prompt, board.list(), deferredAll, live.baseWorktree));
4450
4142
  emit({ kind: "phase", phase: "report" });
4451
- const report = await runCoachReport(deps, session, board);
4143
+ let report = "";
4144
+ await afterDelivery("The closing report", async () => {
4145
+ report = await runCoachReport(deps, live, board);
4146
+ });
4452
4147
  await flushBoard(boardPath);
4453
4148
  const unfinished = board.list().filter((c) => c.id !== REVISION_CARD && c.column !== "MERGED");
4454
4149
  if (!unfinished.length) {
@@ -4469,7 +4164,11 @@ async function runJob(deps, opts) {
4469
4164
  });
4470
4165
  return { kind: "done", wave, revision, report, session, refinedPrompt: up.refinedPrompt };
4471
4166
  } catch (e) {
4472
- if (session) emit({ kind: "note", text: `\u{1F4C4} Work so far is kept at \`${session.baseWorktree}\` (branch \`${session.baseBranch}\`). Re-run the same request to resume from where it stopped.` });
4167
+ if (session) {
4168
+ const tally = tallyOnDisk(session.root);
4169
+ if (tally) emit({ kind: "note", text: `\u{1F4CA} ${describeTally(tally)}` });
4170
+ emit({ kind: "note", text: `\u{1F4C4} Work so far is kept at \`${session.baseWorktree}\` (branch \`${session.baseBranch}\`). Re-run the same request to resume from where it stopped.` });
4171
+ }
4473
4172
  if (session) await curate(deps, opts.prompt, [], [], session.baseWorktree);
4474
4173
  throw e;
4475
4174
  } finally {
@@ -4537,12 +4236,11 @@ function restoreOnExit(h, proc = process, onInterrupt) {
4537
4236
  }
4538
4237
 
4539
4238
  export {
4540
- isAnthropicModel,
4541
- isUnknownModelError,
4542
- OmniRouteProvider,
4543
4239
  defaultCmdRunner,
4544
4240
  detectPlatform,
4545
4241
  makePRAdapter,
4242
+ isUnknownModelError,
4243
+ proposeSplit,
4546
4244
  autonomousAskHuman,
4547
4245
  Timings,
4548
4246
  MAX_PARALLEL_TASKS,
@@ -4550,6 +4248,7 @@ export {
4550
4248
  makeApprove,
4551
4249
  nodeLineReader,
4552
4250
  MemoryStore,
4251
+ describeTally,
4553
4252
  runJob,
4554
4253
  sttySane,
4555
4254
  restoreTerminal,