@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.
- package/README.md +1 -1
- package/dist/{app-UGFQKMLX.js → app-2GPGCDX6.js} +136 -329
- package/dist/{chunk-2DGO2BUB.js → chunk-23CLQ2KO.js} +366 -600
- package/dist/{chunk-JWAEW7AJ.js → chunk-5ZV42XGJ.js} +2 -2
- package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
- package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
- package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
- package/dist/{chunk-3XVZXTB6.js → chunk-7JMWPTJ5.js} +1818 -384
- package/dist/{chunk-YILDXPSI.js → chunk-AE36LLL2.js} +49 -25
- package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
- package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
- package/dist/{chunk-TOPZL5SU.js → chunk-LLL7QWXB.js} +41 -9
- package/dist/{chunk-7TBYMFMG.js → chunk-LNW557IO.js} +2 -2
- package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
- package/dist/chunk-M2RKCIGV.js +11 -0
- package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
- package/dist/{chunk-HBSC2HT2.js → chunk-UGESK765.js} +1 -1
- package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
- package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
- package/dist/cli.js +414 -100
- package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
- package/dist/fix-JOIXQFVP.js +33 -0
- package/dist/git-QZTDZSJY.js +8 -0
- package/dist/{ongoing-OV5XROTU.js → ongoing-WHYXPW24.js} +4 -4
- package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
- package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
- package/dist/{save-skills-NSLBU33X.js → save-skills-X7U3KCPU.js} +1 -1
- package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
- package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
- package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
- package/dist/{triage-2J3T5PVQ.js → triage-FCYHD2AQ.js} +8 -9
- package/dist/{verify-WQ3GHION.js → verify-6SC4I77M.js} +18 -19
- package/package.json +1 -1
- package/dist/chunk-B67BK5GQ.js +0 -34
- package/dist/chunk-F2IALVBU.js +0 -212
- package/dist/chunk-O74BDQKS.js +0 -28
- package/dist/fix-HBBOTUWM.js +0 -34
- package/dist/git-VTSZALSR.js +0 -6
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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-
|
|
47
|
+
} from "./chunk-UGESK765.js";
|
|
47
48
|
import {
|
|
48
49
|
clearCheckpoint,
|
|
49
50
|
isContinuePrompt,
|
|
50
51
|
readCheckpoint,
|
|
51
52
|
writeCheckpoint
|
|
52
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-ZSQ24YDJ.js";
|
|
53
54
|
import {
|
|
54
55
|
defaultGitRunner
|
|
55
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
90
|
+
} from "./chunk-KAGKX2YT.js";
|
|
91
91
|
import {
|
|
92
92
|
loadGraphSync
|
|
93
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1113
|
-
title:
|
|
1114
|
-
body:
|
|
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
|
|
849
|
+
import { z as z4 } from "zod";
|
|
1192
850
|
var REVISION_CARD = "__revision__";
|
|
1193
|
-
var PrincipalReviewSchema =
|
|
1194
|
-
decision:
|
|
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:
|
|
855
|
+
comments: z4.array(z4.string())
|
|
1198
856
|
});
|
|
1199
|
-
var PrincipalFinalSchema =
|
|
1200
|
-
decision:
|
|
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:
|
|
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
|
|
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 =
|
|
1814
|
-
var DepAuditSchema =
|
|
1815
|
-
missing:
|
|
1816
|
-
spurious:
|
|
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(
|
|
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
|
|
3010
|
-
var RefinerSchema =
|
|
3011
|
-
refinedPrompt:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
3366
|
-
var ClarifyStepSchema =
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
3569
|
-
const { currentBranchOf } = await import("./verify-
|
|
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
|
|
3754
|
-
var taskSchema =
|
|
3755
|
-
id:
|
|
3756
|
-
title:
|
|
3757
|
-
deps:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
3801
|
-
var CoverageSchema =
|
|
3802
|
-
missing:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3965
|
-
memories:
|
|
3966
|
-
text:
|
|
3967
|
-
kind:
|
|
3968
|
-
audience:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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,
|