@hizliemre/horse-code 0.1.1 → 0.2.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/README.md +1 -1
  2. package/dist/{app-UGFQKMLX.js → app-2GPGCDX6.js} +136 -329
  3. package/dist/{chunk-2DGO2BUB.js → chunk-23CLQ2KO.js} +366 -600
  4. package/dist/{chunk-JWAEW7AJ.js → chunk-5ZV42XGJ.js} +2 -2
  5. package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
  6. package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
  7. package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
  8. package/dist/{chunk-3XVZXTB6.js → chunk-7JMWPTJ5.js} +1818 -384
  9. package/dist/{chunk-YILDXPSI.js → chunk-AE36LLL2.js} +49 -25
  10. package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
  11. package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
  12. package/dist/{chunk-TOPZL5SU.js → chunk-LLL7QWXB.js} +41 -9
  13. package/dist/{chunk-7TBYMFMG.js → chunk-LNW557IO.js} +2 -2
  14. package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
  15. package/dist/chunk-M2RKCIGV.js +11 -0
  16. package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
  17. package/dist/{chunk-HBSC2HT2.js → chunk-UGESK765.js} +1 -1
  18. package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
  19. package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
  20. package/dist/cli.js +414 -100
  21. package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
  22. package/dist/fix-JOIXQFVP.js +33 -0
  23. package/dist/git-QZTDZSJY.js +8 -0
  24. package/dist/{ongoing-OV5XROTU.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-X7U3KCPU.js} +1 -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-2J3T5PVQ.js → triage-FCYHD2AQ.js} +8 -9
  32. package/dist/{verify-WQ3GHION.js → verify-6SC4I77M.js} +18 -19
  33. package/package.json +1 -1
  34. package/dist/chunk-B67BK5GQ.js +0 -34
  35. package/dist/chunk-F2IALVBU.js +0 -212
  36. package/dist/chunk-O74BDQKS.js +0 -28
  37. package/dist/fix-HBBOTUWM.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-F2IALVBU.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-3XVZXTB6.js";
38
+ writeFileTool,
39
+ writerRegistry
40
+ } from "./chunk-7JMWPTJ5.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-HBSC2HT2.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,533 +72,32 @@ import {
71
72
  relationStrength,
72
73
  supersedes,
73
74
  verifyAnchors
74
- } from "./chunk-TOPZL5SU.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-YILDXPSI.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/omniroute.ts
376
- async function* withIdleTimeout(source, idleMs, onIdle) {
377
- const it = source[Symbol.asyncIterator]();
378
- for (; ; ) {
379
- let timer;
380
- const idle = new Promise((resolve) => {
381
- timer = setTimeout(() => resolve("idle"), idleMs);
382
- });
383
- const next = it.next();
384
- next.catch(() => {
385
- });
386
- const winner = await Promise.race([next, idle]);
387
- if (timer) clearTimeout(timer);
388
- if (winner === "idle") {
389
- onIdle?.();
390
- void Promise.resolve(it.return?.(void 0)).catch(() => {
391
- });
392
- throw new Error(`omniroute: stream stalled (no data for ${Math.round(idleMs / 1e3)}s) \u2014 aborted`);
393
- }
394
- if (winner.done) return;
395
- yield winner.value;
396
- }
397
- }
398
- async function readErrorMessage(res) {
399
- let body;
400
- try {
401
- body = await res.json();
402
- } catch {
403
- return `omniroute ${res.status}`;
404
- }
405
- const err = body.error;
406
- if (typeof err === "string") return err;
407
- if (err && typeof err === "object" && "message" in err) {
408
- return String(err.message);
409
- }
410
- return `omniroute ${res.status}`;
411
- }
412
- function isRetryableStatus(status) {
413
- return status === 429 || status === 404 || status >= 500;
414
- }
415
- function isCapabilityError(message) {
416
- return /long[- ]context|not (yet )?available for this subscription|context[- ](length|window)|too many tokens|maximum context|unsupported|not supported/i.test(message);
417
- }
418
- function isUnknownModelError(message) {
419
- return /unable to determine provider for model|unknown model|model not found|no such model|invalid model/i.test(message);
420
- }
421
- function pathOf2(args) {
422
- return args.match(/"path"\s*:\s*"([^"\\]+)"/)?.[1];
423
- }
424
- function argumentsComplete(args) {
425
- if (!args.trim()) return true;
426
- try {
427
- JSON.parse(args);
428
- return true;
429
- } catch {
430
- return false;
431
- }
432
- }
433
- function isCallerAbort(signal) {
434
- return signal.aborted && signal.reason?.name !== "TimeoutError";
435
- }
436
- function isDeadline(signal) {
437
- return signal.aborted && signal.reason?.name === "TimeoutError";
438
- }
439
- var OmniRouteProvider = class {
440
- apiKey;
441
- baseUrl;
442
- fetchFn;
443
- idleMs;
444
- constructor(opts) {
445
- this.apiKey = opts.apiKey;
446
- this.baseUrl = opts.baseUrl.replace(/\/$/, "");
447
- this.fetchFn = opts.fetch ?? globalThis.fetch;
448
- this.idleMs = opts.idleTimeoutMs ?? 12e4;
449
- }
450
- async *chat(req, signal) {
451
- const native = isAnthropicModel(req.model);
452
- const headers = {
453
- "Content-Type": "application/json",
454
- Accept: "text/event-stream"
455
- };
456
- if (native) headers["anthropic-version"] = "2023-06-01";
457
- if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
458
- const idleAc = new AbortController();
459
- const combined = AbortSignal.any([signal, idleAc.signal]);
460
- let res;
461
- try {
462
- res = await this.fetchFn(`${this.baseUrl}${native ? "/v1/messages" : "/api/v1/chat/completions"}`, {
463
- method: "POST",
464
- headers,
465
- // Sanitised at the socket, not at each of the dozens of places that build a prompt — see
466
- // src/core/surrogates.ts for the four-hour run this cost.
467
- body: JSON.stringify(sanitizeForJson(native ? toAnthropicBody(req) : toOpenAIBody(req))),
468
- signal: combined
469
- });
470
- } catch (e) {
471
- if (isCallerAbort(signal)) {
472
- yield { type: "error", message: "cancelled", retryable: false };
473
- return;
474
- }
475
- if (isDeadline(signal)) {
476
- yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
477
- return;
478
- }
479
- yield { type: "error", message: e instanceof Error ? e.message : String(e), retryable: true };
480
- return;
481
- }
482
- if (!res.ok) {
483
- const message = await readErrorMessage(res);
484
- const capability = isCapabilityError(message);
485
- const unknownModel = isUnknownModelError(message);
486
- yield {
487
- type: "error",
488
- message,
489
- retryable: isRetryableStatus(res.status) || capability || unknownModel,
490
- ...capability && { capability: true },
491
- ...unknownModel && { noBench: true }
492
- };
493
- return;
494
- }
495
- const stream = res.body;
496
- if (!stream) {
497
- yield { type: "error", message: "omniroute: empty response body" };
498
- return;
499
- }
500
- const decoder = native ? new AnthropicDecoder() : void 0;
501
- const toolCalls = /* @__PURE__ */ new Map();
502
- const lastProgress = /* @__PURE__ */ new Map();
503
- let finishReason = "stop";
504
- let sawText = false;
505
- let usage;
506
- const billed = {};
507
- try {
508
- for await (const line of withIdleTimeout(parseSSE(stream), this.idleMs, () => idleAc.abort())) {
509
- if (line.kind === "comment") {
510
- const m = line.value.match(/^x-omniroute-tokens-(in|out)\s*=\s*(\d+)/i);
511
- if (m) billed[m[1] === "in" ? "in" : "out"] = Number(m[2]);
512
- continue;
513
- }
514
- let chunk;
515
- try {
516
- chunk = JSON.parse(line.value);
517
- } catch {
518
- continue;
519
- }
520
- if (decoder) {
521
- for (const ev of decoder.push(chunk)) yield ev;
522
- continue;
523
- }
524
- const u = chunk.usage;
525
- if (u) usage = {
526
- promptTokens: u.prompt_tokens ?? 0,
527
- completionTokens: u.completion_tokens ?? 0,
528
- cachedTokens: u.prompt_tokens_details?.cached_tokens ?? 0
529
- };
530
- const choice = chunk?.choices?.[0];
531
- if (!choice) continue;
532
- const delta = choice.delta ?? {};
533
- if (typeof delta.content === "string" && delta.content.length) {
534
- sawText = true;
535
- yield { type: "text-delta", text: delta.content };
536
- }
537
- const deltaCalls = delta.tool_calls;
538
- if (Array.isArray(deltaCalls)) {
539
- for (const tc of deltaCalls) {
540
- const idx = tc.index ?? 0;
541
- const acc = toolCalls.get(idx) ?? { id: "", name: "", arguments: "" };
542
- if (tc.id) acc.id = tc.id;
543
- if (tc.function?.name) acc.name = tc.function.name;
544
- if (tc.function?.arguments) acc.arguments += tc.function.arguments;
545
- toolCalls.set(idx, acc);
546
- if (acc.name && acc.arguments.length - (lastProgress.get(idx) ?? 0) >= 64) {
547
- lastProgress.set(idx, acc.arguments.length);
548
- yield { type: "tool-progress", name: acc.name, chars: acc.arguments.length, path: pathOf2(acc.arguments) };
549
- }
550
- }
551
- }
552
- if (choice.finish_reason) finishReason = mapFinishReason(choice.finish_reason);
553
- }
554
- } catch (e) {
555
- if (isCallerAbort(signal)) {
556
- yield { type: "error", message: "cancelled", retryable: false };
557
- return;
558
- }
559
- if (isDeadline(signal)) {
560
- yield { type: "error", message: "the model did not answer within its deadline", retryable: true };
561
- return;
562
- }
563
- yield { type: "error", message: e instanceof Error ? e.message : String(e), retryable: true };
564
- return;
565
- }
566
- const cut = sawText ? void 0 : [...toolCalls.values()].find((a) => !argumentsComplete(a.arguments));
567
- if (cut) {
568
- yield {
569
- type: "error",
570
- message: `the stream ended in the middle of ${cut.name || "a tool call"}'s arguments`,
571
- retryable: true
572
- };
573
- return;
574
- }
575
- for (const acc of toolCalls.values()) {
576
- const toolCall = { id: acc.id, name: acc.name, arguments: acc.arguments };
577
- yield { type: "tool-call", toolCall };
578
- }
579
- if (decoder) {
580
- finishReason = decoder.finishReason();
581
- usage = decoder.usage() ?? usage;
582
- }
583
- if (billed.in !== void 0 || billed.out !== void 0) {
584
- yield { type: "usage", promptTokens: billed.in ?? 0, completionTokens: billed.out ?? 0 };
585
- } else if (usage) {
586
- yield { type: "usage", promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, cachedTokens: usage.cachedTokens };
587
- } else {
588
- const inHeader = res.headers.get("X-OmniRoute-Tokens-In");
589
- const outHeader = res.headers.get("X-OmniRoute-Tokens-Out");
590
- if (inHeader !== null || outHeader !== null) {
591
- yield {
592
- type: "usage",
593
- promptTokens: Number(inHeader) || 0,
594
- completionTokens: Number(outHeader) || 0
595
- };
596
- }
597
- }
598
- yield { type: "done", finishReason };
599
- }
600
- };
100
+ } from "./chunk-6W4UH2BQ.js";
601
101
 
602
102
  // src/adapters/pr.ts
603
103
  import { spawn } from "child_process";
@@ -912,8 +412,12 @@ function makePRAdapter(opts) {
912
412
  // src/engine/council.ts
913
413
  import { z } from "zod";
914
414
  var ArchitectPlanSchema = z.object({
915
- rootCause: z.string(),
916
- 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
+ )
917
421
  });
918
422
  async function runEscalationCouncil(deps, board, taskId, cwd, family) {
919
423
  const task = board.get(taskId);
@@ -981,6 +485,127 @@ Identify the root cause and produce a concrete plan.`
981
485
  return v;
982
486
  }
983
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
+
984
609
  // src/engine/escalation.ts
985
610
  function autonomousAskHuman(maxRetries = 2) {
986
611
  const retries = /* @__PURE__ */ new Map();
@@ -1008,16 +633,47 @@ function noChangeStreak(task) {
1008
633
  function tierOf(attempts, rounds) {
1009
634
  return attempts < rounds ? 0 : attempts < 2 * rounds ? 1 : 2;
1010
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
+ }
1011
655
  function attemptError(board, taskId, role, e) {
1012
656
  const msg = e instanceof Error ? e.message : String(e);
657
+ const fleet = isFleetFailure(msg);
1013
658
  board.appendStage(taskId, { role, action: "attempt-error", note: msg });
1014
- 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.`);
1015
660
  board.move(taskId, "TODO", role);
1016
- 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 };
1017
663
  }
1018
664
  async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1019
665
  const task = board.get(taskId);
1020
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
+ }
1021
677
  const noChangeCount = noChangeStreak(task);
1022
678
  if (noChangeCount >= 3) {
1023
679
  const notes = ["Three attempts produced no file changes. The task needs a human decision before another agent is dispatched."];
@@ -1060,6 +716,8 @@ async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1060
716
  v2 = attemptError(board, taskId, role, e);
1061
717
  }
1062
718
  if (v2.verdict === "pass") return v2;
719
+ if (v2.fleetDown) return v2;
720
+ if (v2.fleetFailure) continue;
1063
721
  if (v2.noProgress) {
1064
722
  const tierStart = Math.floor(attempts / deps.rounds) * deps.rounds;
1065
723
  const target = attempts === tierStart ? attempts + 1 : tierStart + deps.rounds;
@@ -1105,13 +763,13 @@ async function runTaskWithEscalation(deps, board, taskId, cwd, slot = 0) {
1105
763
  }
1106
764
 
1107
765
  // src/engine/pr-summary.ts
1108
- import { z as z2 } from "zod";
766
+ import { z as z3 } from "zod";
1109
767
  var MAX_EXAMPLES = 12;
1110
768
  var MERGED_PREFIX = /^Merged PR \d+:\s*/;
1111
769
  var PR_SUMMARY_MAX_TURNS = 3;
1112
- var PRSummarySchema = z2.object({
1113
- title: z2.string().min(1),
1114
- body: z2.string().min(1)
770
+ var PRSummarySchema = z3.object({
771
+ title: z3.string().min(1),
772
+ body: z3.string().min(1)
1115
773
  });
1116
774
  async function recentSubjects(git, cwd, base) {
1117
775
  const read = async (args) => {
@@ -1188,19 +846,19 @@ async function prSummary(deps, opts) {
1188
846
  }
1189
847
 
1190
848
  // src/engine/revision.ts
1191
- import { z as z3 } from "zod";
849
+ import { z as z4 } from "zod";
1192
850
  var REVISION_CARD = "__revision__";
1193
- var PrincipalReviewSchema = z3.object({
1194
- decision: z3.enum(["approve", "request-changes"]).describe(
851
+ var PrincipalReviewSchema = z4.object({
852
+ decision: z4.enum(["approve", "request-changes"]).describe(
1195
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."
1196
854
  ),
1197
- comments: z3.array(z3.string())
855
+ comments: z4.array(z4.string())
1198
856
  });
1199
- var PrincipalFinalSchema = z3.object({
1200
- decision: z3.enum(["accept", "ask-human"]).describe(
857
+ var PrincipalFinalSchema = z4.object({
858
+ decision: z4.enum(["accept", "ask-human"]).describe(
1201
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."
1202
860
  ),
1203
- question: z3.string()
861
+ question: z4.string()
1204
862
  });
1205
863
  async function worktreeState2(deps, base) {
1206
864
  const git = deps.git ?? defaultGitRunner;
@@ -1444,6 +1102,10 @@ async function runWaveTask(deps, session, board, taskId, slot = 0) {
1444
1102
  return { status: "task-failed", task: tw };
1445
1103
  }
1446
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
+ }
1447
1109
  if (v.verdict === "fail") {
1448
1110
  board.appendStage(taskId, { role: "team-lead", action: "task-failed" });
1449
1111
  board.move(taskId, "ABANDONED", "team-lead");
@@ -1718,7 +1380,7 @@ ${card.reviewNotes.map((n) => `- ${n}`).join("\n")}` : "";
1718
1380
  }
1719
1381
 
1720
1382
  // src/engine/team-lead.ts
1721
- import { z as z4 } from "zod";
1383
+ import { z as z5 } from "zod";
1722
1384
 
1723
1385
  // src/engine/waves.ts
1724
1386
  function computeWaves(board) {
@@ -1810,10 +1472,10 @@ function describeWaves(s) {
1810
1472
  }
1811
1473
 
1812
1474
  // src/engine/team-lead.ts
1813
- var fix = z4.object({ task: z4.string(), needs: z4.string(), why: z4.string() });
1814
- var DepAuditSchema = z4.object({
1815
- missing: z4.array(fix).default([]),
1816
- 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([])
1817
1479
  });
1818
1480
  var AUDIT_TIMEOUT_MS = 9e4;
1819
1481
  var MAX_AUDIT_CARDS = 40;
@@ -1944,11 +1606,12 @@ async function runReady(deps, session, board) {
1944
1606
  return h.slice(since + 1).filter((e) => e.action === "conflict:resolve-failed").length;
1945
1607
  };
1946
1608
  const restartsSoFar = (cardId) => (board.get(cardId)?.stageHistory ?? []).filter((e) => e.action === "restarted").length;
1947
- 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));
1948
1610
  for (const id of pending) {
1949
1611
  const c = board.get(id);
1950
1612
  if (!c || c.attempts === 0) continue;
1951
1613
  board.resetAttempts(id);
1614
+ if (c.column === "ABANDONED") board.reopen(id);
1952
1615
  }
1953
1616
  const busy = /* @__PURE__ */ new Set();
1954
1617
  const running = /* @__PURE__ */ new Map();
@@ -1963,7 +1626,7 @@ async function runReady(deps, session, board) {
1963
1626
  board.appendStage(id, {
1964
1627
  role: "team-lead",
1965
1628
  action: "parked",
1966
- 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"
1967
1630
  });
1968
1631
  board.move(id, "PARKED", "team-lead");
1969
1632
  };
@@ -1971,7 +1634,7 @@ async function runReady(deps, session, board) {
1971
1634
  let woken = 0;
1972
1635
  for (const [id, p] of [...parked]) {
1973
1636
  if (p.wakes >= MAX_WAKES) continue;
1974
- 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;
1975
1638
  if (!ready) continue;
1976
1639
  parked.delete(id);
1977
1640
  pending.add(id);
@@ -1995,7 +1658,27 @@ async function runReady(deps, session, board) {
1995
1658
  }
1996
1659
  }
1997
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
+ };
1998
1680
  while (pending.size > 0 || running.size > 0 || parked.size > 0) {
1681
+ absorbNewCards();
1999
1682
  parkUnreachable();
2000
1683
  const limit = ceiling();
2001
1684
  for (const id of [...pending]) {
@@ -2068,7 +1751,12 @@ async function runReady(deps, session, board) {
2068
1751
  });
2069
1752
  park(id, "conflict", void 0, mergedAtStart);
2070
1753
  }
2071
- } 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
+ );
2072
1760
  } finally {
2073
1761
  for (const f of files) busy.delete(f);
2074
1762
  free.push(slot);
@@ -2106,7 +1794,7 @@ async function runReady(deps, session, board) {
2106
1794
  }
2107
1795
  if (rewrote > 0) continue;
2108
1796
  for (const [id, p] of parked) {
2109
- (p.reason === "waiting" ? skipped : failed).push(id);
1797
+ (p.reason === "waiting" || p.reason === "fleet" ? skipped : failed).push(id);
2110
1798
  board.appendStage(id, {
2111
1799
  role: "team-lead",
2112
1800
  action: "abandoned",
@@ -2900,6 +2588,31 @@ ${USAGE_FILE}
2900
2588
  }
2901
2589
  };
2902
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
+
2903
2616
  // src/engine/turn-effect.ts
2904
2617
  import { readFile as readFile3, mkdir as mkdir2 } from "fs/promises";
2905
2618
  import { existsSync as existsSync2 } from "fs";
@@ -3006,9 +2719,9 @@ import { existsSync as existsSync6 } from "fs";
3006
2719
  import { relative as relative3, dirname as dirname4 } from "path";
3007
2720
 
3008
2721
  // src/engine/refiner.ts
3009
- import { z as z5 } from "zod";
3010
- var RefinerSchema = z5.object({
3011
- 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."),
3012
2725
  /**
3013
2726
  * The choice that decides what a run COSTS, described where it is made.
3014
2727
  *
@@ -3020,13 +2733,13 @@ var RefinerSchema = z5.object({
3020
2733
  * needs no specification. The prompt's own words for `verify` describe that request exactly; they were just
3021
2734
  * nowhere near the field being filled in.
3022
2735
  */
3023
- intent: z5.enum(["chat", "feature", "bugfix", "govern", "undo", "verify"]).describe(
2736
+ intent: z6.enum(["chat", "feature", "bugfix", "govern", "undo", "verify"]).describe(
3024
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."
3025
2738
  ),
3026
2739
  // The natural language the user wrote in (English name, e.g. "Turkish") → the coach replies in it.
3027
- language: z5.string().default("English"),
2740
+ language: z6.string().default("English"),
3028
2741
  // A concise 2-5 word English kebab-case summary → used as the worktree/branch name (e.g. "add-login-page").
3029
- title: z5.string().default("task")
2742
+ title: z6.string().default("task")
3030
2743
  });
3031
2744
  async function runRefiner(deps, prompt, history = []) {
3032
2745
  const resolved = deps.roleRegistry.resolve("refiner");
@@ -3345,7 +3058,11 @@ ${carryOver.map((c) => `- ${c}`).join("\n")}` : "";
3345
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.
3346
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.
3347
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.
3348
- 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.
3349
3066
  Follow this template:
3350
3067
 
3351
3068
  ${template}
@@ -3362,10 +3079,10 @@ async function runTasks(p, paths, carryOver) {
3362
3079
  // src/speckit/clarify.ts
3363
3080
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3364
3081
  import { relative as relative2 } from "path";
3365
- import { z as z6 } from "zod";
3366
- var ClarifyStepSchema = z6.object({
3082
+ import { z as z7 } from "zod";
3083
+ var ClarifyStepSchema = z7.object({
3367
3084
  // The single most important clarifying question, or null when the spec is sufficiently clear.
3368
- nextQuestion: z6.string().nullable()
3085
+ nextQuestion: z7.string().nullable()
3369
3086
  });
3370
3087
  async function runClarify(p, paths, maxRounds = 5) {
3371
3088
  const specRel = relative2(p.workdir, paths.spec);
@@ -3500,7 +3217,7 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3500
3217
  emitPhase("verify");
3501
3218
  const cwd = await documentWorkdir(process.cwd(), prompt, ensureWorktree, r.title);
3502
3219
  laneCheckpoint(cwd, "verify", resume, prompt, r);
3503
- const { runVerify, describeVerify, currentBranchOf } = await import("./verify-WQ3GHION.js");
3220
+ const { runVerify, describeVerify, currentBranchOf } = await import("./verify-6SC4I77M.js");
3504
3221
  const branch = await currentBranchOf(cwd);
3505
3222
  const res = await runVerify({
3506
3223
  deps,
@@ -3543,11 +3260,11 @@ async function runUpstream(deps, ensureWorktree, prompt, askUser, maxRounds, his
3543
3260
  if (!resume && !hasPreservedWork && routeIntent(r.intent) === "pipeline") {
3544
3261
  const cwd = workingIn?.() ?? process.cwd();
3545
3262
  emitPhase("sizing");
3546
- const { sizeRequest } = await import("./triage-2J3T5PVQ.js");
3263
+ const { sizeRequest } = await import("./triage-FCYHD2AQ.js");
3547
3264
  const size = await sizeRequest(deps, cwd, r.refinedPrompt);
3548
3265
  let small = size.verdict === "small";
3549
3266
  if (size.verdict === "unsure") {
3550
- const { describeSizeDoubt } = await import("./triage-2J3T5PVQ.js");
3267
+ const { describeSizeDoubt } = await import("./triage-FCYHD2AQ.js");
3551
3268
  const answer = await askInUserLanguage(
3552
3269
  deps,
3553
3270
  askUser,
@@ -3565,8 +3282,8 @@ Which is it?`,
3565
3282
  if (small) {
3566
3283
  emitPhase("small change");
3567
3284
  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");
3285
+ const { runSmallChange, describeSmallChange } = await import("./fix-JOIXQFVP.js");
3286
+ const { currentBranchOf } = await import("./verify-6SC4I77M.js");
3570
3287
  const res = await runSmallChange(deps, cwd, r.title, r.refinedPrompt, size);
3571
3288
  return {
3572
3289
  intent: r.intent,
@@ -3750,17 +3467,17 @@ The last attempt left conflict markers in: ${conflicted.join(", ")}`;
3750
3467
  }
3751
3468
 
3752
3469
  // src/engine/project-manager.ts
3753
- import { z as z7 } from "zod";
3754
- var taskSchema = z7.object({
3755
- id: z7.string(),
3756
- title: z7.string(),
3757
- 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()),
3758
3475
  /**
3759
3476
  * Acceptance criteria: what must be OBSERVABLY true when this task is done — each one checkable against the
3760
3477
  * worktree (a file exists and exports X, a command succeeds, a behavior is covered by a test). Without them
3761
3478
  * "done" is whatever the implementer says it is; with them, completion is verified rather than asserted.
3762
3479
  */
3763
- acceptance: z7.array(z7.string()).default([]).describe(
3480
+ acceptance: z8.array(z8.string()).default([]).describe(
3764
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.'
3765
3482
  ),
3766
3483
  /**
@@ -3771,11 +3488,11 @@ var taskSchema = z7.object({
3771
3488
  * `deps` alone can be trusted to say — a missed dependency does not fail loudly, it surfaces hours later
3772
3489
  * as a merge conflict.
3773
3490
  */
3774
- files: z7.array(z7.string()).default([]).describe(
3491
+ files: z8.array(z8.string()).default([]).describe(
3775
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."
3776
3493
  )
3777
3494
  });
3778
- var TasksSchema = z7.object({ tasks: z7.array(taskSchema) }).superRefine((val, ctx) => {
3495
+ var TasksSchema = z8.object({ tasks: z8.array(taskSchema) }).superRefine((val, ctx) => {
3779
3496
  const ids = /* @__PURE__ */ new Set();
3780
3497
  for (const t of val.tasks) {
3781
3498
  if (ids.has(t.id)) ctx.addIssue({ code: "custom", message: `duplicate task id: ${t.id}` });
@@ -3797,13 +3514,29 @@ async function runProjectManager(opts) {
3797
3514
  }
3798
3515
 
3799
3516
  // src/engine/task-audit.ts
3800
- import { z as z8 } from "zod";
3801
- var CoverageSchema = z8.object({
3802
- 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(
3803
3520
  "Plan requirements that NO task covers. Quote each one from the plan, so it can be found again."
3804
3521
  ),
3805
- 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(
3806
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."
3807
3540
  )
3808
3541
  });
3809
3542
  var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
@@ -3908,9 +3641,9 @@ function structuralFindings(board) {
3908
3641
  }
3909
3642
  return out;
3910
3643
  }
3911
- async function auditBreakdown(opts, board, planText) {
3644
+ async function auditBreakdown(opts, board, planText, alwaysAsk = false) {
3912
3645
  const findings = structuralFindings(board);
3913
- if (findings.length > 0 || !opts) return { findings, asked: false };
3646
+ if (findings.length > 0 && !alwaysAsk || !opts) return { findings, asked: false };
3914
3647
  const cards = board.list().map((c) => `- ${c.id}: "${c.title}"
3915
3648
  writes: ${c.files.join(", ") || "(none)"}
3916
3649
  done when: ${c.acceptance.join("; ")}`).join("\n");
@@ -3923,19 +3656,29 @@ ${planText}
3923
3656
  The tasks it was broken into:
3924
3657
  ${cards}
3925
3658
 
3926
- Two questions, and only these:
3659
+ Three questions, and only these:
3927
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.
3928
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.
3929
3663
 
3930
- 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.`
3931
3665
  };
3932
3666
  try {
3933
3667
  const out = await runStructuredRole({ ...opts, messages: [...opts.messages, msg] }, CoverageSchema);
3934
3668
  return {
3935
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.
3936
3672
  findings: [
3673
+ ...findings,
3937
3674
  ...out.missing.map((m) => ({ issue: `the plan requires this and no task delivers it: ${m}` })),
3938
- ...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
+ }))
3939
3682
  ]
3940
3683
  };
3941
3684
  } catch (e) {
@@ -3956,19 +3699,19 @@ function snapshotBoard(board) {
3956
3699
  }
3957
3700
 
3958
3701
  // src/engine/memory-consolidate.ts
3959
- import { z as z9 } from "zod";
3702
+ import { z as z10 } from "zod";
3960
3703
  var MAX_LEARNED = 5;
3961
3704
  var MAX_EVIDENCE_CHARS = 6e3;
3962
3705
  var MAX_EXISTING_SHOWN = 40;
3963
3706
  var EXTRACTED_CONFIDENCE = 0.75;
3964
- var LearnedSchema = z9.object({
3965
- memories: z9.array(z9.object({
3966
- text: z9.string(),
3967
- kind: z9.enum(["fact", "lesson"]),
3968
- 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(
3969
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."
3970
3713
  ),
3971
- importance: z9.number().min(0).max(1).optional().describe(
3714
+ importance: z10.number().min(0).max(1).optional().describe(
3972
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."
3973
3716
  )
3974
3717
  }))
@@ -4086,7 +3829,7 @@ async function loadBoard(path2) {
4086
3829
  }
4087
3830
 
4088
3831
  // src/engine/job.ts
4089
- import { existsSync as existsSync7 } from "fs";
3832
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
4090
3833
  import { rm as rm2, readFile as readFile5 } from "fs/promises";
4091
3834
  import { dirname as dirname6, join as join6 } from "path";
4092
3835
  function moveNote(card, to, actor) {
@@ -4167,7 +3910,7 @@ async function gateBreakdown(deps, workdir, tasksPath, planPath, emit) {
4167
3910
  emit({ kind: "note", text: `\u26A0\uFE0F The repaired breakdown did not come back \u2014 continuing with the original.` });
4168
3911
  return board;
4169
3912
  }
4170
- const left = await auditBreakdown(auditOpts(deps, workdir), board, planText);
3913
+ const left = await auditBreakdown(auditOpts(deps, workdir), board, planText, true);
4171
3914
  if (left.findings.length) {
4172
3915
  emit({ kind: "note", text: `\u{1F9FE} ${left.findings.length} of those are still open after the repair \u2014 continuing anyway.` });
4173
3916
  }
@@ -4213,6 +3956,15 @@ Give the user a short final report (what happened in each task).` }],
4213
3956
  const msg = await runToCompletion(opts);
4214
3957
  return stripThinking(msg.content);
4215
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
+ }
4216
3968
  async function runJob(deps, opts) {
4217
3969
  const onEvent = opts.onEvent;
4218
3970
  const emit = onEvent ? (ev) => {
@@ -4315,7 +4067,7 @@ async function runJob(deps, opts) {
4315
4067
  board = await loadBoard(boardPath);
4316
4068
  const interrupted = board.list().filter((c) => c.id !== REVISION_CARD && (c.column === "IN-PROGRESS" || c.column === "REVIEW"));
4317
4069
  for (const c of interrupted) board.reopen(c.id);
4318
- 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));
4319
4071
  const rev = board.get(REVISION_CARD);
4320
4072
  if (rev && rev.column !== "TODO") board.move(REVISION_CARD, "TODO", "team-lead");
4321
4073
  for (const c of neverTried) board.reopen(c.id);
@@ -4333,7 +4085,12 @@ async function runJob(deps, opts) {
4333
4085
  };
4334
4086
  board.onMove = (card, _from, to, actor) => emit({ kind: "note", text: moveNote(card, to, actor) });
4335
4087
  emit({ kind: "phase", phase: "waves" });
4336
- 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
+ );
4337
4094
  emit({ kind: "phase", phase: "waves-done", detail: wave.status });
4338
4095
  let revision;
4339
4096
  let deferredAll = [];
@@ -4370,18 +4127,23 @@ async function runJob(deps, opts) {
4370
4127
  }
4371
4128
  }
4372
4129
  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
- }
4130
+ emit({ kind: "note", text: `\u{1F4E6} The work is on \`${wave.delivery.branch}\` \u2014 merge it when you are ready.` });
4381
4131
  }
4382
- 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));
4383
4142
  emit({ kind: "phase", phase: "report" });
4384
- 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
+ });
4385
4147
  await flushBoard(boardPath);
4386
4148
  const unfinished = board.list().filter((c) => c.id !== REVISION_CARD && c.column !== "MERGED");
4387
4149
  if (!unfinished.length) {
@@ -4402,7 +4164,11 @@ async function runJob(deps, opts) {
4402
4164
  });
4403
4165
  return { kind: "done", wave, revision, report, session, refinedPrompt: up.refinedPrompt };
4404
4166
  } catch (e) {
4405
- 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
+ }
4406
4172
  if (session) await curate(deps, opts.prompt, [], [], session.baseWorktree);
4407
4173
  throw e;
4408
4174
  } finally {
@@ -4470,12 +4236,11 @@ function restoreOnExit(h, proc = process, onInterrupt) {
4470
4236
  }
4471
4237
 
4472
4238
  export {
4473
- isAnthropicModel,
4474
- isUnknownModelError,
4475
- OmniRouteProvider,
4476
4239
  defaultCmdRunner,
4477
4240
  detectPlatform,
4478
4241
  makePRAdapter,
4242
+ isUnknownModelError,
4243
+ proposeSplit,
4479
4244
  autonomousAskHuman,
4480
4245
  Timings,
4481
4246
  MAX_PARALLEL_TASKS,
@@ -4483,6 +4248,7 @@ export {
4483
4248
  makeApprove,
4484
4249
  nodeLineReader,
4485
4250
  MemoryStore,
4251
+ describeTally,
4486
4252
  runJob,
4487
4253
  sttySane,
4488
4254
  restoreTerminal,