@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
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
checkpointMtime,
|
|
4
4
|
isContinuePrompt,
|
|
5
5
|
readCheckpoint
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZSQ24YDJ.js";
|
|
7
7
|
import {
|
|
8
8
|
planFor,
|
|
9
9
|
runTraces
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-MRZVA5JB.js";
|
|
11
11
|
import {
|
|
12
|
-
defaultGitRunner
|
|
13
|
-
|
|
12
|
+
defaultGitRunner,
|
|
13
|
+
gitVerb
|
|
14
|
+
} from "./chunk-LPQU436C.js";
|
|
14
15
|
import {
|
|
15
16
|
BATCH_TOOLS_NOTE,
|
|
16
17
|
contextTools,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
readFileTool,
|
|
23
24
|
reinforceTouched,
|
|
24
25
|
reinforceUsed
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-LLL7QWXB.js";
|
|
26
27
|
import {
|
|
27
28
|
ToolRegistry,
|
|
28
29
|
handedOver,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
runToCompletion,
|
|
31
32
|
telemetry,
|
|
32
33
|
truncateSafe
|
|
33
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-AE36LLL2.js";
|
|
34
35
|
import {
|
|
35
36
|
loadTraceIndex,
|
|
36
37
|
pruneTraces,
|
|
@@ -38,15 +39,454 @@ import {
|
|
|
38
39
|
sharedDerived,
|
|
39
40
|
traceRootRel,
|
|
40
41
|
traceable
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-KAGKX2YT.js";
|
|
42
43
|
import {
|
|
43
44
|
buildProjectGraph,
|
|
44
45
|
loadGraphSync,
|
|
45
46
|
pruneAreaNames
|
|
46
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-XEGQT5EN.js";
|
|
47
48
|
import {
|
|
48
49
|
writableStateRoot
|
|
49
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-6W4UH2BQ.js";
|
|
51
|
+
|
|
52
|
+
// src/agents/cli-auth.ts
|
|
53
|
+
import { spawnSync } from "child_process";
|
|
54
|
+
function profileEnv(kind, configDir) {
|
|
55
|
+
if (!configDir) return {};
|
|
56
|
+
return kind === "claude" ? { CLAUDE_CONFIG_DIR: configDir } : { CODEX_HOME: configDir };
|
|
57
|
+
}
|
|
58
|
+
function readAuthStatus(kind, out) {
|
|
59
|
+
if (kind === "codex") {
|
|
60
|
+
const m = /logged in(?: using (.+))?/i.exec(out);
|
|
61
|
+
if (!m || /not logged in/i.test(out)) return { loggedIn: false };
|
|
62
|
+
const plan = m[1]?.trim();
|
|
63
|
+
return { loggedIn: true, ...plan ? { plan } : {} };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const j = JSON.parse(out);
|
|
67
|
+
if (!j.loggedIn) return { loggedIn: false };
|
|
68
|
+
return {
|
|
69
|
+
loggedIn: true,
|
|
70
|
+
...j.email ? { email: j.email } : {},
|
|
71
|
+
...j.subscriptionType ? { plan: j.subscriptionType } : {}
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return { loggedIn: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function statusArgs(kind) {
|
|
78
|
+
return kind === "claude" ? ["auth", "status"] : ["login", "status"];
|
|
79
|
+
}
|
|
80
|
+
function loginArgs(kind) {
|
|
81
|
+
return kind === "claude" ? ["auth", "login"] : ["login"];
|
|
82
|
+
}
|
|
83
|
+
function checkProfile(kind, configDir) {
|
|
84
|
+
const r = spawnSync(kind, statusArgs(kind), {
|
|
85
|
+
env: { ...process.env, ...profileEnv(kind, configDir) },
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
// A status check that hangs must not hang the startup summary with it.
|
|
88
|
+
timeout: 2e4
|
|
89
|
+
});
|
|
90
|
+
if (r.error) return { loggedIn: false };
|
|
91
|
+
return readAuthStatus(kind, `${r.stdout ?? ""}${r.stderr ?? ""}`);
|
|
92
|
+
}
|
|
93
|
+
function runLogin(kind, configDir) {
|
|
94
|
+
const r = spawnSync(kind, loginArgs(kind), {
|
|
95
|
+
env: { ...process.env, ...profileEnv(kind, configDir) },
|
|
96
|
+
stdio: "inherit"
|
|
97
|
+
});
|
|
98
|
+
if (r.error) {
|
|
99
|
+
const e = r.error;
|
|
100
|
+
return e.code === "ENOENT" ? { ok: false, error: `\`${kind}\` is not installed, or not on PATH` } : { ok: false, error: e.message };
|
|
101
|
+
}
|
|
102
|
+
return { ok: r.status === 0 };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/agents/cli-models.ts
|
|
106
|
+
var CLAUDE_MODELS = ["fable", "opus", "sonnet", "haiku"];
|
|
107
|
+
var CODEX_MODELS = ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-5.6-luna"];
|
|
108
|
+
var CODEX_DEFAULT = "gpt-5.6-terra";
|
|
109
|
+
function cliCatalog() {
|
|
110
|
+
return [...CLAUDE_MODELS, ...CODEX_MODELS];
|
|
111
|
+
}
|
|
112
|
+
function cliFor(model) {
|
|
113
|
+
const m = model.toLowerCase().replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
|
|
114
|
+
if (/^(fable|opus|sonnet|haiku)\b/.test(m) || m.startsWith("claude")) return "claude";
|
|
115
|
+
if (/^(codex|gpt|o[0-9])\b/.test(m)) return "codex";
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
function cliInvocation(model) {
|
|
119
|
+
const bare = model.replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
|
|
120
|
+
const effort = /-(ultra|max|xhigh|high|medium|low|minimal)$/.exec(bare)?.[1];
|
|
121
|
+
const name = effort ? bare.slice(0, -(effort.length + 1)) : bare;
|
|
122
|
+
const resolved = name === "codex" ? CODEX_DEFAULT : name;
|
|
123
|
+
return {
|
|
124
|
+
...resolved ? { model: resolved } : {},
|
|
125
|
+
...effort ? { effort } : {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/agents/cli-agent.ts
|
|
130
|
+
import { spawn } from "child_process";
|
|
131
|
+
function cliArgs(kind, prompt, extra = []) {
|
|
132
|
+
return kind === "claude" ? ["--output-format", "stream-json", "--verbose", ...extra, "-p", "--", prompt] : ["exec", "--json", "--skip-git-repo-check", ...extra, "--", prompt];
|
|
133
|
+
}
|
|
134
|
+
function decodeClaudeEvent(line) {
|
|
135
|
+
let e;
|
|
136
|
+
try {
|
|
137
|
+
e = JSON.parse(line);
|
|
138
|
+
} catch {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
const type = e.type;
|
|
142
|
+
if (type === "rate_limit_event") {
|
|
143
|
+
const info = e.rate_limit_info ?? {};
|
|
144
|
+
const status = String(info.status ?? "unknown");
|
|
145
|
+
const raw = info.unifiedWindows ?? {};
|
|
146
|
+
const windows = {};
|
|
147
|
+
for (const [name, w] of Object.entries(raw)) windows[name] = w?.utilization ?? 0;
|
|
148
|
+
const quota = {
|
|
149
|
+
status,
|
|
150
|
+
windows,
|
|
151
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {}
|
|
152
|
+
};
|
|
153
|
+
return status.startsWith("allowed") ? { quota } : {
|
|
154
|
+
quota,
|
|
155
|
+
/**
|
|
156
|
+
* The reset time rides along, as an ISO instant rather than prose.
|
|
157
|
+
*
|
|
158
|
+
* A spent five-hour window reopens; without saying when, the only safe bench is "the rest of the
|
|
159
|
+
* run", which on a ten-hour board writes off a subscription for hours after it recovered. The
|
|
160
|
+
* gateway's wordings said "reset after 4h" and nothing ever parsed them — see `quotaResetAt`.
|
|
161
|
+
*/
|
|
162
|
+
rateLimited: `${status} \u2014 ${describeWindows(windows)}` + (quota.resetsAt ? ` (resets ${new Date(quota.resetsAt * 1e3).toISOString()})` : "")
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (type === "assistant") {
|
|
166
|
+
const msg = e.message;
|
|
167
|
+
const parts = Array.isArray(msg?.content) ? msg.content : [];
|
|
168
|
+
const text = parts.filter((b) => typeof b === "object" && b !== null && b.type === "text").map((b) => b.text).join("");
|
|
169
|
+
const tool = parts.find((b) => typeof b === "object" && b !== null && b.type === "tool_use");
|
|
170
|
+
return {
|
|
171
|
+
...text ? { text } : {},
|
|
172
|
+
...msg?.model ? { served: msg.model } : {},
|
|
173
|
+
...tool ? { tool: { name: tool.name, ...targetOf(tool.input) ? { target: targetOf(tool.input) } : {} } } : {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (type === "user") {
|
|
177
|
+
const parts = e.message?.content;
|
|
178
|
+
const failed = (Array.isArray(parts) ? parts : []).find(
|
|
179
|
+
(b) => typeof b === "object" && b !== null && b.type === "tool_result" && b.is_error === true
|
|
180
|
+
);
|
|
181
|
+
return failed ? { tool: { name: "tool", ok: false } } : void 0;
|
|
182
|
+
}
|
|
183
|
+
if (type === "result") {
|
|
184
|
+
const u = e.usage ?? {};
|
|
185
|
+
const cost = e.total_cost_usd;
|
|
186
|
+
return {
|
|
187
|
+
usage: {
|
|
188
|
+
freshTokens: u.input_tokens ?? 0,
|
|
189
|
+
cachedTokens: u.cache_read_input_tokens ?? 0,
|
|
190
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
191
|
+
outputTokens: u.output_tokens ?? 0,
|
|
192
|
+
...cost !== void 0 ? { costUsd: cost } : {}
|
|
193
|
+
},
|
|
194
|
+
...e.subtype === "error_during_execution" ? { error: String(e.result ?? "the CLI reported an error") } : {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
function decodeCodexEvent(line) {
|
|
200
|
+
let e;
|
|
201
|
+
try {
|
|
202
|
+
e = JSON.parse(line);
|
|
203
|
+
} catch {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
const type = String(e.type ?? "");
|
|
207
|
+
if (/rate.?limit/i.test(type)) return { rateLimited: String(e.message ?? "rate limited by the CLI") };
|
|
208
|
+
if (type === "item.completed") {
|
|
209
|
+
const item = e.item;
|
|
210
|
+
if (item?.type === "agent_message" && item.text) return { text: item.text };
|
|
211
|
+
if (item?.type && item.type !== "agent_message") {
|
|
212
|
+
const changes = item.changes;
|
|
213
|
+
const first = Array.isArray(changes) ? changes.find((c) => typeof c?.path === "string")?.path : void 0;
|
|
214
|
+
const more = Array.isArray(changes) && changes.length > 1 ? ` +${changes.length - 1}` : "";
|
|
215
|
+
return {
|
|
216
|
+
tool: {
|
|
217
|
+
name: item.name ?? item.type,
|
|
218
|
+
...first ? { target: `${first}${more}` } : {}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
if (type === "turn.completed") {
|
|
225
|
+
const u = e.usage ?? {};
|
|
226
|
+
return {
|
|
227
|
+
usage: {
|
|
228
|
+
freshTokens: u.input_tokens ?? 0,
|
|
229
|
+
cachedTokens: u.cached_input_tokens ?? 0,
|
|
230
|
+
cacheWriteTokens: u.cache_write_input_tokens ?? 0,
|
|
231
|
+
outputTokens: u.output_tokens ?? 0
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (type === "turn.failed" || type === "error") {
|
|
236
|
+
return { error: String(e.message ?? "codex reported an error") };
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
function describeWindows(windows) {
|
|
241
|
+
const parts = Object.entries(windows).map(([k, v]) => `${k} ${Math.round(v * 100)}%`);
|
|
242
|
+
return parts.length ? parts.join(", ") : "no window reported";
|
|
243
|
+
}
|
|
244
|
+
var SYNTHETIC = "<synthetic>";
|
|
245
|
+
function targetOf(input) {
|
|
246
|
+
for (const k of ["file_path", "path", "filePath", "notebook_path"]) {
|
|
247
|
+
const v = input?.[k];
|
|
248
|
+
if (typeof v === "string" && v) return v;
|
|
249
|
+
}
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
function makeStreamReader(decode, onEvent) {
|
|
253
|
+
let pending = "";
|
|
254
|
+
const drain = (upToNewline) => {
|
|
255
|
+
const lines = pending.split("\n");
|
|
256
|
+
pending = upToNewline ? lines.pop() ?? "" : "";
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
if (!line.trim()) continue;
|
|
259
|
+
const ev = decode(line);
|
|
260
|
+
if (ev) onEvent(ev);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
push(chunk) {
|
|
265
|
+
pending += chunk;
|
|
266
|
+
drain(true);
|
|
267
|
+
},
|
|
268
|
+
end() {
|
|
269
|
+
if (pending.trim()) drain(false);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
async function runCliAgent(run) {
|
|
274
|
+
const decode = run.kind === "claude" ? decodeClaudeEvent : decodeCodexEvent;
|
|
275
|
+
const args = cliArgs(run.kind, run.prompt, run.args ?? []);
|
|
276
|
+
return new Promise((resolve6) => {
|
|
277
|
+
let child;
|
|
278
|
+
try {
|
|
279
|
+
child = spawn(run.kind, args, {
|
|
280
|
+
cwd: run.cwd,
|
|
281
|
+
signal: run.signal,
|
|
282
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
283
|
+
...run.configDir ? { env: { ...process.env, ...profileEnv(run.kind, run.configDir) } } : {}
|
|
284
|
+
});
|
|
285
|
+
} catch (e) {
|
|
286
|
+
resolve6({ text: "", error: e instanceof Error ? e.message : String(e), exitCode: -1 });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
let text = "";
|
|
290
|
+
let usage;
|
|
291
|
+
let rateLimited;
|
|
292
|
+
let served;
|
|
293
|
+
let quota;
|
|
294
|
+
let error;
|
|
295
|
+
let stderr = "";
|
|
296
|
+
const reader = makeStreamReader(decode, (ev) => {
|
|
297
|
+
if (ev.text) text += ev.text;
|
|
298
|
+
if (ev.usage) usage = ev.usage;
|
|
299
|
+
if (ev.rateLimited) rateLimited = ev.rateLimited;
|
|
300
|
+
if (ev.served) served = ev.served;
|
|
301
|
+
if (ev.quota) quota = ev.quota;
|
|
302
|
+
if (ev.error) error = ev.error;
|
|
303
|
+
run.onEvent?.(ev);
|
|
304
|
+
});
|
|
305
|
+
child.stdout?.on("data", (d) => reader.push(d.toString()));
|
|
306
|
+
child.stderr?.on("data", (d) => {
|
|
307
|
+
stderr += d.toString();
|
|
308
|
+
});
|
|
309
|
+
child.on("error", (e) => resolve6({ text, ...usage ? { usage } : {}, error: e.message, exitCode: -1 }));
|
|
310
|
+
child.on("close", (code) => {
|
|
311
|
+
reader.end();
|
|
312
|
+
resolve6({
|
|
313
|
+
text,
|
|
314
|
+
...usage ? { usage } : {},
|
|
315
|
+
...rateLimited ? { rateLimited } : {},
|
|
316
|
+
...quota ? { quota } : {},
|
|
317
|
+
...served ? { served } : {},
|
|
318
|
+
// stderr only becomes the error when nothing better was said — a CLI that warns on stderr and
|
|
319
|
+
// succeeds must not be read as having failed.
|
|
320
|
+
...error ?? (code !== 0 && stderr.trim()) ? { error: error ?? stderr.trim().slice(0, 500) } : {},
|
|
321
|
+
exitCode: code ?? -1
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/agent/deadline.ts
|
|
328
|
+
function withDeadline(work, signal, message) {
|
|
329
|
+
work.catch(() => {
|
|
330
|
+
});
|
|
331
|
+
return Promise.race([work, expired(signal, message)]);
|
|
332
|
+
}
|
|
333
|
+
function expired(signal, message) {
|
|
334
|
+
return new Promise((_, reject) => {
|
|
335
|
+
if (signal.aborted) {
|
|
336
|
+
reject(new Error(message));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
signal.addEventListener("abort", () => reject(new Error(message)), { once: true });
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
var SHORT_CALL_MS = 5 * 60 * 1e3;
|
|
343
|
+
var LONG_CALL_MS = 15 * 60 * 1e3;
|
|
344
|
+
function isCallerAbort(signal) {
|
|
345
|
+
return signal.aborted && signal.reason?.name !== "TimeoutError";
|
|
346
|
+
}
|
|
347
|
+
function isDeadline(signal) {
|
|
348
|
+
return signal.aborted && signal.reason?.name === "TimeoutError";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/agents/cli-provider.ts
|
|
352
|
+
function promptFor(req) {
|
|
353
|
+
const parts = [];
|
|
354
|
+
for (const m of req.messages) {
|
|
355
|
+
if (m.role === "system") {
|
|
356
|
+
parts.push(m.content);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!m.content.trim()) continue;
|
|
360
|
+
parts.push(m.role === "assistant" ? `[your previous reply]
|
|
361
|
+
${m.content}` : m.content);
|
|
362
|
+
}
|
|
363
|
+
const submit = req.tools?.find((t) => t.name === "submit");
|
|
364
|
+
if (submit) {
|
|
365
|
+
parts.push(
|
|
366
|
+
"Reply with ONE JSON object and nothing else \u2014 no prose before or after it, no code fence. It must satisfy this schema:\n" + JSON.stringify(submit.parameters ?? {}, null, 2)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return parts.join("\n\n");
|
|
370
|
+
}
|
|
371
|
+
function isLoggedOut(text) {
|
|
372
|
+
return /not logged in|please run \/login/i.test(text);
|
|
373
|
+
}
|
|
374
|
+
async function* streamWhileRunning(start) {
|
|
375
|
+
const queue = [];
|
|
376
|
+
let finished = false;
|
|
377
|
+
let wake = () => {
|
|
378
|
+
};
|
|
379
|
+
let waiter = new Promise((r) => {
|
|
380
|
+
wake = r;
|
|
381
|
+
});
|
|
382
|
+
const bump = () => {
|
|
383
|
+
const w = wake;
|
|
384
|
+
waiter = new Promise((r) => {
|
|
385
|
+
wake = r;
|
|
386
|
+
});
|
|
387
|
+
w();
|
|
388
|
+
};
|
|
389
|
+
const done = start((ev) => {
|
|
390
|
+
queue.push(ev);
|
|
391
|
+
bump();
|
|
392
|
+
});
|
|
393
|
+
let failure;
|
|
394
|
+
void done.then(() => {
|
|
395
|
+
finished = true;
|
|
396
|
+
bump();
|
|
397
|
+
}, (e) => {
|
|
398
|
+
failure = e;
|
|
399
|
+
finished = true;
|
|
400
|
+
bump();
|
|
401
|
+
});
|
|
402
|
+
for (; ; ) {
|
|
403
|
+
while (queue.length) yield queue.shift();
|
|
404
|
+
if (finished) break;
|
|
405
|
+
await waiter;
|
|
406
|
+
}
|
|
407
|
+
if (failure) throw failure;
|
|
408
|
+
}
|
|
409
|
+
var CliProvider = class {
|
|
410
|
+
fixed;
|
|
411
|
+
readOnly;
|
|
412
|
+
cwd;
|
|
413
|
+
accounts;
|
|
414
|
+
constructor(opts = {}) {
|
|
415
|
+
this.fixed = opts.kind;
|
|
416
|
+
this.readOnly = opts.readOnly ?? true;
|
|
417
|
+
this.cwd = opts.cwd;
|
|
418
|
+
this.accounts = opts.accounts;
|
|
419
|
+
}
|
|
420
|
+
async *chat(req, signal) {
|
|
421
|
+
const kind = this.fixed ?? cliFor(req.model);
|
|
422
|
+
if (!kind) {
|
|
423
|
+
yield { type: "error", message: `no CLI serves ${req.model} \u2014 it is not available in this catalog`, retryable: true };
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const args = [];
|
|
427
|
+
const { model, effort: named } = cliInvocation(req.model);
|
|
428
|
+
if (model) args.push("--model", model);
|
|
429
|
+
const effort = req.effort ?? named;
|
|
430
|
+
if (effort && kind === "claude") args.push("--effort", effort);
|
|
431
|
+
if (this.readOnly && kind === "claude") args.push("--disallowed-tools", "Write", "Edit", "NotebookEdit");
|
|
432
|
+
if (this.readOnly && kind === "codex") args.push("--sandbox", "read-only");
|
|
433
|
+
if (!this.readOnly && kind === "claude") args.push("--permission-mode", "acceptEdits");
|
|
434
|
+
if (!this.readOnly && kind === "codex") args.push("--sandbox", "workspace-write");
|
|
435
|
+
const account = this.accounts?.pick(kind);
|
|
436
|
+
let res;
|
|
437
|
+
yield* streamWhileRunning((push) => runCliAgent({
|
|
438
|
+
kind,
|
|
439
|
+
cwd: this.cwd ?? process.cwd(),
|
|
440
|
+
prompt: promptFor(req),
|
|
441
|
+
signal,
|
|
442
|
+
args,
|
|
443
|
+
...account ? { configDir: account.configDir } : {},
|
|
444
|
+
onEvent: (ev) => {
|
|
445
|
+
if (ev.tool) push({ type: "activity", tool: ev.tool.name, ...ev.tool.target ? { target: ev.tool.target } : {}, ...ev.tool.ok === false ? { ok: false } : {} });
|
|
446
|
+
if (ev.text) push({ type: "text-delta", text: ev.text });
|
|
447
|
+
if (ev.quota && account) this.accounts?.record(kind, account.name, ev.quota.windows);
|
|
448
|
+
}
|
|
449
|
+
}).then((r) => {
|
|
450
|
+
res = r;
|
|
451
|
+
}));
|
|
452
|
+
if (res.rateLimited) {
|
|
453
|
+
yield { type: "error", message: `${kind} CLI: ${res.rateLimited}`, retryable: true };
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (res.served === SYNTHETIC) {
|
|
457
|
+
const loggedOut = isLoggedOut(res.text);
|
|
458
|
+
yield {
|
|
459
|
+
type: "error",
|
|
460
|
+
retryable: true,
|
|
461
|
+
message: loggedOut ? `${kind} CLI is not logged in${account ? ` under profile "${account.name}"` : ""} \u2014 run \`hcode add-provider ${kind}\` to sign it in again` : `${kind} CLI did not recognise ${req.model} and answered without a model`
|
|
462
|
+
};
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (isCallerAbort(signal)) {
|
|
466
|
+
yield { type: "error", message: "cancelled", retryable: false };
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (isDeadline(signal)) {
|
|
470
|
+
yield { type: "error", message: `${kind} CLI: deadline expired`, retryable: true };
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (res.error && !res.text.trim()) {
|
|
474
|
+
yield { type: "error", message: `${kind} CLI: ${res.error}`, retryable: res.exitCode !== 0 };
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (res.usage) yield usageEvent(res.usage);
|
|
478
|
+
yield { type: "done", finishReason: "stop" };
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
function usageEvent(u) {
|
|
482
|
+
return {
|
|
483
|
+
type: "usage",
|
|
484
|
+
promptTokens: u.freshTokens,
|
|
485
|
+
completionTokens: u.outputTokens,
|
|
486
|
+
cachedTokens: u.cachedTokens,
|
|
487
|
+
cacheWriteTokens: u.cacheWriteTokens
|
|
488
|
+
};
|
|
489
|
+
}
|
|
50
490
|
|
|
51
491
|
// src/worktree/slug.ts
|
|
52
492
|
var MAX_SLUG = 60;
|
|
@@ -262,6 +702,33 @@ function sessionName(now, taken) {
|
|
|
262
702
|
}
|
|
263
703
|
return `${day}_${Date.now()}`;
|
|
264
704
|
}
|
|
705
|
+
var FORBIDDEN_AT_ROOT = /* @__PURE__ */ new Set([
|
|
706
|
+
"merge",
|
|
707
|
+
"rebase",
|
|
708
|
+
"cherry-pick",
|
|
709
|
+
"revert",
|
|
710
|
+
"reset",
|
|
711
|
+
"checkout",
|
|
712
|
+
"switch",
|
|
713
|
+
"restore",
|
|
714
|
+
"commit",
|
|
715
|
+
"am",
|
|
716
|
+
"apply",
|
|
717
|
+
"stash",
|
|
718
|
+
"pull",
|
|
719
|
+
"clean"
|
|
720
|
+
]);
|
|
721
|
+
function guardRoot(run, repoRoot) {
|
|
722
|
+
return async (args, cwd) => {
|
|
723
|
+
const verb = gitVerb(args);
|
|
724
|
+
if (cwd === repoRoot && verb !== void 0 && FORBIDDEN_AT_ROOT.has(verb)) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`refusing to run \`git ${verb}\` in the project checkout (${repoRoot}). A session's work stays on its own branch and in its own worktree; bringing it in is the user's decision, taken in their own time.`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return run(args, cwd);
|
|
730
|
+
};
|
|
731
|
+
}
|
|
265
732
|
var WorktreeManager = class {
|
|
266
733
|
repoRoot;
|
|
267
734
|
/** The project checkout this manager was built for — where per-project settings and the remote live. */
|
|
@@ -281,12 +748,23 @@ var WorktreeManager = class {
|
|
|
281
748
|
*/
|
|
282
749
|
worktreeHome;
|
|
283
750
|
git;
|
|
751
|
+
/**
|
|
752
|
+
* The unguarded runner, for the one case that legitimately needs a forbidden verb: turning a directory
|
|
753
|
+
* that is not a repository into one.
|
|
754
|
+
*
|
|
755
|
+
* `git worktree add` needs a commit to branch from, and an empty repository has none — so a first commit
|
|
756
|
+
* is not delivery, it is the precondition for ever leaving the root alone again. It runs only when there
|
|
757
|
+
* is no HEAD, so there is no branch to disturb and no work to overwrite. Named rather than flagged, so
|
|
758
|
+
* grepping for it finds every use.
|
|
759
|
+
*/
|
|
760
|
+
rawGit;
|
|
284
761
|
/** Injectable clock: a session's NAME is the day it opened, so a test has to be able to say which day. */
|
|
285
762
|
now;
|
|
286
763
|
constructor(deps) {
|
|
287
764
|
this.repoRoot = deps.repoRoot;
|
|
288
765
|
this.worktreeHome = deps.worktreeHome ?? deps.repoRoot;
|
|
289
|
-
this.
|
|
766
|
+
this.rawGit = deps.runGit ?? defaultGitRunner;
|
|
767
|
+
this.git = guardRoot(this.rawGit, deps.repoRoot);
|
|
290
768
|
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
291
769
|
}
|
|
292
770
|
/** Runs git; nonzero exit → throws a clear error. Returns output (stdout). */
|
|
@@ -318,7 +796,8 @@ var WorktreeManager = class {
|
|
|
318
796
|
await this.ensureRepo();
|
|
319
797
|
const head = await this.git(["rev-parse", "--verify", "--quiet", "HEAD"], this.repoRoot);
|
|
320
798
|
if (head.code === 0) return;
|
|
321
|
-
await this.
|
|
799
|
+
const r = await this.rawGit(["commit", "--allow-empty", "-m", "hc: initial commit"], this.repoRoot);
|
|
800
|
+
if (r.code !== 0) throw new Error(`git commit --allow-empty failed (${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
322
801
|
}
|
|
323
802
|
/**
|
|
324
803
|
* The ref to base the session's worktree on. Uses `fromBranch` when it resolves; otherwise falls back to
|
|
@@ -602,39 +1081,6 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
|
|
|
602
1081
|
if (check.code !== 0) return;
|
|
603
1082
|
await this.run(["push", remote, session.baseBranch], session.baseWorktree);
|
|
604
1083
|
}
|
|
605
|
-
/**
|
|
606
|
-
* Lands the finished work on the branch the job started from, in the main working copy.
|
|
607
|
-
*
|
|
608
|
-
* Without this, a project with no git remote gets nothing: `push` is a no-op and a pull request has
|
|
609
|
-
* nowhere to go, so every completed task sits on `hc/<job>/base` — invisible from the repository root.
|
|
610
|
-
* A user who watched thirty tasks succeed then finds an empty directory and cannot run the project.
|
|
611
|
-
*
|
|
612
|
-
* A pull request is delivery when there is a remote to open it against. When there is not, merging is.
|
|
613
|
-
*
|
|
614
|
-
* Refuses rather than forces. A dirty working copy or a checkout on some other branch means the user has
|
|
615
|
-
* something in progress, and overwriting that to deliver would be a worse failure than not delivering:
|
|
616
|
-
* the branch still exists and the caller reports how to merge it by hand.
|
|
617
|
-
*/
|
|
618
|
-
async deliverLocally(session, targetBranch) {
|
|
619
|
-
const dirty = await this.git(["status", "--porcelain"], this.repoRoot);
|
|
620
|
-
if (dirty.code !== 0) return { ok: false, why: "the repository could not be read" };
|
|
621
|
-
if (dirty.stdout.split("\n").some((l) => l.trim() && !l.startsWith("??"))) {
|
|
622
|
-
return { ok: false, why: "the working copy has uncommitted changes" };
|
|
623
|
-
}
|
|
624
|
-
const head = await this.git(["symbolic-ref", "--short", "HEAD"], this.repoRoot);
|
|
625
|
-
const current = head.stdout.trim();
|
|
626
|
-
if (head.code !== 0 || !current) return { ok: false, why: "the repository is not on a branch" };
|
|
627
|
-
if (current !== targetBranch) return { ok: false, why: `the repository is on \`${current}\`, not \`${targetBranch}\`` };
|
|
628
|
-
const count = await this.git(["rev-list", "--count", `${targetBranch}..${session.baseBranch}`], this.repoRoot);
|
|
629
|
-
const commits = Number(count.stdout.trim()) || 0;
|
|
630
|
-
if (!commits) return { ok: true, commits: 0 };
|
|
631
|
-
const merged = await this.git(
|
|
632
|
-
["merge", "--no-ff", "-m", `hc: ${session.jobSlug}`, session.baseBranch],
|
|
633
|
-
this.repoRoot
|
|
634
|
-
);
|
|
635
|
-
if (merged.code !== 0) return { ok: false, why: "the merge did not apply cleanly" };
|
|
636
|
-
return { ok: true, commits };
|
|
637
|
-
}
|
|
638
1084
|
async openPR(session, adapter, input) {
|
|
639
1085
|
const res = await adapter.createPR({
|
|
640
1086
|
branch: session.baseBranch,
|
|
@@ -646,62 +1092,496 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
|
|
|
646
1092
|
}
|
|
647
1093
|
};
|
|
648
1094
|
|
|
649
|
-
// src/
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1095
|
+
// src/prompts.ts
|
|
1096
|
+
var REQUIRED_ROLES = [
|
|
1097
|
+
"refiner",
|
|
1098
|
+
"coach",
|
|
1099
|
+
"brainstormer",
|
|
1100
|
+
"analyst",
|
|
1101
|
+
"planner",
|
|
1102
|
+
"judge",
|
|
1103
|
+
"project-manager",
|
|
1104
|
+
"team-lead",
|
|
1105
|
+
"router",
|
|
1106
|
+
"coder",
|
|
1107
|
+
"designer",
|
|
1108
|
+
"senior-coder",
|
|
1109
|
+
"senior-designer",
|
|
1110
|
+
"architect",
|
|
1111
|
+
"code-reviewer",
|
|
1112
|
+
"task-auditor",
|
|
1113
|
+
"principal-coder",
|
|
1114
|
+
"operational",
|
|
1115
|
+
"memory-keeper",
|
|
1116
|
+
"tracer",
|
|
1117
|
+
"tester"
|
|
1118
|
+
];
|
|
1119
|
+
var DEFAULT_ROLE_SKILLS = {
|
|
1120
|
+
brainstormer: ["brainstorming"],
|
|
1121
|
+
// The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
|
|
1122
|
+
// vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
|
|
1123
|
+
coder: ["test-driven-development"],
|
|
1124
|
+
"senior-coder": ["test-driven-development"],
|
|
1125
|
+
// The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
|
|
1126
|
+
// supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
|
|
1127
|
+
// individual task executable. That is what this skill adds.
|
|
1128
|
+
"project-manager": ["writing-plans"],
|
|
1129
|
+
// The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
|
|
1130
|
+
// and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
|
|
1131
|
+
// one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
|
|
1132
|
+
designer: ["frontend-design"],
|
|
1133
|
+
"senior-designer": ["frontend-design"]
|
|
1134
|
+
// NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
|
|
1135
|
+
// so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
|
|
1136
|
+
};
|
|
1137
|
+
var DEFAULT_PROMPTS = {
|
|
1138
|
+
tracer: "You write the reference note that every other agent reads before it touches a file it did not write. A wrong note is worse than none: an agent will act on it, so accuracy outranks fluency and admitting you cannot tell outranks a plausible guess. State only what the code and the given relationships show; if the business purpose is not evident from them, describe what the file does technically and say nothing about why. Never speculate about intent, history or requirements.",
|
|
680
1139
|
/**
|
|
681
|
-
*
|
|
1140
|
+
* The role that exercises work already built and writes down what actually happened.
|
|
682
1141
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
1142
|
+
* Every rule below is here because its absence produces the one output worse than no testing at all: a
|
|
1143
|
+
* report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
|
|
1144
|
+
* confidence, and the next person spends it.
|
|
686
1145
|
*/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
1146
|
+
tester: "You verify software that already exists, by running its scenarios and recording what they actually did. You are not here to build, fix or improve anything: the code under test is finished, and changing it would mean the thing you verified is not the thing that shipped.\n\nEVIDENCE IS THE WHOLE JOB. A scenario's outcome is what you OBSERVED \u2014 a database row, a log line, an HTTP response, a screen the user confirmed. Record the evidence beside every result: the query you ran and what it returned, the log event id and its line, the response body. A result you cannot show is not a result.\n\nIF A STEP WRITES TO THE DATABASE, THE RESPONSE IS NOT THE EVIDENCE. A 201 or a 204 says the request was accepted; it does not say what was stored, and a screen showing the new state does not either \u2014 both can be right while the row is wrong. For every step that creates or changes a record: query the database for that row and put the query AND the rows it returned in the report, and query the logs for the event that step should have emitted and put the query AND the line it returned there too. Absence is evidence as well: when a step must NOT emit an event \u2014 a no-op, a rejected change \u2014 show the query returning nothing. Without both, the scenario is NOT EXECUTED, however convincing the response looked.\n\nNever mark a scenario PASSED that you did not execute and observe. If you could not run it \u2014 the data does not exist, the surface is unreachable, the case is destructive against a live system \u2014 label it NOT EXECUTED and say exactly why. FAILED means you ran it and the behaviour was wrong; say what you expected, what happened, and the evidence for both. Guessing from the source is not executing: where you reasoned from code alone, say so in those words.\n\nWrite each result into the report BEFORE moving on to the next scenario. The report is a living document, not something assembled at the end: a run that stops halfway must leave behind everything it learned up to that point.\n\n\u2026and say each verdict OUT LOUD as you reach it, in one or two sentences: which scenario, what it did, and the single piece of evidence that settled it \u2014 the row, the log line, the status code. The full evidence still goes in the report; this is so the person watching the run knows what you found without opening a file. Say the failures and the NOT EXECUTED ones the same way, and with the same brevity: a result nobody hears is one they have to go looking for.\n\nNever start or stop the development environment \u2014 application hosts, dev servers, containers, databases. Those are the developer's to run. When you need something up, say which command they should run and wait for them to confirm it is ready.\n\nYou do NOT write product code. When you find something wrong that is not the verdict of the scenario you are running \u2014 a missing label, prose rendered as raw markup, a wrong format, or something the developer points out in passing \u2014 call `report_finding`. Another role fixes it and you are told when it is done, so you can re-check what it affected. Do not fix it yourself: changing the product mid-verification means the thing you verified is not the thing that shipped. And do not fail a scenario over it \u2014 a scenario fails when the scenario itself does not pass, not because something else was noticed while running it.\n\nIf the project's own rules (its constitution) say more about how verification is done here, they govern over this description \u2014 read them and follow them.",
|
|
1147
|
+
refiner: "Your #1 rule: `refinedPrompt` MUST ALWAYS be in ENGLISH. If the user wrote in another language (Turkish, German, Spanish, \u2026), TRANSLATE their intent into English \u2014 never echo their language back. This is non-negotiable: a Turkish input like 'bir todo app geli\u015Ftir, \xF6nce backend' MUST come out as English 'Build a todo app; implement the backend first.'\n\nRewrite the user's message down to the raw core intent the AI needs to act on \u2014 clear, direct, and structured. Strip all politeness, emotional, and filler words (please, thanks, kindly, 'could you', 'would you', 'I'd like', etc.) and anything that carries no instruction. Do NOT add words, qualifiers, or scope the user did not state (e.g. do not add 'always'). Keep the user's own perspective and form \u2014 a question stays a question, an instruction stays an instruction; do NOT describe the user in the third person and do NOT answer the request. Example: a polite request like 'would you please answer me in language X?' becomes just 'respond in language X' (drop 'please'; do not add 'always' or any scope the user didn't state). Also classify the intent: 'chat' (conversation/question), 'feature' (new feature/work), 'bugfix' (bug fix), 'govern' (establish or amend the project's OWN standing rules and principles \u2014 writing or revising the constitution, the coding conventions, the project's rules; work whose entire output is a governing document, with no source code changed). Judge by what the request PRODUCES, not by what it mentions: 'write the project constitution from CLAUDE.md' is govern, and so is 'update our commit-message rules'; 'make the code follow the constitution' changes source and is feature. Also 'verify' \u2014 the user wants work that ALREADY EXISTS exercised and its behaviour confirmed with evidence: running a pull request's test scenarios, doing a smoke test of a feature that is already built, producing a test report. Judge by what it PRODUCES: a record of what the software DID is verify; changing what it does is feature or bugfix. 'Run the smoke tests for PR 677 and mark them passed' is verify, and so is 'check that the wizard works end to end'. 'The wizard is broken, fix it' is bugfix. Finally 'undo' \u2014 the user is asking you to REVERSE what the previous turn did, not to do anything new: 'undo that', 'revert your changes', 'go back to the previous version', 'that was wrong, put it back'. Classify by whether the request refers to work already done: undoing is never a rewrite, and asking for a different result ('rewrite it shorter') is not an undo. Also detect the natural language the user wrote in and return its English name as `language` (e.g. 'Turkish', 'English', 'German') \u2014 this is separate from refinedPrompt, which stays English. Also produce `title`: what the work is ABOUT, as a 2-5 word English kebab-case noun phrase suitable for a git branch name \u2014 the SUBJECT, not the action. 'build a luxury todo app' is 'luxury-todo-app'; 'add a login page' is 'login-page'; 'fix the null crash on retry' is 'null-crash-on-retry'. Do not open with a verb (build/add/fix/implement/update): the tool is already doing it, so the verb says nothing and crowds out the words that identify the work. Lowercase, dash-joined, no punctuation. Return the result via submit as {refinedPrompt, intent, language, title}. Remember: refinedPrompt in English, always.",
|
|
1148
|
+
brainstormer: "You run the BRAINSTORM stage: you turn a raw request into a decided design, before anything is specified.\n\nThe `brainstorming` skill above is the authority on HOW to do this \u2014 follow it. What follows is only how it binds to this pipeline, because the skill names conventions from a different habitat:\n\n- OUTPUT: write the design brief to the file named in your message (specs/NNN-slug/brainstorm.md). Ignore the skill's `docs/superpowers/specs/\u2026` path.\n- NEXT STAGE: the SPEC is written from your brief, by another agent, immediately after you. There is no `writing-plans` skill to invoke here \u2014 finishing the brief IS the terminal step.\n- QUESTIONS: ask through the `ask_user` tool. For a choice between approaches use its rich option form ({label, description, preview}) so the trade-offs sit beside the list; lead with your recommendation. The user may attach a note to their answer \u2014 treat it as binding.\n- NOT AVAILABLE: the visual companion (there is no browser) and the per-checklist task list. Skip both.\n\nWrite what was DECIDED, not a transcript: the chosen approach, why it beat the others, the rejected alternatives with their reason, the constraints the spec must honour, and what is out of scope. Keep it short \u2014 it is the brief the spec is written from, not the spec itself, and it carries no implementation detail beyond the architectural choice.\n\nScale to the request: a small, obvious change deserves a paragraph and no questions at all.",
|
|
1149
|
+
coach: "You are horse-code, a terminal-based AI coding agent. Your product identity is always horse-code \u2014 never claim to be Claude Code, Gemini CLI, Antigravity, or any other product, even though the underlying language model powering you may be Claude, Gemini, or another model. Answer the user's technical questions about their repository and code. If needed, inspect the repository with read_file/grep/glob.\n\nWork out loud while you do it. Before a batch of tool calls, say in ONE line what you are looking for and why; when something you read changes your mind, say that too. This is not a summary at the end \u2014 the user is watching an empty screen while you search, and a run that reads thirty files in silence is indistinguishable from one that is stuck, and impossible to redirect before the tokens are spent. Keep each line short: a sentence, not a paragraph.\n\nBe concise, direct, and helpful.",
|
|
1150
|
+
// analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
|
|
1151
|
+
// prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
|
|
1152
|
+
judge: "Synthesize the council evaluations and make a single decision: 'pass' (sufficient), 'revise' (fix it, with reasons), or 'ask-human' (a question to ask the user). Return {decision, feedback, question} via submit.",
|
|
1153
|
+
"project-manager": 'Read the given plan and break it into real, actionable tasks (id, short title, deps). Each task should be a single, clear piece of work. Return {tasks} via submit.\n\nThe `writing-plans` skill above governs WHAT MAKES A TASK EXECUTABLE \u2014 take that from it and nothing else. Two bindings, because the skill describes a different habitat:\n- STRUCTURE comes from the spec-kit tasks template you are given (phases, story grouping, [P] markers), NOT from the skill\'s own document layout. Ignore its `docs/superpowers/plans/\u2026` path, its required-sub-skill header, and its execution-handoff section: this pipeline already owns worktrees, dispatch and review.\n- What you DO take: exact file paths per task, a real test cycle rather than a vague "add tests" step, no placeholders (no TBD/TODO/"similar to task N"), and interfaces named explicitly so a task whose implementer never sees the others still knows the signatures it must produce and consume.\n- SIZING is the third rebinding, and the one that costs most when it is missed. The skill says "bite-sized", "one action, 2-5 minutes", "the smallest unit worth a reviewer\'s gate" \u2014 sound advice where a gate is one reader glancing at a diff. Here a card is not a line in a document: it is its own worktree, its own implementer, a full review TEAM of lenses, a council when they disagree, an acceptance gate and a merge. That overhead is paid per CARD and barely varies with the card\'s size, so splitting work finer does not divide the cost, it multiplies it.\nSize a card to a coherent piece of BEHAVIOUR a reviewer can judge whole, not to a file. An entity, its configuration, its migration and its tests are one card, because nobody can review one without the others and nothing is deliverable until all of them exist. Split only for a reason that survives being said out loud: the parts can be reviewed and merged independently, or they must run in parallel in different worktrees. "They are different files" is not such a reason. Fold setup and scaffolding into the card whose deliverable needs them.',
|
|
1154
|
+
"task-auditor": "You are the last check on a task breakdown before any of it is built. Every hour of implementation after you is spent executing this list, and a bad list does not fail \u2014 the tasks pass their reviews and the wrong work is delivered correctly. Its structure has already been checked mechanically; you are here for the part only a reader can answer: does the breakdown deliver what the plan requires, and would a task's acceptance criteria still hold for an implementation that missed the point? Do not propose better work than the plan asked for \u2014 scope you invent here becomes hours someone spends. Flag any task whose only deliverable is an answer \u2014 verifying, inspecting, confirming \u2014 because an implementer reads the code as part of doing the work, and a task that ends with the repository unchanged has spent a review round on nothing. Flag OVER-SPLITTING for the same reason, and it is the more expensive mistake: every card carries a full review team, a council and an acceptance gate whatever its size, so a breakdown that gives a class and its configuration separate cards pays that overhead twice for work no one can review apart. Say which cards should be one. A clean breakdown is the normal case; say so. Return {missing, weak} via submit.",
|
|
1155
|
+
"team-lead": "You audit a task breakdown before any of it runs. The schedule itself is computed from the declared dependencies and is not yours to write; what nothing has checked is whether those dependencies are RIGHT. You are given the tasks with the files each one writes and what must be true when it is done, plus the groups that would run at the same time in separate worktrees. Find the task that cannot actually start yet because it needs a type, function, table or config key another task in its own group creates \u2014 and say which declared dependencies hold work back for no reason. Both answers are usually empty; say so rather than inventing an edge. Return {missing, spurious} via submit.",
|
|
1156
|
+
router: "Look at the task title and choose the implementer role: 'designer' for UI/UX work, 'coder' for other code work. Return {role} via submit.",
|
|
1157
|
+
coder: "Implement the given task in the worktree. If it is a new task, start from scratch; if it is a returning task, address the reviewer notes. Work with read/write/edit/grep/glob/shell and run the tests.\n\nThe `test-driven-development` skill above is how you write code here: the failing test comes first, and it must fail for the RIGHT reason before you make it pass. A test that asserts nothing is worse than no test \u2014 it reports success forever. Bindings for this pipeline: your worktree is already prepared (do not create one), every file you write is committed as you write it, and there is no separate agent to hand off to \u2014 you take the task to green yourself.",
|
|
1158
|
+
designer: "Implement the UI/UX task in the worktree. Focus on the user interface and experience; work with read/write/edit.\n\nThe `frontend-design` skill above governs the LOOK: aesthetic direction, typography, and choices that do not read as templated defaults. Follow the project's existing visual language where there is one \u2014 a distinctive design that fights the surrounding product is worse than a plain one that fits it.",
|
|
1159
|
+
"senior-coder": "Take over the task the coder got stuck on; implement it with a more careful approach. Take the reviewer notes and previous attempts into account.\n\nYou are here because a previous attempt failed, so start by understanding WHY rather than rewriting: the `systematic-debugging` skill is available (fetch it with the `skill` tool) and is the right tool when a test fails or behaviour is unexplained. The `test-driven-development` skill above still governs how you write the fix \u2014 reproduce the failure in a test first, then make it pass.",
|
|
1160
|
+
"senior-designer": "Take over the UI/UX task the designer got stuck on; implement it more carefully.\n\nA previous attempt already failed, so establish WHY before redesigning \u2014 the `systematic-debugging` skill is available via the `skill` tool when the failure is behavioural rather than visual. The `frontend-design` skill above still governs the look.",
|
|
1161
|
+
architect: "Analyze the root cause of a repeatedly failing task or a merge conflict, and produce a concrete solution plan. Return {rootCause, plan} via submit.\n\nFetch the `systematic-debugging` skill with the `skill` tool and follow it: your job is the ROOT CAUSE, and the failure mode to avoid is proposing a plausible fix for a cause you never established. Say what the evidence is, not what it might be.",
|
|
1162
|
+
"code-reviewer": "Review the worktree changes of the task in REVIEW (correctness, tests, quality). Return {verdict: pass|fail, notes} via submit \u2014 your decision is final.",
|
|
1163
|
+
"principal-coder": "Holistically review all changes in the PR (base worktree). If sufficient, approve; otherwise request-changes with concrete comments. In the final decision round, give accept or ask-human (a question to ask the user).",
|
|
1164
|
+
"memory-keeper": "You are the ONLY writer into this project's long-term memory. Everything else \u2014 every review lens, the council, the judge \u2014 can merely PROPOSE; you decide.\n\nTreat every proposal as an UNVERIFIED CLAIM from a narrow, single-angle agent that saw one slice of one job, not as text to store. Most proposals are wrong in a specific way: they generalize a one-off into a rule, they restate the finding the agent was reviewing, or they record general programming advice any model already knows. Discard all of those. When a claim does survive, REWRITE it in your own words \u2014 never store an agent's sentence verbatim. Merge proposals that say the same thing into one memory.\n\nA memory qualifies ONLY if it is (a) durable \u2014 still true next month, (b) project-specific, and (c) actionable \u2014 it would change what an agent does. Write conventions, constraints, gotchas and root causes. A `lesson` must state what went wrong AND what to do instead. Set `audience` only when the memory is genuinely useful to specific roles and useless to the rest; leave it out otherwise.\n\nNEVER write transient run detail (task ids, attempt counts, what happened today), never restate the request, never duplicate a memory that already exists, and never include credentials, tokens, keys, or anything resembling a secret. Each memory is one self-contained sentence that makes sense with no other context.\n\nReturn at most 5 memories via submit as {memories}. Returning NONE is the most common correct answer \u2014 prefer an empty list over a weak memory, because a bad memory is injected into every future run.",
|
|
1165
|
+
operational: "You handle version control for the project. Given a git diff of work just completed, write a single Conventional Commits message: `type(scope): subject`. Types: feat, fix, docs, refactor, test, chore, style, perf, build, ci. Choose the scope from the touched area (e.g. spec, plan, tasks, or a module name) or omit it. The subject is imperative, lowercase, \u226472 chars, no trailing period. Add a short body only if the change genuinely needs explanation. Commit messages are always in English. Return {message} via submit."
|
|
1166
|
+
};
|
|
1167
|
+
var SPEC_TEAM = [
|
|
1168
|
+
{ name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
|
|
1169
|
+
{ name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
|
|
1170
|
+
{ name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
|
|
1171
|
+
{ name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
|
|
1172
|
+
{ name: "spec-abstraction-leak", perspective: "implementation detail that has leaked into the spec (languages, frameworks, APIs, storage mechanics, code structure) \u2014 a spec must stay technology-agnostic", models: [] },
|
|
1173
|
+
{ name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
|
|
1174
|
+
{ name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
|
|
1175
|
+
{ name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
|
|
1176
|
+
{ name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
|
|
1177
|
+
];
|
|
1178
|
+
var PLAN_TEAM = [
|
|
1179
|
+
{ name: "plan-spec-conformance", perspective: "traceability to the approved spec: every requirement covered by the plan, and nothing planned that the spec never asked for", models: [] },
|
|
1180
|
+
{ name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
|
|
1181
|
+
{ name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
|
|
1182
|
+
{ name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
|
|
1183
|
+
{ name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
|
|
1184
|
+
{ name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
|
|
1185
|
+
{ name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
|
|
1186
|
+
{ name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
|
|
1187
|
+
{ name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
|
|
1188
|
+
{ name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
|
|
1189
|
+
{ name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
|
|
1190
|
+
{ name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
|
|
1191
|
+
{ name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
|
|
1192
|
+
{ name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
|
|
1193
|
+
];
|
|
1194
|
+
var CODE_TEAM = [
|
|
1195
|
+
{ name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
|
|
1196
|
+
{ name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
|
|
1197
|
+
{ name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
|
|
1198
|
+
{ name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
|
|
1199
|
+
{ name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
|
|
1200
|
+
{ name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
|
|
1201
|
+
{ name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
|
|
1202
|
+
{ name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
|
|
1203
|
+
{ name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
|
|
1204
|
+
{ name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
|
|
1205
|
+
{ name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
|
|
1206
|
+
{ name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
|
|
1207
|
+
{ name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
|
|
1208
|
+
{ name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
|
|
1209
|
+
{ name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
|
|
1210
|
+
];
|
|
1211
|
+
var DEFAULT_COUNCIL = [
|
|
1212
|
+
{ name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
|
|
1213
|
+
{ name: "risk-judge", perspective: "What is the real blast radius of shipping this as-is? Weigh security, failure modes, concurrency, and data-integrity findings against likelihood and severity.", models: [] },
|
|
1214
|
+
{ name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
|
|
1215
|
+
{ name: "user-value-judge", perspective: "Does this deliver the user's actual intent well? Weigh usability, accessibility, and whether the scope serves the request without gold-plating.", models: [] },
|
|
1216
|
+
{ name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
|
|
1217
|
+
];
|
|
1218
|
+
function placedSkills() {
|
|
1219
|
+
return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/providers/anthropic.ts
|
|
1223
|
+
function isAnthropicModel(model) {
|
|
1224
|
+
return /(^|\/)(claude|fable|mythos)/i.test(model) || /claude/i.test(model);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/tui/role-models.ts
|
|
1228
|
+
var WEAK_RE = /\b(flash|mini|nano|haiku|lite|small|turbo|fast|\d{1,2}b)\b/i;
|
|
1229
|
+
var FLAGSHIP_ROLES = ["judge", "principal-coder"];
|
|
1230
|
+
var COUNCIL_ROLES = DEFAULT_COUNCIL.map((c) => c.name);
|
|
1231
|
+
var SPEC_LENS_ROLES = SPEC_TEAM.map((c) => c.name);
|
|
1232
|
+
var PLAN_LENS_ROLES = PLAN_TEAM.map((c) => c.name);
|
|
1233
|
+
var CODE_LENS_ROLES = CODE_TEAM.map((c) => c.name);
|
|
1234
|
+
var STRONG_ROLES = [
|
|
1235
|
+
"brainstormer",
|
|
1236
|
+
"analyst",
|
|
1237
|
+
"planner",
|
|
1238
|
+
"architect",
|
|
1239
|
+
"senior-coder",
|
|
1240
|
+
"senior-designer",
|
|
1241
|
+
...COUNCIL_ROLES,
|
|
1242
|
+
...PLAN_LENS_ROLES,
|
|
1243
|
+
...CODE_LENS_ROLES
|
|
1244
|
+
];
|
|
1245
|
+
var MID_ROLES = ["coach", "coder", "designer", "code-reviewer", "operational", "memory-keeper", "task-auditor", ...SPEC_LENS_ROLES];
|
|
1246
|
+
var FAST_ROLES = ["refiner", "router", "project-manager", "team-lead"];
|
|
1247
|
+
var CAPABLE_ROLES = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES]);
|
|
1248
|
+
var ROLE_PROFILES = {
|
|
1249
|
+
tracer: "Writes the per-file reference note every other agent reads before changing unfamiliar code \u2014 high volume, but its output is a COMMITTED FILE, not a turn in a conversation: a shallow note is believed by every agent that opens that file, forever, and nothing later corrects it. Give it the MOST capable non-[flagship] model in the catalogue, not merely one that qualifies as [strong]. Volume is not a reason to go cheaper here.",
|
|
1250
|
+
refiner: "Classifies intent and rewrites the prompt every turn \u2014 highest call volume, trivial task \u2192 a fast, cheap model.",
|
|
1251
|
+
router: "Picks coder-vs-designer for a task \u2014 tiny and frequent \u2192 fast, cheap.",
|
|
1252
|
+
"project-manager": "Turns a task list into board items \u2014 light and structured \u2192 fast, cheap.",
|
|
1253
|
+
"task-auditor": "The only check on the task breakdown before hours of implementation are spent executing it \u2014 reads the plan against the task list and finds what was dropped. Low volume, and everything downstream depends on it \u2192 a capable model, never the cheapest.",
|
|
1254
|
+
"team-lead": "Coordinates implementation waves \u2014 light orchestration \u2192 fast, cheap.",
|
|
1255
|
+
coach: "Your main interactive assistant, used constantly all session (highest interaction volume) \u2192 a capable but EFFICIENT model, never the costly flagship.",
|
|
1256
|
+
brainstormer: "Turns a raw request into a decided design before the spec: explores the repo, weighs 2-3 approaches, gets the user to choose. Low volume, sets the direction for everything downstream \u2192 a strong reasoning model.",
|
|
1257
|
+
analyst: "Authors the spec and constitution \u2192 a strong reasoning model (Opus-tier).",
|
|
1258
|
+
planner: "Designs the implementation plan \u2192 a strong reasoning model (Opus-tier).",
|
|
1259
|
+
architect: "Diagnoses stuck tasks and produces recovery plans \u2014 serious design work \u2192 a strong model.",
|
|
1260
|
+
judge: "Critiques specs/plans and makes the final review call \u2014 low volume, high stakes \u2192 the most capable flagship model.",
|
|
1261
|
+
coder: "Writes the bulk of the implementation \u2014 very high work volume \u2192 a good high-throughput coding model (Sonnet-tier), NOT the flagship (wasteful at this volume).",
|
|
1262
|
+
"senior-coder": "Reviews and revises above the coder \u2014 must be MORE capable than the coder (Opus-tier).",
|
|
1263
|
+
"principal-coder": "Final code decision-maker \u2014 low volume, high stakes \u2192 the flagship is appropriate.",
|
|
1264
|
+
designer: "Builds UI \u2014 high volume \u2192 a capable coding/design model, not the flagship.",
|
|
1265
|
+
"senior-designer": "Senior UI reviewer \u2014 more capable than the designer.",
|
|
1266
|
+
"code-reviewer": "Reviews diffs \u2014 moderate volume \u2192 a solid capable model.",
|
|
1267
|
+
"memory-keeper": "Decides what a finished job taught the project and writes it to durable memory \u2014 low volume, but a bad memory poisons every later run \u2192 a capable, efficient model, never the cheapest.",
|
|
1268
|
+
operational: "Handles version control: writes conventional commit messages and (later) drives merges/conflicts \u2014 high volume \u2192 a capable, efficient model."
|
|
1269
|
+
};
|
|
1270
|
+
for (const [stage, lenses, heft] of [
|
|
1271
|
+
["spec", SPEC_TEAM, "a capable, efficient model (a spec is a short business-level doc)"],
|
|
1272
|
+
["plan", PLAN_TEAM, "a strong model (technical design judgment)"],
|
|
1273
|
+
["code", CODE_TEAM, "a strong model (reads real implementations)"]
|
|
1274
|
+
]) {
|
|
1275
|
+
for (const l of lenses) ROLE_PROFILES[l.name] = `${stage.toUpperCase()}-review lens \u2014 ${l.perspective}. Low volume, quality-critical \u2192 ${heft}.`;
|
|
1276
|
+
}
|
|
1277
|
+
for (const c of DEFAULT_COUNCIL) {
|
|
1278
|
+
ROLE_PROFILES[c.name] = `Review COUNCIL decider \u2014 ${c.perspective} Casts the binding pass/revise vote on contested work \u2192 a strong model.`;
|
|
1279
|
+
}
|
|
1280
|
+
var ROLE_ADVICE = ROLE_PROFILES;
|
|
1281
|
+
function filterModelsForRole(role, all, exclude = []) {
|
|
1282
|
+
const advice = ROLE_ADVICE[role];
|
|
1283
|
+
const excluded2 = new Set(exclude);
|
|
1284
|
+
const avail = all.filter((m) => !excluded2.has(m));
|
|
1285
|
+
if (CAPABLE_ROLES.has(role)) {
|
|
1286
|
+
const strong = avail.filter((m) => !WEAK_RE.test(m));
|
|
1287
|
+
if (strong.length === 0) return { models: avail.length ? avail : all, note: advice ? `${advice} (No strong models detected \u2014 showing all.)` : void 0 };
|
|
1288
|
+
return { models: strong, note: `${advice ?? ""} Showing ${strong.length} of ${avail.length} models (fast/weak models hidden for this role).`.trim() };
|
|
1289
|
+
}
|
|
1290
|
+
if (FAST_ROLES.includes(role)) {
|
|
1291
|
+
const fast = avail.filter((m) => WEAK_RE.test(m));
|
|
1292
|
+
if (fast.length === 0) return { models: avail.length ? avail : all, note: advice };
|
|
1293
|
+
return { models: fast, note: `${advice ?? ""} Showing ${fast.length} of ${avail.length} fast/cheap models.`.trim() };
|
|
1294
|
+
}
|
|
1295
|
+
return { models: avail.length ? avail : all };
|
|
1296
|
+
}
|
|
1297
|
+
function effortFor(role, model) {
|
|
1298
|
+
if (!isAnthropicModel(model)) return void 0;
|
|
1299
|
+
if (FLAGSHIP_ROLES.includes(role)) return "max";
|
|
1300
|
+
if (STRONG_ROLES.includes(role)) return "xhigh";
|
|
1301
|
+
if (FAST_ROLES.includes(role)) return "low";
|
|
1302
|
+
if (MID_ROLES.includes(role)) return "high";
|
|
1303
|
+
return void 0;
|
|
1304
|
+
}
|
|
1305
|
+
var effortBump = (s) => /-(ultra|max|xhigh)/.test(s) ? 4 : /-high/.test(s) ? 3 : /-medium/.test(s) ? 2 : /-low/.test(s) ? 1 : 0;
|
|
1306
|
+
var versionBump = (s, family) => {
|
|
1307
|
+
if (family) {
|
|
1308
|
+
const m = s.match(new RegExp(`${family}[-_. ]?(\\d+)(?:[-.](\\d+))?`));
|
|
1309
|
+
if (m) {
|
|
1310
|
+
const major = Number(m[1]);
|
|
1311
|
+
const minor = m[2] === void 0 ? 0 : Number(m[2]);
|
|
1312
|
+
if (major < 100) return major + (minor < 10 ? minor / 10 : minor / 100);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const g = s.match(/(\d)[-.](\d)\b/);
|
|
1316
|
+
return g ? Number(g[1]) + Number(g[2]) / 10 : 0;
|
|
1317
|
+
};
|
|
1318
|
+
var KNOWN_FAMILY_RE = /(fable|mythos|opus|sonnet|haiku|claude|codex|gpt-|\bo\d\b|gemini|deepseek|llama|qwen|kimi|glm|mistral|grok|nova|command-r|phi-\d)/i;
|
|
1319
|
+
var NON_TEXT_RE = /\b(image|imagen|vision|video|veo|tts|audio|speech|voice|embed|embedding|rerank|ocr|computer-use|realtime|moderation)\b/i;
|
|
1320
|
+
function isKnownModel(model) {
|
|
1321
|
+
return KNOWN_FAMILY_RE.test(model) && !NON_TEXT_RE.test(model);
|
|
1322
|
+
}
|
|
1323
|
+
var UNRANKED_SCORE = 50;
|
|
1324
|
+
function capabilityScore(model) {
|
|
1325
|
+
const s = model.toLowerCase();
|
|
1326
|
+
if (WEAK_RE.test(s)) return 20 + effortBump(s);
|
|
1327
|
+
if (/fable|mythos/.test(s)) return 100;
|
|
1328
|
+
if (/opus/.test(s)) return 88 + versionBump(s, "opus");
|
|
1329
|
+
if (/codex|gpt-5|\bo3\b/.test(s)) return 82 + effortBump(s) + versionBump(s, "gpt") / 100;
|
|
1330
|
+
if (/sonnet/.test(s)) return 78 + versionBump(s, "sonnet");
|
|
1331
|
+
if (/gemini/.test(s) && /pro/.test(s)) return 76 + versionBump(s, "gemini") + effortBump(s);
|
|
1332
|
+
if (/gpt-4/.test(s)) return 65;
|
|
1333
|
+
if (/deepseek/.test(s)) return 55;
|
|
1334
|
+
return UNRANKED_SCORE;
|
|
1335
|
+
}
|
|
1336
|
+
function mostCapable(models) {
|
|
1337
|
+
return [...models].sort((a, b) => capabilityScore(b) - capabilityScore(a))[0] ?? "";
|
|
1338
|
+
}
|
|
1339
|
+
function modelBand(model) {
|
|
1340
|
+
if (WEAK_RE.test(model)) return "fast";
|
|
1341
|
+
const s = capabilityScore(model);
|
|
1342
|
+
if (s >= 95) return "flagship";
|
|
1343
|
+
if (s >= 84) return "strong";
|
|
1344
|
+
if (s <= UNRANKED_SCORE) return "fast";
|
|
1345
|
+
return "mid";
|
|
1346
|
+
}
|
|
1347
|
+
function baseModel(model) {
|
|
1348
|
+
const segs = model.toLowerCase().split("/");
|
|
1349
|
+
let s = segs[segs.length - 1];
|
|
1350
|
+
s = s.replace(/-(ultra|max|xhigh|high|medium|low|free|thinking|preview)\b/g, "");
|
|
1351
|
+
s = s.replace(/-\d{6,8}\b/g, "");
|
|
1352
|
+
return s.replace(/-+$/, "");
|
|
1353
|
+
}
|
|
1354
|
+
function modelFamily(model) {
|
|
1355
|
+
return baseModel(model).replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
1356
|
+
}
|
|
1357
|
+
function latestFirst(models) {
|
|
1358
|
+
const best = /* @__PURE__ */ new Map();
|
|
1359
|
+
for (const m of models) {
|
|
1360
|
+
const key2 = modelFamily(m);
|
|
1361
|
+
const cur = best.get(key2);
|
|
1362
|
+
if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
|
|
1363
|
+
}
|
|
1364
|
+
const isLatest = (m) => best.get(modelFamily(m)) === m;
|
|
1365
|
+
return [...models.filter(isLatest), ...models.filter((m) => !isLatest(m))];
|
|
1366
|
+
}
|
|
1367
|
+
function versionlessId(model) {
|
|
1368
|
+
const cut = model.lastIndexOf("/");
|
|
1369
|
+
const prefix = cut >= 0 ? model.slice(0, cut + 1) : "";
|
|
1370
|
+
const name = model.slice(cut + 1).toLowerCase().replace(/-\d{6,8}\b/g, "").replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
1371
|
+
return prefix + name;
|
|
1372
|
+
}
|
|
1373
|
+
var DURABLE_ROLES = ["tracer"];
|
|
1374
|
+
function strongestPrimary(chain, pool) {
|
|
1375
|
+
const head = chain[0];
|
|
1376
|
+
if (!head) return chain;
|
|
1377
|
+
let best = head;
|
|
1378
|
+
for (const m of pool) {
|
|
1379
|
+
if (modelBand(m) === "flagship" || !isKnownModel(m)) continue;
|
|
1380
|
+
if (capabilityScore(m) > capabilityScore(best)) best = m;
|
|
1381
|
+
}
|
|
1382
|
+
if (best === head) return chain;
|
|
1383
|
+
const at = chain.indexOf(best);
|
|
1384
|
+
if (at > 0) {
|
|
1385
|
+
const next = [...chain];
|
|
1386
|
+
next[at] = head;
|
|
1387
|
+
next[0] = best;
|
|
1388
|
+
return next;
|
|
1389
|
+
}
|
|
1390
|
+
return [best, ...chain.slice(1)];
|
|
1391
|
+
}
|
|
1392
|
+
function newestPrimary(chain, pool) {
|
|
1393
|
+
const head = chain[0];
|
|
1394
|
+
if (!head) return chain;
|
|
1395
|
+
const key2 = versionlessId(head);
|
|
1396
|
+
let best = head;
|
|
1397
|
+
for (const m of pool) {
|
|
1398
|
+
if (versionlessId(m) !== key2) continue;
|
|
1399
|
+
if (capabilityScore(m) > capabilityScore(best)) best = m;
|
|
1400
|
+
}
|
|
1401
|
+
if (best === head) return chain;
|
|
1402
|
+
const at = chain.indexOf(best);
|
|
1403
|
+
if (at > 0) {
|
|
1404
|
+
const next = [...chain];
|
|
1405
|
+
next[at] = head;
|
|
1406
|
+
next[0] = best;
|
|
1407
|
+
return next;
|
|
1408
|
+
}
|
|
1409
|
+
return [best, ...chain.slice(1)];
|
|
1410
|
+
}
|
|
1411
|
+
function dedupBest(models) {
|
|
1412
|
+
const best = /* @__PURE__ */ new Map();
|
|
1413
|
+
for (const m of models) {
|
|
1414
|
+
const key2 = baseModel(m);
|
|
1415
|
+
const cur = best.get(key2);
|
|
1416
|
+
if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
|
|
1417
|
+
}
|
|
1418
|
+
return [...best.values()].sort((a, b) => capabilityScore(b) - capabilityScore(a));
|
|
1419
|
+
}
|
|
1420
|
+
function sourceOf(model) {
|
|
1421
|
+
const s = model.toLowerCase().replace(/^no-think\//, "");
|
|
1422
|
+
return cliFor(s) ?? s.split("/")[0];
|
|
1423
|
+
}
|
|
1424
|
+
function interleaveBySource(pool) {
|
|
1425
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1426
|
+
for (const m of pool) {
|
|
1427
|
+
const s = sourceOf(m);
|
|
1428
|
+
const q = bySource.get(s);
|
|
1429
|
+
if (q) q.push(m);
|
|
1430
|
+
else bySource.set(s, [m]);
|
|
1431
|
+
}
|
|
1432
|
+
const queues = [...bySource.values()];
|
|
1433
|
+
const out = [];
|
|
1434
|
+
for (let more = true; more; ) {
|
|
1435
|
+
more = false;
|
|
1436
|
+
for (const q of queues) {
|
|
1437
|
+
const m = q.shift();
|
|
1438
|
+
if (m !== void 0) {
|
|
1439
|
+
out.push(m);
|
|
1440
|
+
more = true;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return out;
|
|
1445
|
+
}
|
|
1446
|
+
var BAND_ORDER = { fast: 0, mid: 1, strong: 2, flagship: 3 };
|
|
1447
|
+
function bandDistance(primary, candidate) {
|
|
1448
|
+
const p = BAND_ORDER[modelBand(primary)];
|
|
1449
|
+
const c = BAND_ORDER[modelBand(candidate)];
|
|
1450
|
+
return Math.abs(c - p) * 2 + (c < p ? 1 : 0);
|
|
1451
|
+
}
|
|
1452
|
+
function pickFallbacks(primary, pool, n) {
|
|
1453
|
+
const chosen = [];
|
|
1454
|
+
const usedModels = /* @__PURE__ */ new Set([baseModel(primary)]);
|
|
1455
|
+
const usedSources = /* @__PURE__ */ new Set([sourceOf(primary)]);
|
|
1456
|
+
const byHeft = pool.map((m, i) => ({ m, i })).sort((a, b) => bandDistance(primary, a.m) - bandDistance(primary, b.m) || a.i - b.i).map((x) => x.m);
|
|
1457
|
+
for (const m of byHeft) {
|
|
1458
|
+
if (chosen.length >= n) break;
|
|
1459
|
+
if (usedModels.has(baseModel(m)) || usedSources.has(sourceOf(m))) continue;
|
|
1460
|
+
chosen.push(m);
|
|
1461
|
+
usedModels.add(baseModel(m));
|
|
1462
|
+
usedSources.add(sourceOf(m));
|
|
1463
|
+
}
|
|
1464
|
+
for (const m of byHeft) {
|
|
1465
|
+
if (chosen.length >= n) break;
|
|
1466
|
+
if (usedModels.has(baseModel(m))) continue;
|
|
1467
|
+
chosen.push(m);
|
|
1468
|
+
usedModels.add(baseModel(m));
|
|
1469
|
+
}
|
|
1470
|
+
return chosen;
|
|
1471
|
+
}
|
|
1472
|
+
var FALLBACK_COUNT = 2;
|
|
1473
|
+
function adjustRoleModels(roles, models, unfit) {
|
|
1474
|
+
if (models.length === 0) return [];
|
|
1475
|
+
const recognised = models.filter(isKnownModel);
|
|
1476
|
+
const pick = recognised.length ? recognised : models;
|
|
1477
|
+
const capable = dedupBest(pick.filter((m) => !WEAK_RE.test(m)));
|
|
1478
|
+
const fast = dedupBest(pick.filter((m) => WEAK_RE.test(m)));
|
|
1479
|
+
const capablePool = capable.length ? capable : fast;
|
|
1480
|
+
const fastPool = fast.length ? fast : capable;
|
|
1481
|
+
const primaryPool = latestFirst(capablePool);
|
|
1482
|
+
const primaryFast = latestFirst(fastPool);
|
|
1483
|
+
const nonFlagship = primaryPool.filter((m) => modelBand(m) !== "flagship");
|
|
1484
|
+
const strongPool = primaryPool.filter((m) => modelBand(m) === "strong");
|
|
1485
|
+
const midPool = primaryPool.filter((m) => modelBand(m) === "mid");
|
|
1486
|
+
const wanted = new Set(roles);
|
|
1487
|
+
const forRole = (role, pool) => {
|
|
1488
|
+
if (!unfit) return pool;
|
|
1489
|
+
const fit = pool.filter((m) => !unfit(role, m));
|
|
1490
|
+
return fit.length ? fit : pool;
|
|
1491
|
+
};
|
|
1492
|
+
const known = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES, ...FAST_ROLES]);
|
|
1493
|
+
const primary = /* @__PURE__ */ new Map();
|
|
1494
|
+
const flagSrc = primaryPool;
|
|
1495
|
+
FLAGSHIP_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1496
|
+
const src = forRole(r, flagSrc);
|
|
1497
|
+
primary.set(r, src[i % src.length]);
|
|
1498
|
+
});
|
|
1499
|
+
const strongSrc = interleaveBySource(strongPool.length ? strongPool : nonFlagship.length ? nonFlagship : primaryPool);
|
|
1500
|
+
STRONG_ROLES.filter((r) => wanted.has(r)).concat(roles.filter((r) => !known.has(r))).forEach((r, i) => {
|
|
1501
|
+
const src = forRole(r, strongSrc);
|
|
1502
|
+
primary.set(r, src[i % src.length]);
|
|
1503
|
+
});
|
|
1504
|
+
const midSrc = interleaveBySource(midPool.length ? midPool : nonFlagship.length ? nonFlagship : primaryPool);
|
|
1505
|
+
MID_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1506
|
+
const src = forRole(r, midSrc);
|
|
1507
|
+
primary.set(r, src[i % src.length]);
|
|
1508
|
+
});
|
|
1509
|
+
FAST_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1510
|
+
const src = forRole(r, primaryFast);
|
|
1511
|
+
primary.set(r, src[i % src.length]);
|
|
1512
|
+
});
|
|
1513
|
+
return roles.map((role) => {
|
|
1514
|
+
const head = primary.get(role) ?? primaryPool[0];
|
|
1515
|
+
const capForFb = MID_ROLES.includes(role) ? capablePool.filter((m) => modelBand(m) !== "flagship") : capablePool;
|
|
1516
|
+
const pool = FAST_ROLES.includes(role) ? [...fastPool, ...capForFb] : [...capForFb, ...fastPool];
|
|
1517
|
+
return { role, models: newestPrimary([head, ...pickFallbacks(head, forRole(role, pool), FALLBACK_COUNT)], models) };
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/skills/apply.ts
|
|
1522
|
+
import { readFile } from "fs/promises";
|
|
1523
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1524
|
+
import { resolve as resolve2, sep as sep3 } from "path";
|
|
1525
|
+
import { z } from "zod";
|
|
1526
|
+
function applySkills(basePrompt, mandatory, registry) {
|
|
1527
|
+
const parts = [basePrompt];
|
|
1528
|
+
if (mandatory.length) {
|
|
1529
|
+
const sections = mandatory.map((name) => {
|
|
1530
|
+
const skill = registry.get(name);
|
|
1531
|
+
if (!skill) throw new Error(`applySkills: undefined skill: ${name}`);
|
|
1532
|
+
const where = skill.dir ? `
|
|
1533
|
+
_Skill base directory: ${skill.dir}_
|
|
1534
|
+
` : "";
|
|
1535
|
+
return `## ${skill.name}${where}
|
|
1536
|
+
${skill.content}`;
|
|
1537
|
+
});
|
|
1538
|
+
parts.push(`# Mandatory Skills
|
|
1539
|
+
${sections.join("\n\n")}`);
|
|
1540
|
+
}
|
|
1541
|
+
const mandatorySet = new Set(mandatory);
|
|
1542
|
+
const available = registry.list().filter((s) => !mandatorySet.has(s.name));
|
|
1543
|
+
if (available.length) {
|
|
1544
|
+
const lines = available.map((s) => `- ${s.name}: ${s.description}`);
|
|
1545
|
+
parts.push(`# Discoverable Skills (call the skill tool to fetch its content)
|
|
1546
|
+
${lines.join("\n")}`);
|
|
1547
|
+
}
|
|
1548
|
+
return parts.join("\n\n");
|
|
1549
|
+
}
|
|
1550
|
+
var skillParams = z.object({
|
|
1551
|
+
name: z.string().describe("The skill's name, exactly as it is listed."),
|
|
1552
|
+
/**
|
|
1553
|
+
* A supporting document inside the skill's own directory, e.g. "reference/critique.md".
|
|
1554
|
+
*
|
|
1555
|
+
* Described, because an undescribed optional string gets filled in. Measured: four consecutive calls sent
|
|
1556
|
+
* `file: ""` and every one of them failed — the skill was there, its content was one branch away, and an
|
|
1557
|
+
* empty string took the other branch.
|
|
1558
|
+
*/
|
|
1559
|
+
file: z.string().optional().describe('Optional. A supporting document inside the skill, e.g. "reference/critique.md". Omit it to read the skill itself \u2014 do not pass an empty string.')
|
|
1560
|
+
});
|
|
1561
|
+
var DOCS_SHOWN = 12;
|
|
1562
|
+
function docsIn(dir) {
|
|
1563
|
+
try {
|
|
1564
|
+
return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.name !== "SKILL.md" && !e.name.startsWith(".")).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort().slice(0, DOCS_SHOWN);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return [];
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
var MAX_SKILL_DOC_CHARS = 3e4;
|
|
1570
|
+
var MAX_SKILLS_LISTED = 12;
|
|
1571
|
+
function noSuchSkill(name, available) {
|
|
1572
|
+
const shape = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1573
|
+
const same = available.filter((s) => shape(s) === shape(name));
|
|
1574
|
+
if (same.length === 1) {
|
|
1575
|
+
return `skill not found: ${name} \u2014 did you mean \`${same[0]}\`? Call it with that exact name.`;
|
|
1576
|
+
}
|
|
1577
|
+
if (!available.length) return `skill not found: ${name}. This project has no skills installed, so carry on without one.`;
|
|
1578
|
+
const near = available.filter((s) => shape(s).includes(shape(name)) || shape(name).includes(shape(s)));
|
|
1579
|
+
if (near.length && near.length <= MAX_SKILLS_LISTED) {
|
|
1580
|
+
return `skill not found: ${name}. Closest by name: ${near.map((s) => `\`${s}\``).join(", ")}. Call one of those exactly if it is what you meant, or carry on without a skill.`;
|
|
1581
|
+
}
|
|
1582
|
+
const shown = available.slice(0, MAX_SKILLS_LISTED);
|
|
1583
|
+
const rest = available.length - shown.length;
|
|
1584
|
+
return `skill not found: ${name}. Available: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more \u2014 the full list is in your system prompt` : ""}. Use one of these exactly, or carry on without a skill \u2014 do not guess another name.`;
|
|
705
1585
|
}
|
|
706
1586
|
function buildSkillTool(registry) {
|
|
707
1587
|
return {
|
|
@@ -826,6 +1706,14 @@ var READ_ONLY = /* @__PURE__ */ new Set([
|
|
|
826
1706
|
"show-ref",
|
|
827
1707
|
"for-each-ref",
|
|
828
1708
|
"ls-remote",
|
|
1709
|
+
/**
|
|
1710
|
+
* `git grep` searches tracked content and has no writing form at all — the same standing as `log`.
|
|
1711
|
+
*
|
|
1712
|
+
* Left out, it was refused twice in one 36-minute run while agents fell back to `find | xargs grep`
|
|
1713
|
+
* through the shell, which is slower on a repository this size and searches build output and
|
|
1714
|
+
* `node_modules` unless every caller remembers to prune them. git already knows what is tracked.
|
|
1715
|
+
*/
|
|
1716
|
+
"grep",
|
|
829
1717
|
/**
|
|
830
1718
|
* `check-ignore` asks whether a path is ignored — it reads `.gitignore` and answers, and changes nothing.
|
|
831
1719
|
*
|
|
@@ -837,18 +1725,34 @@ var READ_ONLY = /* @__PURE__ */ new Set([
|
|
|
837
1725
|
]);
|
|
838
1726
|
var READ_ONLY_PAIRS = /* @__PURE__ */ new Set([
|
|
839
1727
|
"worktree list",
|
|
840
|
-
"branch --list",
|
|
841
|
-
"branch -l",
|
|
842
|
-
"branch -a",
|
|
843
|
-
"branch -v",
|
|
844
|
-
"branch -r",
|
|
845
1728
|
"tag --list",
|
|
846
1729
|
"tag -l",
|
|
847
1730
|
"stash list",
|
|
848
1731
|
"remote -v",
|
|
849
1732
|
"remote show",
|
|
850
1733
|
"config --get",
|
|
851
|
-
"config --list"
|
|
1734
|
+
"config --list",
|
|
1735
|
+
/**
|
|
1736
|
+
* The long forms of what is already allowed, and the queries that only ask.
|
|
1737
|
+
*
|
|
1738
|
+
* `branch -a` was allowed and `branch --all` was not — the same command spelled the way git's own
|
|
1739
|
+
* documentation spells it. Measured in one run: an agent asked for `branch --all` and then `branch
|
|
1740
|
+
* --show-current`, and paid a refused turn for each while `-a` sat in this list. A short flag admitted and
|
|
1741
|
+
* its long twin refused is not a security boundary, it is a typo in one.
|
|
1742
|
+
*
|
|
1743
|
+
* These are the closure of what this set already permits, not new ground: every one of them prints
|
|
1744
|
+
* information about branches or tags and none of them can create, move or delete a ref. The forms that
|
|
1745
|
+
* write — `-d`, `-D`, `-m`, `-M`, `-c`, `-C`, `--delete`, `--move`, `--copy`, `--set-upstream-to`,
|
|
1746
|
+
* `--edit-description` — are still absent, and a first argument that is not a flag never reaches here.
|
|
1747
|
+
*/
|
|
1748
|
+
"tag --contains",
|
|
1749
|
+
"tag --no-contains",
|
|
1750
|
+
"tag --merged",
|
|
1751
|
+
"tag --points-at",
|
|
1752
|
+
"tag -n",
|
|
1753
|
+
"remote --verbose",
|
|
1754
|
+
"remote get-url",
|
|
1755
|
+
"stash show"
|
|
852
1756
|
]);
|
|
853
1757
|
var REFUSED_ARG = /^(--output|-c$|--config-env|--exec-path|-C$|--git-dir|--work-tree|--upload-pack|--receive-pack)/;
|
|
854
1758
|
var params = z2.object({
|
|
@@ -859,21 +1763,130 @@ var params = z2.object({
|
|
|
859
1763
|
var MAX_GIT_OUTPUT = 6e4;
|
|
860
1764
|
var GIT_TIMEOUT_MS = 3e4;
|
|
861
1765
|
var GIT_PUSH_TIMEOUT_MS = 12e4;
|
|
862
|
-
var ANSWERS_WITH_ONE = /* @__PURE__ */ new Set([
|
|
1766
|
+
var ANSWERS_WITH_ONE = /* @__PURE__ */ new Set([
|
|
1767
|
+
"diff",
|
|
1768
|
+
"diff-index",
|
|
1769
|
+
"diff-tree",
|
|
1770
|
+
"diff-files",
|
|
1771
|
+
"merge-base",
|
|
1772
|
+
"check-ignore",
|
|
1773
|
+
/**
|
|
1774
|
+
* `grep` says "no match" with exit 1, exactly as the others say their own no.
|
|
1775
|
+
*
|
|
1776
|
+
* Admitted to the read-only set earlier tonight and left out of this one, so a search that found nothing
|
|
1777
|
+
* came back as `git failed with no output.` — a fault where there was an answer. Measured live within
|
|
1778
|
+
* minutes: `git grep -n -i ExportReportService.cs` twice, both reported as failures, for a file that
|
|
1779
|
+
* simply is not in the repository.
|
|
1780
|
+
*/
|
|
1781
|
+
"grep"
|
|
1782
|
+
]);
|
|
863
1783
|
function answeredWithOne(args, code) {
|
|
864
1784
|
return code === 1 && ANSWERS_WITH_ONE.has(args[0] ?? "");
|
|
865
1785
|
}
|
|
1786
|
+
function howToNarrow(args) {
|
|
1787
|
+
const verb = gitVerb(args) ?? "";
|
|
1788
|
+
if (verb === "diff" || verb === "show") {
|
|
1789
|
+
return "narrow the range, or put `--stat` directly after the subcommand (git " + verb + " --stat <rest>), which git requires";
|
|
1790
|
+
}
|
|
1791
|
+
if (verb === "log") return "ask for fewer commits (-n 20) or just their subjects (--oneline)";
|
|
1792
|
+
if (verb === "ls-files" || verb === "ls-tree") return "narrow the pathspec to one directory at a time";
|
|
1793
|
+
if (verb === "blame") return "limit it to a range of lines (-L 40,120)";
|
|
1794
|
+
return "ask for a narrower part of it";
|
|
1795
|
+
}
|
|
866
1796
|
function answerOfOne(args) {
|
|
867
1797
|
const verb = args[0] ?? "";
|
|
868
1798
|
if (verb === "check-ignore") {
|
|
869
1799
|
return "No \u2014 that path is not ignored by this repository's rules. (git exit code 1, which is the answer here.)";
|
|
870
1800
|
}
|
|
1801
|
+
if (verb === "grep") {
|
|
1802
|
+
return "No match \u2014 nothing in the tracked files matches that pattern. (git exit code 1, which is the answer here.)";
|
|
1803
|
+
}
|
|
871
1804
|
if (verb === "merge-base") {
|
|
872
1805
|
return args.includes("--is-ancestor") ? "No \u2014 the first commit is not an ancestor of the second. (git exit code 1, which is the answer here.)" : "No merge base: these commits share no common ancestor. (git exit code 1, which is the answer here.)";
|
|
873
1806
|
}
|
|
874
1807
|
return "There ARE differences \u2014 the comparison is not empty. Nothing failed; `--quiet`/`--exit-code` reports this as exit code 1. Re-run without it to see them.";
|
|
875
1808
|
}
|
|
1809
|
+
function packedArgument(a) {
|
|
1810
|
+
if (!/\s/.test(a)) return void 0;
|
|
1811
|
+
if (/^--?[\w-]+[=:]/.test(a)) return void 0;
|
|
1812
|
+
const parts = a.trim().split(/\s+/).filter(Boolean);
|
|
1813
|
+
if (parts.length < 2) return void 0;
|
|
1814
|
+
if (parts[0] === "--") return parts;
|
|
1815
|
+
return parts.slice(1).some((p) => p === "--" || p.startsWith("-") || p.includes("/")) ? parts : void 0;
|
|
1816
|
+
}
|
|
1817
|
+
var GLUED_PATHSPEC = /^--[^\s=]*\/[^\s=]*$/;
|
|
1818
|
+
var BRANCH_TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
1819
|
+
"--contains",
|
|
1820
|
+
"--no-contains",
|
|
1821
|
+
"--merged",
|
|
1822
|
+
"--no-merged",
|
|
1823
|
+
"--points-at",
|
|
1824
|
+
"--format",
|
|
1825
|
+
"--sort",
|
|
1826
|
+
"--color",
|
|
1827
|
+
"--abbrev",
|
|
1828
|
+
"-u",
|
|
1829
|
+
"--set-upstream-to",
|
|
1830
|
+
"-t",
|
|
1831
|
+
"--track"
|
|
1832
|
+
]);
|
|
1833
|
+
var BRANCH_WRITERS = /* @__PURE__ */ new Set([
|
|
1834
|
+
"-d",
|
|
1835
|
+
"-D",
|
|
1836
|
+
"--delete",
|
|
1837
|
+
"-m",
|
|
1838
|
+
"-M",
|
|
1839
|
+
"--move",
|
|
1840
|
+
"-c",
|
|
1841
|
+
"-C",
|
|
1842
|
+
"--copy",
|
|
1843
|
+
"--edit-description",
|
|
1844
|
+
"--set-upstream",
|
|
1845
|
+
"--set-upstream-to",
|
|
1846
|
+
"--unset-upstream",
|
|
1847
|
+
"-u",
|
|
1848
|
+
"-t",
|
|
1849
|
+
"--track",
|
|
1850
|
+
"--no-track",
|
|
1851
|
+
"-f",
|
|
1852
|
+
"--force"
|
|
1853
|
+
]);
|
|
1854
|
+
function branchWrites(rest) {
|
|
1855
|
+
const listing = rest.some((a) => a === "--list" || a === "-l");
|
|
1856
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1857
|
+
const a = rest[i];
|
|
1858
|
+
if (a === void 0) continue;
|
|
1859
|
+
if (a === "--") return "`git branch` with a pathspec is not a thing this tool needs to run.";
|
|
1860
|
+
if (a.startsWith("--")) {
|
|
1861
|
+
const name = a.split("=")[0];
|
|
1862
|
+
if (BRANCH_WRITERS.has(name)) return `\`git branch ${name}\` changes a branch. Only listing is allowed.`;
|
|
1863
|
+
if (BRANCH_TAKES_VALUE.has(name) && !a.includes("=")) i++;
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
if (a.startsWith("-")) {
|
|
1867
|
+
const bad = [...a.slice(1)].find((c) => BRANCH_WRITERS.has(`-${c}`));
|
|
1868
|
+
if (bad) return `\`git branch -${bad}\` changes a branch. Only listing is allowed.`;
|
|
1869
|
+
if (BRANCH_TAKES_VALUE.has(a)) i++;
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (listing) continue;
|
|
1873
|
+
if (/[*?\[]/.test(a)) {
|
|
1874
|
+
return `\`git branch ${a}\` would create a branch with that literal name. To search for branches, put the pattern after --list: \`git branch --list ${a}\`.`;
|
|
1875
|
+
}
|
|
1876
|
+
return `\`git branch ${a}\` creates a branch. Only listing is allowed \u2014 git_write owns the rest.`;
|
|
1877
|
+
}
|
|
1878
|
+
return void 0;
|
|
1879
|
+
}
|
|
876
1880
|
function refuse(args) {
|
|
1881
|
+
const packed = args.find((a) => packedArgument(a) !== void 0 || GLUED_PATHSPEC.test(a));
|
|
1882
|
+
if (packed !== void 0) {
|
|
1883
|
+
const split = packedArgument(packed) ?? ["--", packed.slice(2)];
|
|
1884
|
+
if (split[0] !== "--") {
|
|
1885
|
+
return `each item in the list is ONE argument \u2014 this one holds several: ${JSON.stringify([packed]).slice(0, 90)}. Send ${JSON.stringify(split).slice(0, 130)} instead.`;
|
|
1886
|
+
}
|
|
1887
|
+
const parts = split.slice(1);
|
|
1888
|
+
return `\`--\` is the separator and must be its own element of the list \u2014 it is never part of a path. You sent ${JSON.stringify([packed]).slice(0, 90)}; send ${JSON.stringify(["--", ...parts]).slice(0, 130)} instead (however many paths follow, they are separate elements too).`;
|
|
1889
|
+
}
|
|
877
1890
|
const bad = args.find((a) => REFUSED_ARG.test(a));
|
|
878
1891
|
if (bad) {
|
|
879
1892
|
return `\`${bad}\` is not allowed: it can write a file, run a program through git's configuration, or point git at another repository.`;
|
|
@@ -881,6 +1894,16 @@ function refuse(args) {
|
|
|
881
1894
|
const [sub, second] = args;
|
|
882
1895
|
if (!sub || sub.startsWith("-")) return "The first argument must be a git subcommand, e.g. `status`.";
|
|
883
1896
|
if (READ_ONLY.has(sub)) return void 0;
|
|
1897
|
+
if (sub === "branch") return branchWrites(args.slice(1));
|
|
1898
|
+
if (sub === "reflog") {
|
|
1899
|
+
return second === "expire" || second === "delete" ? `\`git reflog ${second}\` rewrites the reflog. Only reading it is allowed.` : void 0;
|
|
1900
|
+
}
|
|
1901
|
+
if (sub === "config") {
|
|
1902
|
+
if (second !== void 0 && !second.startsWith("-") && args.length === 2) return void 0;
|
|
1903
|
+
if (args.length > 2 && !args.some((a) => a.startsWith("--get") || a === "--list")) {
|
|
1904
|
+
return "`git config <key> <value>` writes configuration. Read one with `git config <key>`.";
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
884
1907
|
if (second && READ_ONLY_PAIRS.has(`${sub} ${second}`)) return void 0;
|
|
885
1908
|
return `\`git ${sub}\` is not available here \u2014 this tool reads history and state, it never changes them. Available: ${[...READ_ONLY].sort().join(", ")}; also ${[...READ_ONLY_PAIRS].sort().join(", ")}.`;
|
|
886
1909
|
}
|
|
@@ -922,7 +1945,7 @@ var gitTool = {
|
|
|
922
1945
|
return { content: out.code === 0 ? "(no output)" : "git failed with no output.", isError: failed };
|
|
923
1946
|
}
|
|
924
1947
|
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
925
|
-
\u2026[truncated \u2014
|
|
1948
|
+
\u2026[truncated \u2014 ${howToNarrow(args)}]` : out.text;
|
|
926
1949
|
return { content: clipped, isError: failed };
|
|
927
1950
|
}
|
|
928
1951
|
};
|
|
@@ -970,184 +1993,57 @@ var gitWriteTool = {
|
|
|
970
1993
|
if (!parsed.success) {
|
|
971
1994
|
return { content: `git_write: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
|
|
972
1995
|
}
|
|
973
|
-
const args = parsed.data.args;
|
|
974
|
-
const why = refuseWrite(args);
|
|
975
|
-
if (why) return { content: why, isError: true, settled: true };
|
|
976
|
-
const out = await new Promise((resolve6) => {
|
|
977
|
-
const child = execFile("git", args, {
|
|
978
|
-
cwd: ctx.cwd,
|
|
979
|
-
// A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
|
|
980
|
-
// pessimistic one for a repository with anything in it.
|
|
981
|
-
timeout: args[0] === "push" ? GIT_PUSH_TIMEOUT_MS : GIT_TIMEOUT_MS,
|
|
982
|
-
maxBuffer: MAX_GIT_OUTPUT * 4,
|
|
983
|
-
// GIT_TERMINAL_PROMPT=0: a push that needs credentials fails with a message instead of blocking on a
|
|
984
|
-
// prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
|
|
985
|
-
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
|
|
986
|
-
}, (err, stdout, stderr) => {
|
|
987
|
-
resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
|
|
988
|
-
});
|
|
989
|
-
ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
990
|
-
});
|
|
991
|
-
if (!out.text) return { content: out.code === 0 ? "(done)" : "git failed with no output.", isError: out.code !== 0 };
|
|
992
|
-
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
993
|
-
\u2026[truncated]` : out.text;
|
|
994
|
-
return { content: clipped, isError: out.code !== 0 };
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
// src/tools/remember.ts
|
|
999
|
-
import { z as z3 } from "zod";
|
|
1000
|
-
var params2 = z3.object({
|
|
1001
|
-
fact: z3.string().describe(
|
|
1002
|
-
"One short sentence, durable and project-specific: where something lives, which command builds it, a convention this codebase follows, a schema detail that cost you a search. Not what you did, not what is true of the language in general \u2014 something the next agent would otherwise have to rediscover."
|
|
1003
|
-
)
|
|
1004
|
-
});
|
|
1005
|
-
function buildRememberTool(sink) {
|
|
1006
|
-
return {
|
|
1007
|
-
name: "remember_fact",
|
|
1008
|
-
description: "Save a short, durable fact worth recalling in future sessions \u2014 a project convention, where something lives, a schema detail, a command that works. It is written straight away, so a session that stops early still leaves it behind. Use it the moment you learn something you would not want to work out twice; skip anything transient or specific to the task in hand.",
|
|
1009
|
-
permissionLevel: "safe",
|
|
1010
|
-
parameters: params2,
|
|
1011
|
-
async run(rawArgs, ctx) {
|
|
1012
|
-
const parsed = params2.safeParse(rawArgs);
|
|
1013
|
-
if (!parsed.success) return { content: "remember_fact: invalid args (expected { fact })", isError: true };
|
|
1014
|
-
const fact = parsed.data.fact.trim();
|
|
1015
|
-
if (!fact) return { content: "remember_fact: empty fact", isError: true };
|
|
1016
|
-
const write = sink ?? ctx.remember;
|
|
1017
|
-
if (!write) return { content: "remember_fact: memory is not available in this context", isError: true };
|
|
1018
|
-
write(fact);
|
|
1019
|
-
return { content: `Remembered: ${fact}`, isError: false };
|
|
1020
|
-
}
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
var rememberFactTool = buildRememberTool();
|
|
1024
|
-
|
|
1025
|
-
// src/prompts.ts
|
|
1026
|
-
var REQUIRED_ROLES = [
|
|
1027
|
-
"refiner",
|
|
1028
|
-
"coach",
|
|
1029
|
-
"brainstormer",
|
|
1030
|
-
"analyst",
|
|
1031
|
-
"planner",
|
|
1032
|
-
"judge",
|
|
1033
|
-
"project-manager",
|
|
1034
|
-
"team-lead",
|
|
1035
|
-
"router",
|
|
1036
|
-
"coder",
|
|
1037
|
-
"designer",
|
|
1038
|
-
"senior-coder",
|
|
1039
|
-
"senior-designer",
|
|
1040
|
-
"architect",
|
|
1041
|
-
"code-reviewer",
|
|
1042
|
-
"task-auditor",
|
|
1043
|
-
"principal-coder",
|
|
1044
|
-
"operational",
|
|
1045
|
-
"memory-keeper",
|
|
1046
|
-
"tracer",
|
|
1047
|
-
"tester"
|
|
1048
|
-
];
|
|
1049
|
-
var DEFAULT_ROLE_SKILLS = {
|
|
1050
|
-
brainstormer: ["brainstorming"],
|
|
1051
|
-
// The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
|
|
1052
|
-
// vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
|
|
1053
|
-
coder: ["test-driven-development"],
|
|
1054
|
-
"senior-coder": ["test-driven-development"],
|
|
1055
|
-
// The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
|
|
1056
|
-
// supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
|
|
1057
|
-
// individual task executable. That is what this skill adds.
|
|
1058
|
-
"project-manager": ["writing-plans"],
|
|
1059
|
-
// The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
|
|
1060
|
-
// and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
|
|
1061
|
-
// one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
|
|
1062
|
-
designer: ["frontend-design"],
|
|
1063
|
-
"senior-designer": ["frontend-design"]
|
|
1064
|
-
// NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
|
|
1065
|
-
// so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
|
|
1066
|
-
};
|
|
1067
|
-
var DEFAULT_PROMPTS = {
|
|
1068
|
-
tracer: "You write the reference note that every other agent reads before it touches a file it did not write. A wrong note is worse than none: an agent will act on it, so accuracy outranks fluency and admitting you cannot tell outranks a plausible guess. State only what the code and the given relationships show; if the business purpose is not evident from them, describe what the file does technically and say nothing about why. Never speculate about intent, history or requirements.",
|
|
1069
|
-
/**
|
|
1070
|
-
* The role that exercises work already built and writes down what actually happened.
|
|
1071
|
-
*
|
|
1072
|
-
* Every rule below is here because its absence produces the one output worse than no testing at all: a
|
|
1073
|
-
* report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
|
|
1074
|
-
* confidence, and the next person spends it.
|
|
1075
|
-
*/
|
|
1076
|
-
tester: "You verify software that already exists, by running its scenarios and recording what they actually did. You are not here to build, fix or improve anything: the code under test is finished, and changing it would mean the thing you verified is not the thing that shipped.\n\nEVIDENCE IS THE WHOLE JOB. A scenario's outcome is what you OBSERVED \u2014 a database row, a log line, an HTTP response, a screen the user confirmed. Record the evidence beside every result: the query you ran and what it returned, the log event id and its line, the response body. A result you cannot show is not a result.\n\nIF A STEP WRITES TO THE DATABASE, THE RESPONSE IS NOT THE EVIDENCE. A 201 or a 204 says the request was accepted; it does not say what was stored, and a screen showing the new state does not either \u2014 both can be right while the row is wrong. For every step that creates or changes a record: query the database for that row and put the query AND the rows it returned in the report, and query the logs for the event that step should have emitted and put the query AND the line it returned there too. Absence is evidence as well: when a step must NOT emit an event \u2014 a no-op, a rejected change \u2014 show the query returning nothing. Without both, the scenario is NOT EXECUTED, however convincing the response looked.\n\nNever mark a scenario PASSED that you did not execute and observe. If you could not run it \u2014 the data does not exist, the surface is unreachable, the case is destructive against a live system \u2014 label it NOT EXECUTED and say exactly why. FAILED means you ran it and the behaviour was wrong; say what you expected, what happened, and the evidence for both. Guessing from the source is not executing: where you reasoned from code alone, say so in those words.\n\nWrite each result into the report BEFORE moving on to the next scenario. The report is a living document, not something assembled at the end: a run that stops halfway must leave behind everything it learned up to that point.\n\n\u2026and say each verdict OUT LOUD as you reach it, in one or two sentences: which scenario, what it did, and the single piece of evidence that settled it \u2014 the row, the log line, the status code. The full evidence still goes in the report; this is so the person watching the run knows what you found without opening a file. Say the failures and the NOT EXECUTED ones the same way, and with the same brevity: a result nobody hears is one they have to go looking for.\n\nNever start or stop the development environment \u2014 application hosts, dev servers, containers, databases. Those are the developer's to run. When you need something up, say which command they should run and wait for them to confirm it is ready.\n\nYou do NOT write product code. When you find something wrong that is not the verdict of the scenario you are running \u2014 a missing label, prose rendered as raw markup, a wrong format, or something the developer points out in passing \u2014 call `report_finding`. Another role fixes it and you are told when it is done, so you can re-check what it affected. Do not fix it yourself: changing the product mid-verification means the thing you verified is not the thing that shipped. And do not fail a scenario over it \u2014 a scenario fails when the scenario itself does not pass, not because something else was noticed while running it.\n\nIf the project's own rules (its constitution) say more about how verification is done here, they govern over this description \u2014 read them and follow them.",
|
|
1077
|
-
refiner: "Your #1 rule: `refinedPrompt` MUST ALWAYS be in ENGLISH. If the user wrote in another language (Turkish, German, Spanish, \u2026), TRANSLATE their intent into English \u2014 never echo their language back. This is non-negotiable: a Turkish input like 'bir todo app geli\u015Ftir, \xF6nce backend' MUST come out as English 'Build a todo app; implement the backend first.'\n\nRewrite the user's message down to the raw core intent the AI needs to act on \u2014 clear, direct, and structured. Strip all politeness, emotional, and filler words (please, thanks, kindly, 'could you', 'would you', 'I'd like', etc.) and anything that carries no instruction. Do NOT add words, qualifiers, or scope the user did not state (e.g. do not add 'always'). Keep the user's own perspective and form \u2014 a question stays a question, an instruction stays an instruction; do NOT describe the user in the third person and do NOT answer the request. Example: a polite request like 'would you please answer me in language X?' becomes just 'respond in language X' (drop 'please'; do not add 'always' or any scope the user didn't state). Also classify the intent: 'chat' (conversation/question), 'feature' (new feature/work), 'bugfix' (bug fix), 'govern' (establish or amend the project's OWN standing rules and principles \u2014 writing or revising the constitution, the coding conventions, the project's rules; work whose entire output is a governing document, with no source code changed). Judge by what the request PRODUCES, not by what it mentions: 'write the project constitution from CLAUDE.md' is govern, and so is 'update our commit-message rules'; 'make the code follow the constitution' changes source and is feature. Also 'verify' \u2014 the user wants work that ALREADY EXISTS exercised and its behaviour confirmed with evidence: running a pull request's test scenarios, doing a smoke test of a feature that is already built, producing a test report. Judge by what it PRODUCES: a record of what the software DID is verify; changing what it does is feature or bugfix. 'Run the smoke tests for PR 677 and mark them passed' is verify, and so is 'check that the wizard works end to end'. 'The wizard is broken, fix it' is bugfix. Finally 'undo' \u2014 the user is asking you to REVERSE what the previous turn did, not to do anything new: 'undo that', 'revert your changes', 'go back to the previous version', 'that was wrong, put it back'. Classify by whether the request refers to work already done: undoing is never a rewrite, and asking for a different result ('rewrite it shorter') is not an undo. Also detect the natural language the user wrote in and return its English name as `language` (e.g. 'Turkish', 'English', 'German') \u2014 this is separate from refinedPrompt, which stays English. Also produce `title`: what the work is ABOUT, as a 2-5 word English kebab-case noun phrase suitable for a git branch name \u2014 the SUBJECT, not the action. 'build a luxury todo app' is 'luxury-todo-app'; 'add a login page' is 'login-page'; 'fix the null crash on retry' is 'null-crash-on-retry'. Do not open with a verb (build/add/fix/implement/update): the tool is already doing it, so the verb says nothing and crowds out the words that identify the work. Lowercase, dash-joined, no punctuation. Return the result via submit as {refinedPrompt, intent, language, title}. Remember: refinedPrompt in English, always.",
|
|
1078
|
-
brainstormer: "You run the BRAINSTORM stage: you turn a raw request into a decided design, before anything is specified.\n\nThe `brainstorming` skill above is the authority on HOW to do this \u2014 follow it. What follows is only how it binds to this pipeline, because the skill names conventions from a different habitat:\n\n- OUTPUT: write the design brief to the file named in your message (specs/NNN-slug/brainstorm.md). Ignore the skill's `docs/superpowers/specs/\u2026` path.\n- NEXT STAGE: the SPEC is written from your brief, by another agent, immediately after you. There is no `writing-plans` skill to invoke here \u2014 finishing the brief IS the terminal step.\n- QUESTIONS: ask through the `ask_user` tool. For a choice between approaches use its rich option form ({label, description, preview}) so the trade-offs sit beside the list; lead with your recommendation. The user may attach a note to their answer \u2014 treat it as binding.\n- NOT AVAILABLE: the visual companion (there is no browser) and the per-checklist task list. Skip both.\n\nWrite what was DECIDED, not a transcript: the chosen approach, why it beat the others, the rejected alternatives with their reason, the constraints the spec must honour, and what is out of scope. Keep it short \u2014 it is the brief the spec is written from, not the spec itself, and it carries no implementation detail beyond the architectural choice.\n\nScale to the request: a small, obvious change deserves a paragraph and no questions at all.",
|
|
1079
|
-
coach: "You are horse-code, a terminal-based AI coding agent. Your product identity is always horse-code \u2014 never claim to be Claude Code, Gemini CLI, Antigravity, or any other product, even though the underlying language model powering you may be Claude, Gemini, or another model. Answer the user's technical questions about their repository and code. If needed, inspect the repository with read_file/grep/glob.\n\nWork out loud while you do it. Before a batch of tool calls, say in ONE line what you are looking for and why; when something you read changes your mind, say that too. This is not a summary at the end \u2014 the user is watching an empty screen while you search, and a run that reads thirty files in silence is indistinguishable from one that is stuck, and impossible to redirect before the tokens are spent. Keep each line short: a sentence, not a paragraph.\n\nBe concise, direct, and helpful.",
|
|
1080
|
-
// analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
|
|
1081
|
-
// prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
|
|
1082
|
-
judge: "Synthesize the council evaluations and make a single decision: 'pass' (sufficient), 'revise' (fix it, with reasons), or 'ask-human' (a question to ask the user). Return {decision, feedback, question} via submit.",
|
|
1083
|
-
"project-manager": 'Read the given plan and break it into real, actionable tasks (id, short title, deps). Each task should be a single, clear piece of work. Return {tasks} via submit.\n\nThe `writing-plans` skill above governs WHAT MAKES A TASK EXECUTABLE \u2014 take that from it and nothing else. Two bindings, because the skill describes a different habitat:\n- STRUCTURE comes from the spec-kit tasks template you are given (phases, story grouping, [P] markers), NOT from the skill\'s own document layout. Ignore its `docs/superpowers/plans/\u2026` path, its required-sub-skill header, and its execution-handoff section: this pipeline already owns worktrees, dispatch and review.\n- What you DO take: exact file paths per task, a real test cycle rather than a vague "add tests" step, no placeholders (no TBD/TODO/"similar to task N"), and interfaces named explicitly so a task whose implementer never sees the others still knows the signatures it must produce and consume.\nRight-size the same way the skill does: a task is the smallest unit worth its own test cycle and its own review. Fold setup and scaffolding into the task whose deliverable needs them.',
|
|
1084
|
-
"task-auditor": "You are the last check on a task breakdown before any of it is built. Every hour of implementation after you is spent executing this list, and a bad list does not fail \u2014 the tasks pass their reviews and the wrong work is delivered correctly. Its structure has already been checked mechanically; you are here for the part only a reader can answer: does the breakdown deliver what the plan requires, and would a task's acceptance criteria still hold for an implementation that missed the point? Do not propose better work than the plan asked for \u2014 scope you invent here becomes hours someone spends. Flag any task whose only deliverable is an answer \u2014 verifying, inspecting, confirming \u2014 because an implementer reads the code as part of doing the work, and a task that ends with the repository unchanged has spent a review round on nothing. A clean breakdown is the normal case; say so. Return {missing, weak} via submit.",
|
|
1085
|
-
"team-lead": "You audit a task breakdown before any of it runs. The schedule itself is computed from the declared dependencies and is not yours to write; what nothing has checked is whether those dependencies are RIGHT. You are given the tasks with the files each one writes and what must be true when it is done, plus the groups that would run at the same time in separate worktrees. Find the task that cannot actually start yet because it needs a type, function, table or config key another task in its own group creates \u2014 and say which declared dependencies hold work back for no reason. Both answers are usually empty; say so rather than inventing an edge. Return {missing, spurious} via submit.",
|
|
1086
|
-
router: "Look at the task title and choose the implementer role: 'designer' for UI/UX work, 'coder' for other code work. Return {role} via submit.",
|
|
1087
|
-
coder: "Implement the given task in the worktree. If it is a new task, start from scratch; if it is a returning task, address the reviewer notes. Work with read/write/edit/grep/glob/shell and run the tests.\n\nThe `test-driven-development` skill above is how you write code here: the failing test comes first, and it must fail for the RIGHT reason before you make it pass. A test that asserts nothing is worse than no test \u2014 it reports success forever. Bindings for this pipeline: your worktree is already prepared (do not create one), every file you write is committed as you write it, and there is no separate agent to hand off to \u2014 you take the task to green yourself.",
|
|
1088
|
-
designer: "Implement the UI/UX task in the worktree. Focus on the user interface and experience; work with read/write/edit.\n\nThe `frontend-design` skill above governs the LOOK: aesthetic direction, typography, and choices that do not read as templated defaults. Follow the project's existing visual language where there is one \u2014 a distinctive design that fights the surrounding product is worse than a plain one that fits it.",
|
|
1089
|
-
"senior-coder": "Take over the task the coder got stuck on; implement it with a more careful approach. Take the reviewer notes and previous attempts into account.\n\nYou are here because a previous attempt failed, so start by understanding WHY rather than rewriting: the `systematic-debugging` skill is available (fetch it with the `skill` tool) and is the right tool when a test fails or behaviour is unexplained. The `test-driven-development` skill above still governs how you write the fix \u2014 reproduce the failure in a test first, then make it pass.",
|
|
1090
|
-
"senior-designer": "Take over the UI/UX task the designer got stuck on; implement it more carefully.\n\nA previous attempt already failed, so establish WHY before redesigning \u2014 the `systematic-debugging` skill is available via the `skill` tool when the failure is behavioural rather than visual. The `frontend-design` skill above still governs the look.",
|
|
1091
|
-
architect: "Analyze the root cause of a repeatedly failing task or a merge conflict, and produce a concrete solution plan. Return {rootCause, plan} via submit.\n\nFetch the `systematic-debugging` skill with the `skill` tool and follow it: your job is the ROOT CAUSE, and the failure mode to avoid is proposing a plausible fix for a cause you never established. Say what the evidence is, not what it might be.",
|
|
1092
|
-
"code-reviewer": "Review the worktree changes of the task in REVIEW (correctness, tests, quality). Return {verdict: pass|fail, notes} via submit \u2014 your decision is final.",
|
|
1093
|
-
"principal-coder": "Holistically review all changes in the PR (base worktree). If sufficient, approve; otherwise request-changes with concrete comments. In the final decision round, give accept or ask-human (a question to ask the user).",
|
|
1094
|
-
"memory-keeper": "You are the ONLY writer into this project's long-term memory. Everything else \u2014 every review lens, the council, the judge \u2014 can merely PROPOSE; you decide.\n\nTreat every proposal as an UNVERIFIED CLAIM from a narrow, single-angle agent that saw one slice of one job, not as text to store. Most proposals are wrong in a specific way: they generalize a one-off into a rule, they restate the finding the agent was reviewing, or they record general programming advice any model already knows. Discard all of those. When a claim does survive, REWRITE it in your own words \u2014 never store an agent's sentence verbatim. Merge proposals that say the same thing into one memory.\n\nA memory qualifies ONLY if it is (a) durable \u2014 still true next month, (b) project-specific, and (c) actionable \u2014 it would change what an agent does. Write conventions, constraints, gotchas and root causes. A `lesson` must state what went wrong AND what to do instead. Set `audience` only when the memory is genuinely useful to specific roles and useless to the rest; leave it out otherwise.\n\nNEVER write transient run detail (task ids, attempt counts, what happened today), never restate the request, never duplicate a memory that already exists, and never include credentials, tokens, keys, or anything resembling a secret. Each memory is one self-contained sentence that makes sense with no other context.\n\nReturn at most 5 memories via submit as {memories}. Returning NONE is the most common correct answer \u2014 prefer an empty list over a weak memory, because a bad memory is injected into every future run.",
|
|
1095
|
-
operational: "You handle version control for the project. Given a git diff of work just completed, write a single Conventional Commits message: `type(scope): subject`. Types: feat, fix, docs, refactor, test, chore, style, perf, build, ci. Choose the scope from the touched area (e.g. spec, plan, tasks, or a module name) or omit it. The subject is imperative, lowercase, \u226472 chars, no trailing period. Add a short body only if the change genuinely needs explanation. Commit messages are always in English. Return {message} via submit."
|
|
1096
|
-
};
|
|
1097
|
-
var SPEC_TEAM = [
|
|
1098
|
-
{ name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
|
|
1099
|
-
{ name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
|
|
1100
|
-
{ name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
|
|
1101
|
-
{ name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
|
|
1102
|
-
{ name: "spec-abstraction-leak", perspective: "implementation detail that has leaked into the spec (languages, frameworks, APIs, storage mechanics, code structure) \u2014 a spec must stay technology-agnostic", models: [] },
|
|
1103
|
-
{ name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
|
|
1104
|
-
{ name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
|
|
1105
|
-
{ name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
|
|
1106
|
-
{ name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
|
|
1107
|
-
];
|
|
1108
|
-
var PLAN_TEAM = [
|
|
1109
|
-
{ name: "plan-spec-conformance", perspective: "traceability to the approved spec: every requirement covered by the plan, and nothing planned that the spec never asked for", models: [] },
|
|
1110
|
-
{ name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
|
|
1111
|
-
{ name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
|
|
1112
|
-
{ name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
|
|
1113
|
-
{ name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
|
|
1114
|
-
{ name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
|
|
1115
|
-
{ name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
|
|
1116
|
-
{ name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
|
|
1117
|
-
{ name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
|
|
1118
|
-
{ name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
|
|
1119
|
-
{ name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
|
|
1120
|
-
{ name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
|
|
1121
|
-
{ name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
|
|
1122
|
-
{ name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
|
|
1123
|
-
];
|
|
1124
|
-
var CODE_TEAM = [
|
|
1125
|
-
{ name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
|
|
1126
|
-
{ name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
|
|
1127
|
-
{ name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
|
|
1128
|
-
{ name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
|
|
1129
|
-
{ name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
|
|
1130
|
-
{ name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
|
|
1131
|
-
{ name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
|
|
1132
|
-
{ name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
|
|
1133
|
-
{ name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
|
|
1134
|
-
{ name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
|
|
1135
|
-
{ name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
|
|
1136
|
-
{ name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
|
|
1137
|
-
{ name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
|
|
1138
|
-
{ name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
|
|
1139
|
-
{ name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
|
|
1140
|
-
];
|
|
1141
|
-
var DEFAULT_COUNCIL = [
|
|
1142
|
-
{ name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
|
|
1143
|
-
{ name: "risk-judge", perspective: "What is the real blast radius of shipping this as-is? Weigh security, failure modes, concurrency, and data-integrity findings against likelihood and severity.", models: [] },
|
|
1144
|
-
{ name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
|
|
1145
|
-
{ name: "user-value-judge", perspective: "Does this deliver the user's actual intent well? Weigh usability, accessibility, and whether the scope serves the request without gold-plating.", models: [] },
|
|
1146
|
-
{ name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
|
|
1147
|
-
];
|
|
1148
|
-
function placedSkills() {
|
|
1149
|
-
return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
|
|
1996
|
+
const args = parsed.data.args;
|
|
1997
|
+
const why = refuseWrite(args);
|
|
1998
|
+
if (why) return { content: why, isError: true, settled: true };
|
|
1999
|
+
const out = await new Promise((resolve6) => {
|
|
2000
|
+
const child = execFile("git", args, {
|
|
2001
|
+
cwd: ctx.cwd,
|
|
2002
|
+
// A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
|
|
2003
|
+
// pessimistic one for a repository with anything in it.
|
|
2004
|
+
timeout: args[0] === "push" ? GIT_PUSH_TIMEOUT_MS : GIT_TIMEOUT_MS,
|
|
2005
|
+
maxBuffer: MAX_GIT_OUTPUT * 4,
|
|
2006
|
+
// GIT_TERMINAL_PROMPT=0: a push that needs credentials fails with a message instead of blocking on a
|
|
2007
|
+
// prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
|
|
2008
|
+
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
|
|
2009
|
+
}, (err, stdout, stderr) => {
|
|
2010
|
+
resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
|
|
2011
|
+
});
|
|
2012
|
+
ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
2013
|
+
});
|
|
2014
|
+
if (!out.text) return { content: out.code === 0 ? "(done)" : "git failed with no output.", isError: out.code !== 0 };
|
|
2015
|
+
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
2016
|
+
\u2026[truncated]` : out.text;
|
|
2017
|
+
return { content: clipped, isError: out.code !== 0 };
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
// src/tools/remember.ts
|
|
2022
|
+
import { z as z3 } from "zod";
|
|
2023
|
+
var params2 = z3.object({
|
|
2024
|
+
fact: z3.string().describe(
|
|
2025
|
+
"One short sentence, durable and project-specific: where something lives, which command builds it, a convention this codebase follows, a schema detail that cost you a search. Not what you did, not what is true of the language in general \u2014 something the next agent would otherwise have to rediscover."
|
|
2026
|
+
)
|
|
2027
|
+
});
|
|
2028
|
+
function buildRememberTool(sink) {
|
|
2029
|
+
return {
|
|
2030
|
+
name: "remember_fact",
|
|
2031
|
+
description: "Save a short, durable fact worth recalling in future sessions \u2014 a project convention, where something lives, a schema detail, a command that works. It is written straight away, so a session that stops early still leaves it behind. Use it the moment you learn something you would not want to work out twice; skip anything transient or specific to the task in hand.",
|
|
2032
|
+
permissionLevel: "safe",
|
|
2033
|
+
parameters: params2,
|
|
2034
|
+
async run(rawArgs, ctx) {
|
|
2035
|
+
const parsed = params2.safeParse(rawArgs);
|
|
2036
|
+
if (!parsed.success) return { content: "remember_fact: invalid args (expected { fact })", isError: true };
|
|
2037
|
+
const fact = parsed.data.fact.trim();
|
|
2038
|
+
if (!fact) return { content: "remember_fact: empty fact", isError: true };
|
|
2039
|
+
const write = sink ?? ctx.remember;
|
|
2040
|
+
if (!write) return { content: "remember_fact: memory is not available in this context", isError: true };
|
|
2041
|
+
write(fact);
|
|
2042
|
+
return { content: `Remembered: ${fact}`, isError: false };
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
1150
2045
|
}
|
|
2046
|
+
var rememberFactTool = buildRememberTool();
|
|
1151
2047
|
|
|
1152
2048
|
// src/speckit/layout.ts
|
|
1153
2049
|
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync4 } from "fs";
|
|
@@ -1748,6 +2644,7 @@ function isNonImplementing(description) {
|
|
|
1748
2644
|
return /\bread[- ]only\b/i.test(description) || /\bdoes\s+not\s+(implement|apply|execute|write)\b/i.test(description);
|
|
1749
2645
|
}
|
|
1750
2646
|
var MATCH_BAR = 3;
|
|
2647
|
+
var MIN_DENSITY = 0.1;
|
|
1751
2648
|
var MAX_ROUTED = 3;
|
|
1752
2649
|
var MAX_ROUTED_CHARS = 24e3;
|
|
1753
2650
|
function scoreSkill(task, description) {
|
|
@@ -1769,7 +2666,7 @@ function routeSkills(task, registry, already = [], opts = {}) {
|
|
|
1769
2666
|
const paths = (opts.files ?? []).join(" ");
|
|
1770
2667
|
const base = [opts.role ?? "", routingSubject(task), paths].filter(Boolean).join(" ");
|
|
1771
2668
|
const subject = [base, expandExtensions(opts.files ?? []), expandAbbreviations(base)].filter(Boolean).join(" ");
|
|
1772
|
-
return registry.list().filter((s) => !have.has(s.name)).filter((s) => !placed.has(s.name)).filter((s) => !isExplicitOnly(s.description)).filter((s) => !(opts.implementing && isNonImplementing(s.description))).map((s) => ({ name: s.name, ...scoreSkill(subject, s.description) })).filter((m) => m.score >= bar).sort((a, b) => b.score - a.score || b.density - a.density || a.name.localeCompare(b.name)).slice(0, opts.max ?? MAX_ROUTED).filter((m) => {
|
|
2669
|
+
return registry.list().filter((s) => !have.has(s.name)).filter((s) => !placed.has(s.name)).filter((s) => !isExplicitOnly(s.description)).filter((s) => !(opts.implementing && isNonImplementing(s.description))).map((s) => ({ name: s.name, ...scoreSkill(subject, s.description) })).filter((m) => m.score >= bar).sort((a, b) => b.score - a.score || b.density - a.density || a.name.localeCompare(b.name)).filter((m, i) => i === 0 || m.density >= MIN_DENSITY).slice(0, opts.max ?? MAX_ROUTED).filter((m) => {
|
|
1773
2670
|
const len = registry.get(m.name)?.content.length ?? 0;
|
|
1774
2671
|
if (spent + len > MAX_ROUTED_CHARS) return false;
|
|
1775
2672
|
spent += len;
|
|
@@ -1858,7 +2755,7 @@ async function diffSince(cwd, sinceRef, git = defaultGitRunner) {
|
|
|
1858
2755
|
|
|
1859
2756
|
// src/engine/reviewer.ts
|
|
1860
2757
|
var CODE_REVIEW_MAX_TURNS = 25;
|
|
1861
|
-
var CODE_REVIEW_TIMEOUT_MS =
|
|
2758
|
+
var CODE_REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1862
2759
|
var VerdictSchema = z8.object({
|
|
1863
2760
|
verdict: z8.enum(["pass", "fail"]).describe(
|
|
1864
2761
|
"`fail` only if the code does not do what the task required, or does it wrongly. Style you would have written differently is a note on a `pass` \u2014 a fail sends the task back around the whole cycle."
|
|
@@ -1992,6 +2889,7 @@ var params7 = z10.object({
|
|
|
1992
2889
|
replaceAll: z10.boolean().optional()
|
|
1993
2890
|
});
|
|
1994
2891
|
var NEAR_MISS_CHARS = 600;
|
|
2892
|
+
var MAX_MATCH_LINES = 5;
|
|
1995
2893
|
var norm = (t) => t.replace(/[ \t]+/g, " ").replace(/[ \t]+$/gm, "").trim();
|
|
1996
2894
|
function shortPath(path, cwd) {
|
|
1997
2895
|
const abs = resolve4(cwd, path);
|
|
@@ -2058,8 +2956,14 @@ var editFileTool = {
|
|
|
2058
2956
|
};
|
|
2059
2957
|
}
|
|
2060
2958
|
if (count > 1 && !a.replaceAll) {
|
|
2959
|
+
const lines = content.split("\n");
|
|
2960
|
+
const at = [];
|
|
2961
|
+
for (let i = 0; i < lines.length && at.length <= MAX_MATCH_LINES; i++) {
|
|
2962
|
+
if (lines[i]?.includes(a.oldString.split("\n")[0] ?? "")) at.push(i + 1);
|
|
2963
|
+
}
|
|
2964
|
+
const where = at.length ? ` First seen at line${at.length > 1 ? "s" : ""} ${at.slice(0, MAX_MATCH_LINES).join(", ")}${at.length > MAX_MATCH_LINES ? ", \u2026" : ""}.` : "";
|
|
2061
2965
|
return {
|
|
2062
|
-
content: `edit_file: oldString
|
|
2966
|
+
content: `edit_file: oldString matches ${count} places in ${shortPath(a.path, cwdResolved)}.${where} Add surrounding lines to oldString so it names the one you mean \u2014 or pass replaceAll: true if every occurrence should change.`,
|
|
2063
2967
|
isError: true
|
|
2064
2968
|
};
|
|
2065
2969
|
}
|
|
@@ -2084,7 +2988,7 @@ var editFileTool = {
|
|
|
2084
2988
|
};
|
|
2085
2989
|
|
|
2086
2990
|
// src/tools/shell.ts
|
|
2087
|
-
import { spawn } from "child_process";
|
|
2991
|
+
import { spawn as spawn2 } from "child_process";
|
|
2088
2992
|
import { resolve as resolve5, sep as sep6 } from "path";
|
|
2089
2993
|
import { z as z11 } from "zod";
|
|
2090
2994
|
var params8 = z11.object({
|
|
@@ -2148,7 +3052,7 @@ function destroysWork(command) {
|
|
|
2148
3052
|
for (const seg of command.split(/&&|\|\||;|\|/)) {
|
|
2149
3053
|
const m = /^\s*git\s+(.*)$/.exec(seg.trim());
|
|
2150
3054
|
if (!m) continue;
|
|
2151
|
-
const rest = (m[1] ?? "").replace(/^(
|
|
3055
|
+
const rest = (m[1] ?? "").replace(/^(?:(?:-[cC]|--(?:git-dir|work-tree|namespace|exec-path|config-env))\s+\S+|-\S+)\s+/g, "").trim();
|
|
2152
3056
|
const hit = DESTROYS_WORK.find((d) => d.re.test(rest));
|
|
2153
3057
|
if (hit) return hit.what;
|
|
2154
3058
|
}
|
|
@@ -2201,7 +3105,7 @@ var shellTool = {
|
|
|
2201
3105
|
return new Promise((resolvePromise) => {
|
|
2202
3106
|
let child;
|
|
2203
3107
|
try {
|
|
2204
|
-
child =
|
|
3108
|
+
child = spawn2(a.command, {
|
|
2205
3109
|
cwd: ctx.cwd,
|
|
2206
3110
|
shell: true,
|
|
2207
3111
|
signal: ctx.signal,
|
|
@@ -2388,6 +3292,188 @@ async function commitRefreshed(git, baseWorktree, traceRootRel2) {
|
|
|
2388
3292
|
return r.code === 0;
|
|
2389
3293
|
}
|
|
2390
3294
|
|
|
3295
|
+
// src/engine/writer-registry.ts
|
|
3296
|
+
import { z as z14 } from "zod";
|
|
3297
|
+
|
|
3298
|
+
// src/engine/normalize-question.ts
|
|
3299
|
+
import { z as z13 } from "zod";
|
|
3300
|
+
var NormalizedQuestionSchema = z13.object({
|
|
3301
|
+
question: z13.string().describe("The core question, concise, WITHOUT the embedded options table/list."),
|
|
3302
|
+
options: z13.array(z13.string()).describe("Each selectable choice as a SHORT label; the recommended one first, suffixed ' (recommended)'. Empty when the question is genuinely open-ended."),
|
|
3303
|
+
multiSelect: z13.boolean().describe("true only if the user may pick more than one.")
|
|
3304
|
+
});
|
|
3305
|
+
var PROMPT = "You reformat an agent's question for a terminal UI that renders selectable options (arrow keys + Enter). Given the raw question text \u2014 which may embed choices as a markdown table, an A/B/C/D list, or a 'recommended' suggestion \u2014 extract exactly:\n- `question`: the core question, concise, WITHOUT the embedded options table/list.\n- `options`: each selectable choice as a SHORT label. If one choice is recommended, list it FIRST and append ' (recommended)'. Do NOT add an 'other' / free-text / 'answer in your own words' option \u2014 the UI already provides that.\n- `multiSelect`: true only if the user may pick several.\nIf the text is genuinely open-ended (no discrete choices), return options: []. Preserve the user's language. Return the result via submit.";
|
|
3306
|
+
function looksLikeChoices(text) {
|
|
3307
|
+
return /\|[^\n]*\|[^\n]*\|/.test(text) || /(^|\n)\s*[A-Ea-e][).\-:]\s/.test(text) || /(^|\n)\s*[-*]\s+\S.*(\n\s*[-*]\s+\S.*){1,}/.test(text) || /\b(option|seçenek|choice|önerilen|recommended)\b/i.test(text);
|
|
3308
|
+
}
|
|
3309
|
+
async function normalizeQuestion(deps, raw) {
|
|
3310
|
+
const { role: agentRole, model, fallbacks, onExhausted, onFallback } = deps.roleRegistry.fallbackOpts("refiner");
|
|
3311
|
+
return runStructuredRole({
|
|
3312
|
+
provider: deps.provider,
|
|
3313
|
+
role: agentRole,
|
|
3314
|
+
model,
|
|
3315
|
+
fallbacks,
|
|
3316
|
+
onExhausted,
|
|
3317
|
+
onFallback,
|
|
3318
|
+
// This shapes the question the USER reads; a rule like "always ask in Turkish" belongs here.
|
|
3319
|
+
systemPrompt: PROMPT + deps.roleRegistry.ruleSuffix(),
|
|
3320
|
+
tools: new ToolRegistry(),
|
|
3321
|
+
messages: [{ role: "user", content: raw }],
|
|
3322
|
+
permission: deps.permission,
|
|
3323
|
+
approve: deps.approve,
|
|
3324
|
+
cwd: ".",
|
|
3325
|
+
signal: deps.signal
|
|
3326
|
+
}, NormalizedQuestionSchema);
|
|
3327
|
+
}
|
|
3328
|
+
function extractChoicesFrom(text) {
|
|
3329
|
+
const lines = text.split("\n");
|
|
3330
|
+
const rows = lines.map((l) => l.trim()).filter((l) => l.startsWith("|") && l.endsWith("|") && !/^\|[\s|:-]+\|$/.test(l)).map((l) => l.slice(1, -1).split("|").map((c) => c.trim()));
|
|
3331
|
+
const body = rows.filter((cells) => cells.length >= 2 && cells[0] && cells[1]).filter((cells, i) => !(i === 0 && /^(option|seçenek|choice|alternatif)$/i.test(cells[0])));
|
|
3332
|
+
if (body.length >= 2) {
|
|
3333
|
+
const question = lines.filter((l) => !/^\s*\|.*\|\s*$/.test(l)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3334
|
+
return { choices: body.map((cells) => ({ label: cells[0], description: cells.slice(1).join(" \u2014 ") })), question };
|
|
3335
|
+
}
|
|
3336
|
+
const isLettered = (l) => /^\s*([A-Ea-e])[).\-:]\s+(\S.*)$/.exec(l.trim());
|
|
3337
|
+
const lettered = lines.map(isLettered).filter((m) => !!m).map((m) => ({ label: `${m[1].toUpperCase()} \u2014 ${m[2]}` }));
|
|
3338
|
+
if (lettered.length >= 2) {
|
|
3339
|
+
const question = lines.filter((l) => !isLettered(l)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3340
|
+
return { choices: lettered, question };
|
|
3341
|
+
}
|
|
3342
|
+
const MARKER = /\(([A-Ea-e])\)\s*/g;
|
|
3343
|
+
const marks = [...text.matchAll(MARKER)];
|
|
3344
|
+
const inOrder = marks.length >= 2 && marks.every((m, i) => m[1].toUpperCase() === String.fromCharCode(65 + i));
|
|
3345
|
+
if (inOrder) {
|
|
3346
|
+
const first = marks[0].index ?? 0;
|
|
3347
|
+
const parts = marks.map((m, i) => {
|
|
3348
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
3349
|
+
const end = i + 1 < marks.length ? marks[i + 1].index ?? text.length : text.length;
|
|
3350
|
+
return { letter: m[1].toUpperCase(), body: text.slice(start, end).trim().replace(/\s+/g, " ") };
|
|
3351
|
+
}).filter((p) => p.body);
|
|
3352
|
+
if (parts.length >= 2) {
|
|
3353
|
+
const question = text.slice(0, first).replace(/\s*(Se\u00e7enekler|Options|Choices)\s*:?\s*$/i, "").trim();
|
|
3354
|
+
return {
|
|
3355
|
+
choices: parts.map((p) => ({ label: `${p.letter} \u2014 ${clipLabel(p.body)}`, description: p.body })),
|
|
3356
|
+
question: question || text
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return { choices: [], question: text };
|
|
3361
|
+
}
|
|
3362
|
+
var clipLabel = (body) => {
|
|
3363
|
+
const cut = body.replace(/^\[[^\]]*\]\s*/, "");
|
|
3364
|
+
const stop = cut.search(/[—–.;:]\s/);
|
|
3365
|
+
const head = stop > 12 ? cut.slice(0, stop) : cut;
|
|
3366
|
+
return head.length > 72 ? `${head.slice(0, 71)}\u2026` : head;
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
// src/engine/writer-registry.ts
|
|
3370
|
+
var askUserParams = z14.object({
|
|
3371
|
+
question: z14.string(),
|
|
3372
|
+
// For a multiple-choice question, list the choices here → the UI shows a selectable checkbox/radio list
|
|
3373
|
+
// (arrow keys + Enter) instead of a free-text box. Omit for an open-ended question.
|
|
3374
|
+
//
|
|
3375
|
+
// A choice may be a plain string, or an object carrying what the label alone cannot say: a one-line
|
|
3376
|
+
// `description`, and a `preview` rendered in a panel beside the list while that option is focused. Use the
|
|
3377
|
+
// rich form when the decision turns on the trade-offs rather than the name (e.g. "which approach?").
|
|
3378
|
+
options: z14.array(z14.union([
|
|
3379
|
+
z14.string(),
|
|
3380
|
+
z14.object({ label: z14.string(), description: z14.string().optional(), preview: z14.string().optional() })
|
|
3381
|
+
])).optional().describe(
|
|
3382
|
+
"The choices, when the question has discrete answers \u2014 the UI renders a selectable list instead of a free-text box. Omit for an open-ended question. A choice may be a plain string, or an object with a one-line `description` and a `preview` shown beside the list; use the rich form when the decision turns on trade-offs rather than on the name."
|
|
3383
|
+
),
|
|
3384
|
+
multiSelect: z14.boolean().optional().describe(
|
|
3385
|
+
"True when the user may pick more than one (checkboxes); omitted means pick exactly one (radio)."
|
|
3386
|
+
),
|
|
3387
|
+
/**
|
|
3388
|
+
* What the user has to DO before they can answer — one action per entry.
|
|
3389
|
+
*
|
|
3390
|
+
* Present ⇒ this is a hand-off, not a question: the run has stopped because only a person can carry the
|
|
3391
|
+
* next step, and the UI says so rather than showing a bare "? Question".
|
|
3392
|
+
*/
|
|
3393
|
+
steps: z14.array(z14.string()).optional().describe(
|
|
3394
|
+
'What the user has to DO before they can answer \u2014 one action per entry. Supplying this makes it a HAND-OFF rather than a question: the run has stopped because only a person can carry the next step, and the UI says so instead of showing a bare "? Question". Use it whenever you are asking someone to go and perform something and report back; leave it out when you only want an answer.'
|
|
3395
|
+
)
|
|
3396
|
+
});
|
|
3397
|
+
var POINTS_ELSEWHERE = /\b(above|below|earlier|previously|as listed|as described)\b|yukarı|aşağı|altında|önceki|birazdan|listelenen/i;
|
|
3398
|
+
var ITEM_REFERENCE = /(?:^|[^\w])(?:q|soru|question|madde|item)\s*\.?\s*(\d{1,2})\b/gi;
|
|
3399
|
+
var ITEM_STATED = /^[\s>*\-–—#]*(?:q|soru|question|madde|item)\s*\.?\s*\d{1,2}\s*[:).]/gim;
|
|
3400
|
+
function danglingItems(question) {
|
|
3401
|
+
const referenced = new Set([...question.matchAll(ITEM_REFERENCE)].map((m) => Number(m[1])));
|
|
3402
|
+
if (referenced.size < 2) return [];
|
|
3403
|
+
const stated = question.match(ITEM_STATED)?.length ?? 0;
|
|
3404
|
+
return stated >= referenced.size ? [] : [...referenced].sort((a, b) => a - b);
|
|
3405
|
+
}
|
|
3406
|
+
var ENUMERATOR = /(?:^|[^\w])\(?(\d{1,2})[).:]/g;
|
|
3407
|
+
function packedQuestions(question) {
|
|
3408
|
+
if ((question.match(/\?/g)?.length ?? 0) < 2) return 0;
|
|
3409
|
+
const numbered = new Set([...question.matchAll(ENUMERATOR)].map((m) => Number(m[1])));
|
|
3410
|
+
return numbered.size;
|
|
3411
|
+
}
|
|
3412
|
+
var ONE_AT_A_TIME = "This tool asks ONE question and takes ONE answer. Ask the first one on its own, with its own `options`, and call this tool again for the next once you have the answer \u2014 the answer to one of these usually changes what the next one should be.";
|
|
3413
|
+
function buildAskUserTool(askUser, normalize) {
|
|
3414
|
+
return {
|
|
3415
|
+
name: "ask_user",
|
|
3416
|
+
description: 'Ask the user ONE question and get their answer. Several decisions are several calls: ask the first, read the answer, then ask the next \u2014 the user has one answer field, so four questions in one box means four decisions they must hold in their head and answer in prose. For a multiple-choice question, pass `options` (the choices) \u2014 the UI shows a selectable list the user checks off; set `multiSelect: true` when they may pick several. Omit `options` for an open-ended (free-text) question. An option may be a plain string, or {label, description, preview} when the decision turns on trade-offs the label cannot carry \u2014 the preview is shown beside the list as the user moves the cursor. A `label` is a SHORT single line (a few words, no line breaks): it is a name for the choice, not the argument for it. Put the reasoning in `description` (one sentence) and the detail in `preview`. If you have findings to report, WRITE THEM as your message before calling this \u2014 a question that says "the evaluation is above" when you never wrote one leaves the user choosing between options whose basis they cannot see. The user may attach a free-text note to their choice, which arrives appended to the answer.\n\nWhen you need the user to DO something first \u2014 click through a screen, run a scenario, look at a network response \u2014 put each action in `steps`, one per entry, and ask in `question` for what they should report back. The user reads THIS BOX and the chat; a file you wrote is not on their screen, so "the steps above" points at nothing they can see. With `steps` the UI shows a hand-off \u2014 the numbered actions and then the question \u2014 instead of a bare question.',
|
|
3417
|
+
permissionLevel: "safe",
|
|
3418
|
+
parameters: askUserParams,
|
|
3419
|
+
run: async (rawArgs, ctx) => {
|
|
3420
|
+
const parsed = askUserParams.safeParse(rawArgs);
|
|
3421
|
+
if (!parsed.success) {
|
|
3422
|
+
return { content: `ask_user: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
|
|
3423
|
+
}
|
|
3424
|
+
const { question, options, multiSelect, steps } = parsed.data;
|
|
3425
|
+
const asker = ctx.role || ctx.model ? { asker: { ...ctx.role ? { role: ctx.role } : {}, ...ctx.model ? { model: ctx.model } : {} } } : {};
|
|
3426
|
+
if (POINTS_ELSEWHERE.test(question) && !steps?.length && !ctx.said?.trim()) {
|
|
3427
|
+
return {
|
|
3428
|
+
content: "ask_user: this question refers to something the user cannot see. You wrote no message this turn, so there is nothing above it \u2014 and a file you wrote is not on their screen. Put what they must do in `steps` (one action per entry), or write it out in `question`, and ask again.",
|
|
3429
|
+
isError: true
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
const dangling = danglingItems(question);
|
|
3433
|
+
if (dangling.length && !steps?.length && !ctx.said?.trim()) {
|
|
3434
|
+
return {
|
|
3435
|
+
content: `ask_user: this asks the user to answer ${dangling.map((n) => `Q${n}`).join(", ")}, and none of them is on their screen \u2014 you did not write them this turn, and a file you wrote is not something they are looking at. ${ONE_AT_A_TIME}`,
|
|
3436
|
+
isError: true
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
const packed = packedQuestions(question);
|
|
3440
|
+
if (packed >= 2 && !steps?.length && !options?.length) {
|
|
3441
|
+
return {
|
|
3442
|
+
content: `ask_user: this is ${packed} questions in one box, and the user has one answer field for all of them. ${ONE_AT_A_TIME}`,
|
|
3443
|
+
isError: true
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
if ((!options || options.length === 0) && !steps?.length && looksLikeChoices(question)) {
|
|
3447
|
+
const found = extractChoicesFrom(question);
|
|
3448
|
+
if (found.choices.length >= 2) {
|
|
3449
|
+
return { content: await askUser(found.question, { options: found.choices, ...asker }), isError: false };
|
|
3450
|
+
}
|
|
3451
|
+
if (normalize) {
|
|
3452
|
+
try {
|
|
3453
|
+
const n = await normalize(question);
|
|
3454
|
+
if (n.options.length > 0) {
|
|
3455
|
+
return { content: await askUser(n.question, { options: n.options, multiSelect: n.multiSelect, ...asker }), isError: false };
|
|
3456
|
+
}
|
|
3457
|
+
} catch {
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
return { content: await askUser(question, { options, multiSelect, steps, ...asker }), isError: false };
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
function writerRegistry(skillRegistry, extra = []) {
|
|
3466
|
+
const r = new ToolRegistry();
|
|
3467
|
+
r.register(readFileTool);
|
|
3468
|
+
r.register(writeFileTool);
|
|
3469
|
+
r.register(editFileTool);
|
|
3470
|
+
r.register(grepTool);
|
|
3471
|
+
r.register(globTool);
|
|
3472
|
+
r.register(buildSkillTool(skillRegistry));
|
|
3473
|
+
for (const t of extra) r.register(t);
|
|
3474
|
+
return r;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
2391
3477
|
// src/engine/role-fitness.ts
|
|
2392
3478
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
2393
3479
|
import { dirname as dirname4 } from "path";
|
|
@@ -2503,7 +3589,7 @@ var RoleFitness = class {
|
|
|
2503
3589
|
};
|
|
2504
3590
|
|
|
2505
3591
|
// src/engine/routing.ts
|
|
2506
|
-
import { z as
|
|
3592
|
+
import { z as z15 } from "zod";
|
|
2507
3593
|
|
|
2508
3594
|
// src/engine/route-role.ts
|
|
2509
3595
|
var STYLE_EXT = [".css", ".scss", ".sass", ".less", ".styl"];
|
|
@@ -2569,27 +3655,9 @@ function routeByEvidence(card) {
|
|
|
2569
3655
|
return { why: hasComponent && !hasStyle && !hasMarkup ? "component files could be either" : "the evidence points both ways" };
|
|
2570
3656
|
}
|
|
2571
3657
|
|
|
2572
|
-
// src/agent/deadline.ts
|
|
2573
|
-
function withDeadline(work, signal, message) {
|
|
2574
|
-
work.catch(() => {
|
|
2575
|
-
});
|
|
2576
|
-
return Promise.race([work, expired(signal, message)]);
|
|
2577
|
-
}
|
|
2578
|
-
function expired(signal, message) {
|
|
2579
|
-
return new Promise((_, reject) => {
|
|
2580
|
-
if (signal.aborted) {
|
|
2581
|
-
reject(new Error(message));
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
signal.addEventListener("abort", () => reject(new Error(message)), { once: true });
|
|
2585
|
-
});
|
|
2586
|
-
}
|
|
2587
|
-
var SHORT_CALL_MS = 3 * 60 * 1e3;
|
|
2588
|
-
var LONG_CALL_MS = 15 * 60 * 1e3;
|
|
2589
|
-
|
|
2590
3658
|
// src/engine/routing.ts
|
|
2591
|
-
var RouteSchema =
|
|
2592
|
-
role:
|
|
3659
|
+
var RouteSchema = z15.object({
|
|
3660
|
+
role: z15.enum(["coder", "designer"]).describe(
|
|
2593
3661
|
"Who should implement this. `designer` when the work IS how the thing looks or behaves to a person \u2014 layout, spacing, colour, copy, interaction. `coder` for everything else. Judge by what the work is, not by the file type: a component file holding a data hook is code work; a component file whose whole job is appearance is design work."
|
|
2594
3662
|
)
|
|
2595
3663
|
});
|
|
@@ -2651,9 +3719,9 @@ function createDefaultRegistry() {
|
|
|
2651
3719
|
}
|
|
2652
3720
|
|
|
2653
3721
|
// src/engine/operational.ts
|
|
2654
|
-
import { z as
|
|
2655
|
-
var CommitSchema =
|
|
2656
|
-
message:
|
|
3722
|
+
import { z as z16 } from "zod";
|
|
3723
|
+
var CommitSchema = z16.object({
|
|
3724
|
+
message: z16.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
|
|
2657
3725
|
});
|
|
2658
3726
|
var MAX_DIFF = 12e3;
|
|
2659
3727
|
var OPERATIONAL_MAX_TURNS = 3;
|
|
@@ -2750,6 +3818,39 @@ async function squashTask(deps, worktree, baseRef, title, git = defaultGitRunner
|
|
|
2750
3818
|
return message;
|
|
2751
3819
|
}
|
|
2752
3820
|
|
|
3821
|
+
// src/engine/touched.ts
|
|
3822
|
+
async function changedPaths(cwd, git = defaultGitRunner) {
|
|
3823
|
+
const res = await git(["status", "--porcelain", "-uall"], cwd);
|
|
3824
|
+
if (res.code !== 0) return [];
|
|
3825
|
+
const out = [];
|
|
3826
|
+
for (const line of res.stdout.split("\n")) {
|
|
3827
|
+
if (!line.trim()) continue;
|
|
3828
|
+
const path = line.slice(3).trim();
|
|
3829
|
+
const arrow = path.lastIndexOf(" -> ");
|
|
3830
|
+
const p = arrow >= 0 ? path.slice(arrow + 4) : path;
|
|
3831
|
+
out.push(p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p);
|
|
3832
|
+
}
|
|
3833
|
+
return out;
|
|
3834
|
+
}
|
|
3835
|
+
async function reconcileTouched(deps, cwd, touched, git = defaultGitRunner) {
|
|
3836
|
+
let changed;
|
|
3837
|
+
try {
|
|
3838
|
+
changed = await changedPaths(cwd, git);
|
|
3839
|
+
} catch {
|
|
3840
|
+
return [];
|
|
3841
|
+
}
|
|
3842
|
+
const known = new Set(touched);
|
|
3843
|
+
const extra = changed.filter((p) => !known.has(p));
|
|
3844
|
+
for (const p of extra) {
|
|
3845
|
+
touched.push(p);
|
|
3846
|
+
try {
|
|
3847
|
+
await commitFile(deps, cwd, p, git);
|
|
3848
|
+
} catch {
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
return extra;
|
|
3852
|
+
}
|
|
3853
|
+
|
|
2753
3854
|
// src/skills/adjudicate.ts
|
|
2754
3855
|
async function adjudicateSkills(opts) {
|
|
2755
3856
|
const { confident, borderline } = partitionByConfidence(opts.matches, opts.bar, opts.margin);
|
|
@@ -2804,6 +3905,42 @@ function parseKeep(text) {
|
|
|
2804
3905
|
}
|
|
2805
3906
|
}
|
|
2806
3907
|
|
|
3908
|
+
// src/engine/group-notes.ts
|
|
3909
|
+
function subjectOf(note) {
|
|
3910
|
+
const path = /\b((?:[\w.-]+\/)+[\w.-]+\.(?:cs|ts|tsx|json|sql|md))\b/.exec(note)?.[1];
|
|
3911
|
+
if (path) return path.split("/").pop().replace(/\.\w+$/, "");
|
|
3912
|
+
return /\b([A-Z][a-z]+(?:[A-Z][a-z0-9]+){1,})\b/.exec(note)?.[1];
|
|
3913
|
+
}
|
|
3914
|
+
function groupNotes(notes) {
|
|
3915
|
+
const order = [];
|
|
3916
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
3917
|
+
const loose = [];
|
|
3918
|
+
for (const note of notes) {
|
|
3919
|
+
const s = subjectOf(note);
|
|
3920
|
+
if (!s) {
|
|
3921
|
+
loose.push(note);
|
|
3922
|
+
continue;
|
|
3923
|
+
}
|
|
3924
|
+
if (!bySubject.has(s)) {
|
|
3925
|
+
bySubject.set(s, []);
|
|
3926
|
+
order.push(s);
|
|
3927
|
+
}
|
|
3928
|
+
bySubject.get(s).push(note);
|
|
3929
|
+
}
|
|
3930
|
+
const blocks = [];
|
|
3931
|
+
for (const s of order) {
|
|
3932
|
+
const group = bySubject.get(s);
|
|
3933
|
+
if (group.length === 1) {
|
|
3934
|
+
loose.push(group[0]);
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3937
|
+
blocks.push(`${s} \u2014 ${group.length} notes, one subject:
|
|
3938
|
+
${group.map((n) => ` - ${n}`).join("\n")}`);
|
|
3939
|
+
}
|
|
3940
|
+
if (loose.length) blocks.push(loose.map((n) => `- ${n}`).join("\n"));
|
|
3941
|
+
return blocks.join("\n\n");
|
|
3942
|
+
}
|
|
3943
|
+
|
|
2807
3944
|
// src/engine/implementer.ts
|
|
2808
3945
|
var IMPLEMENTER_MAX_TURNS = 200;
|
|
2809
3946
|
var WHAT_IT_COST = "Before you stop: did anything here cost you more than one attempt \u2014 a command that had to be invoked a particular way, a file that was not where it should have been, a trap you fell into? If you have not already recorded it with `remember_fact`, do that now. The next agent pays for it again otherwise.";
|
|
@@ -2854,6 +3991,7 @@ async function runImplementer(deps, role, task, cwd, slot = 0) {
|
|
|
2854
3991
|
tools.register(buildSkillTool(deps.skillRegistry));
|
|
2855
3992
|
for (const t of contextTools(deps)) tools.register(t);
|
|
2856
3993
|
tools.register(buildRememberTool(deps.rememberFact));
|
|
3994
|
+
if (deps.askUser) tools.register(buildAskUserTool(deps.askUser));
|
|
2857
3995
|
const returning = task.reviewNotes.length > 0;
|
|
2858
3996
|
const brief = [
|
|
2859
3997
|
task.acceptance.length ? `It is done when ALL of these are true \u2014 they are exactly what the review will check:
|
|
@@ -2869,7 +4007,7 @@ ${WHAT_IT_COST}
|
|
|
2869
4007
|
|
|
2870
4008
|
Staging, committing and branches are this tool's business, never the developer's: do not end by asking them to \`git add\`, commit, or tidy the tree. Ask them only for what nobody else can do \u2014 look at a screen, start an environment, decide something. If a file you wrote is not in git yet, that is a fault to report, not an errand to hand over.`;
|
|
2871
4009
|
const content = (returning ? `This is a RETURNING task: "${task.title}". Address the reviewer notes:
|
|
2872
|
-
${task.reviewNotes
|
|
4010
|
+
${groupNotes(task.reviewNotes)}` : `This is a NEW task: "${task.title}". Implement it.`) + (brief ? `
|
|
2873
4011
|
|
|
2874
4012
|
${brief}` : "") + `
|
|
2875
4013
|
|
|
@@ -2879,7 +4017,7 @@ ${handOver}`;
|
|
|
2879
4017
|
const hints = memoryHints(deps, `${task.title} ${task.reviewNotes.join(" ")}`, { role, operations: true });
|
|
2880
4018
|
const touched = [];
|
|
2881
4019
|
const attached = deps.roleRegistry.skillsFor(role);
|
|
2882
|
-
const subject = `${task.title} ${task.acceptance.join(" ")}
|
|
4020
|
+
const subject = `${task.title} ${task.acceptance.join(" ")}`;
|
|
2883
4021
|
const routed = routeSkills(subject, deps.skillRegistry, attached, {
|
|
2884
4022
|
role,
|
|
2885
4023
|
implementing: true,
|
|
@@ -2928,7 +4066,8 @@ ${handOver}`;
|
|
|
2928
4066
|
cwd,
|
|
2929
4067
|
{ role, files: task.files, title: task.title }
|
|
2930
4068
|
) : "";
|
|
2931
|
-
const
|
|
4069
|
+
const delegate = deps.delegateTo ? cliFor(chain[0] ?? "") ?? deps.delegateTo : void 0;
|
|
4070
|
+
const systemPrompt = withSkills + law + (delegate ? "" : projectToolsNote(tools.list(), !!loadGraphSync(cwd))) + BATCH_TOOLS_NOTE;
|
|
2932
4071
|
const budgetMs = attemptBudget(task, deps.implementerTimeoutMs ?? IMPLEMENTER_TIMEOUT_MS);
|
|
2933
4072
|
const budget = AbortSignal.timeout(budgetMs);
|
|
2934
4073
|
let warned = false;
|
|
@@ -2940,7 +4079,12 @@ ${handOver}`;
|
|
|
2940
4079
|
return note;
|
|
2941
4080
|
};
|
|
2942
4081
|
const opts = {
|
|
2943
|
-
|
|
4082
|
+
/**
|
|
4083
|
+
* A delegated implementer gets a provider bound to its worktree and allowed to write; every other role
|
|
4084
|
+
* keeps the one the composition root built. Per call rather than shared, because the worktree differs
|
|
4085
|
+
* per task and a provider shared across parallel implementers could only hold one of them.
|
|
4086
|
+
*/
|
|
4087
|
+
provider: delegate ? new CliProvider({ kind: delegate, readOnly: false, cwd, ...deps.accounts ? { accounts: deps.accounts } : {} }) : deps.provider,
|
|
2944
4088
|
...resolved,
|
|
2945
4089
|
systemPrompt,
|
|
2946
4090
|
...chain.length ? { model: chain[0], fallbacks: chain.slice(1) } : {},
|
|
@@ -3013,6 +4157,10 @@ ${handOver}`;
|
|
|
3013
4157
|
if (deps.signal.aborted || !budget.aborted) throw e;
|
|
3014
4158
|
throw new Error(overran);
|
|
3015
4159
|
} finally {
|
|
4160
|
+
const alsoChanged = await reconcileTouched(deps, cwd, touched);
|
|
4161
|
+
if (alsoChanged.length) {
|
|
4162
|
+
deps.note?.(`\u{1F4DD} ${alsoChanged.length} file(s) changed outside a write tool \u2014 checkpointed and credited.`);
|
|
4163
|
+
}
|
|
3016
4164
|
reinforceTouched(deps, hints.ids, touched, role);
|
|
3017
4165
|
reinforceUsed(deps, hints.ids, await writtenText(cwd, touched), role);
|
|
3018
4166
|
}
|
|
@@ -3021,7 +4169,7 @@ ${handOver}`;
|
|
|
3021
4169
|
// src/engine/review.ts
|
|
3022
4170
|
import { existsSync as existsSync8 } from "fs";
|
|
3023
4171
|
import { isAbsolute, join as join8 } from "path";
|
|
3024
|
-
import { z as
|
|
4172
|
+
import { z as z17 } from "zod";
|
|
3025
4173
|
|
|
3026
4174
|
// src/agent/roles.ts
|
|
3027
4175
|
function isTransientFailure(reason) {
|
|
@@ -3029,6 +4177,45 @@ function isTransientFailure(reason) {
|
|
|
3029
4177
|
if (/\b(429|rate.?limit|quota|exhaust|insufficient|billing|credit)\b/.test(r)) return false;
|
|
3030
4178
|
return /overload|529|50[0234]|timeout|timed out|deadline|econnreset|epipe|socket hang up|stream ended|temporar|unavailable|try again/.test(r);
|
|
3031
4179
|
}
|
|
4180
|
+
function isSourceCapacity(reason) {
|
|
4181
|
+
return /capacity is (?:temporarily unavailable|busy)/i.test(reason);
|
|
4182
|
+
}
|
|
4183
|
+
function sourcePrefix(model) {
|
|
4184
|
+
const s = model.replace(/^no-think\//, "");
|
|
4185
|
+
const cli = cliFor(s);
|
|
4186
|
+
if (cli) return cli;
|
|
4187
|
+
const i = s.indexOf("/");
|
|
4188
|
+
return i > 0 ? s.slice(0, i) : void 0;
|
|
4189
|
+
}
|
|
4190
|
+
function weightedCycle(sources, weights) {
|
|
4191
|
+
const queues = sources.map((s) => Array(Math.max(1, weights[s] ?? 1)).fill(s));
|
|
4192
|
+
const out = [];
|
|
4193
|
+
for (let more = true; more; ) {
|
|
4194
|
+
more = false;
|
|
4195
|
+
for (const q of queues) {
|
|
4196
|
+
const m = q.shift();
|
|
4197
|
+
if (m !== void 0) {
|
|
4198
|
+
out.push(m);
|
|
4199
|
+
more = true;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
return out;
|
|
4204
|
+
}
|
|
4205
|
+
function canonicalSource(name) {
|
|
4206
|
+
const s = name.toLowerCase().replace(/^no-think\//, "");
|
|
4207
|
+
if (s === "cc" || s === "claude") return "claude";
|
|
4208
|
+
if (s === "cx") return "codex";
|
|
4209
|
+
return sourcePrefix(s) ?? s;
|
|
4210
|
+
}
|
|
4211
|
+
function providerOutage(reason) {
|
|
4212
|
+
return /no active credentials for provider:?\s*([\w.-]+)/i.exec(reason)?.[1] ?? /provider\s+'?([\w.-]+)'?\s+is not configured/i.exec(reason)?.[1] ?? /all\s+([\w.-]+)\s+accounts have exhausted their quota/i.exec(reason)?.[1] ?? /shared egress ip quota exhausted\s*\(([\w.-]+)\)/i.exec(reason)?.[1] ?? /^\s*(claude|codex)\s+CLI:\s*rejected\b/i.exec(reason)?.[1]?.toLowerCase();
|
|
4213
|
+
}
|
|
4214
|
+
function quotaResetAt(reason) {
|
|
4215
|
+
const iso = /\(resets\s+([0-9T:.\-]+Z)\)/i.exec(reason)?.[1];
|
|
4216
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
4217
|
+
return Number.isFinite(t) ? t : void 0;
|
|
4218
|
+
}
|
|
3032
4219
|
var RoleRegistry = class _RoleRegistry {
|
|
3033
4220
|
// durable behavioral rules → appended to EVERY role's prompt
|
|
3034
4221
|
constructor(roles, defaultPrompts = {}, skillRegistry) {
|
|
@@ -3120,6 +4307,27 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3120
4307
|
this.quarantine.set(model, { at: now, reason, ...ends !== void 0 && { until: ends } });
|
|
3121
4308
|
this.onQuarantine?.(model, reason, ends);
|
|
3122
4309
|
}
|
|
4310
|
+
/** Every model any role's chain names — the pool this registry can actually reach for. */
|
|
4311
|
+
knownModels() {
|
|
4312
|
+
return [...new Set(Object.values(this.roles).flatMap((r) => r.models ?? []))];
|
|
4313
|
+
}
|
|
4314
|
+
/**
|
|
4315
|
+
* Benches every model of one provider, for a failure that is about the provider itself.
|
|
4316
|
+
*
|
|
4317
|
+
* Returns what it took out, so the caller can say so once instead of six times. Falls back to benching the
|
|
4318
|
+
* single model when the pool names none of that provider — an unknown provider is still a real failure.
|
|
4319
|
+
*/
|
|
4320
|
+
markProviderExhausted(provider, model, reason, now = Date.now()) {
|
|
4321
|
+
const want = canonicalSource(provider);
|
|
4322
|
+
const hit = this.knownModels().filter((m) => sourcePrefix(m) === want);
|
|
4323
|
+
const until = quotaResetAt(reason);
|
|
4324
|
+
for (const m of hit) this.markExhausted(m, reason, now, until);
|
|
4325
|
+
if (!hit.length) {
|
|
4326
|
+
this.markExhausted(model, reason, now);
|
|
4327
|
+
return [model];
|
|
4328
|
+
}
|
|
4329
|
+
return hit;
|
|
4330
|
+
}
|
|
3123
4331
|
/**
|
|
3124
4332
|
* How long a BEHAVIOURAL bench lasts before the model is tried again.
|
|
3125
4333
|
*
|
|
@@ -3250,9 +4458,28 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3250
4458
|
*/
|
|
3251
4459
|
chainFor(roleName, slot = 0) {
|
|
3252
4460
|
const c = this.chain(roleName);
|
|
3253
|
-
|
|
4461
|
+
if (c.length < 2) return c;
|
|
4462
|
+
const order = [];
|
|
4463
|
+
for (const m of c) {
|
|
4464
|
+
const s = sourceOf(m);
|
|
4465
|
+
if (!order.includes(s)) order.push(s);
|
|
4466
|
+
}
|
|
4467
|
+
const cycle = weightedCycle(order, this.sourceWeights?.() ?? {});
|
|
4468
|
+
if (cycle.length) {
|
|
4469
|
+
const want = cycle[(slot % cycle.length + cycle.length) % cycle.length];
|
|
4470
|
+
const i = c.findIndex((m) => sourceOf(m) === want);
|
|
4471
|
+
if (i > 0) return [c[i], ...c.filter((_, j) => j !== i)];
|
|
4472
|
+
if (i === 0) return c;
|
|
4473
|
+
}
|
|
4474
|
+
const k = (slot % c.length + c.length) % c.length;
|
|
3254
4475
|
return k === 0 ? c : [...c.slice(k), ...c.slice(0, k)];
|
|
3255
4476
|
}
|
|
4477
|
+
/** How many accounts each source has connected — set at the composition root; equal weights without it. */
|
|
4478
|
+
sourceWeights;
|
|
4479
|
+
/** Wire the account weights (called once the pool exists). */
|
|
4480
|
+
setSourceWeights(fn) {
|
|
4481
|
+
this.sourceWeights = fn;
|
|
4482
|
+
}
|
|
3256
4483
|
/** The model a role would use next (chain head), for UI display only. */
|
|
3257
4484
|
peekModel(roleName) {
|
|
3258
4485
|
return this.chain(roleName)[0] ?? "";
|
|
@@ -3280,7 +4507,12 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3280
4507
|
role: roleName,
|
|
3281
4508
|
model: chain[0] ?? "",
|
|
3282
4509
|
fallbacks: chain.slice(1),
|
|
3283
|
-
onExhausted: (m, reason) =>
|
|
4510
|
+
onExhausted: (m, reason) => {
|
|
4511
|
+
const why = reason ?? "unavailable";
|
|
4512
|
+
const source = providerOutage(why) ?? (isSourceCapacity(why) ? sourcePrefix(m) : void 0);
|
|
4513
|
+
if (source) this.markProviderExhausted(source, m, why);
|
|
4514
|
+
else this.markExhausted(m, why);
|
|
4515
|
+
},
|
|
3284
4516
|
onStructuralFailure: (m, reason) => this.markStructuralFailure(m, reason, roleName),
|
|
3285
4517
|
onFallback: notify ? (from, to, reason) => notify(`\u2935 \`${from}\` \u2192 \`${to}\` \u2014 ${reason}`) : void 0
|
|
3286
4518
|
};
|
|
@@ -3314,14 +4546,14 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3314
4546
|
function asChoice(o) {
|
|
3315
4547
|
return typeof o === "string" ? { label: o } : o;
|
|
3316
4548
|
}
|
|
3317
|
-
var AssessmentSchema =
|
|
3318
|
-
findings:
|
|
3319
|
-
severity:
|
|
4549
|
+
var AssessmentSchema = z17.object({
|
|
4550
|
+
findings: z17.array(z17.object({
|
|
4551
|
+
severity: z17.enum(["critical", "medium", "low"]).describe(
|
|
3320
4552
|
"`critical`: shipping it this way causes real harm \u2014 wrong behaviour, data loss, a security hole. `medium`: it should be fixed but nothing breaks if it ships. `low`: a preference or a tidy-up."
|
|
3321
4553
|
),
|
|
3322
|
-
note:
|
|
4554
|
+
note: z17.string()
|
|
3323
4555
|
})).default([]),
|
|
3324
|
-
recommendation:
|
|
4556
|
+
recommendation: z17.enum(["approve", "revise"]).describe(
|
|
3325
4557
|
"`revise` only if at least one finding must be addressed before this can ship; otherwise `approve` and leave the findings as notes. Findings you would not block on do not make it a revise."
|
|
3326
4558
|
)
|
|
3327
4559
|
});
|
|
@@ -3362,18 +4594,18 @@ function coverage(assessments) {
|
|
|
3362
4594
|
const verified = assessments.length - unverified;
|
|
3363
4595
|
return { verified, unverified, enough: !assessments.length || verified / assessments.length >= TEAM_MIN_COVERAGE };
|
|
3364
4596
|
}
|
|
3365
|
-
var CouncilVoteSchema =
|
|
3366
|
-
vote:
|
|
4597
|
+
var CouncilVoteSchema = z17.object({
|
|
4598
|
+
vote: z17.enum(["pass", "revise"]).describe(
|
|
3367
4599
|
"`revise` only if something must change before this can ship. A concern you would not block on is a `pass` with the concern in the rationale."
|
|
3368
4600
|
),
|
|
3369
|
-
rationale:
|
|
4601
|
+
rationale: z17.string()
|
|
3370
4602
|
});
|
|
3371
|
-
var JudgeSchema =
|
|
3372
|
-
decision:
|
|
4603
|
+
var JudgeSchema = z17.object({
|
|
4604
|
+
decision: z17.enum(["pass", "revise", "ask-human"]).describe(
|
|
3373
4605
|
"`pass`: it can ship. `revise`: it can be fixed from the feedback below, without anyone being asked. `ask-human` ONLY when the decision is genuinely not yours \u2014 the reviewers disagree on something a person owns, or the answer depends on intent nobody wrote down. It stops the run and costs someone their attention; do not use it for a call you can make."
|
|
3374
4606
|
),
|
|
3375
|
-
feedback:
|
|
3376
|
-
question:
|
|
4607
|
+
feedback: z17.array(z17.string()),
|
|
4608
|
+
question: z17.string()
|
|
3377
4609
|
});
|
|
3378
4610
|
var STAGE_FRAMING = {
|
|
3379
4611
|
spec: `You are reviewing a SPECIFICATION: it states WHAT the product must do and WHY, written for business stakeholders. By design it MUST NOT contain implementation detail (languages, frameworks, APIs, storage mechanics, code structure) \u2014 those decisions belong to the LATER plan stage.
|
|
@@ -3385,7 +4617,7 @@ OUT OF SCOPE here: re-litigating the approved spec or plan, and demanding refact
|
|
|
3385
4617
|
SEVERITY: "critical" = breaks correctness, security or data integrity, or the task's requirement is not actually implemented. "medium" = a real defect or risk worth fixing now. "low" = style/polish.`
|
|
3386
4618
|
};
|
|
3387
4619
|
var REVIEW_MAX_TURNS = 15;
|
|
3388
|
-
var REVIEW_TIMEOUT_MS =
|
|
4620
|
+
var REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3389
4621
|
function isDeadlineFailure(e) {
|
|
3390
4622
|
return /within its deadline|timed? ?out/i.test(e instanceof Error ? e.message : String(e));
|
|
3391
4623
|
}
|
|
@@ -3692,6 +4924,17 @@ ${ask}` };
|
|
|
3692
4924
|
return d;
|
|
3693
4925
|
}
|
|
3694
4926
|
var TEAM_CONSENSUS = 0.7;
|
|
4927
|
+
var CORROBORATION_FLOOR = 5;
|
|
4928
|
+
function corroboratedCriticals(assessments) {
|
|
4929
|
+
const criticals = assessments.flatMap((a) => a.findings.filter((f) => f.severity === "critical").map((f) => ({ lens: a.name, note: f.note })));
|
|
4930
|
+
if (assessments.length < CORROBORATION_FLOOR) return criticals.length;
|
|
4931
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
4932
|
+
for (const c of criticals) {
|
|
4933
|
+
const key2 = subjectOf(c.note) ?? `${c.lens}:${c.note.slice(0, 40)}`;
|
|
4934
|
+
(bySubject.get(key2) ?? bySubject.set(key2, /* @__PURE__ */ new Set()).get(key2)).add(c.lens);
|
|
4935
|
+
}
|
|
4936
|
+
return [...bySubject.values()].filter((lenses) => lenses.size > 1).length;
|
|
4937
|
+
}
|
|
3695
4938
|
var REVIEW_SIGNATURES_LOGGED = 8;
|
|
3696
4939
|
var SIGNATURE_CHARS = 120;
|
|
3697
4940
|
var COUNCIL_SUPERMAJORITY = 0.8;
|
|
@@ -3870,10 +5113,20 @@ async function changeUnderReview(deps, workdir) {
|
|
|
3870
5113
|
return workingTreeDiff(workdir);
|
|
3871
5114
|
}
|
|
3872
5115
|
async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
3873
|
-
}, attempt = 0) {
|
|
5116
|
+
}, attempt = 0, cleared = []) {
|
|
3874
5117
|
const diff = await changeUnderReview(deps, workdir);
|
|
3875
|
-
const
|
|
5118
|
+
const scoped = lensesFor(deps.teams.code, diff);
|
|
5119
|
+
const remaining = cleared.length ? scoped.filter((c) => !cleared.includes(c.name)) : scoped;
|
|
5120
|
+
const named = scoped.filter((c) => CORE_CODE_LENSES.includes(c.name));
|
|
5121
|
+
const core = named.length ? named : scoped.slice(0, CORE_CODE_LENSES.length);
|
|
5122
|
+
const team = remaining.length ? remaining : core;
|
|
3876
5123
|
const scaled = team.length < deps.teams.code.length;
|
|
5124
|
+
if (!remaining.length) {
|
|
5125
|
+
emit({ kind: "note", text: `\u{1F465} **Team** \u2014 every lens had approved; re-running the ${team.length} core lens(es) on the new code.` });
|
|
5126
|
+
}
|
|
5127
|
+
if (cleared.length) {
|
|
5128
|
+
emit({ kind: "note", text: `\u{1F465} **Team** \u2014 ${team.length} lens(es) to re-run; ${cleared.length} approved on an earlier attempt.` });
|
|
5129
|
+
}
|
|
3877
5130
|
telemetry().event("decision.review_scale", {
|
|
3878
5131
|
"hc.decision": "review_scale",
|
|
3879
5132
|
"hc.changed_lines": changedLines(diff),
|
|
@@ -3887,22 +5140,30 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
|
3887
5140
|
const approve = assessments.filter((a) => a.recommendation === "approve").length;
|
|
3888
5141
|
const crit = severityTotal(assessments, "critical");
|
|
3889
5142
|
const med = severityTotal(assessments, "medium");
|
|
5143
|
+
const approvedLenses = [
|
|
5144
|
+
...cleared,
|
|
5145
|
+
...assessments.filter((a) => !a.unverified && !a.findings.some((f) => f.severity === "critical")).map((a) => a.name)
|
|
5146
|
+
];
|
|
3890
5147
|
const cover = coverage(assessments);
|
|
3891
5148
|
if (!cover.enough) {
|
|
3892
5149
|
const why = `${cover.unverified} of ${assessments.length} lens(es) never returned a verdict \u2014 too little of the review ran to judge this change. Re-run it; if it keeps happening the lens's model chain is the fault.`;
|
|
3893
5150
|
emit({ kind: "note", text: `\u26A0\uFE0F **Code review** \u2014 ${why}` });
|
|
3894
5151
|
return { verdict: "fail", notes: [why] };
|
|
3895
5152
|
}
|
|
3896
|
-
|
|
3897
|
-
|
|
5153
|
+
const corroborated = corroboratedCriticals(assessments);
|
|
5154
|
+
if (crit > 0 && corroborated === 0) {
|
|
5155
|
+
emit({ kind: "note", text: `\u2705 **Team** \u2014 ${crit} critical finding(s), none corroborated by a second lens \u2192 deferred to the revision pass.` });
|
|
5156
|
+
}
|
|
5157
|
+
if (corroborated === 0) {
|
|
5158
|
+
const deferred = [...findingNotes(assessments, "code", ["critical"]), ...nonBlockingNotes(assessments, "code")];
|
|
3898
5159
|
if (!deferred.length) {
|
|
3899
5160
|
if (attempt > 0 || !assessments.length || approve / assessments.length >= TEAM_CONSENSUS) {
|
|
3900
5161
|
emit({ kind: "note", text: `\u2705 **Team** \u2014 nothing to fix \u2192 the code passed.` });
|
|
3901
|
-
return { verdict: "pass", notes: [] };
|
|
5162
|
+
return { verdict: "pass", notes: [], approvedLenses };
|
|
3902
5163
|
}
|
|
3903
5164
|
}
|
|
3904
|
-
emit({ kind: "note", text: `\u2705 **Team** \u2014
|
|
3905
|
-
return { verdict: "pass", notes: [], deferred };
|
|
5165
|
+
emit({ kind: "note", text: `\u2705 **Team** \u2014 nothing corroborated; ${deferred.length} note(s) deferred to the revision pass.` });
|
|
5166
|
+
return { verdict: "pass", notes: [], deferred, approvedLenses };
|
|
3906
5167
|
}
|
|
3907
5168
|
const reason = crit || med ? `surfaced ${crit} critical / ${med} medium finding(s)` : `is split (${approve}/${assessments.length} approve)`;
|
|
3908
5169
|
emit({ kind: "note", text: `\u{1F465} **Team** ${reason} \u2192 handed the decision to the **council** (${deps.council.length} members vote).` });
|
|
@@ -3912,25 +5173,122 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
|
3912
5173
|
const blocking = assessments.flatMap((a) => a.findings.filter((f) => f.severity !== "low").map((f) => `[${f.severity}] ${a.name}: ${f.note}`));
|
|
3913
5174
|
if (tally === "pass") {
|
|
3914
5175
|
emit({ kind: "note", text: `\u2705 **Council** voted to approve (${passVotes}/${votes.length} pass) \u2192 the code passed.` });
|
|
3915
|
-
return { verdict: "pass", notes: [] };
|
|
5176
|
+
return { verdict: "pass", notes: [], approvedLenses };
|
|
3916
5177
|
}
|
|
3917
5178
|
if (tally === "revise") {
|
|
3918
5179
|
emit({ kind: "note", text: `\u{1F504} **Council** voted to revise (${votes.length - passVotes}/${votes.length}) \u2192 sending the code back.` });
|
|
3919
|
-
return {
|
|
5180
|
+
return {
|
|
5181
|
+
verdict: "fail",
|
|
5182
|
+
approvedLenses,
|
|
5183
|
+
notes: blocking.length ? blocking : votes.filter((v) => v.vote === "revise").map((v) => v.rationale)
|
|
5184
|
+
};
|
|
3920
5185
|
}
|
|
3921
5186
|
emit({ kind: "note", text: `\u{1F528} **Council** was split (${passVotes}/${votes.length} pass) \u2192 deferred the final decision to the **judge**.` });
|
|
3922
5187
|
const d = await runJudge(deps, "code", workdir, taskTitle, assessments, votes, request, emit);
|
|
3923
|
-
if (d.decision === "pass") return { verdict: "pass", notes: [] };
|
|
3924
|
-
return { verdict: "fail", notes: d.feedback.length ? d.feedback : blocking };
|
|
5188
|
+
if (d.decision === "pass") return { verdict: "pass", notes: [], approvedLenses };
|
|
5189
|
+
return { verdict: "fail", approvedLenses, notes: d.feedback.length ? d.feedback : blocking };
|
|
3925
5190
|
}
|
|
3926
5191
|
|
|
3927
5192
|
// src/engine/acceptance.ts
|
|
3928
|
-
import { z as
|
|
5193
|
+
import { z as z18 } from "zod";
|
|
5194
|
+
|
|
5195
|
+
// src/engine/criterion-commands.ts
|
|
5196
|
+
import { spawn as spawn3 } from "child_process";
|
|
5197
|
+
var RUNNABLE_COMMANDS = [
|
|
5198
|
+
"dotnet",
|
|
5199
|
+
"npm",
|
|
5200
|
+
"npx",
|
|
5201
|
+
"pnpm",
|
|
5202
|
+
"yarn",
|
|
5203
|
+
"nx",
|
|
5204
|
+
"prettier",
|
|
5205
|
+
"eslint",
|
|
5206
|
+
"tsc",
|
|
5207
|
+
"cargo",
|
|
5208
|
+
"go",
|
|
5209
|
+
"make",
|
|
5210
|
+
"mvn",
|
|
5211
|
+
"gradle"
|
|
5212
|
+
];
|
|
5213
|
+
var SHELL_SYNTAX = /[;&|><$(){}\n]|`/;
|
|
5214
|
+
var CRITERION_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5215
|
+
var MAX_OUTPUT = 2e3;
|
|
5216
|
+
function commandsIn(criterion) {
|
|
5217
|
+
const out = [];
|
|
5218
|
+
for (const m of criterion.matchAll(/`([^`\n]+)`/g)) {
|
|
5219
|
+
const span = m[1].trim();
|
|
5220
|
+
if (!span || SHELL_SYNTAX.test(span)) continue;
|
|
5221
|
+
const argv = span.split(/\s+/);
|
|
5222
|
+
if (!RUNNABLE_COMMANDS.includes(argv[0])) continue;
|
|
5223
|
+
out.push(argv);
|
|
5224
|
+
}
|
|
5225
|
+
return out;
|
|
5226
|
+
}
|
|
5227
|
+
async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
|
|
5228
|
+
const [bin, ...args] = argv;
|
|
5229
|
+
return new Promise((resolve6) => {
|
|
5230
|
+
let child;
|
|
5231
|
+
try {
|
|
5232
|
+
child = spawn3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
|
|
5233
|
+
} catch (e) {
|
|
5234
|
+
resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e instanceof Error ? e.message : String(e) });
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
let out = "";
|
|
5238
|
+
const take = (d) => {
|
|
5239
|
+
out += d.toString();
|
|
5240
|
+
if (out.length > MAX_OUTPUT * 4) out = out.slice(-MAX_OUTPUT * 2);
|
|
5241
|
+
};
|
|
5242
|
+
child.stdout.on("data", take);
|
|
5243
|
+
child.stderr.on("data", take);
|
|
5244
|
+
let timedOut = false;
|
|
5245
|
+
const timer = setTimeout(() => {
|
|
5246
|
+
timedOut = true;
|
|
5247
|
+
child.kill("SIGKILL");
|
|
5248
|
+
}, timeoutMs);
|
|
5249
|
+
child.on("error", (e) => {
|
|
5250
|
+
clearTimeout(timer);
|
|
5251
|
+
resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
|
|
5252
|
+
});
|
|
5253
|
+
child.on("close", (code) => {
|
|
5254
|
+
clearTimeout(timer);
|
|
5255
|
+
resolve6({ argv, passed: !timedOut && code === 0, exitCode: code, timedOut, output: out.slice(-MAX_OUTPUT) });
|
|
5256
|
+
});
|
|
5257
|
+
});
|
|
5258
|
+
}
|
|
5259
|
+
async function runCriterionCommands(cwd, criteria, timeoutMs = CRITERION_TIMEOUT_MS) {
|
|
5260
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5261
|
+
const argvs = [];
|
|
5262
|
+
for (const c of criteria) {
|
|
5263
|
+
for (const argv of commandsIn(c)) {
|
|
5264
|
+
const key2 = argv.join(" ");
|
|
5265
|
+
if (seen.has(key2)) continue;
|
|
5266
|
+
seen.add(key2);
|
|
5267
|
+
argvs.push(argv);
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
const runs = [];
|
|
5271
|
+
for (const argv of argvs) runs.push(await runCommand(cwd, argv, timeoutMs));
|
|
5272
|
+
return runs;
|
|
5273
|
+
}
|
|
5274
|
+
function describeCommandRuns(runs) {
|
|
5275
|
+
if (!runs.length) return "";
|
|
5276
|
+
const lines = runs.map((r) => {
|
|
5277
|
+
const cmd = `\`${r.argv.join(" ")}\``;
|
|
5278
|
+
if (r.timedOut) return `- ${cmd} \u2014 TIMED OUT after ${Math.round(CRITERION_TIMEOUT_MS / 6e4)} minutes.`;
|
|
5279
|
+
if (r.passed) return `- ${cmd} \u2014 SUCCEEDED (exit 0).`;
|
|
5280
|
+
return `- ${cmd} \u2014 FAILED (exit ${r.exitCode ?? "none"}):
|
|
5281
|
+
\`\`\`
|
|
5282
|
+
${r.output.slice(-800)}
|
|
5283
|
+
\`\`\``;
|
|
5284
|
+
});
|
|
5285
|
+
return "The harness has already RUN the commands these criteria name, to completion, and this is what happened. Use this as the evidence for any criterion about one of them \u2014 do not run it again, and do not report a criterion unmet because you could not wait for it:\n" + lines.join("\n");
|
|
5286
|
+
}
|
|
3929
5287
|
|
|
3930
5288
|
// src/engine/test-runner.ts
|
|
3931
5289
|
import { readFile as readFile3 } from "fs/promises";
|
|
3932
5290
|
import { existsSync as existsSync9 } from "fs";
|
|
3933
|
-
import { spawn as
|
|
5291
|
+
import { spawn as spawn4 } from "child_process";
|
|
3934
5292
|
import { join as join9 } from "path";
|
|
3935
5293
|
var TEST_TIMEOUT_MS = 6e5;
|
|
3936
5294
|
var MAX_TEST_OUTPUT = 12e3;
|
|
@@ -3962,7 +5320,7 @@ async function runProjectTests(cwd, cmd) {
|
|
|
3962
5320
|
if (!command) return { skipped: true, passed: true, output: "", timedOut: false };
|
|
3963
5321
|
const [bin, ...args] = command.argv;
|
|
3964
5322
|
return new Promise((resolve6) => {
|
|
3965
|
-
const child =
|
|
5323
|
+
const child = spawn4(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
|
|
3966
5324
|
let out = "";
|
|
3967
5325
|
const take = (d) => {
|
|
3968
5326
|
out += d.toString();
|
|
@@ -4016,20 +5374,28 @@ ${run.output}`;
|
|
|
4016
5374
|
}
|
|
4017
5375
|
|
|
4018
5376
|
// src/engine/acceptance.ts
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
5377
|
+
function normalizeCriterion(s) {
|
|
5378
|
+
return s.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().replace(/[.,;:!?…]+$/, "");
|
|
5379
|
+
}
|
|
5380
|
+
var AcceptanceSchema = z18.object({
|
|
5381
|
+
checks: z18.array(z18.object({
|
|
5382
|
+
criterion: z18.string().describe(
|
|
5383
|
+
"Copy the criterion VERBATIM from the numbered list you were given, including any backticks and punctuation. Do not paraphrase, renumber or reformat it \u2014 it is matched back to the task by text."
|
|
5384
|
+
),
|
|
5385
|
+
met: z18.boolean(),
|
|
5386
|
+
evidence: z18.string().describe(
|
|
4024
5387
|
'Where you SAW it: a file path and what it contains, a symbol, a test name. "It looks fine" is not evidence.'
|
|
4025
5388
|
)
|
|
4026
5389
|
}))
|
|
4027
5390
|
});
|
|
4028
|
-
var
|
|
5391
|
+
var PROMPT2 = `You are the acceptance gate for one implementation task. You are given the task's acceptance criteria and the worktree. For EACH criterion decide whether it is ACTUALLY satisfied by the code that is present, and cite the concrete evidence you saw (file path, symbol, test name, config key).
|
|
4029
5392
|
Rules:
|
|
4030
5393
|
- Verify by LOOKING. If you did not open the file, the criterion is not met.
|
|
5394
|
+
- A criterion about a COMMAND is settled by the run reported to you above, not by looking and not by running it yourself. If that report says the command succeeded, the criterion is met; if it failed, quote the error. Never mark such a criterion unmet for want of waiting.
|
|
4031
5395
|
- "met" means observably true right now \u2014 not "planned", "close enough" or "the implementer says so".
|
|
4032
5396
|
- Judge ONLY the listed criteria. Code quality, style and scope opinions belong to the code review, not here.
|
|
5397
|
+
- Report EVERY criterion you were given \u2014 one check each, none dropped.
|
|
5398
|
+
- Copy each criterion into the "criterion" field VERBATIM from the numbered list, including any backticks and punctuation. It is matched back to the task by text; a paraphrase loses the pairing.
|
|
4033
5399
|
- Write the evidence in ENGLISH (it is a technical record).`;
|
|
4034
5400
|
async function verifyAcceptance(deps, card, cwd, emit = () => {
|
|
4035
5401
|
}) {
|
|
@@ -4059,6 +5425,14 @@ ${tests.output.slice(-4e3)}`,
|
|
|
4059
5425
|
if (!card.acceptance.length) return { passed: true, unmet: [], tests: testEvidence };
|
|
4060
5426
|
const diff = deps.baseRef ? await taskDiff(cwd, deps.baseRef) : deps.inPlaceBase ? await diffSince(cwd, deps.inPlaceBase) : "";
|
|
4061
5427
|
const resolved = deps.roleRegistry.resolve("code-reviewer");
|
|
5428
|
+
const commandRuns = await telemetry().span(
|
|
5429
|
+
"stage.criterion_commands",
|
|
5430
|
+
{ "hc.stage": "criterion commands" },
|
|
5431
|
+
() => runCriterionCommands(cwd, card.acceptance)
|
|
5432
|
+
);
|
|
5433
|
+
for (const r of commandRuns) {
|
|
5434
|
+
emit({ kind: "note", text: r.passed ? `\u2705 \`${r.argv.join(" ")}\` \u2014 exit 0` : `\u274C \`${r.argv.join(" ")}\` \u2014 ${r.timedOut ? "timed out" : `exit ${r.exitCode ?? "none"}`}` });
|
|
5435
|
+
}
|
|
4062
5436
|
const hints = memoryHints(deps, `${card.title} ${card.acceptance.join(" ")}`, { role: "code-reviewer" });
|
|
4063
5437
|
const ask = { role: "user", content: `Task: "${card.title}".
|
|
4064
5438
|
|
|
@@ -4069,11 +5443,13 @@ Check each one against the worktree and report met/unmet with the evidence you s
|
|
|
4069
5443
|
|
|
4070
5444
|
${describeTestRun(tests)}
|
|
4071
5445
|
|
|
5446
|
+
${describeCommandRuns(commandRuns)}
|
|
5447
|
+
|
|
4072
5448
|
${describeDiff(diff)}` };
|
|
4073
5449
|
const opts = {
|
|
4074
5450
|
provider: deps.provider,
|
|
4075
5451
|
...resolved,
|
|
4076
|
-
systemPrompt: `${
|
|
5452
|
+
systemPrompt: `${PROMPT2}${deps.roleRegistry.ruleSuffix()}`,
|
|
4077
5453
|
tools: readOnlyRegistry(deps),
|
|
4078
5454
|
messages: hints.message ? [{ role: "user", content: hints.message }, ask] : [ask],
|
|
4079
5455
|
permission: deps.permission,
|
|
@@ -4095,10 +5471,25 @@ ${describeDiff(diff)}` };
|
|
|
4095
5471
|
tests: testEvidence
|
|
4096
5472
|
};
|
|
4097
5473
|
}
|
|
4098
|
-
const byCriterion = new Map(checks.map((c) => [c.criterion
|
|
5474
|
+
const byCriterion = new Map(checks.map((c) => [normalizeCriterion(c.criterion), c]));
|
|
5475
|
+
const paired = /* @__PURE__ */ new Map();
|
|
5476
|
+
const usedChecks = /* @__PURE__ */ new Set();
|
|
5477
|
+
const unpairedCriteria = [];
|
|
5478
|
+
for (const c of card.acceptance) {
|
|
5479
|
+
const key2 = normalizeCriterion(c);
|
|
5480
|
+
const hit = byCriterion.get(key2) ?? checks.find((x) => normalizeCriterion(x.criterion).includes(key2.slice(0, 40)));
|
|
5481
|
+
if (hit) {
|
|
5482
|
+
paired.set(c, hit);
|
|
5483
|
+
usedChecks.add(hit);
|
|
5484
|
+
} else unpairedCriteria.push(c);
|
|
5485
|
+
}
|
|
5486
|
+
const unusedChecks = checks.filter((x) => !usedChecks.has(x));
|
|
5487
|
+
if (unpairedCriteria.length === 1 && unusedChecks.length === 1) {
|
|
5488
|
+
paired.set(unpairedCriteria[0], unusedChecks[0]);
|
|
5489
|
+
}
|
|
4099
5490
|
const unmet = [];
|
|
4100
5491
|
for (const c of card.acceptance) {
|
|
4101
|
-
const hit =
|
|
5492
|
+
const hit = paired.get(c);
|
|
4102
5493
|
if (!hit) unmet.push(`${c} (not reported by the acceptance gate)`);
|
|
4103
5494
|
else if (!hit.met) unmet.push(`${c} \u2014 ${hit.evidence}`);
|
|
4104
5495
|
}
|
|
@@ -4161,9 +5552,18 @@ async function runCycleWithRole(deps, board, taskId, cwd, role, git = defaultGit
|
|
|
4161
5552
|
deps.fitness?.ok(role, deps.roleRegistry.chainFor(role, rotation)[0] ?? "");
|
|
4162
5553
|
board.move(taskId, "REVIEW", role);
|
|
4163
5554
|
const card = board.get(taskId);
|
|
4164
|
-
const review = () => runCodeReview(
|
|
4165
|
-
|
|
4166
|
-
|
|
5555
|
+
const review = () => runCodeReview(
|
|
5556
|
+
rdeps,
|
|
5557
|
+
cwd,
|
|
5558
|
+
card.title,
|
|
5559
|
+
void 0,
|
|
5560
|
+
(ev) => {
|
|
5561
|
+
if (ev.kind === "note") deps.note?.(ev.text);
|
|
5562
|
+
},
|
|
5563
|
+
card.attempts,
|
|
5564
|
+
// Only what objected last time is asked again — see `runCodeReview`'s `cleared`.
|
|
5565
|
+
card.clearedLenses ?? []
|
|
5566
|
+
);
|
|
4167
5567
|
let v;
|
|
4168
5568
|
try {
|
|
4169
5569
|
v = await telemetry().span(
|
|
@@ -4186,6 +5586,8 @@ async function runCycleWithRole(deps, board, taskId, cwd, role, git = defaultGit
|
|
|
4186
5586
|
board.appendStage(taskId, { role: "code-reviewer", action: "reviewed:cancelled", note });
|
|
4187
5587
|
return { verdict: "fail", notes: [note] };
|
|
4188
5588
|
}
|
|
5589
|
+
const approved = v.approvedLenses ?? [];
|
|
5590
|
+
if (approved.length) board.markLensesCleared(taskId, approved);
|
|
4189
5591
|
if (v.verdict === "pass") {
|
|
4190
5592
|
const check = () => verifyAcceptance(rdeps, board.get(taskId), cwd, (ev) => {
|
|
4191
5593
|
if (ev.kind === "note") deps.note?.(ev.text);
|
|
@@ -4229,28 +5631,31 @@ async function runTaskCycle(deps, board, taskId, worktreePath, slot = 0) {
|
|
|
4229
5631
|
}
|
|
4230
5632
|
|
|
4231
5633
|
// src/board/board.ts
|
|
4232
|
-
import { z as
|
|
5634
|
+
import { z as z19 } from "zod";
|
|
4233
5635
|
var MAX_STAGE_EVENTS = 200;
|
|
4234
|
-
var stageEventSchema =
|
|
4235
|
-
role:
|
|
4236
|
-
action:
|
|
4237
|
-
note:
|
|
5636
|
+
var stageEventSchema = z19.object({
|
|
5637
|
+
role: z19.string(),
|
|
5638
|
+
action: z19.string(),
|
|
5639
|
+
note: z19.string().optional()
|
|
4238
5640
|
});
|
|
4239
|
-
var cardSchema =
|
|
4240
|
-
id:
|
|
4241
|
-
title:
|
|
4242
|
-
column:
|
|
4243
|
-
worktree:
|
|
4244
|
-
deps:
|
|
4245
|
-
acceptance:
|
|
5641
|
+
var cardSchema = z19.object({
|
|
5642
|
+
id: z19.string(),
|
|
5643
|
+
title: z19.string(),
|
|
5644
|
+
column: z19.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
|
|
5645
|
+
worktree: z19.string().optional(),
|
|
5646
|
+
deps: z19.array(z19.string()),
|
|
5647
|
+
acceptance: z19.array(z19.string()).default([]),
|
|
4246
5648
|
// default: boards persisted before the gate existed still load
|
|
4247
|
-
files:
|
|
5649
|
+
files: z19.array(z19.string()).default([]),
|
|
4248
5650
|
// ditto — a board written before file lists existed still loads
|
|
4249
|
-
reviewNotes:
|
|
4250
|
-
|
|
4251
|
-
|
|
5651
|
+
reviewNotes: z19.array(z19.string()),
|
|
5652
|
+
// Optional rather than defaulted: a board written before this existed must round-trip unchanged, and an
|
|
5653
|
+
// empty list is the same statement as no list at all.
|
|
5654
|
+
clearedLenses: z19.array(z19.string()).optional(),
|
|
5655
|
+
attempts: z19.number(),
|
|
5656
|
+
stageHistory: z19.array(stageEventSchema)
|
|
4252
5657
|
});
|
|
4253
|
-
var boardDataSchema =
|
|
5658
|
+
var boardDataSchema = z19.object({ version: z19.literal(1), cards: z19.array(cardSchema) });
|
|
4254
5659
|
function migrateDelivered(c) {
|
|
4255
5660
|
if (c.column !== "DONE") return c;
|
|
4256
5661
|
return c.stageHistory.some((e) => e.action === "merged") ? { ...c, column: "MERGED" } : c;
|
|
@@ -4262,6 +5667,7 @@ function cloneCard(c) {
|
|
|
4262
5667
|
acceptance: [...c.acceptance],
|
|
4263
5668
|
files: [...c.files],
|
|
4264
5669
|
reviewNotes: [...c.reviewNotes],
|
|
5670
|
+
...c.clearedLenses?.length ? { clearedLenses: [...c.clearedLenses] } : {},
|
|
4265
5671
|
stageHistory: c.stageHistory.map((e) => ({ ...e }))
|
|
4266
5672
|
};
|
|
4267
5673
|
}
|
|
@@ -4377,6 +5783,12 @@ var Board = class _Board {
|
|
|
4377
5783
|
this.require(id).reviewNotes = [];
|
|
4378
5784
|
this.onChange?.();
|
|
4379
5785
|
}
|
|
5786
|
+
/** Remember which lenses approved, so the next attempt does not ask them again. */
|
|
5787
|
+
markLensesCleared(id, lenses) {
|
|
5788
|
+
const c = this.require(id);
|
|
5789
|
+
c.clearedLenses = [.../* @__PURE__ */ new Set([...c.clearedLenses ?? [], ...lenses])];
|
|
5790
|
+
this.onChange?.();
|
|
5791
|
+
}
|
|
4380
5792
|
/**
|
|
4381
5793
|
* Starts the escalation ladder over for one card, keeping its history.
|
|
4382
5794
|
*
|
|
@@ -4411,19 +5823,17 @@ var Board = class _Board {
|
|
|
4411
5823
|
};
|
|
4412
5824
|
|
|
4413
5825
|
export {
|
|
5826
|
+
checkProfile,
|
|
5827
|
+
runLogin,
|
|
5828
|
+
SHORT_CALL_MS,
|
|
5829
|
+
LONG_CALL_MS,
|
|
5830
|
+
cliCatalog,
|
|
5831
|
+
CliProvider,
|
|
4414
5832
|
describeInherited,
|
|
4415
5833
|
describeTopUp,
|
|
4416
5834
|
toSlug,
|
|
4417
5835
|
mainWorktreeRoot,
|
|
4418
5836
|
WorktreeManager,
|
|
4419
|
-
applySkills,
|
|
4420
|
-
buildSkillTool,
|
|
4421
|
-
RoleRegistry,
|
|
4422
|
-
unfinishedSessions,
|
|
4423
|
-
describeUnfinished,
|
|
4424
|
-
gitTool,
|
|
4425
|
-
buildRememberTool,
|
|
4426
|
-
routeSkills,
|
|
4427
5837
|
REQUIRED_ROLES,
|
|
4428
5838
|
DEFAULT_ROLE_SKILLS,
|
|
4429
5839
|
DEFAULT_PROMPTS,
|
|
@@ -4432,6 +5842,26 @@ export {
|
|
|
4432
5842
|
CODE_TEAM,
|
|
4433
5843
|
DEFAULT_COUNCIL,
|
|
4434
5844
|
placedSkills,
|
|
5845
|
+
ROLE_PROFILES,
|
|
5846
|
+
filterModelsForRole,
|
|
5847
|
+
effortFor,
|
|
5848
|
+
isKnownModel,
|
|
5849
|
+
capabilityScore,
|
|
5850
|
+
mostCapable,
|
|
5851
|
+
modelBand,
|
|
5852
|
+
DURABLE_ROLES,
|
|
5853
|
+
strongestPrimary,
|
|
5854
|
+
newestPrimary,
|
|
5855
|
+
sourceOf,
|
|
5856
|
+
adjustRoleModels,
|
|
5857
|
+
applySkills,
|
|
5858
|
+
buildSkillTool,
|
|
5859
|
+
RoleRegistry,
|
|
5860
|
+
unfinishedSessions,
|
|
5861
|
+
describeUnfinished,
|
|
5862
|
+
gitTool,
|
|
5863
|
+
buildRememberTool,
|
|
5864
|
+
routeSkills,
|
|
4435
5865
|
specsDir,
|
|
4436
5866
|
constitutionPath,
|
|
4437
5867
|
verifyPaths,
|
|
@@ -4442,6 +5872,7 @@ export {
|
|
|
4442
5872
|
readOnlyRegistry,
|
|
4443
5873
|
deferMcp,
|
|
4444
5874
|
runReviewer,
|
|
5875
|
+
subjectOf,
|
|
4445
5876
|
asChoice,
|
|
4446
5877
|
buildTeamRegistry,
|
|
4447
5878
|
buildCouncilRegistry,
|
|
@@ -4453,8 +5884,11 @@ export {
|
|
|
4453
5884
|
createDefaultRegistry,
|
|
4454
5885
|
changedByMerge,
|
|
4455
5886
|
refreshAfterChange,
|
|
4456
|
-
LONG_CALL_MS,
|
|
4457
5887
|
routeTask,
|
|
5888
|
+
normalizeQuestion,
|
|
5889
|
+
extractChoicesFrom,
|
|
5890
|
+
buildAskUserTool,
|
|
5891
|
+
writerRegistry,
|
|
4458
5892
|
commitFile,
|
|
4459
5893
|
commitStep,
|
|
4460
5894
|
squashTask,
|