@hizliemre/horse-code 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-LAJN3TWC.js → app-5FXHE7GX.js} +354 -350
- 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-VPAWRRHL.js → chunk-AE36LLL2.js} +31 -21
- package/dist/{chunk-4EWK7HWQ.js → chunk-EAF22QIG.js} +32 -0
- package/dist/chunk-G45RWL7S.js +289 -0
- package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
- package/dist/{chunk-DRZSUQ7Q.js → chunk-LLL7QWXB.js} +18 -7
- package/dist/{chunk-WYQBRCKY.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-RPVAIS3P.js → chunk-UEWVVN5L.js} +1511 -305
- package/dist/{chunk-EQX7BQYN.js → chunk-UGESK765.js} +1 -1
- package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
- package/dist/{chunk-K2VERI5Q.js → chunk-XYZVZPAY.js} +365 -666
- package/dist/{chunk-KOWMHL23.js → chunk-YPZP7LYL.js} +2 -2
- package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
- package/dist/cli.js +675 -95
- package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
- package/dist/fix-ONLA45HD.js +34 -0
- package/dist/git-QZTDZSJY.js +8 -0
- package/dist/{ongoing-XP6WXNI7.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-ZW5GY6KV.js} +2 -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-ES5OHZOS.js → triage-FCYHD2AQ.js} +8 -9
- package/dist/{verify-3R7DGUSI.js → verify-LC57A6H2.js} +19 -19
- package/package.json +1 -1
- package/dist/chunk-3UYA3KUG.js +0 -212
- package/dist/chunk-B67BK5GQ.js +0 -34
- package/dist/chunk-O74BDQKS.js +0 -28
- package/dist/fix-CMARU6JR.js +0 -34
- package/dist/git-VTSZALSR.js +0 -6
|
@@ -3,14 +3,20 @@ import {
|
|
|
3
3
|
checkpointMtime,
|
|
4
4
|
isContinuePrompt,
|
|
5
5
|
readCheckpoint
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZSQ24YDJ.js";
|
|
7
|
+
import {
|
|
8
|
+
CLI_KINDS,
|
|
9
|
+
SYNTHETIC,
|
|
10
|
+
runCliAgent
|
|
11
|
+
} from "./chunk-G45RWL7S.js";
|
|
7
12
|
import {
|
|
8
13
|
planFor,
|
|
9
14
|
runTraces
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-MRZVA5JB.js";
|
|
11
16
|
import {
|
|
12
|
-
defaultGitRunner
|
|
13
|
-
|
|
17
|
+
defaultGitRunner,
|
|
18
|
+
gitVerb
|
|
19
|
+
} from "./chunk-LPQU436C.js";
|
|
14
20
|
import {
|
|
15
21
|
BATCH_TOOLS_NOTE,
|
|
16
22
|
contextTools,
|
|
@@ -22,7 +28,7 @@ import {
|
|
|
22
28
|
readFileTool,
|
|
23
29
|
reinforceTouched,
|
|
24
30
|
reinforceUsed
|
|
25
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-LLL7QWXB.js";
|
|
26
32
|
import {
|
|
27
33
|
ToolRegistry,
|
|
28
34
|
handedOver,
|
|
@@ -30,7 +36,7 @@ import {
|
|
|
30
36
|
runToCompletion,
|
|
31
37
|
telemetry,
|
|
32
38
|
truncateSafe
|
|
33
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-AE36LLL2.js";
|
|
34
40
|
import {
|
|
35
41
|
loadTraceIndex,
|
|
36
42
|
pruneTraces,
|
|
@@ -38,15 +44,234 @@ import {
|
|
|
38
44
|
sharedDerived,
|
|
39
45
|
traceRootRel,
|
|
40
46
|
traceable
|
|
41
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-KAGKX2YT.js";
|
|
42
48
|
import {
|
|
43
49
|
buildProjectGraph,
|
|
44
50
|
loadGraphSync,
|
|
45
51
|
pruneAreaNames
|
|
46
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-XEGQT5EN.js";
|
|
47
53
|
import {
|
|
48
54
|
writableStateRoot
|
|
49
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-6W4UH2BQ.js";
|
|
56
|
+
|
|
57
|
+
// src/agents/cli-models.ts
|
|
58
|
+
var CLAUDE_MODELS = ["fable", "opus", "sonnet", "haiku"];
|
|
59
|
+
var CODEX_MODELS = ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-5.6-luna"];
|
|
60
|
+
var CODEX_DEFAULT = "gpt-5.6-terra";
|
|
61
|
+
var GROK_MODELS = ["grok-4.6", "grok-4.5"];
|
|
62
|
+
function grokEffort(effort) {
|
|
63
|
+
const e = effort.toLowerCase();
|
|
64
|
+
if (e === "xhigh" || e === "high" || e === "medium" || e === "low") return e;
|
|
65
|
+
if (e === "max" || e === "ultra") return "xhigh";
|
|
66
|
+
if (e === "minimal") return "low";
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
var ZAI_MODELS = ["glm-5.3", "glm-5.3-flash"];
|
|
70
|
+
function modelsFor(kind) {
|
|
71
|
+
if (kind === "claude") return CLAUDE_MODELS;
|
|
72
|
+
if (kind === "codex") return CODEX_MODELS;
|
|
73
|
+
if (kind === "grok") return GROK_MODELS;
|
|
74
|
+
return ZAI_MODELS;
|
|
75
|
+
}
|
|
76
|
+
function cliCatalog() {
|
|
77
|
+
return CLI_KINDS.flatMap((k) => [...modelsFor(k)]);
|
|
78
|
+
}
|
|
79
|
+
function cliFor(model) {
|
|
80
|
+
const m = model.toLowerCase().replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
|
|
81
|
+
if (/^(fable|opus|sonnet|haiku)\b/.test(m) || m.startsWith("claude")) return "claude";
|
|
82
|
+
if (/^(codex|gpt|o[0-9])\b/.test(m)) return "codex";
|
|
83
|
+
if (/^grok(-|$)/.test(m)) return "grok";
|
|
84
|
+
if (/^glm(-|$)/.test(m)) return "zai";
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
function cliInvocation(model) {
|
|
88
|
+
const bare = model.replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
|
|
89
|
+
const effort = /-(ultra|max|xhigh|high|medium|low|minimal)$/.exec(bare)?.[1];
|
|
90
|
+
const name = effort ? bare.slice(0, -(effort.length + 1)) : bare;
|
|
91
|
+
const resolved = name === "codex" ? CODEX_DEFAULT : name;
|
|
92
|
+
return {
|
|
93
|
+
...resolved ? { model: resolved } : {},
|
|
94
|
+
...effort ? { effort } : {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/agent/deadline.ts
|
|
99
|
+
function withDeadline(work, signal, message) {
|
|
100
|
+
work.catch(() => {
|
|
101
|
+
});
|
|
102
|
+
return Promise.race([work, expired(signal, message)]);
|
|
103
|
+
}
|
|
104
|
+
function expired(signal, message) {
|
|
105
|
+
return new Promise((_, reject) => {
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
reject(new Error(message));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
signal.addEventListener("abort", () => reject(new Error(message)), { once: true });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
var SHORT_CALL_MS = 5 * 60 * 1e3;
|
|
114
|
+
var LONG_CALL_MS = 15 * 60 * 1e3;
|
|
115
|
+
function isCallerAbort(signal) {
|
|
116
|
+
return signal.aborted && signal.reason?.name !== "TimeoutError";
|
|
117
|
+
}
|
|
118
|
+
function isDeadline(signal) {
|
|
119
|
+
return signal.aborted && signal.reason?.name === "TimeoutError";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/agents/cli-provider.ts
|
|
123
|
+
function promptFor(req) {
|
|
124
|
+
const parts = [];
|
|
125
|
+
for (const m of req.messages) {
|
|
126
|
+
if (m.role === "system") {
|
|
127
|
+
parts.push(m.content);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!m.content.trim()) continue;
|
|
131
|
+
parts.push(m.role === "assistant" ? `[your previous reply]
|
|
132
|
+
${m.content}` : m.content);
|
|
133
|
+
}
|
|
134
|
+
const submit = req.tools?.find((t) => t.name === "submit");
|
|
135
|
+
if (submit) {
|
|
136
|
+
parts.push(
|
|
137
|
+
"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)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return parts.join("\n\n");
|
|
141
|
+
}
|
|
142
|
+
function isLoggedOut(text) {
|
|
143
|
+
return /not logged in|not signed in|please run \/login/i.test(text);
|
|
144
|
+
}
|
|
145
|
+
async function* streamWhileRunning(start) {
|
|
146
|
+
const queue = [];
|
|
147
|
+
let finished = false;
|
|
148
|
+
let wake = () => {
|
|
149
|
+
};
|
|
150
|
+
let waiter = new Promise((r) => {
|
|
151
|
+
wake = r;
|
|
152
|
+
});
|
|
153
|
+
const bump = () => {
|
|
154
|
+
const w = wake;
|
|
155
|
+
waiter = new Promise((r) => {
|
|
156
|
+
wake = r;
|
|
157
|
+
});
|
|
158
|
+
w();
|
|
159
|
+
};
|
|
160
|
+
const done = start((ev) => {
|
|
161
|
+
queue.push(ev);
|
|
162
|
+
bump();
|
|
163
|
+
});
|
|
164
|
+
let failure;
|
|
165
|
+
void done.then(() => {
|
|
166
|
+
finished = true;
|
|
167
|
+
bump();
|
|
168
|
+
}, (e) => {
|
|
169
|
+
failure = e;
|
|
170
|
+
finished = true;
|
|
171
|
+
bump();
|
|
172
|
+
});
|
|
173
|
+
for (; ; ) {
|
|
174
|
+
while (queue.length) yield queue.shift();
|
|
175
|
+
if (finished) break;
|
|
176
|
+
await waiter;
|
|
177
|
+
}
|
|
178
|
+
if (failure) throw failure;
|
|
179
|
+
}
|
|
180
|
+
var CliProvider = class {
|
|
181
|
+
fixed;
|
|
182
|
+
readOnly;
|
|
183
|
+
cwd;
|
|
184
|
+
accounts;
|
|
185
|
+
constructor(opts = {}) {
|
|
186
|
+
this.fixed = opts.kind;
|
|
187
|
+
this.readOnly = opts.readOnly ?? true;
|
|
188
|
+
this.cwd = opts.cwd;
|
|
189
|
+
this.accounts = opts.accounts;
|
|
190
|
+
}
|
|
191
|
+
async *chat(req, signal) {
|
|
192
|
+
const kind = this.fixed ?? cliFor(req.model);
|
|
193
|
+
if (!kind) {
|
|
194
|
+
yield { type: "error", message: `no CLI serves ${req.model} \u2014 it is not available in this catalog`, retryable: true };
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const args = [];
|
|
198
|
+
const { model, effort: named } = cliInvocation(req.model);
|
|
199
|
+
if (model) args.push("--model", model);
|
|
200
|
+
const effort = req.effort ?? named;
|
|
201
|
+
if (effort && (kind === "claude" || kind === "zai")) args.push("--effort", effort);
|
|
202
|
+
if (effort && kind === "grok") {
|
|
203
|
+
const level = grokEffort(effort);
|
|
204
|
+
if (level) args.push("--reasoning-effort", level);
|
|
205
|
+
}
|
|
206
|
+
if (this.readOnly && (kind === "claude" || kind === "zai")) args.push("--disallowed-tools", "Write", "Edit", "NotebookEdit");
|
|
207
|
+
if (this.readOnly && kind === "codex") args.push("--sandbox", "read-only");
|
|
208
|
+
if (this.readOnly && kind === "grok") args.push("--disallowed-tools", "write,search_replace");
|
|
209
|
+
if (!this.readOnly && (kind === "claude" || kind === "zai")) args.push("--permission-mode", "acceptEdits");
|
|
210
|
+
if (!this.readOnly && kind === "codex") args.push("--sandbox", "workspace-write");
|
|
211
|
+
if (!this.readOnly && kind === "grok") args.push("--permission-mode", "acceptEdits");
|
|
212
|
+
const account = this.accounts?.pick(kind);
|
|
213
|
+
let res;
|
|
214
|
+
yield* streamWhileRunning((push) => runCliAgent({
|
|
215
|
+
kind,
|
|
216
|
+
cwd: this.cwd ?? process.cwd(),
|
|
217
|
+
prompt: promptFor(req),
|
|
218
|
+
signal,
|
|
219
|
+
args,
|
|
220
|
+
...account ? { configDir: account.configDir } : {},
|
|
221
|
+
onEvent: (ev) => {
|
|
222
|
+
if (ev.tool) push({ type: "activity", tool: ev.tool.name, ...ev.tool.target ? { target: ev.tool.target } : {}, ...ev.tool.ok === false ? { ok: false } : {} });
|
|
223
|
+
if (ev.text) push({ type: "text-delta", text: ev.text });
|
|
224
|
+
if (ev.quota && account) this.accounts?.record(kind, account.name, ev.quota.windows);
|
|
225
|
+
}
|
|
226
|
+
}).then((r) => {
|
|
227
|
+
res = r;
|
|
228
|
+
}));
|
|
229
|
+
if (res.rateLimited) {
|
|
230
|
+
yield { type: "error", message: `${kind} CLI: ${res.rateLimited}`, retryable: true };
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (res.served === SYNTHETIC) {
|
|
234
|
+
const loggedOut = isLoggedOut(res.text);
|
|
235
|
+
yield {
|
|
236
|
+
type: "error",
|
|
237
|
+
retryable: true,
|
|
238
|
+
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`
|
|
239
|
+
};
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (isCallerAbort(signal)) {
|
|
243
|
+
yield { type: "error", message: "cancelled", retryable: false };
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (isDeadline(signal)) {
|
|
247
|
+
yield { type: "error", message: `${kind} CLI: deadline expired`, retryable: true };
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (res.error && !res.text.trim()) {
|
|
251
|
+
if (isLoggedOut(res.error)) {
|
|
252
|
+
yield {
|
|
253
|
+
type: "error",
|
|
254
|
+
retryable: true,
|
|
255
|
+
message: `${kind} CLI is not logged in${account ? ` under profile "${account.name}"` : ""} \u2014 run \`hcode add-provider ${kind}\` to sign it in again`
|
|
256
|
+
};
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
yield { type: "error", message: `${kind} CLI: ${res.error}`, retryable: res.exitCode !== 0 };
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (res.usage) yield usageEvent(res.usage);
|
|
263
|
+
yield { type: "done", finishReason: "stop" };
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
function usageEvent(u) {
|
|
267
|
+
return {
|
|
268
|
+
type: "usage",
|
|
269
|
+
promptTokens: u.freshTokens,
|
|
270
|
+
completionTokens: u.outputTokens,
|
|
271
|
+
cachedTokens: u.cachedTokens,
|
|
272
|
+
cacheWriteTokens: u.cacheWriteTokens
|
|
273
|
+
};
|
|
274
|
+
}
|
|
50
275
|
|
|
51
276
|
// src/worktree/slug.ts
|
|
52
277
|
var MAX_SLUG = 60;
|
|
@@ -278,16 +503,6 @@ var FORBIDDEN_AT_ROOT = /* @__PURE__ */ new Set([
|
|
|
278
503
|
"pull",
|
|
279
504
|
"clean"
|
|
280
505
|
]);
|
|
281
|
-
var TAKES_A_VALUE = /* @__PURE__ */ new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"]);
|
|
282
|
-
function gitVerb(args) {
|
|
283
|
-
for (let i = 0; i < args.length; i++) {
|
|
284
|
-
const a = args[i];
|
|
285
|
-
if (a === void 0) continue;
|
|
286
|
-
if (!a.startsWith("-")) return a;
|
|
287
|
-
if (TAKES_A_VALUE.has(a)) i++;
|
|
288
|
-
}
|
|
289
|
-
return void 0;
|
|
290
|
-
}
|
|
291
506
|
function guardRoot(run, repoRoot) {
|
|
292
507
|
return async (args, cwd) => {
|
|
293
508
|
const verb = gitVerb(args);
|
|
@@ -662,6 +877,434 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
|
|
|
662
877
|
}
|
|
663
878
|
};
|
|
664
879
|
|
|
880
|
+
// src/prompts.ts
|
|
881
|
+
var REQUIRED_ROLES = [
|
|
882
|
+
"refiner",
|
|
883
|
+
"coach",
|
|
884
|
+
"brainstormer",
|
|
885
|
+
"analyst",
|
|
886
|
+
"planner",
|
|
887
|
+
"judge",
|
|
888
|
+
"project-manager",
|
|
889
|
+
"team-lead",
|
|
890
|
+
"router",
|
|
891
|
+
"coder",
|
|
892
|
+
"designer",
|
|
893
|
+
"senior-coder",
|
|
894
|
+
"senior-designer",
|
|
895
|
+
"architect",
|
|
896
|
+
"code-reviewer",
|
|
897
|
+
"task-auditor",
|
|
898
|
+
"principal-coder",
|
|
899
|
+
"operational",
|
|
900
|
+
"memory-keeper",
|
|
901
|
+
"tracer",
|
|
902
|
+
"tester"
|
|
903
|
+
];
|
|
904
|
+
var DEFAULT_ROLE_SKILLS = {
|
|
905
|
+
brainstormer: ["brainstorming"],
|
|
906
|
+
// The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
|
|
907
|
+
// vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
|
|
908
|
+
coder: ["test-driven-development"],
|
|
909
|
+
"senior-coder": ["test-driven-development"],
|
|
910
|
+
// The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
|
|
911
|
+
// supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
|
|
912
|
+
// individual task executable. That is what this skill adds.
|
|
913
|
+
"project-manager": ["writing-plans"],
|
|
914
|
+
// The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
|
|
915
|
+
// and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
|
|
916
|
+
// one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
|
|
917
|
+
designer: ["frontend-design"],
|
|
918
|
+
"senior-designer": ["frontend-design"]
|
|
919
|
+
// NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
|
|
920
|
+
// so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
|
|
921
|
+
};
|
|
922
|
+
var DEFAULT_PROMPTS = {
|
|
923
|
+
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.",
|
|
924
|
+
/**
|
|
925
|
+
* The role that exercises work already built and writes down what actually happened.
|
|
926
|
+
*
|
|
927
|
+
* Every rule below is here because its absence produces the one output worse than no testing at all: a
|
|
928
|
+
* report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
|
|
929
|
+
* confidence, and the next person spends it.
|
|
930
|
+
*/
|
|
931
|
+
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.",
|
|
932
|
+
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.",
|
|
933
|
+
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.",
|
|
934
|
+
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.",
|
|
935
|
+
// analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
|
|
936
|
+
// prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
|
|
937
|
+
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.",
|
|
938
|
+
"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.',
|
|
939
|
+
"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.",
|
|
940
|
+
"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.",
|
|
941
|
+
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.",
|
|
942
|
+
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.",
|
|
943
|
+
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.",
|
|
944
|
+
"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.",
|
|
945
|
+
"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.",
|
|
946
|
+
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.",
|
|
947
|
+
"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.",
|
|
948
|
+
"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).",
|
|
949
|
+
"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.",
|
|
950
|
+
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."
|
|
951
|
+
};
|
|
952
|
+
var SPEC_TEAM = [
|
|
953
|
+
{ name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
|
|
954
|
+
{ name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
|
|
955
|
+
{ name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
|
|
956
|
+
{ name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
|
|
957
|
+
{ 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: [] },
|
|
958
|
+
{ name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
|
|
959
|
+
{ name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
|
|
960
|
+
{ name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
|
|
961
|
+
{ name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
|
|
962
|
+
];
|
|
963
|
+
var PLAN_TEAM = [
|
|
964
|
+
{ 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: [] },
|
|
965
|
+
{ name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
|
|
966
|
+
{ name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
|
|
967
|
+
{ name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
|
|
968
|
+
{ name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
|
|
969
|
+
{ name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
|
|
970
|
+
{ name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
|
|
971
|
+
{ name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
|
|
972
|
+
{ name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
|
|
973
|
+
{ name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
|
|
974
|
+
{ name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
|
|
975
|
+
{ name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
|
|
976
|
+
{ name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
|
|
977
|
+
{ name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
|
|
978
|
+
];
|
|
979
|
+
var CODE_TEAM = [
|
|
980
|
+
{ name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
|
|
981
|
+
{ name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
|
|
982
|
+
{ name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
|
|
983
|
+
{ name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
|
|
984
|
+
{ name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
|
|
985
|
+
{ name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
|
|
986
|
+
{ name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
|
|
987
|
+
{ name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
|
|
988
|
+
{ name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
|
|
989
|
+
{ name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
|
|
990
|
+
{ name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
|
|
991
|
+
{ name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
|
|
992
|
+
{ name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
|
|
993
|
+
{ name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
|
|
994
|
+
{ name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
|
|
995
|
+
];
|
|
996
|
+
var DEFAULT_COUNCIL = [
|
|
997
|
+
{ name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
|
|
998
|
+
{ 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: [] },
|
|
999
|
+
{ name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
|
|
1000
|
+
{ 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: [] },
|
|
1001
|
+
{ name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
|
|
1002
|
+
];
|
|
1003
|
+
function placedSkills() {
|
|
1004
|
+
return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/providers/anthropic.ts
|
|
1008
|
+
function isAnthropicModel(model) {
|
|
1009
|
+
return /(^|\/)(claude|fable|mythos)/i.test(model) || /claude/i.test(model);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/tui/role-models.ts
|
|
1013
|
+
var WEAK_RE = /\b(flash|mini|nano|haiku|lite|small|turbo|fast|\d{1,2}b)\b/i;
|
|
1014
|
+
var FLAGSHIP_ROLES = ["judge", "principal-coder"];
|
|
1015
|
+
var COUNCIL_ROLES = DEFAULT_COUNCIL.map((c) => c.name);
|
|
1016
|
+
var SPEC_LENS_ROLES = SPEC_TEAM.map((c) => c.name);
|
|
1017
|
+
var PLAN_LENS_ROLES = PLAN_TEAM.map((c) => c.name);
|
|
1018
|
+
var CODE_LENS_ROLES = CODE_TEAM.map((c) => c.name);
|
|
1019
|
+
var STRONG_ROLES = [
|
|
1020
|
+
"brainstormer",
|
|
1021
|
+
"analyst",
|
|
1022
|
+
"planner",
|
|
1023
|
+
"architect",
|
|
1024
|
+
"senior-coder",
|
|
1025
|
+
"senior-designer",
|
|
1026
|
+
...COUNCIL_ROLES,
|
|
1027
|
+
...PLAN_LENS_ROLES,
|
|
1028
|
+
...CODE_LENS_ROLES
|
|
1029
|
+
];
|
|
1030
|
+
var MID_ROLES = ["coach", "coder", "designer", "code-reviewer", "operational", "memory-keeper", "task-auditor", ...SPEC_LENS_ROLES];
|
|
1031
|
+
var FAST_ROLES = ["refiner", "router", "project-manager", "team-lead"];
|
|
1032
|
+
var CAPABLE_ROLES = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES]);
|
|
1033
|
+
var ROLE_PROFILES = {
|
|
1034
|
+
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.",
|
|
1035
|
+
refiner: "Classifies intent and rewrites the prompt every turn \u2014 highest call volume, trivial task \u2192 a fast, cheap model.",
|
|
1036
|
+
router: "Picks coder-vs-designer for a task \u2014 tiny and frequent \u2192 fast, cheap.",
|
|
1037
|
+
"project-manager": "Turns a task list into board items \u2014 light and structured \u2192 fast, cheap.",
|
|
1038
|
+
"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.",
|
|
1039
|
+
"team-lead": "Coordinates implementation waves \u2014 light orchestration \u2192 fast, cheap.",
|
|
1040
|
+
coach: "Your main interactive assistant, used constantly all session (highest interaction volume) \u2192 a capable but EFFICIENT model, never the costly flagship.",
|
|
1041
|
+
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.",
|
|
1042
|
+
analyst: "Authors the spec and constitution \u2192 a strong reasoning model (Opus-tier).",
|
|
1043
|
+
planner: "Designs the implementation plan \u2192 a strong reasoning model (Opus-tier).",
|
|
1044
|
+
architect: "Diagnoses stuck tasks and produces recovery plans \u2014 serious design work \u2192 a strong model.",
|
|
1045
|
+
judge: "Critiques specs/plans and makes the final review call \u2014 low volume, high stakes \u2192 the most capable flagship model.",
|
|
1046
|
+
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).",
|
|
1047
|
+
"senior-coder": "Reviews and revises above the coder \u2014 must be MORE capable than the coder (Opus-tier).",
|
|
1048
|
+
"principal-coder": "Final code decision-maker \u2014 low volume, high stakes \u2192 the flagship is appropriate.",
|
|
1049
|
+
designer: "Builds UI \u2014 high volume \u2192 a capable coding/design model, not the flagship.",
|
|
1050
|
+
"senior-designer": "Senior UI reviewer \u2014 more capable than the designer.",
|
|
1051
|
+
"code-reviewer": "Reviews diffs \u2014 moderate volume \u2192 a solid capable model.",
|
|
1052
|
+
"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.",
|
|
1053
|
+
operational: "Handles version control: writes conventional commit messages and (later) drives merges/conflicts \u2014 high volume \u2192 a capable, efficient model."
|
|
1054
|
+
};
|
|
1055
|
+
for (const [stage, lenses, heft] of [
|
|
1056
|
+
["spec", SPEC_TEAM, "a capable, efficient model (a spec is a short business-level doc)"],
|
|
1057
|
+
["plan", PLAN_TEAM, "a strong model (technical design judgment)"],
|
|
1058
|
+
["code", CODE_TEAM, "a strong model (reads real implementations)"]
|
|
1059
|
+
]) {
|
|
1060
|
+
for (const l of lenses) ROLE_PROFILES[l.name] = `${stage.toUpperCase()}-review lens \u2014 ${l.perspective}. Low volume, quality-critical \u2192 ${heft}.`;
|
|
1061
|
+
}
|
|
1062
|
+
for (const c of DEFAULT_COUNCIL) {
|
|
1063
|
+
ROLE_PROFILES[c.name] = `Review COUNCIL decider \u2014 ${c.perspective} Casts the binding pass/revise vote on contested work \u2192 a strong model.`;
|
|
1064
|
+
}
|
|
1065
|
+
var ROLE_ADVICE = ROLE_PROFILES;
|
|
1066
|
+
function filterModelsForRole(role, all, exclude = []) {
|
|
1067
|
+
const advice = ROLE_ADVICE[role];
|
|
1068
|
+
const excluded2 = new Set(exclude);
|
|
1069
|
+
const avail = all.filter((m) => !excluded2.has(m));
|
|
1070
|
+
if (CAPABLE_ROLES.has(role)) {
|
|
1071
|
+
const strong = avail.filter((m) => !WEAK_RE.test(m));
|
|
1072
|
+
if (strong.length === 0) return { models: avail.length ? avail : all, note: advice ? `${advice} (No strong models detected \u2014 showing all.)` : void 0 };
|
|
1073
|
+
return { models: strong, note: `${advice ?? ""} Showing ${strong.length} of ${avail.length} models (fast/weak models hidden for this role).`.trim() };
|
|
1074
|
+
}
|
|
1075
|
+
if (FAST_ROLES.includes(role)) {
|
|
1076
|
+
const fast = avail.filter((m) => WEAK_RE.test(m));
|
|
1077
|
+
if (fast.length === 0) return { models: avail.length ? avail : all, note: advice };
|
|
1078
|
+
return { models: fast, note: `${advice ?? ""} Showing ${fast.length} of ${avail.length} fast/cheap models.`.trim() };
|
|
1079
|
+
}
|
|
1080
|
+
return { models: avail.length ? avail : all };
|
|
1081
|
+
}
|
|
1082
|
+
function effortFor(role, model) {
|
|
1083
|
+
if (!isAnthropicModel(model)) return void 0;
|
|
1084
|
+
if (FLAGSHIP_ROLES.includes(role)) return "max";
|
|
1085
|
+
if (STRONG_ROLES.includes(role)) return "xhigh";
|
|
1086
|
+
if (FAST_ROLES.includes(role)) return "low";
|
|
1087
|
+
if (MID_ROLES.includes(role)) return "high";
|
|
1088
|
+
return void 0;
|
|
1089
|
+
}
|
|
1090
|
+
var effortBump = (s) => /-(ultra|max|xhigh)/.test(s) ? 4 : /-high/.test(s) ? 3 : /-medium/.test(s) ? 2 : /-low/.test(s) ? 1 : 0;
|
|
1091
|
+
var versionBump = (s, family) => {
|
|
1092
|
+
if (family) {
|
|
1093
|
+
const m = s.match(new RegExp(`${family}[-_. ]?(\\d+)(?:[-.](\\d+))?`));
|
|
1094
|
+
if (m) {
|
|
1095
|
+
const major = Number(m[1]);
|
|
1096
|
+
const minor = m[2] === void 0 ? 0 : Number(m[2]);
|
|
1097
|
+
if (major < 100) return major + (minor < 10 ? minor / 10 : minor / 100);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const g = s.match(/(\d)[-.](\d)\b/);
|
|
1101
|
+
return g ? Number(g[1]) + Number(g[2]) / 10 : 0;
|
|
1102
|
+
};
|
|
1103
|
+
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;
|
|
1104
|
+
var NON_TEXT_RE = /\b(image|imagen|vision|video|veo|tts|audio|speech|voice|embed|embedding|rerank|ocr|computer-use|realtime|moderation)\b/i;
|
|
1105
|
+
function isKnownModel(model) {
|
|
1106
|
+
return KNOWN_FAMILY_RE.test(model) && !NON_TEXT_RE.test(model);
|
|
1107
|
+
}
|
|
1108
|
+
var UNRANKED_SCORE = 50;
|
|
1109
|
+
function capabilityScore(model) {
|
|
1110
|
+
const s = model.toLowerCase();
|
|
1111
|
+
if (WEAK_RE.test(s)) return 20 + effortBump(s);
|
|
1112
|
+
if (/fable|mythos/.test(s)) return 100;
|
|
1113
|
+
if (/opus/.test(s)) return 88 + versionBump(s, "opus");
|
|
1114
|
+
if (/codex|gpt-5|\bo3\b/.test(s)) return 82 + effortBump(s) + versionBump(s, "gpt") / 100;
|
|
1115
|
+
if (/sonnet/.test(s)) return 78 + versionBump(s, "sonnet");
|
|
1116
|
+
if (/grok/.test(s)) return 78 + versionBump(s, "grok");
|
|
1117
|
+
if (/glm/.test(s)) return 78 + versionBump(s, "glm");
|
|
1118
|
+
if (/gemini/.test(s) && /pro/.test(s)) return 76 + versionBump(s, "gemini") + effortBump(s);
|
|
1119
|
+
if (/gpt-4/.test(s)) return 65;
|
|
1120
|
+
if (/deepseek/.test(s)) return 55;
|
|
1121
|
+
return UNRANKED_SCORE;
|
|
1122
|
+
}
|
|
1123
|
+
function mostCapable(models) {
|
|
1124
|
+
return [...models].sort((a, b) => capabilityScore(b) - capabilityScore(a))[0] ?? "";
|
|
1125
|
+
}
|
|
1126
|
+
function modelBand(model) {
|
|
1127
|
+
if (WEAK_RE.test(model)) return "fast";
|
|
1128
|
+
const s = capabilityScore(model);
|
|
1129
|
+
if (s >= 95) return "flagship";
|
|
1130
|
+
if (s >= 84) return "strong";
|
|
1131
|
+
if (s <= UNRANKED_SCORE) return "fast";
|
|
1132
|
+
return "mid";
|
|
1133
|
+
}
|
|
1134
|
+
function baseModel(model) {
|
|
1135
|
+
const segs = model.toLowerCase().split("/");
|
|
1136
|
+
let s = segs[segs.length - 1];
|
|
1137
|
+
s = s.replace(/-(ultra|max|xhigh|high|medium|low|free|thinking|preview)\b/g, "");
|
|
1138
|
+
s = s.replace(/-\d{6,8}\b/g, "");
|
|
1139
|
+
return s.replace(/-+$/, "");
|
|
1140
|
+
}
|
|
1141
|
+
function modelFamily(model) {
|
|
1142
|
+
return baseModel(model).replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
1143
|
+
}
|
|
1144
|
+
function latestFirst(models) {
|
|
1145
|
+
const best = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const m of models) {
|
|
1147
|
+
const key2 = modelFamily(m);
|
|
1148
|
+
const cur = best.get(key2);
|
|
1149
|
+
if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
|
|
1150
|
+
}
|
|
1151
|
+
const isLatest = (m) => best.get(modelFamily(m)) === m;
|
|
1152
|
+
return [...models.filter(isLatest), ...models.filter((m) => !isLatest(m))];
|
|
1153
|
+
}
|
|
1154
|
+
function versionlessId(model) {
|
|
1155
|
+
const cut = model.lastIndexOf("/");
|
|
1156
|
+
const prefix = cut >= 0 ? model.slice(0, cut + 1) : "";
|
|
1157
|
+
const name = model.slice(cut + 1).toLowerCase().replace(/-\d{6,8}\b/g, "").replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
1158
|
+
return prefix + name;
|
|
1159
|
+
}
|
|
1160
|
+
var DURABLE_ROLES = ["tracer"];
|
|
1161
|
+
function strongestPrimary(chain, pool) {
|
|
1162
|
+
const head = chain[0];
|
|
1163
|
+
if (!head) return chain;
|
|
1164
|
+
let best = head;
|
|
1165
|
+
for (const m of pool) {
|
|
1166
|
+
if (modelBand(m) === "flagship" || !isKnownModel(m)) continue;
|
|
1167
|
+
if (capabilityScore(m) > capabilityScore(best)) best = m;
|
|
1168
|
+
}
|
|
1169
|
+
if (best === head) return chain;
|
|
1170
|
+
const at = chain.indexOf(best);
|
|
1171
|
+
if (at > 0) {
|
|
1172
|
+
const next = [...chain];
|
|
1173
|
+
next[at] = head;
|
|
1174
|
+
next[0] = best;
|
|
1175
|
+
return next;
|
|
1176
|
+
}
|
|
1177
|
+
return [best, ...chain.slice(1)];
|
|
1178
|
+
}
|
|
1179
|
+
function newestPrimary(chain, pool) {
|
|
1180
|
+
const head = chain[0];
|
|
1181
|
+
if (!head) return chain;
|
|
1182
|
+
const key2 = versionlessId(head);
|
|
1183
|
+
let best = head;
|
|
1184
|
+
for (const m of pool) {
|
|
1185
|
+
if (versionlessId(m) !== key2) continue;
|
|
1186
|
+
if (capabilityScore(m) > capabilityScore(best)) best = m;
|
|
1187
|
+
}
|
|
1188
|
+
if (best === head) return chain;
|
|
1189
|
+
const at = chain.indexOf(best);
|
|
1190
|
+
if (at > 0) {
|
|
1191
|
+
const next = [...chain];
|
|
1192
|
+
next[at] = head;
|
|
1193
|
+
next[0] = best;
|
|
1194
|
+
return next;
|
|
1195
|
+
}
|
|
1196
|
+
return [best, ...chain.slice(1)];
|
|
1197
|
+
}
|
|
1198
|
+
function dedupBest(models) {
|
|
1199
|
+
const best = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const m of models) {
|
|
1201
|
+
const key2 = baseModel(m);
|
|
1202
|
+
const cur = best.get(key2);
|
|
1203
|
+
if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
|
|
1204
|
+
}
|
|
1205
|
+
return [...best.values()].sort((a, b) => capabilityScore(b) - capabilityScore(a));
|
|
1206
|
+
}
|
|
1207
|
+
function sourceOf(model) {
|
|
1208
|
+
const s = model.toLowerCase().replace(/^no-think\//, "");
|
|
1209
|
+
return cliFor(s) ?? s.split("/")[0];
|
|
1210
|
+
}
|
|
1211
|
+
function interleaveBySource(pool) {
|
|
1212
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1213
|
+
for (const m of pool) {
|
|
1214
|
+
const s = sourceOf(m);
|
|
1215
|
+
const q = bySource.get(s);
|
|
1216
|
+
if (q) q.push(m);
|
|
1217
|
+
else bySource.set(s, [m]);
|
|
1218
|
+
}
|
|
1219
|
+
const queues = [...bySource.values()];
|
|
1220
|
+
const out = [];
|
|
1221
|
+
for (let more = true; more; ) {
|
|
1222
|
+
more = false;
|
|
1223
|
+
for (const q of queues) {
|
|
1224
|
+
const m = q.shift();
|
|
1225
|
+
if (m !== void 0) {
|
|
1226
|
+
out.push(m);
|
|
1227
|
+
more = true;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return out;
|
|
1232
|
+
}
|
|
1233
|
+
var BAND_ORDER = { fast: 0, mid: 1, strong: 2, flagship: 3 };
|
|
1234
|
+
function bandDistance(primary, candidate) {
|
|
1235
|
+
const p = BAND_ORDER[modelBand(primary)];
|
|
1236
|
+
const c = BAND_ORDER[modelBand(candidate)];
|
|
1237
|
+
return Math.abs(c - p) * 2 + (c < p ? 1 : 0);
|
|
1238
|
+
}
|
|
1239
|
+
function pickFallbacks(primary, pool, n) {
|
|
1240
|
+
const chosen = [];
|
|
1241
|
+
const usedModels = /* @__PURE__ */ new Set([baseModel(primary)]);
|
|
1242
|
+
const usedSources = /* @__PURE__ */ new Set([sourceOf(primary)]);
|
|
1243
|
+
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);
|
|
1244
|
+
for (const m of byHeft) {
|
|
1245
|
+
if (chosen.length >= n) break;
|
|
1246
|
+
if (usedModels.has(baseModel(m)) || usedSources.has(sourceOf(m))) continue;
|
|
1247
|
+
chosen.push(m);
|
|
1248
|
+
usedModels.add(baseModel(m));
|
|
1249
|
+
usedSources.add(sourceOf(m));
|
|
1250
|
+
}
|
|
1251
|
+
for (const m of byHeft) {
|
|
1252
|
+
if (chosen.length >= n) break;
|
|
1253
|
+
if (usedModels.has(baseModel(m))) continue;
|
|
1254
|
+
chosen.push(m);
|
|
1255
|
+
usedModels.add(baseModel(m));
|
|
1256
|
+
}
|
|
1257
|
+
return chosen;
|
|
1258
|
+
}
|
|
1259
|
+
var FALLBACK_COUNT = 2;
|
|
1260
|
+
function adjustRoleModels(roles, models, unfit) {
|
|
1261
|
+
if (models.length === 0) return [];
|
|
1262
|
+
const recognised = models.filter(isKnownModel);
|
|
1263
|
+
const pick = recognised.length ? recognised : models;
|
|
1264
|
+
const capable = dedupBest(pick.filter((m) => !WEAK_RE.test(m)));
|
|
1265
|
+
const fast = dedupBest(pick.filter((m) => WEAK_RE.test(m)));
|
|
1266
|
+
const capablePool = capable.length ? capable : fast;
|
|
1267
|
+
const fastPool = fast.length ? fast : capable;
|
|
1268
|
+
const primaryPool = latestFirst(capablePool);
|
|
1269
|
+
const primaryFast = latestFirst(fastPool);
|
|
1270
|
+
const nonFlagship = primaryPool.filter((m) => modelBand(m) !== "flagship");
|
|
1271
|
+
const strongPool = primaryPool.filter((m) => modelBand(m) === "strong");
|
|
1272
|
+
const midPool = primaryPool.filter((m) => modelBand(m) === "mid");
|
|
1273
|
+
const wanted = new Set(roles);
|
|
1274
|
+
const forRole = (role, pool) => {
|
|
1275
|
+
if (!unfit) return pool;
|
|
1276
|
+
const fit = pool.filter((m) => !unfit(role, m));
|
|
1277
|
+
return fit.length ? fit : pool;
|
|
1278
|
+
};
|
|
1279
|
+
const known = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES, ...FAST_ROLES]);
|
|
1280
|
+
const primary = /* @__PURE__ */ new Map();
|
|
1281
|
+
const flagSrc = primaryPool;
|
|
1282
|
+
FLAGSHIP_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1283
|
+
const src = forRole(r, flagSrc);
|
|
1284
|
+
primary.set(r, src[i % src.length]);
|
|
1285
|
+
});
|
|
1286
|
+
const strongSrc = interleaveBySource(strongPool.length ? strongPool : nonFlagship.length ? nonFlagship : primaryPool);
|
|
1287
|
+
STRONG_ROLES.filter((r) => wanted.has(r)).concat(roles.filter((r) => !known.has(r))).forEach((r, i) => {
|
|
1288
|
+
const src = forRole(r, strongSrc);
|
|
1289
|
+
primary.set(r, src[i % src.length]);
|
|
1290
|
+
});
|
|
1291
|
+
const midSrc = interleaveBySource(midPool.length ? midPool : nonFlagship.length ? nonFlagship : primaryPool);
|
|
1292
|
+
MID_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1293
|
+
const src = forRole(r, midSrc);
|
|
1294
|
+
primary.set(r, src[i % src.length]);
|
|
1295
|
+
});
|
|
1296
|
+
FAST_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
|
|
1297
|
+
const src = forRole(r, primaryFast);
|
|
1298
|
+
primary.set(r, src[i % src.length]);
|
|
1299
|
+
});
|
|
1300
|
+
return roles.map((role) => {
|
|
1301
|
+
const head = primary.get(role) ?? primaryPool[0];
|
|
1302
|
+
const capForFb = MID_ROLES.includes(role) ? capablePool.filter((m) => modelBand(m) !== "flagship") : capablePool;
|
|
1303
|
+
const pool = FAST_ROLES.includes(role) ? [...fastPool, ...capForFb] : [...capForFb, ...fastPool];
|
|
1304
|
+
return { role, models: newestPrimary([head, ...pickFallbacks(head, forRole(role, pool), FALLBACK_COUNT)], models) };
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
665
1308
|
// src/skills/apply.ts
|
|
666
1309
|
import { readFile } from "fs/promises";
|
|
667
1310
|
import { readdirSync as readdirSync2 } from "fs";
|
|
@@ -711,13 +1354,21 @@ function docsIn(dir) {
|
|
|
711
1354
|
}
|
|
712
1355
|
}
|
|
713
1356
|
var MAX_SKILL_DOC_CHARS = 3e4;
|
|
1357
|
+
var MAX_SKILLS_LISTED = 12;
|
|
714
1358
|
function noSuchSkill(name, available) {
|
|
715
1359
|
const shape = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
716
1360
|
const same = available.filter((s) => shape(s) === shape(name));
|
|
717
1361
|
if (same.length === 1) {
|
|
718
1362
|
return `skill not found: ${name} \u2014 did you mean \`${same[0]}\`? Call it with that exact name.`;
|
|
719
1363
|
}
|
|
720
|
-
|
|
1364
|
+
if (!available.length) return `skill not found: ${name}. This project has no skills installed, so carry on without one.`;
|
|
1365
|
+
const near = available.filter((s) => shape(s).includes(shape(name)) || shape(name).includes(shape(s)));
|
|
1366
|
+
if (near.length && near.length <= MAX_SKILLS_LISTED) {
|
|
1367
|
+
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.`;
|
|
1368
|
+
}
|
|
1369
|
+
const shown = available.slice(0, MAX_SKILLS_LISTED);
|
|
1370
|
+
const rest = available.length - shown.length;
|
|
1371
|
+
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.`;
|
|
721
1372
|
}
|
|
722
1373
|
function buildSkillTool(registry) {
|
|
723
1374
|
return {
|
|
@@ -842,6 +1493,14 @@ var READ_ONLY = /* @__PURE__ */ new Set([
|
|
|
842
1493
|
"show-ref",
|
|
843
1494
|
"for-each-ref",
|
|
844
1495
|
"ls-remote",
|
|
1496
|
+
/**
|
|
1497
|
+
* `git grep` searches tracked content and has no writing form at all — the same standing as `log`.
|
|
1498
|
+
*
|
|
1499
|
+
* Left out, it was refused twice in one 36-minute run while agents fell back to `find | xargs grep`
|
|
1500
|
+
* through the shell, which is slower on a repository this size and searches build output and
|
|
1501
|
+
* `node_modules` unless every caller remembers to prune them. git already knows what is tracked.
|
|
1502
|
+
*/
|
|
1503
|
+
"grep",
|
|
845
1504
|
/**
|
|
846
1505
|
* `check-ignore` asks whether a path is ignored — it reads `.gitignore` and answers, and changes nothing.
|
|
847
1506
|
*
|
|
@@ -853,18 +1512,34 @@ var READ_ONLY = /* @__PURE__ */ new Set([
|
|
|
853
1512
|
]);
|
|
854
1513
|
var READ_ONLY_PAIRS = /* @__PURE__ */ new Set([
|
|
855
1514
|
"worktree list",
|
|
856
|
-
"branch --list",
|
|
857
|
-
"branch -l",
|
|
858
|
-
"branch -a",
|
|
859
|
-
"branch -v",
|
|
860
|
-
"branch -r",
|
|
861
1515
|
"tag --list",
|
|
862
1516
|
"tag -l",
|
|
863
1517
|
"stash list",
|
|
864
1518
|
"remote -v",
|
|
865
1519
|
"remote show",
|
|
866
1520
|
"config --get",
|
|
867
|
-
"config --list"
|
|
1521
|
+
"config --list",
|
|
1522
|
+
/**
|
|
1523
|
+
* The long forms of what is already allowed, and the queries that only ask.
|
|
1524
|
+
*
|
|
1525
|
+
* `branch -a` was allowed and `branch --all` was not — the same command spelled the way git's own
|
|
1526
|
+
* documentation spells it. Measured in one run: an agent asked for `branch --all` and then `branch
|
|
1527
|
+
* --show-current`, and paid a refused turn for each while `-a` sat in this list. A short flag admitted and
|
|
1528
|
+
* its long twin refused is not a security boundary, it is a typo in one.
|
|
1529
|
+
*
|
|
1530
|
+
* These are the closure of what this set already permits, not new ground: every one of them prints
|
|
1531
|
+
* information about branches or tags and none of them can create, move or delete a ref. The forms that
|
|
1532
|
+
* write — `-d`, `-D`, `-m`, `-M`, `-c`, `-C`, `--delete`, `--move`, `--copy`, `--set-upstream-to`,
|
|
1533
|
+
* `--edit-description` — are still absent, and a first argument that is not a flag never reaches here.
|
|
1534
|
+
*/
|
|
1535
|
+
"tag --contains",
|
|
1536
|
+
"tag --no-contains",
|
|
1537
|
+
"tag --merged",
|
|
1538
|
+
"tag --points-at",
|
|
1539
|
+
"tag -n",
|
|
1540
|
+
"remote --verbose",
|
|
1541
|
+
"remote get-url",
|
|
1542
|
+
"stash show"
|
|
868
1543
|
]);
|
|
869
1544
|
var REFUSED_ARG = /^(--output|-c$|--config-env|--exec-path|-C$|--git-dir|--work-tree|--upload-pack|--receive-pack)/;
|
|
870
1545
|
var params = z2.object({
|
|
@@ -875,21 +1550,130 @@ var params = z2.object({
|
|
|
875
1550
|
var MAX_GIT_OUTPUT = 6e4;
|
|
876
1551
|
var GIT_TIMEOUT_MS = 3e4;
|
|
877
1552
|
var GIT_PUSH_TIMEOUT_MS = 12e4;
|
|
878
|
-
var ANSWERS_WITH_ONE = /* @__PURE__ */ new Set([
|
|
1553
|
+
var ANSWERS_WITH_ONE = /* @__PURE__ */ new Set([
|
|
1554
|
+
"diff",
|
|
1555
|
+
"diff-index",
|
|
1556
|
+
"diff-tree",
|
|
1557
|
+
"diff-files",
|
|
1558
|
+
"merge-base",
|
|
1559
|
+
"check-ignore",
|
|
1560
|
+
/**
|
|
1561
|
+
* `grep` says "no match" with exit 1, exactly as the others say their own no.
|
|
1562
|
+
*
|
|
1563
|
+
* Admitted to the read-only set earlier tonight and left out of this one, so a search that found nothing
|
|
1564
|
+
* came back as `git failed with no output.` — a fault where there was an answer. Measured live within
|
|
1565
|
+
* minutes: `git grep -n -i ExportReportService.cs` twice, both reported as failures, for a file that
|
|
1566
|
+
* simply is not in the repository.
|
|
1567
|
+
*/
|
|
1568
|
+
"grep"
|
|
1569
|
+
]);
|
|
879
1570
|
function answeredWithOne(args, code) {
|
|
880
1571
|
return code === 1 && ANSWERS_WITH_ONE.has(args[0] ?? "");
|
|
881
1572
|
}
|
|
1573
|
+
function howToNarrow(args) {
|
|
1574
|
+
const verb = gitVerb(args) ?? "";
|
|
1575
|
+
if (verb === "diff" || verb === "show") {
|
|
1576
|
+
return "narrow the range, or put `--stat` directly after the subcommand (git " + verb + " --stat <rest>), which git requires";
|
|
1577
|
+
}
|
|
1578
|
+
if (verb === "log") return "ask for fewer commits (-n 20) or just their subjects (--oneline)";
|
|
1579
|
+
if (verb === "ls-files" || verb === "ls-tree") return "narrow the pathspec to one directory at a time";
|
|
1580
|
+
if (verb === "blame") return "limit it to a range of lines (-L 40,120)";
|
|
1581
|
+
return "ask for a narrower part of it";
|
|
1582
|
+
}
|
|
882
1583
|
function answerOfOne(args) {
|
|
883
1584
|
const verb = args[0] ?? "";
|
|
884
1585
|
if (verb === "check-ignore") {
|
|
885
1586
|
return "No \u2014 that path is not ignored by this repository's rules. (git exit code 1, which is the answer here.)";
|
|
886
1587
|
}
|
|
1588
|
+
if (verb === "grep") {
|
|
1589
|
+
return "No match \u2014 nothing in the tracked files matches that pattern. (git exit code 1, which is the answer here.)";
|
|
1590
|
+
}
|
|
887
1591
|
if (verb === "merge-base") {
|
|
888
1592
|
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.)";
|
|
889
1593
|
}
|
|
890
1594
|
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.";
|
|
891
1595
|
}
|
|
1596
|
+
function packedArgument(a) {
|
|
1597
|
+
if (!/\s/.test(a)) return void 0;
|
|
1598
|
+
if (/^--?[\w-]+[=:]/.test(a)) return void 0;
|
|
1599
|
+
const parts = a.trim().split(/\s+/).filter(Boolean);
|
|
1600
|
+
if (parts.length < 2) return void 0;
|
|
1601
|
+
if (parts[0] === "--") return parts;
|
|
1602
|
+
return parts.slice(1).some((p) => p === "--" || p.startsWith("-") || p.includes("/")) ? parts : void 0;
|
|
1603
|
+
}
|
|
1604
|
+
var GLUED_PATHSPEC = /^--[^\s=]*\/[^\s=]*$/;
|
|
1605
|
+
var BRANCH_TAKES_VALUE = /* @__PURE__ */ new Set([
|
|
1606
|
+
"--contains",
|
|
1607
|
+
"--no-contains",
|
|
1608
|
+
"--merged",
|
|
1609
|
+
"--no-merged",
|
|
1610
|
+
"--points-at",
|
|
1611
|
+
"--format",
|
|
1612
|
+
"--sort",
|
|
1613
|
+
"--color",
|
|
1614
|
+
"--abbrev",
|
|
1615
|
+
"-u",
|
|
1616
|
+
"--set-upstream-to",
|
|
1617
|
+
"-t",
|
|
1618
|
+
"--track"
|
|
1619
|
+
]);
|
|
1620
|
+
var BRANCH_WRITERS = /* @__PURE__ */ new Set([
|
|
1621
|
+
"-d",
|
|
1622
|
+
"-D",
|
|
1623
|
+
"--delete",
|
|
1624
|
+
"-m",
|
|
1625
|
+
"-M",
|
|
1626
|
+
"--move",
|
|
1627
|
+
"-c",
|
|
1628
|
+
"-C",
|
|
1629
|
+
"--copy",
|
|
1630
|
+
"--edit-description",
|
|
1631
|
+
"--set-upstream",
|
|
1632
|
+
"--set-upstream-to",
|
|
1633
|
+
"--unset-upstream",
|
|
1634
|
+
"-u",
|
|
1635
|
+
"-t",
|
|
1636
|
+
"--track",
|
|
1637
|
+
"--no-track",
|
|
1638
|
+
"-f",
|
|
1639
|
+
"--force"
|
|
1640
|
+
]);
|
|
1641
|
+
function branchWrites(rest) {
|
|
1642
|
+
const listing = rest.some((a) => a === "--list" || a === "-l");
|
|
1643
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1644
|
+
const a = rest[i];
|
|
1645
|
+
if (a === void 0) continue;
|
|
1646
|
+
if (a === "--") return "`git branch` with a pathspec is not a thing this tool needs to run.";
|
|
1647
|
+
if (a.startsWith("--")) {
|
|
1648
|
+
const name = a.split("=")[0];
|
|
1649
|
+
if (BRANCH_WRITERS.has(name)) return `\`git branch ${name}\` changes a branch. Only listing is allowed.`;
|
|
1650
|
+
if (BRANCH_TAKES_VALUE.has(name) && !a.includes("=")) i++;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (a.startsWith("-")) {
|
|
1654
|
+
const bad = [...a.slice(1)].find((c) => BRANCH_WRITERS.has(`-${c}`));
|
|
1655
|
+
if (bad) return `\`git branch -${bad}\` changes a branch. Only listing is allowed.`;
|
|
1656
|
+
if (BRANCH_TAKES_VALUE.has(a)) i++;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (listing) continue;
|
|
1660
|
+
if (/[*?\[]/.test(a)) {
|
|
1661
|
+
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}\`.`;
|
|
1662
|
+
}
|
|
1663
|
+
return `\`git branch ${a}\` creates a branch. Only listing is allowed \u2014 git_write owns the rest.`;
|
|
1664
|
+
}
|
|
1665
|
+
return void 0;
|
|
1666
|
+
}
|
|
892
1667
|
function refuse(args) {
|
|
1668
|
+
const packed = args.find((a) => packedArgument(a) !== void 0 || GLUED_PATHSPEC.test(a));
|
|
1669
|
+
if (packed !== void 0) {
|
|
1670
|
+
const split = packedArgument(packed) ?? ["--", packed.slice(2)];
|
|
1671
|
+
if (split[0] !== "--") {
|
|
1672
|
+
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.`;
|
|
1673
|
+
}
|
|
1674
|
+
const parts = split.slice(1);
|
|
1675
|
+
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).`;
|
|
1676
|
+
}
|
|
893
1677
|
const bad = args.find((a) => REFUSED_ARG.test(a));
|
|
894
1678
|
if (bad) {
|
|
895
1679
|
return `\`${bad}\` is not allowed: it can write a file, run a program through git's configuration, or point git at another repository.`;
|
|
@@ -897,6 +1681,16 @@ function refuse(args) {
|
|
|
897
1681
|
const [sub, second] = args;
|
|
898
1682
|
if (!sub || sub.startsWith("-")) return "The first argument must be a git subcommand, e.g. `status`.";
|
|
899
1683
|
if (READ_ONLY.has(sub)) return void 0;
|
|
1684
|
+
if (sub === "branch") return branchWrites(args.slice(1));
|
|
1685
|
+
if (sub === "reflog") {
|
|
1686
|
+
return second === "expire" || second === "delete" ? `\`git reflog ${second}\` rewrites the reflog. Only reading it is allowed.` : void 0;
|
|
1687
|
+
}
|
|
1688
|
+
if (sub === "config") {
|
|
1689
|
+
if (second !== void 0 && !second.startsWith("-") && args.length === 2) return void 0;
|
|
1690
|
+
if (args.length > 2 && !args.some((a) => a.startsWith("--get") || a === "--list")) {
|
|
1691
|
+
return "`git config <key> <value>` writes configuration. Read one with `git config <key>`.";
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
900
1694
|
if (second && READ_ONLY_PAIRS.has(`${sub} ${second}`)) return void 0;
|
|
901
1695
|
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(", ")}.`;
|
|
902
1696
|
}
|
|
@@ -938,7 +1732,7 @@ var gitTool = {
|
|
|
938
1732
|
return { content: out.code === 0 ? "(no output)" : "git failed with no output.", isError: failed };
|
|
939
1733
|
}
|
|
940
1734
|
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
941
|
-
\u2026[truncated \u2014
|
|
1735
|
+
\u2026[truncated \u2014 ${howToNarrow(args)}]` : out.text;
|
|
942
1736
|
return { content: clipped, isError: failed };
|
|
943
1737
|
}
|
|
944
1738
|
};
|
|
@@ -985,185 +1779,58 @@ var gitWriteTool = {
|
|
|
985
1779
|
const parsed = params.safeParse(rawArgs);
|
|
986
1780
|
if (!parsed.success) {
|
|
987
1781
|
return { content: `git_write: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
|
|
988
|
-
}
|
|
989
|
-
const args = parsed.data.args;
|
|
990
|
-
const why = refuseWrite(args);
|
|
991
|
-
if (why) return { content: why, isError: true, settled: true };
|
|
992
|
-
const out = await new Promise((resolve6) => {
|
|
993
|
-
const child = execFile("git", args, {
|
|
994
|
-
cwd: ctx.cwd,
|
|
995
|
-
// A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
|
|
996
|
-
// pessimistic one for a repository with anything in it.
|
|
997
|
-
timeout: args[0] === "push" ? GIT_PUSH_TIMEOUT_MS : GIT_TIMEOUT_MS,
|
|
998
|
-
maxBuffer: MAX_GIT_OUTPUT * 4,
|
|
999
|
-
// GIT_TERMINAL_PROMPT=0: a push that needs credentials fails with a message instead of blocking on a
|
|
1000
|
-
// prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
|
|
1001
|
-
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
|
|
1002
|
-
}, (err, stdout, stderr) => {
|
|
1003
|
-
resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
|
|
1004
|
-
});
|
|
1005
|
-
ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
1006
|
-
});
|
|
1007
|
-
if (!out.text) return { content: out.code === 0 ? "(done)" : "git failed with no output.", isError: out.code !== 0 };
|
|
1008
|
-
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
1009
|
-
\u2026[truncated]` : out.text;
|
|
1010
|
-
return { content: clipped, isError: out.code !== 0 };
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
// src/tools/remember.ts
|
|
1015
|
-
import { z as z3 } from "zod";
|
|
1016
|
-
var params2 = z3.object({
|
|
1017
|
-
fact: z3.string().describe(
|
|
1018
|
-
"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."
|
|
1019
|
-
)
|
|
1020
|
-
});
|
|
1021
|
-
function buildRememberTool(sink) {
|
|
1022
|
-
return {
|
|
1023
|
-
name: "remember_fact",
|
|
1024
|
-
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.",
|
|
1025
|
-
permissionLevel: "safe",
|
|
1026
|
-
parameters: params2,
|
|
1027
|
-
async run(rawArgs, ctx) {
|
|
1028
|
-
const parsed = params2.safeParse(rawArgs);
|
|
1029
|
-
if (!parsed.success) return { content: "remember_fact: invalid args (expected { fact })", isError: true };
|
|
1030
|
-
const fact = parsed.data.fact.trim();
|
|
1031
|
-
if (!fact) return { content: "remember_fact: empty fact", isError: true };
|
|
1032
|
-
const write = sink ?? ctx.remember;
|
|
1033
|
-
if (!write) return { content: "remember_fact: memory is not available in this context", isError: true };
|
|
1034
|
-
write(fact);
|
|
1035
|
-
return { content: `Remembered: ${fact}`, isError: false };
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
var rememberFactTool = buildRememberTool();
|
|
1040
|
-
|
|
1041
|
-
// src/prompts.ts
|
|
1042
|
-
var REQUIRED_ROLES = [
|
|
1043
|
-
"refiner",
|
|
1044
|
-
"coach",
|
|
1045
|
-
"brainstormer",
|
|
1046
|
-
"analyst",
|
|
1047
|
-
"planner",
|
|
1048
|
-
"judge",
|
|
1049
|
-
"project-manager",
|
|
1050
|
-
"team-lead",
|
|
1051
|
-
"router",
|
|
1052
|
-
"coder",
|
|
1053
|
-
"designer",
|
|
1054
|
-
"senior-coder",
|
|
1055
|
-
"senior-designer",
|
|
1056
|
-
"architect",
|
|
1057
|
-
"code-reviewer",
|
|
1058
|
-
"task-auditor",
|
|
1059
|
-
"principal-coder",
|
|
1060
|
-
"operational",
|
|
1061
|
-
"memory-keeper",
|
|
1062
|
-
"tracer",
|
|
1063
|
-
"tester"
|
|
1064
|
-
];
|
|
1065
|
-
var DEFAULT_ROLE_SKILLS = {
|
|
1066
|
-
brainstormer: ["brainstorming"],
|
|
1067
|
-
// The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
|
|
1068
|
-
// vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
|
|
1069
|
-
coder: ["test-driven-development"],
|
|
1070
|
-
"senior-coder": ["test-driven-development"],
|
|
1071
|
-
// The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
|
|
1072
|
-
// supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
|
|
1073
|
-
// individual task executable. That is what this skill adds.
|
|
1074
|
-
"project-manager": ["writing-plans"],
|
|
1075
|
-
// The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
|
|
1076
|
-
// and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
|
|
1077
|
-
// one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
|
|
1078
|
-
designer: ["frontend-design"],
|
|
1079
|
-
"senior-designer": ["frontend-design"]
|
|
1080
|
-
// NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
|
|
1081
|
-
// so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
|
|
1082
|
-
};
|
|
1083
|
-
var DEFAULT_PROMPTS = {
|
|
1084
|
-
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.",
|
|
1085
|
-
/**
|
|
1086
|
-
* The role that exercises work already built and writes down what actually happened.
|
|
1087
|
-
*
|
|
1088
|
-
* Every rule below is here because its absence produces the one output worse than no testing at all: a
|
|
1089
|
-
* report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
|
|
1090
|
-
* confidence, and the next person spends it.
|
|
1091
|
-
*/
|
|
1092
|
-
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.",
|
|
1093
|
-
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.",
|
|
1094
|
-
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.",
|
|
1095
|
-
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.",
|
|
1096
|
-
// analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
|
|
1097
|
-
// prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
|
|
1098
|
-
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.",
|
|
1099
|
-
"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.',
|
|
1100
|
-
"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.",
|
|
1101
|
-
"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.",
|
|
1102
|
-
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.",
|
|
1103
|
-
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.",
|
|
1104
|
-
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.",
|
|
1105
|
-
"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.",
|
|
1106
|
-
"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.",
|
|
1107
|
-
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.",
|
|
1108
|
-
"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.",
|
|
1109
|
-
"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).",
|
|
1110
|
-
"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.",
|
|
1111
|
-
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."
|
|
1112
|
-
};
|
|
1113
|
-
var SPEC_TEAM = [
|
|
1114
|
-
{ name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
|
|
1115
|
-
{ name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
|
|
1116
|
-
{ name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
|
|
1117
|
-
{ name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
|
|
1118
|
-
{ 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: [] },
|
|
1119
|
-
{ name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
|
|
1120
|
-
{ name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
|
|
1121
|
-
{ name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
|
|
1122
|
-
{ name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
|
|
1123
|
-
];
|
|
1124
|
-
var PLAN_TEAM = [
|
|
1125
|
-
{ 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: [] },
|
|
1126
|
-
{ name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
|
|
1127
|
-
{ name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
|
|
1128
|
-
{ name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
|
|
1129
|
-
{ name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
|
|
1130
|
-
{ name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
|
|
1131
|
-
{ name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
|
|
1132
|
-
{ name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
|
|
1133
|
-
{ name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
|
|
1134
|
-
{ name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
|
|
1135
|
-
{ name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
|
|
1136
|
-
{ name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
|
|
1137
|
-
{ name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
|
|
1138
|
-
{ name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
|
|
1139
|
-
];
|
|
1140
|
-
var CODE_TEAM = [
|
|
1141
|
-
{ name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
|
|
1142
|
-
{ name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
|
|
1143
|
-
{ name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
|
|
1144
|
-
{ name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
|
|
1145
|
-
{ name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
|
|
1146
|
-
{ name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
|
|
1147
|
-
{ name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
|
|
1148
|
-
{ name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
|
|
1149
|
-
{ name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
|
|
1150
|
-
{ name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
|
|
1151
|
-
{ name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
|
|
1152
|
-
{ name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
|
|
1153
|
-
{ name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
|
|
1154
|
-
{ name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
|
|
1155
|
-
{ name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
|
|
1156
|
-
];
|
|
1157
|
-
var DEFAULT_COUNCIL = [
|
|
1158
|
-
{ name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
|
|
1159
|
-
{ 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: [] },
|
|
1160
|
-
{ name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
|
|
1161
|
-
{ 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: [] },
|
|
1162
|
-
{ name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
|
|
1163
|
-
];
|
|
1164
|
-
function placedSkills() {
|
|
1165
|
-
return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
|
|
1782
|
+
}
|
|
1783
|
+
const args = parsed.data.args;
|
|
1784
|
+
const why = refuseWrite(args);
|
|
1785
|
+
if (why) return { content: why, isError: true, settled: true };
|
|
1786
|
+
const out = await new Promise((resolve6) => {
|
|
1787
|
+
const child = execFile("git", args, {
|
|
1788
|
+
cwd: ctx.cwd,
|
|
1789
|
+
// A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
|
|
1790
|
+
// pessimistic one for a repository with anything in it.
|
|
1791
|
+
timeout: args[0] === "push" ? GIT_PUSH_TIMEOUT_MS : GIT_TIMEOUT_MS,
|
|
1792
|
+
maxBuffer: MAX_GIT_OUTPUT * 4,
|
|
1793
|
+
// GIT_TERMINAL_PROMPT=0: a push that needs credentials fails with a message instead of blocking on a
|
|
1794
|
+
// prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
|
|
1795
|
+
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
|
|
1796
|
+
}, (err, stdout, stderr) => {
|
|
1797
|
+
resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
|
|
1798
|
+
});
|
|
1799
|
+
ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
|
|
1800
|
+
});
|
|
1801
|
+
if (!out.text) return { content: out.code === 0 ? "(done)" : "git failed with no output.", isError: out.code !== 0 };
|
|
1802
|
+
const clipped = out.text.length > MAX_GIT_OUTPUT ? `${truncateSafe(out.text, MAX_GIT_OUTPUT)}
|
|
1803
|
+
\u2026[truncated]` : out.text;
|
|
1804
|
+
return { content: clipped, isError: out.code !== 0 };
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
// src/tools/remember.ts
|
|
1809
|
+
import { z as z3 } from "zod";
|
|
1810
|
+
var params2 = z3.object({
|
|
1811
|
+
fact: z3.string().describe(
|
|
1812
|
+
"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."
|
|
1813
|
+
)
|
|
1814
|
+
});
|
|
1815
|
+
function buildRememberTool(sink) {
|
|
1816
|
+
return {
|
|
1817
|
+
name: "remember_fact",
|
|
1818
|
+
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.",
|
|
1819
|
+
permissionLevel: "safe",
|
|
1820
|
+
parameters: params2,
|
|
1821
|
+
async run(rawArgs, ctx) {
|
|
1822
|
+
const parsed = params2.safeParse(rawArgs);
|
|
1823
|
+
if (!parsed.success) return { content: "remember_fact: invalid args (expected { fact })", isError: true };
|
|
1824
|
+
const fact = parsed.data.fact.trim();
|
|
1825
|
+
if (!fact) return { content: "remember_fact: empty fact", isError: true };
|
|
1826
|
+
const write = sink ?? ctx.remember;
|
|
1827
|
+
if (!write) return { content: "remember_fact: memory is not available in this context", isError: true };
|
|
1828
|
+
write(fact);
|
|
1829
|
+
return { content: `Remembered: ${fact}`, isError: false };
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1166
1832
|
}
|
|
1833
|
+
var rememberFactTool = buildRememberTool();
|
|
1167
1834
|
|
|
1168
1835
|
// src/speckit/layout.ts
|
|
1169
1836
|
import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync4 } from "fs";
|
|
@@ -1764,6 +2431,7 @@ function isNonImplementing(description) {
|
|
|
1764
2431
|
return /\bread[- ]only\b/i.test(description) || /\bdoes\s+not\s+(implement|apply|execute|write)\b/i.test(description);
|
|
1765
2432
|
}
|
|
1766
2433
|
var MATCH_BAR = 3;
|
|
2434
|
+
var MIN_DENSITY = 0.1;
|
|
1767
2435
|
var MAX_ROUTED = 3;
|
|
1768
2436
|
var MAX_ROUTED_CHARS = 24e3;
|
|
1769
2437
|
function scoreSkill(task, description) {
|
|
@@ -1785,7 +2453,7 @@ function routeSkills(task, registry, already = [], opts = {}) {
|
|
|
1785
2453
|
const paths = (opts.files ?? []).join(" ");
|
|
1786
2454
|
const base = [opts.role ?? "", routingSubject(task), paths].filter(Boolean).join(" ");
|
|
1787
2455
|
const subject = [base, expandExtensions(opts.files ?? []), expandAbbreviations(base)].filter(Boolean).join(" ");
|
|
1788
|
-
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) => {
|
|
2456
|
+
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) => {
|
|
1789
2457
|
const len = registry.get(m.name)?.content.length ?? 0;
|
|
1790
2458
|
if (spent + len > MAX_ROUTED_CHARS) return false;
|
|
1791
2459
|
spent += len;
|
|
@@ -1874,7 +2542,7 @@ async function diffSince(cwd, sinceRef, git = defaultGitRunner) {
|
|
|
1874
2542
|
|
|
1875
2543
|
// src/engine/reviewer.ts
|
|
1876
2544
|
var CODE_REVIEW_MAX_TURNS = 25;
|
|
1877
|
-
var CODE_REVIEW_TIMEOUT_MS =
|
|
2545
|
+
var CODE_REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1878
2546
|
var VerdictSchema = z8.object({
|
|
1879
2547
|
verdict: z8.enum(["pass", "fail"]).describe(
|
|
1880
2548
|
"`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."
|
|
@@ -2008,6 +2676,7 @@ var params7 = z10.object({
|
|
|
2008
2676
|
replaceAll: z10.boolean().optional()
|
|
2009
2677
|
});
|
|
2010
2678
|
var NEAR_MISS_CHARS = 600;
|
|
2679
|
+
var MAX_MATCH_LINES = 5;
|
|
2011
2680
|
var norm = (t) => t.replace(/[ \t]+/g, " ").replace(/[ \t]+$/gm, "").trim();
|
|
2012
2681
|
function shortPath(path, cwd) {
|
|
2013
2682
|
const abs = resolve4(cwd, path);
|
|
@@ -2074,8 +2743,14 @@ var editFileTool = {
|
|
|
2074
2743
|
};
|
|
2075
2744
|
}
|
|
2076
2745
|
if (count > 1 && !a.replaceAll) {
|
|
2746
|
+
const lines = content.split("\n");
|
|
2747
|
+
const at = [];
|
|
2748
|
+
for (let i = 0; i < lines.length && at.length <= MAX_MATCH_LINES; i++) {
|
|
2749
|
+
if (lines[i]?.includes(a.oldString.split("\n")[0] ?? "")) at.push(i + 1);
|
|
2750
|
+
}
|
|
2751
|
+
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" : ""}.` : "";
|
|
2077
2752
|
return {
|
|
2078
|
-
content: `edit_file: oldString
|
|
2753
|
+
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.`,
|
|
2079
2754
|
isError: true
|
|
2080
2755
|
};
|
|
2081
2756
|
}
|
|
@@ -2404,6 +3079,188 @@ async function commitRefreshed(git, baseWorktree, traceRootRel2) {
|
|
|
2404
3079
|
return r.code === 0;
|
|
2405
3080
|
}
|
|
2406
3081
|
|
|
3082
|
+
// src/engine/writer-registry.ts
|
|
3083
|
+
import { z as z14 } from "zod";
|
|
3084
|
+
|
|
3085
|
+
// src/engine/normalize-question.ts
|
|
3086
|
+
import { z as z13 } from "zod";
|
|
3087
|
+
var NormalizedQuestionSchema = z13.object({
|
|
3088
|
+
question: z13.string().describe("The core question, concise, WITHOUT the embedded options table/list."),
|
|
3089
|
+
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."),
|
|
3090
|
+
multiSelect: z13.boolean().describe("true only if the user may pick more than one.")
|
|
3091
|
+
});
|
|
3092
|
+
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.";
|
|
3093
|
+
function looksLikeChoices(text) {
|
|
3094
|
+
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);
|
|
3095
|
+
}
|
|
3096
|
+
async function normalizeQuestion(deps, raw) {
|
|
3097
|
+
const { role: agentRole, model, fallbacks, onExhausted, onFallback } = deps.roleRegistry.fallbackOpts("refiner");
|
|
3098
|
+
return runStructuredRole({
|
|
3099
|
+
provider: deps.provider,
|
|
3100
|
+
role: agentRole,
|
|
3101
|
+
model,
|
|
3102
|
+
fallbacks,
|
|
3103
|
+
onExhausted,
|
|
3104
|
+
onFallback,
|
|
3105
|
+
// This shapes the question the USER reads; a rule like "always ask in Turkish" belongs here.
|
|
3106
|
+
systemPrompt: PROMPT + deps.roleRegistry.ruleSuffix(),
|
|
3107
|
+
tools: new ToolRegistry(),
|
|
3108
|
+
messages: [{ role: "user", content: raw }],
|
|
3109
|
+
permission: deps.permission,
|
|
3110
|
+
approve: deps.approve,
|
|
3111
|
+
cwd: ".",
|
|
3112
|
+
signal: deps.signal
|
|
3113
|
+
}, NormalizedQuestionSchema);
|
|
3114
|
+
}
|
|
3115
|
+
function extractChoicesFrom(text) {
|
|
3116
|
+
const lines = text.split("\n");
|
|
3117
|
+
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()));
|
|
3118
|
+
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])));
|
|
3119
|
+
if (body.length >= 2) {
|
|
3120
|
+
const question = lines.filter((l) => !/^\s*\|.*\|\s*$/.test(l)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3121
|
+
return { choices: body.map((cells) => ({ label: cells[0], description: cells.slice(1).join(" \u2014 ") })), question };
|
|
3122
|
+
}
|
|
3123
|
+
const isLettered = (l) => /^\s*([A-Ea-e])[).\-:]\s+(\S.*)$/.exec(l.trim());
|
|
3124
|
+
const lettered = lines.map(isLettered).filter((m) => !!m).map((m) => ({ label: `${m[1].toUpperCase()} \u2014 ${m[2]}` }));
|
|
3125
|
+
if (lettered.length >= 2) {
|
|
3126
|
+
const question = lines.filter((l) => !isLettered(l)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3127
|
+
return { choices: lettered, question };
|
|
3128
|
+
}
|
|
3129
|
+
const MARKER = /\(([A-Ea-e])\)\s*/g;
|
|
3130
|
+
const marks = [...text.matchAll(MARKER)];
|
|
3131
|
+
const inOrder = marks.length >= 2 && marks.every((m, i) => m[1].toUpperCase() === String.fromCharCode(65 + i));
|
|
3132
|
+
if (inOrder) {
|
|
3133
|
+
const first = marks[0].index ?? 0;
|
|
3134
|
+
const parts = marks.map((m, i) => {
|
|
3135
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
3136
|
+
const end = i + 1 < marks.length ? marks[i + 1].index ?? text.length : text.length;
|
|
3137
|
+
return { letter: m[1].toUpperCase(), body: text.slice(start, end).trim().replace(/\s+/g, " ") };
|
|
3138
|
+
}).filter((p) => p.body);
|
|
3139
|
+
if (parts.length >= 2) {
|
|
3140
|
+
const question = text.slice(0, first).replace(/\s*(Se\u00e7enekler|Options|Choices)\s*:?\s*$/i, "").trim();
|
|
3141
|
+
return {
|
|
3142
|
+
choices: parts.map((p) => ({ label: `${p.letter} \u2014 ${clipLabel(p.body)}`, description: p.body })),
|
|
3143
|
+
question: question || text
|
|
3144
|
+
};
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
return { choices: [], question: text };
|
|
3148
|
+
}
|
|
3149
|
+
var clipLabel = (body) => {
|
|
3150
|
+
const cut = body.replace(/^\[[^\]]*\]\s*/, "");
|
|
3151
|
+
const stop = cut.search(/[—–.;:]\s/);
|
|
3152
|
+
const head = stop > 12 ? cut.slice(0, stop) : cut;
|
|
3153
|
+
return head.length > 72 ? `${head.slice(0, 71)}\u2026` : head;
|
|
3154
|
+
};
|
|
3155
|
+
|
|
3156
|
+
// src/engine/writer-registry.ts
|
|
3157
|
+
var askUserParams = z14.object({
|
|
3158
|
+
question: z14.string(),
|
|
3159
|
+
// For a multiple-choice question, list the choices here → the UI shows a selectable checkbox/radio list
|
|
3160
|
+
// (arrow keys + Enter) instead of a free-text box. Omit for an open-ended question.
|
|
3161
|
+
//
|
|
3162
|
+
// A choice may be a plain string, or an object carrying what the label alone cannot say: a one-line
|
|
3163
|
+
// `description`, and a `preview` rendered in a panel beside the list while that option is focused. Use the
|
|
3164
|
+
// rich form when the decision turns on the trade-offs rather than the name (e.g. "which approach?").
|
|
3165
|
+
options: z14.array(z14.union([
|
|
3166
|
+
z14.string(),
|
|
3167
|
+
z14.object({ label: z14.string(), description: z14.string().optional(), preview: z14.string().optional() })
|
|
3168
|
+
])).optional().describe(
|
|
3169
|
+
"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."
|
|
3170
|
+
),
|
|
3171
|
+
multiSelect: z14.boolean().optional().describe(
|
|
3172
|
+
"True when the user may pick more than one (checkboxes); omitted means pick exactly one (radio)."
|
|
3173
|
+
),
|
|
3174
|
+
/**
|
|
3175
|
+
* What the user has to DO before they can answer — one action per entry.
|
|
3176
|
+
*
|
|
3177
|
+
* Present ⇒ this is a hand-off, not a question: the run has stopped because only a person can carry the
|
|
3178
|
+
* next step, and the UI says so rather than showing a bare "? Question".
|
|
3179
|
+
*/
|
|
3180
|
+
steps: z14.array(z14.string()).optional().describe(
|
|
3181
|
+
'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.'
|
|
3182
|
+
)
|
|
3183
|
+
});
|
|
3184
|
+
var POINTS_ELSEWHERE = /\b(above|below|earlier|previously|as listed|as described)\b|yukarı|aşağı|altında|önceki|birazdan|listelenen/i;
|
|
3185
|
+
var ITEM_REFERENCE = /(?:^|[^\w])(?:q|soru|question|madde|item)\s*\.?\s*(\d{1,2})\b/gi;
|
|
3186
|
+
var ITEM_STATED = /^[\s>*\-–—#]*(?:q|soru|question|madde|item)\s*\.?\s*\d{1,2}\s*[:).]/gim;
|
|
3187
|
+
function danglingItems(question) {
|
|
3188
|
+
const referenced = new Set([...question.matchAll(ITEM_REFERENCE)].map((m) => Number(m[1])));
|
|
3189
|
+
if (referenced.size < 2) return [];
|
|
3190
|
+
const stated = question.match(ITEM_STATED)?.length ?? 0;
|
|
3191
|
+
return stated >= referenced.size ? [] : [...referenced].sort((a, b) => a - b);
|
|
3192
|
+
}
|
|
3193
|
+
var ENUMERATOR = /(?:^|[^\w])\(?(\d{1,2})[).:]/g;
|
|
3194
|
+
function packedQuestions(question) {
|
|
3195
|
+
if ((question.match(/\?/g)?.length ?? 0) < 2) return 0;
|
|
3196
|
+
const numbered = new Set([...question.matchAll(ENUMERATOR)].map((m) => Number(m[1])));
|
|
3197
|
+
return numbered.size;
|
|
3198
|
+
}
|
|
3199
|
+
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.";
|
|
3200
|
+
function buildAskUserTool(askUser, normalize) {
|
|
3201
|
+
return {
|
|
3202
|
+
name: "ask_user",
|
|
3203
|
+
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.',
|
|
3204
|
+
permissionLevel: "safe",
|
|
3205
|
+
parameters: askUserParams,
|
|
3206
|
+
run: async (rawArgs, ctx) => {
|
|
3207
|
+
const parsed = askUserParams.safeParse(rawArgs);
|
|
3208
|
+
if (!parsed.success) {
|
|
3209
|
+
return { content: `ask_user: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
|
|
3210
|
+
}
|
|
3211
|
+
const { question, options, multiSelect, steps } = parsed.data;
|
|
3212
|
+
const asker = ctx.role || ctx.model ? { asker: { ...ctx.role ? { role: ctx.role } : {}, ...ctx.model ? { model: ctx.model } : {} } } : {};
|
|
3213
|
+
if (POINTS_ELSEWHERE.test(question) && !steps?.length && !ctx.said?.trim()) {
|
|
3214
|
+
return {
|
|
3215
|
+
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.",
|
|
3216
|
+
isError: true
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
const dangling = danglingItems(question);
|
|
3220
|
+
if (dangling.length && !steps?.length && !ctx.said?.trim()) {
|
|
3221
|
+
return {
|
|
3222
|
+
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}`,
|
|
3223
|
+
isError: true
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
const packed = packedQuestions(question);
|
|
3227
|
+
if (packed >= 2 && !steps?.length && !options?.length) {
|
|
3228
|
+
return {
|
|
3229
|
+
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}`,
|
|
3230
|
+
isError: true
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
if ((!options || options.length === 0) && !steps?.length && looksLikeChoices(question)) {
|
|
3234
|
+
const found = extractChoicesFrom(question);
|
|
3235
|
+
if (found.choices.length >= 2) {
|
|
3236
|
+
return { content: await askUser(found.question, { options: found.choices, ...asker }), isError: false };
|
|
3237
|
+
}
|
|
3238
|
+
if (normalize) {
|
|
3239
|
+
try {
|
|
3240
|
+
const n = await normalize(question);
|
|
3241
|
+
if (n.options.length > 0) {
|
|
3242
|
+
return { content: await askUser(n.question, { options: n.options, multiSelect: n.multiSelect, ...asker }), isError: false };
|
|
3243
|
+
}
|
|
3244
|
+
} catch {
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
return { content: await askUser(question, { options, multiSelect, steps, ...asker }), isError: false };
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
function writerRegistry(skillRegistry, extra = []) {
|
|
3253
|
+
const r = new ToolRegistry();
|
|
3254
|
+
r.register(readFileTool);
|
|
3255
|
+
r.register(writeFileTool);
|
|
3256
|
+
r.register(editFileTool);
|
|
3257
|
+
r.register(grepTool);
|
|
3258
|
+
r.register(globTool);
|
|
3259
|
+
r.register(buildSkillTool(skillRegistry));
|
|
3260
|
+
for (const t of extra) r.register(t);
|
|
3261
|
+
return r;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
2407
3264
|
// src/engine/role-fitness.ts
|
|
2408
3265
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
2409
3266
|
import { dirname as dirname4 } from "path";
|
|
@@ -2519,7 +3376,7 @@ var RoleFitness = class {
|
|
|
2519
3376
|
};
|
|
2520
3377
|
|
|
2521
3378
|
// src/engine/routing.ts
|
|
2522
|
-
import { z as
|
|
3379
|
+
import { z as z15 } from "zod";
|
|
2523
3380
|
|
|
2524
3381
|
// src/engine/route-role.ts
|
|
2525
3382
|
var STYLE_EXT = [".css", ".scss", ".sass", ".less", ".styl"];
|
|
@@ -2585,27 +3442,9 @@ function routeByEvidence(card) {
|
|
|
2585
3442
|
return { why: hasComponent && !hasStyle && !hasMarkup ? "component files could be either" : "the evidence points both ways" };
|
|
2586
3443
|
}
|
|
2587
3444
|
|
|
2588
|
-
// src/agent/deadline.ts
|
|
2589
|
-
function withDeadline(work, signal, message) {
|
|
2590
|
-
work.catch(() => {
|
|
2591
|
-
});
|
|
2592
|
-
return Promise.race([work, expired(signal, message)]);
|
|
2593
|
-
}
|
|
2594
|
-
function expired(signal, message) {
|
|
2595
|
-
return new Promise((_, reject) => {
|
|
2596
|
-
if (signal.aborted) {
|
|
2597
|
-
reject(new Error(message));
|
|
2598
|
-
return;
|
|
2599
|
-
}
|
|
2600
|
-
signal.addEventListener("abort", () => reject(new Error(message)), { once: true });
|
|
2601
|
-
});
|
|
2602
|
-
}
|
|
2603
|
-
var SHORT_CALL_MS = 3 * 60 * 1e3;
|
|
2604
|
-
var LONG_CALL_MS = 15 * 60 * 1e3;
|
|
2605
|
-
|
|
2606
3445
|
// src/engine/routing.ts
|
|
2607
|
-
var RouteSchema =
|
|
2608
|
-
role:
|
|
3446
|
+
var RouteSchema = z15.object({
|
|
3447
|
+
role: z15.enum(["coder", "designer"]).describe(
|
|
2609
3448
|
"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."
|
|
2610
3449
|
)
|
|
2611
3450
|
});
|
|
@@ -2667,9 +3506,9 @@ function createDefaultRegistry() {
|
|
|
2667
3506
|
}
|
|
2668
3507
|
|
|
2669
3508
|
// src/engine/operational.ts
|
|
2670
|
-
import { z as
|
|
2671
|
-
var CommitSchema =
|
|
2672
|
-
message:
|
|
3509
|
+
import { z as z16 } from "zod";
|
|
3510
|
+
var CommitSchema = z16.object({
|
|
3511
|
+
message: z16.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
|
|
2673
3512
|
});
|
|
2674
3513
|
var MAX_DIFF = 12e3;
|
|
2675
3514
|
var OPERATIONAL_MAX_TURNS = 3;
|
|
@@ -2766,6 +3605,39 @@ async function squashTask(deps, worktree, baseRef, title, git = defaultGitRunner
|
|
|
2766
3605
|
return message;
|
|
2767
3606
|
}
|
|
2768
3607
|
|
|
3608
|
+
// src/engine/touched.ts
|
|
3609
|
+
async function changedPaths(cwd, git = defaultGitRunner) {
|
|
3610
|
+
const res = await git(["status", "--porcelain", "-uall"], cwd);
|
|
3611
|
+
if (res.code !== 0) return [];
|
|
3612
|
+
const out = [];
|
|
3613
|
+
for (const line of res.stdout.split("\n")) {
|
|
3614
|
+
if (!line.trim()) continue;
|
|
3615
|
+
const path = line.slice(3).trim();
|
|
3616
|
+
const arrow = path.lastIndexOf(" -> ");
|
|
3617
|
+
const p = arrow >= 0 ? path.slice(arrow + 4) : path;
|
|
3618
|
+
out.push(p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p);
|
|
3619
|
+
}
|
|
3620
|
+
return out;
|
|
3621
|
+
}
|
|
3622
|
+
async function reconcileTouched(deps, cwd, touched, git = defaultGitRunner) {
|
|
3623
|
+
let changed;
|
|
3624
|
+
try {
|
|
3625
|
+
changed = await changedPaths(cwd, git);
|
|
3626
|
+
} catch {
|
|
3627
|
+
return [];
|
|
3628
|
+
}
|
|
3629
|
+
const known = new Set(touched);
|
|
3630
|
+
const extra = changed.filter((p) => !known.has(p));
|
|
3631
|
+
for (const p of extra) {
|
|
3632
|
+
touched.push(p);
|
|
3633
|
+
try {
|
|
3634
|
+
await commitFile(deps, cwd, p, git);
|
|
3635
|
+
} catch {
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
return extra;
|
|
3639
|
+
}
|
|
3640
|
+
|
|
2769
3641
|
// src/skills/adjudicate.ts
|
|
2770
3642
|
async function adjudicateSkills(opts) {
|
|
2771
3643
|
const { confident, borderline } = partitionByConfidence(opts.matches, opts.bar, opts.margin);
|
|
@@ -2820,6 +3692,42 @@ function parseKeep(text) {
|
|
|
2820
3692
|
}
|
|
2821
3693
|
}
|
|
2822
3694
|
|
|
3695
|
+
// src/engine/group-notes.ts
|
|
3696
|
+
function subjectOf(note) {
|
|
3697
|
+
const path = /\b((?:[\w.-]+\/)+[\w.-]+\.(?:cs|ts|tsx|json|sql|md))\b/.exec(note)?.[1];
|
|
3698
|
+
if (path) return path.split("/").pop().replace(/\.\w+$/, "");
|
|
3699
|
+
return /\b([A-Z][a-z]+(?:[A-Z][a-z0-9]+){1,})\b/.exec(note)?.[1];
|
|
3700
|
+
}
|
|
3701
|
+
function groupNotes(notes) {
|
|
3702
|
+
const order = [];
|
|
3703
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
3704
|
+
const loose = [];
|
|
3705
|
+
for (const note of notes) {
|
|
3706
|
+
const s = subjectOf(note);
|
|
3707
|
+
if (!s) {
|
|
3708
|
+
loose.push(note);
|
|
3709
|
+
continue;
|
|
3710
|
+
}
|
|
3711
|
+
if (!bySubject.has(s)) {
|
|
3712
|
+
bySubject.set(s, []);
|
|
3713
|
+
order.push(s);
|
|
3714
|
+
}
|
|
3715
|
+
bySubject.get(s).push(note);
|
|
3716
|
+
}
|
|
3717
|
+
const blocks = [];
|
|
3718
|
+
for (const s of order) {
|
|
3719
|
+
const group = bySubject.get(s);
|
|
3720
|
+
if (group.length === 1) {
|
|
3721
|
+
loose.push(group[0]);
|
|
3722
|
+
continue;
|
|
3723
|
+
}
|
|
3724
|
+
blocks.push(`${s} \u2014 ${group.length} notes, one subject:
|
|
3725
|
+
${group.map((n) => ` - ${n}`).join("\n")}`);
|
|
3726
|
+
}
|
|
3727
|
+
if (loose.length) blocks.push(loose.map((n) => `- ${n}`).join("\n"));
|
|
3728
|
+
return blocks.join("\n\n");
|
|
3729
|
+
}
|
|
3730
|
+
|
|
2823
3731
|
// src/engine/implementer.ts
|
|
2824
3732
|
var IMPLEMENTER_MAX_TURNS = 200;
|
|
2825
3733
|
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.";
|
|
@@ -2870,6 +3778,7 @@ async function runImplementer(deps, role, task, cwd, slot = 0) {
|
|
|
2870
3778
|
tools.register(buildSkillTool(deps.skillRegistry));
|
|
2871
3779
|
for (const t of contextTools(deps)) tools.register(t);
|
|
2872
3780
|
tools.register(buildRememberTool(deps.rememberFact));
|
|
3781
|
+
if (deps.askUser) tools.register(buildAskUserTool(deps.askUser));
|
|
2873
3782
|
const returning = task.reviewNotes.length > 0;
|
|
2874
3783
|
const brief = [
|
|
2875
3784
|
task.acceptance.length ? `It is done when ALL of these are true \u2014 they are exactly what the review will check:
|
|
@@ -2885,7 +3794,7 @@ ${WHAT_IT_COST}
|
|
|
2885
3794
|
|
|
2886
3795
|
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.`;
|
|
2887
3796
|
const content = (returning ? `This is a RETURNING task: "${task.title}". Address the reviewer notes:
|
|
2888
|
-
${task.reviewNotes
|
|
3797
|
+
${groupNotes(task.reviewNotes)}` : `This is a NEW task: "${task.title}". Implement it.`) + (brief ? `
|
|
2889
3798
|
|
|
2890
3799
|
${brief}` : "") + `
|
|
2891
3800
|
|
|
@@ -2895,7 +3804,7 @@ ${handOver}`;
|
|
|
2895
3804
|
const hints = memoryHints(deps, `${task.title} ${task.reviewNotes.join(" ")}`, { role, operations: true });
|
|
2896
3805
|
const touched = [];
|
|
2897
3806
|
const attached = deps.roleRegistry.skillsFor(role);
|
|
2898
|
-
const subject = `${task.title} ${task.acceptance.join(" ")}
|
|
3807
|
+
const subject = `${task.title} ${task.acceptance.join(" ")}`;
|
|
2899
3808
|
const routed = routeSkills(subject, deps.skillRegistry, attached, {
|
|
2900
3809
|
role,
|
|
2901
3810
|
implementing: true,
|
|
@@ -2944,7 +3853,8 @@ ${handOver}`;
|
|
|
2944
3853
|
cwd,
|
|
2945
3854
|
{ role, files: task.files, title: task.title }
|
|
2946
3855
|
) : "";
|
|
2947
|
-
const
|
|
3856
|
+
const delegate = deps.delegateTo ? cliFor(chain[0] ?? "") ?? deps.delegateTo : void 0;
|
|
3857
|
+
const systemPrompt = withSkills + law + (delegate ? "" : projectToolsNote(tools.list(), !!loadGraphSync(cwd))) + BATCH_TOOLS_NOTE;
|
|
2948
3858
|
const budgetMs = attemptBudget(task, deps.implementerTimeoutMs ?? IMPLEMENTER_TIMEOUT_MS);
|
|
2949
3859
|
const budget = AbortSignal.timeout(budgetMs);
|
|
2950
3860
|
let warned = false;
|
|
@@ -2956,7 +3866,12 @@ ${handOver}`;
|
|
|
2956
3866
|
return note;
|
|
2957
3867
|
};
|
|
2958
3868
|
const opts = {
|
|
2959
|
-
|
|
3869
|
+
/**
|
|
3870
|
+
* A delegated implementer gets a provider bound to its worktree and allowed to write; every other role
|
|
3871
|
+
* keeps the one the composition root built. Per call rather than shared, because the worktree differs
|
|
3872
|
+
* per task and a provider shared across parallel implementers could only hold one of them.
|
|
3873
|
+
*/
|
|
3874
|
+
provider: delegate ? new CliProvider({ kind: delegate, readOnly: false, cwd, ...deps.accounts ? { accounts: deps.accounts } : {} }) : deps.provider,
|
|
2960
3875
|
...resolved,
|
|
2961
3876
|
systemPrompt,
|
|
2962
3877
|
...chain.length ? { model: chain[0], fallbacks: chain.slice(1) } : {},
|
|
@@ -3029,6 +3944,10 @@ ${handOver}`;
|
|
|
3029
3944
|
if (deps.signal.aborted || !budget.aborted) throw e;
|
|
3030
3945
|
throw new Error(overran);
|
|
3031
3946
|
} finally {
|
|
3947
|
+
const alsoChanged = await reconcileTouched(deps, cwd, touched);
|
|
3948
|
+
if (alsoChanged.length) {
|
|
3949
|
+
deps.note?.(`\u{1F4DD} ${alsoChanged.length} file(s) changed outside a write tool \u2014 checkpointed and credited.`);
|
|
3950
|
+
}
|
|
3032
3951
|
reinforceTouched(deps, hints.ids, touched, role);
|
|
3033
3952
|
reinforceUsed(deps, hints.ids, await writtenText(cwd, touched), role);
|
|
3034
3953
|
}
|
|
@@ -3037,7 +3956,7 @@ ${handOver}`;
|
|
|
3037
3956
|
// src/engine/review.ts
|
|
3038
3957
|
import { existsSync as existsSync8 } from "fs";
|
|
3039
3958
|
import { isAbsolute, join as join8 } from "path";
|
|
3040
|
-
import { z as
|
|
3959
|
+
import { z as z17 } from "zod";
|
|
3041
3960
|
|
|
3042
3961
|
// src/agent/roles.ts
|
|
3043
3962
|
function isTransientFailure(reason) {
|
|
@@ -3045,6 +3964,45 @@ function isTransientFailure(reason) {
|
|
|
3045
3964
|
if (/\b(429|rate.?limit|quota|exhaust|insufficient|billing|credit)\b/.test(r)) return false;
|
|
3046
3965
|
return /overload|529|50[0234]|timeout|timed out|deadline|econnreset|epipe|socket hang up|stream ended|temporar|unavailable|try again/.test(r);
|
|
3047
3966
|
}
|
|
3967
|
+
function isSourceCapacity(reason) {
|
|
3968
|
+
return /capacity is (?:temporarily unavailable|busy)/i.test(reason);
|
|
3969
|
+
}
|
|
3970
|
+
function sourcePrefix(model) {
|
|
3971
|
+
const s = model.replace(/^no-think\//, "");
|
|
3972
|
+
const cli = cliFor(s);
|
|
3973
|
+
if (cli) return cli;
|
|
3974
|
+
const i = s.indexOf("/");
|
|
3975
|
+
return i > 0 ? s.slice(0, i) : void 0;
|
|
3976
|
+
}
|
|
3977
|
+
function weightedCycle(sources, weights) {
|
|
3978
|
+
const queues = sources.map((s) => Array(Math.max(1, weights[s] ?? 1)).fill(s));
|
|
3979
|
+
const out = [];
|
|
3980
|
+
for (let more = true; more; ) {
|
|
3981
|
+
more = false;
|
|
3982
|
+
for (const q of queues) {
|
|
3983
|
+
const m = q.shift();
|
|
3984
|
+
if (m !== void 0) {
|
|
3985
|
+
out.push(m);
|
|
3986
|
+
more = true;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
return out;
|
|
3991
|
+
}
|
|
3992
|
+
function canonicalSource(name) {
|
|
3993
|
+
const s = name.toLowerCase().replace(/^no-think\//, "");
|
|
3994
|
+
if (s === "cc" || s === "claude") return "claude";
|
|
3995
|
+
if (s === "cx") return "codex";
|
|
3996
|
+
return sourcePrefix(s) ?? s;
|
|
3997
|
+
}
|
|
3998
|
+
function providerOutage(reason) {
|
|
3999
|
+
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|grok|zai)\s+CLI:\s*rejected\b/i.exec(reason)?.[1]?.toLowerCase();
|
|
4000
|
+
}
|
|
4001
|
+
function quotaResetAt(reason) {
|
|
4002
|
+
const iso = /\(resets\s+([0-9T:.\-]+Z)\)/i.exec(reason)?.[1];
|
|
4003
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
4004
|
+
return Number.isFinite(t) ? t : void 0;
|
|
4005
|
+
}
|
|
3048
4006
|
var RoleRegistry = class _RoleRegistry {
|
|
3049
4007
|
// durable behavioral rules → appended to EVERY role's prompt
|
|
3050
4008
|
constructor(roles, defaultPrompts = {}, skillRegistry) {
|
|
@@ -3136,6 +4094,27 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3136
4094
|
this.quarantine.set(model, { at: now, reason, ...ends !== void 0 && { until: ends } });
|
|
3137
4095
|
this.onQuarantine?.(model, reason, ends);
|
|
3138
4096
|
}
|
|
4097
|
+
/** Every model any role's chain names — the pool this registry can actually reach for. */
|
|
4098
|
+
knownModels() {
|
|
4099
|
+
return [...new Set(Object.values(this.roles).flatMap((r) => r.models ?? []))];
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Benches every model of one provider, for a failure that is about the provider itself.
|
|
4103
|
+
*
|
|
4104
|
+
* Returns what it took out, so the caller can say so once instead of six times. Falls back to benching the
|
|
4105
|
+
* single model when the pool names none of that provider — an unknown provider is still a real failure.
|
|
4106
|
+
*/
|
|
4107
|
+
markProviderExhausted(provider, model, reason, now = Date.now()) {
|
|
4108
|
+
const want = canonicalSource(provider);
|
|
4109
|
+
const hit = this.knownModels().filter((m) => sourcePrefix(m) === want);
|
|
4110
|
+
const until = quotaResetAt(reason);
|
|
4111
|
+
for (const m of hit) this.markExhausted(m, reason, now, until);
|
|
4112
|
+
if (!hit.length) {
|
|
4113
|
+
this.markExhausted(model, reason, now);
|
|
4114
|
+
return [model];
|
|
4115
|
+
}
|
|
4116
|
+
return hit;
|
|
4117
|
+
}
|
|
3139
4118
|
/**
|
|
3140
4119
|
* How long a BEHAVIOURAL bench lasts before the model is tried again.
|
|
3141
4120
|
*
|
|
@@ -3266,9 +4245,28 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3266
4245
|
*/
|
|
3267
4246
|
chainFor(roleName, slot = 0) {
|
|
3268
4247
|
const c = this.chain(roleName);
|
|
3269
|
-
|
|
4248
|
+
if (c.length < 2) return c;
|
|
4249
|
+
const order = [];
|
|
4250
|
+
for (const m of c) {
|
|
4251
|
+
const s = sourceOf(m);
|
|
4252
|
+
if (!order.includes(s)) order.push(s);
|
|
4253
|
+
}
|
|
4254
|
+
const cycle = weightedCycle(order, this.sourceWeights?.() ?? {});
|
|
4255
|
+
if (cycle.length) {
|
|
4256
|
+
const want = cycle[(slot % cycle.length + cycle.length) % cycle.length];
|
|
4257
|
+
const i = c.findIndex((m) => sourceOf(m) === want);
|
|
4258
|
+
if (i > 0) return [c[i], ...c.filter((_, j) => j !== i)];
|
|
4259
|
+
if (i === 0) return c;
|
|
4260
|
+
}
|
|
4261
|
+
const k = (slot % c.length + c.length) % c.length;
|
|
3270
4262
|
return k === 0 ? c : [...c.slice(k), ...c.slice(0, k)];
|
|
3271
4263
|
}
|
|
4264
|
+
/** How many accounts each source has connected — set at the composition root; equal weights without it. */
|
|
4265
|
+
sourceWeights;
|
|
4266
|
+
/** Wire the account weights (called once the pool exists). */
|
|
4267
|
+
setSourceWeights(fn) {
|
|
4268
|
+
this.sourceWeights = fn;
|
|
4269
|
+
}
|
|
3272
4270
|
/** The model a role would use next (chain head), for UI display only. */
|
|
3273
4271
|
peekModel(roleName) {
|
|
3274
4272
|
return this.chain(roleName)[0] ?? "";
|
|
@@ -3296,7 +4294,12 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3296
4294
|
role: roleName,
|
|
3297
4295
|
model: chain[0] ?? "",
|
|
3298
4296
|
fallbacks: chain.slice(1),
|
|
3299
|
-
onExhausted: (m, reason) =>
|
|
4297
|
+
onExhausted: (m, reason) => {
|
|
4298
|
+
const why = reason ?? "unavailable";
|
|
4299
|
+
const source = providerOutage(why) ?? (isSourceCapacity(why) ? sourcePrefix(m) : void 0);
|
|
4300
|
+
if (source) this.markProviderExhausted(source, m, why);
|
|
4301
|
+
else this.markExhausted(m, why);
|
|
4302
|
+
},
|
|
3300
4303
|
onStructuralFailure: (m, reason) => this.markStructuralFailure(m, reason, roleName),
|
|
3301
4304
|
onFallback: notify ? (from, to, reason) => notify(`\u2935 \`${from}\` \u2192 \`${to}\` \u2014 ${reason}`) : void 0
|
|
3302
4305
|
};
|
|
@@ -3330,14 +4333,14 @@ ${rules.map((r) => `- ${r}`).join("\n")}` : "";
|
|
|
3330
4333
|
function asChoice(o) {
|
|
3331
4334
|
return typeof o === "string" ? { label: o } : o;
|
|
3332
4335
|
}
|
|
3333
|
-
var AssessmentSchema =
|
|
3334
|
-
findings:
|
|
3335
|
-
severity:
|
|
4336
|
+
var AssessmentSchema = z17.object({
|
|
4337
|
+
findings: z17.array(z17.object({
|
|
4338
|
+
severity: z17.enum(["critical", "medium", "low"]).describe(
|
|
3336
4339
|
"`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."
|
|
3337
4340
|
),
|
|
3338
|
-
note:
|
|
4341
|
+
note: z17.string()
|
|
3339
4342
|
})).default([]),
|
|
3340
|
-
recommendation:
|
|
4343
|
+
recommendation: z17.enum(["approve", "revise"]).describe(
|
|
3341
4344
|
"`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."
|
|
3342
4345
|
)
|
|
3343
4346
|
});
|
|
@@ -3378,18 +4381,18 @@ function coverage(assessments) {
|
|
|
3378
4381
|
const verified = assessments.length - unverified;
|
|
3379
4382
|
return { verified, unverified, enough: !assessments.length || verified / assessments.length >= TEAM_MIN_COVERAGE };
|
|
3380
4383
|
}
|
|
3381
|
-
var CouncilVoteSchema =
|
|
3382
|
-
vote:
|
|
4384
|
+
var CouncilVoteSchema = z17.object({
|
|
4385
|
+
vote: z17.enum(["pass", "revise"]).describe(
|
|
3383
4386
|
"`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."
|
|
3384
4387
|
),
|
|
3385
|
-
rationale:
|
|
4388
|
+
rationale: z17.string()
|
|
3386
4389
|
});
|
|
3387
|
-
var JudgeSchema =
|
|
3388
|
-
decision:
|
|
4390
|
+
var JudgeSchema = z17.object({
|
|
4391
|
+
decision: z17.enum(["pass", "revise", "ask-human"]).describe(
|
|
3389
4392
|
"`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."
|
|
3390
4393
|
),
|
|
3391
|
-
feedback:
|
|
3392
|
-
question:
|
|
4394
|
+
feedback: z17.array(z17.string()),
|
|
4395
|
+
question: z17.string()
|
|
3393
4396
|
});
|
|
3394
4397
|
var STAGE_FRAMING = {
|
|
3395
4398
|
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.
|
|
@@ -3401,7 +4404,7 @@ OUT OF SCOPE here: re-litigating the approved spec or plan, and demanding refact
|
|
|
3401
4404
|
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.`
|
|
3402
4405
|
};
|
|
3403
4406
|
var REVIEW_MAX_TURNS = 15;
|
|
3404
|
-
var REVIEW_TIMEOUT_MS =
|
|
4407
|
+
var REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3405
4408
|
function isDeadlineFailure(e) {
|
|
3406
4409
|
return /within its deadline|timed? ?out/i.test(e instanceof Error ? e.message : String(e));
|
|
3407
4410
|
}
|
|
@@ -3708,6 +4711,17 @@ ${ask}` };
|
|
|
3708
4711
|
return d;
|
|
3709
4712
|
}
|
|
3710
4713
|
var TEAM_CONSENSUS = 0.7;
|
|
4714
|
+
var CORROBORATION_FLOOR = 5;
|
|
4715
|
+
function corroboratedCriticals(assessments) {
|
|
4716
|
+
const criticals = assessments.flatMap((a) => a.findings.filter((f) => f.severity === "critical").map((f) => ({ lens: a.name, note: f.note })));
|
|
4717
|
+
if (assessments.length < CORROBORATION_FLOOR) return criticals.length;
|
|
4718
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
4719
|
+
for (const c of criticals) {
|
|
4720
|
+
const key2 = subjectOf(c.note) ?? `${c.lens}:${c.note.slice(0, 40)}`;
|
|
4721
|
+
(bySubject.get(key2) ?? bySubject.set(key2, /* @__PURE__ */ new Set()).get(key2)).add(c.lens);
|
|
4722
|
+
}
|
|
4723
|
+
return [...bySubject.values()].filter((lenses) => lenses.size > 1).length;
|
|
4724
|
+
}
|
|
3711
4725
|
var REVIEW_SIGNATURES_LOGGED = 8;
|
|
3712
4726
|
var SIGNATURE_CHARS = 120;
|
|
3713
4727
|
var COUNCIL_SUPERMAJORITY = 0.8;
|
|
@@ -3886,10 +4900,20 @@ async function changeUnderReview(deps, workdir) {
|
|
|
3886
4900
|
return workingTreeDiff(workdir);
|
|
3887
4901
|
}
|
|
3888
4902
|
async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
3889
|
-
}, attempt = 0) {
|
|
4903
|
+
}, attempt = 0, cleared = []) {
|
|
3890
4904
|
const diff = await changeUnderReview(deps, workdir);
|
|
3891
|
-
const
|
|
4905
|
+
const scoped = lensesFor(deps.teams.code, diff);
|
|
4906
|
+
const remaining = cleared.length ? scoped.filter((c) => !cleared.includes(c.name)) : scoped;
|
|
4907
|
+
const named = scoped.filter((c) => CORE_CODE_LENSES.includes(c.name));
|
|
4908
|
+
const core = named.length ? named : scoped.slice(0, CORE_CODE_LENSES.length);
|
|
4909
|
+
const team = remaining.length ? remaining : core;
|
|
3892
4910
|
const scaled = team.length < deps.teams.code.length;
|
|
4911
|
+
if (!remaining.length) {
|
|
4912
|
+
emit({ kind: "note", text: `\u{1F465} **Team** \u2014 every lens had approved; re-running the ${team.length} core lens(es) on the new code.` });
|
|
4913
|
+
}
|
|
4914
|
+
if (cleared.length) {
|
|
4915
|
+
emit({ kind: "note", text: `\u{1F465} **Team** \u2014 ${team.length} lens(es) to re-run; ${cleared.length} approved on an earlier attempt.` });
|
|
4916
|
+
}
|
|
3893
4917
|
telemetry().event("decision.review_scale", {
|
|
3894
4918
|
"hc.decision": "review_scale",
|
|
3895
4919
|
"hc.changed_lines": changedLines(diff),
|
|
@@ -3903,22 +4927,30 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
|
3903
4927
|
const approve = assessments.filter((a) => a.recommendation === "approve").length;
|
|
3904
4928
|
const crit = severityTotal(assessments, "critical");
|
|
3905
4929
|
const med = severityTotal(assessments, "medium");
|
|
4930
|
+
const approvedLenses = [
|
|
4931
|
+
...cleared,
|
|
4932
|
+
...assessments.filter((a) => !a.unverified && !a.findings.some((f) => f.severity === "critical")).map((a) => a.name)
|
|
4933
|
+
];
|
|
3906
4934
|
const cover = coverage(assessments);
|
|
3907
4935
|
if (!cover.enough) {
|
|
3908
4936
|
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.`;
|
|
3909
4937
|
emit({ kind: "note", text: `\u26A0\uFE0F **Code review** \u2014 ${why}` });
|
|
3910
4938
|
return { verdict: "fail", notes: [why] };
|
|
3911
4939
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
4940
|
+
const corroborated = corroboratedCriticals(assessments);
|
|
4941
|
+
if (crit > 0 && corroborated === 0) {
|
|
4942
|
+
emit({ kind: "note", text: `\u2705 **Team** \u2014 ${crit} critical finding(s), none corroborated by a second lens \u2192 deferred to the revision pass.` });
|
|
4943
|
+
}
|
|
4944
|
+
if (corroborated === 0) {
|
|
4945
|
+
const deferred = [...findingNotes(assessments, "code", ["critical"]), ...nonBlockingNotes(assessments, "code")];
|
|
3914
4946
|
if (!deferred.length) {
|
|
3915
4947
|
if (attempt > 0 || !assessments.length || approve / assessments.length >= TEAM_CONSENSUS) {
|
|
3916
4948
|
emit({ kind: "note", text: `\u2705 **Team** \u2014 nothing to fix \u2192 the code passed.` });
|
|
3917
|
-
return { verdict: "pass", notes: [] };
|
|
4949
|
+
return { verdict: "pass", notes: [], approvedLenses };
|
|
3918
4950
|
}
|
|
3919
4951
|
}
|
|
3920
|
-
emit({ kind: "note", text: `\u2705 **Team** \u2014
|
|
3921
|
-
return { verdict: "pass", notes: [], deferred };
|
|
4952
|
+
emit({ kind: "note", text: `\u2705 **Team** \u2014 nothing corroborated; ${deferred.length} note(s) deferred to the revision pass.` });
|
|
4953
|
+
return { verdict: "pass", notes: [], deferred, approvedLenses };
|
|
3922
4954
|
}
|
|
3923
4955
|
const reason = crit || med ? `surfaced ${crit} critical / ${med} medium finding(s)` : `is split (${approve}/${assessments.length} approve)`;
|
|
3924
4956
|
emit({ kind: "note", text: `\u{1F465} **Team** ${reason} \u2192 handed the decision to the **council** (${deps.council.length} members vote).` });
|
|
@@ -3928,25 +4960,122 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
|
|
|
3928
4960
|
const blocking = assessments.flatMap((a) => a.findings.filter((f) => f.severity !== "low").map((f) => `[${f.severity}] ${a.name}: ${f.note}`));
|
|
3929
4961
|
if (tally === "pass") {
|
|
3930
4962
|
emit({ kind: "note", text: `\u2705 **Council** voted to approve (${passVotes}/${votes.length} pass) \u2192 the code passed.` });
|
|
3931
|
-
return { verdict: "pass", notes: [] };
|
|
4963
|
+
return { verdict: "pass", notes: [], approvedLenses };
|
|
3932
4964
|
}
|
|
3933
4965
|
if (tally === "revise") {
|
|
3934
4966
|
emit({ kind: "note", text: `\u{1F504} **Council** voted to revise (${votes.length - passVotes}/${votes.length}) \u2192 sending the code back.` });
|
|
3935
|
-
return {
|
|
4967
|
+
return {
|
|
4968
|
+
verdict: "fail",
|
|
4969
|
+
approvedLenses,
|
|
4970
|
+
notes: blocking.length ? blocking : votes.filter((v) => v.vote === "revise").map((v) => v.rationale)
|
|
4971
|
+
};
|
|
3936
4972
|
}
|
|
3937
4973
|
emit({ kind: "note", text: `\u{1F528} **Council** was split (${passVotes}/${votes.length} pass) \u2192 deferred the final decision to the **judge**.` });
|
|
3938
4974
|
const d = await runJudge(deps, "code", workdir, taskTitle, assessments, votes, request, emit);
|
|
3939
|
-
if (d.decision === "pass") return { verdict: "pass", notes: [] };
|
|
3940
|
-
return { verdict: "fail", notes: d.feedback.length ? d.feedback : blocking };
|
|
4975
|
+
if (d.decision === "pass") return { verdict: "pass", notes: [], approvedLenses };
|
|
4976
|
+
return { verdict: "fail", approvedLenses, notes: d.feedback.length ? d.feedback : blocking };
|
|
3941
4977
|
}
|
|
3942
4978
|
|
|
3943
4979
|
// src/engine/acceptance.ts
|
|
3944
|
-
import { z as
|
|
4980
|
+
import { z as z18 } from "zod";
|
|
4981
|
+
|
|
4982
|
+
// src/engine/criterion-commands.ts
|
|
4983
|
+
import { spawn as spawn2 } from "child_process";
|
|
4984
|
+
var RUNNABLE_COMMANDS = [
|
|
4985
|
+
"dotnet",
|
|
4986
|
+
"npm",
|
|
4987
|
+
"npx",
|
|
4988
|
+
"pnpm",
|
|
4989
|
+
"yarn",
|
|
4990
|
+
"nx",
|
|
4991
|
+
"prettier",
|
|
4992
|
+
"eslint",
|
|
4993
|
+
"tsc",
|
|
4994
|
+
"cargo",
|
|
4995
|
+
"go",
|
|
4996
|
+
"make",
|
|
4997
|
+
"mvn",
|
|
4998
|
+
"gradle"
|
|
4999
|
+
];
|
|
5000
|
+
var SHELL_SYNTAX = /[;&|><$(){}\n]|`/;
|
|
5001
|
+
var CRITERION_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5002
|
+
var MAX_OUTPUT = 2e3;
|
|
5003
|
+
function commandsIn(criterion) {
|
|
5004
|
+
const out = [];
|
|
5005
|
+
for (const m of criterion.matchAll(/`([^`\n]+)`/g)) {
|
|
5006
|
+
const span = m[1].trim();
|
|
5007
|
+
if (!span || SHELL_SYNTAX.test(span)) continue;
|
|
5008
|
+
const argv = span.split(/\s+/);
|
|
5009
|
+
if (!RUNNABLE_COMMANDS.includes(argv[0])) continue;
|
|
5010
|
+
out.push(argv);
|
|
5011
|
+
}
|
|
5012
|
+
return out;
|
|
5013
|
+
}
|
|
5014
|
+
async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
|
|
5015
|
+
const [bin, ...args] = argv;
|
|
5016
|
+
return new Promise((resolve6) => {
|
|
5017
|
+
let child;
|
|
5018
|
+
try {
|
|
5019
|
+
child = spawn2(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
|
|
5020
|
+
} catch (e) {
|
|
5021
|
+
resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e instanceof Error ? e.message : String(e) });
|
|
5022
|
+
return;
|
|
5023
|
+
}
|
|
5024
|
+
let out = "";
|
|
5025
|
+
const take = (d) => {
|
|
5026
|
+
out += d.toString();
|
|
5027
|
+
if (out.length > MAX_OUTPUT * 4) out = out.slice(-MAX_OUTPUT * 2);
|
|
5028
|
+
};
|
|
5029
|
+
child.stdout.on("data", take);
|
|
5030
|
+
child.stderr.on("data", take);
|
|
5031
|
+
let timedOut = false;
|
|
5032
|
+
const timer = setTimeout(() => {
|
|
5033
|
+
timedOut = true;
|
|
5034
|
+
child.kill("SIGKILL");
|
|
5035
|
+
}, timeoutMs);
|
|
5036
|
+
child.on("error", (e) => {
|
|
5037
|
+
clearTimeout(timer);
|
|
5038
|
+
resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
|
|
5039
|
+
});
|
|
5040
|
+
child.on("close", (code) => {
|
|
5041
|
+
clearTimeout(timer);
|
|
5042
|
+
resolve6({ argv, passed: !timedOut && code === 0, exitCode: code, timedOut, output: out.slice(-MAX_OUTPUT) });
|
|
5043
|
+
});
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
async function runCriterionCommands(cwd, criteria, timeoutMs = CRITERION_TIMEOUT_MS) {
|
|
5047
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5048
|
+
const argvs = [];
|
|
5049
|
+
for (const c of criteria) {
|
|
5050
|
+
for (const argv of commandsIn(c)) {
|
|
5051
|
+
const key2 = argv.join(" ");
|
|
5052
|
+
if (seen.has(key2)) continue;
|
|
5053
|
+
seen.add(key2);
|
|
5054
|
+
argvs.push(argv);
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
const runs = [];
|
|
5058
|
+
for (const argv of argvs) runs.push(await runCommand(cwd, argv, timeoutMs));
|
|
5059
|
+
return runs;
|
|
5060
|
+
}
|
|
5061
|
+
function describeCommandRuns(runs) {
|
|
5062
|
+
if (!runs.length) return "";
|
|
5063
|
+
const lines = runs.map((r) => {
|
|
5064
|
+
const cmd = `\`${r.argv.join(" ")}\``;
|
|
5065
|
+
if (r.timedOut) return `- ${cmd} \u2014 TIMED OUT after ${Math.round(CRITERION_TIMEOUT_MS / 6e4)} minutes.`;
|
|
5066
|
+
if (r.passed) return `- ${cmd} \u2014 SUCCEEDED (exit 0).`;
|
|
5067
|
+
return `- ${cmd} \u2014 FAILED (exit ${r.exitCode ?? "none"}):
|
|
5068
|
+
\`\`\`
|
|
5069
|
+
${r.output.slice(-800)}
|
|
5070
|
+
\`\`\``;
|
|
5071
|
+
});
|
|
5072
|
+
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");
|
|
5073
|
+
}
|
|
3945
5074
|
|
|
3946
5075
|
// src/engine/test-runner.ts
|
|
3947
5076
|
import { readFile as readFile3 } from "fs/promises";
|
|
3948
5077
|
import { existsSync as existsSync9 } from "fs";
|
|
3949
|
-
import { spawn as
|
|
5078
|
+
import { spawn as spawn3 } from "child_process";
|
|
3950
5079
|
import { join as join9 } from "path";
|
|
3951
5080
|
var TEST_TIMEOUT_MS = 6e5;
|
|
3952
5081
|
var MAX_TEST_OUTPUT = 12e3;
|
|
@@ -3978,7 +5107,7 @@ async function runProjectTests(cwd, cmd) {
|
|
|
3978
5107
|
if (!command) return { skipped: true, passed: true, output: "", timedOut: false };
|
|
3979
5108
|
const [bin, ...args] = command.argv;
|
|
3980
5109
|
return new Promise((resolve6) => {
|
|
3981
|
-
const child =
|
|
5110
|
+
const child = spawn3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
|
|
3982
5111
|
let out = "";
|
|
3983
5112
|
const take = (d) => {
|
|
3984
5113
|
out += d.toString();
|
|
@@ -4032,20 +5161,28 @@ ${run.output}`;
|
|
|
4032
5161
|
}
|
|
4033
5162
|
|
|
4034
5163
|
// src/engine/acceptance.ts
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
5164
|
+
function normalizeCriterion(s) {
|
|
5165
|
+
return s.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().replace(/[.,;:!?…]+$/, "");
|
|
5166
|
+
}
|
|
5167
|
+
var AcceptanceSchema = z18.object({
|
|
5168
|
+
checks: z18.array(z18.object({
|
|
5169
|
+
criterion: z18.string().describe(
|
|
5170
|
+
"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."
|
|
5171
|
+
),
|
|
5172
|
+
met: z18.boolean(),
|
|
5173
|
+
evidence: z18.string().describe(
|
|
4040
5174
|
'Where you SAW it: a file path and what it contains, a symbol, a test name. "It looks fine" is not evidence.'
|
|
4041
5175
|
)
|
|
4042
5176
|
}))
|
|
4043
5177
|
});
|
|
4044
|
-
var
|
|
5178
|
+
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).
|
|
4045
5179
|
Rules:
|
|
4046
5180
|
- Verify by LOOKING. If you did not open the file, the criterion is not met.
|
|
5181
|
+
- 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.
|
|
4047
5182
|
- "met" means observably true right now \u2014 not "planned", "close enough" or "the implementer says so".
|
|
4048
5183
|
- Judge ONLY the listed criteria. Code quality, style and scope opinions belong to the code review, not here.
|
|
5184
|
+
- Report EVERY criterion you were given \u2014 one check each, none dropped.
|
|
5185
|
+
- 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.
|
|
4049
5186
|
- Write the evidence in ENGLISH (it is a technical record).`;
|
|
4050
5187
|
async function verifyAcceptance(deps, card, cwd, emit = () => {
|
|
4051
5188
|
}) {
|
|
@@ -4075,6 +5212,14 @@ ${tests.output.slice(-4e3)}`,
|
|
|
4075
5212
|
if (!card.acceptance.length) return { passed: true, unmet: [], tests: testEvidence };
|
|
4076
5213
|
const diff = deps.baseRef ? await taskDiff(cwd, deps.baseRef) : deps.inPlaceBase ? await diffSince(cwd, deps.inPlaceBase) : "";
|
|
4077
5214
|
const resolved = deps.roleRegistry.resolve("code-reviewer");
|
|
5215
|
+
const commandRuns = await telemetry().span(
|
|
5216
|
+
"stage.criterion_commands",
|
|
5217
|
+
{ "hc.stage": "criterion commands" },
|
|
5218
|
+
() => runCriterionCommands(cwd, card.acceptance)
|
|
5219
|
+
);
|
|
5220
|
+
for (const r of commandRuns) {
|
|
5221
|
+
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"}`}` });
|
|
5222
|
+
}
|
|
4078
5223
|
const hints = memoryHints(deps, `${card.title} ${card.acceptance.join(" ")}`, { role: "code-reviewer" });
|
|
4079
5224
|
const ask = { role: "user", content: `Task: "${card.title}".
|
|
4080
5225
|
|
|
@@ -4085,11 +5230,13 @@ Check each one against the worktree and report met/unmet with the evidence you s
|
|
|
4085
5230
|
|
|
4086
5231
|
${describeTestRun(tests)}
|
|
4087
5232
|
|
|
5233
|
+
${describeCommandRuns(commandRuns)}
|
|
5234
|
+
|
|
4088
5235
|
${describeDiff(diff)}` };
|
|
4089
5236
|
const opts = {
|
|
4090
5237
|
provider: deps.provider,
|
|
4091
5238
|
...resolved,
|
|
4092
|
-
systemPrompt: `${
|
|
5239
|
+
systemPrompt: `${PROMPT2}${deps.roleRegistry.ruleSuffix()}`,
|
|
4093
5240
|
tools: readOnlyRegistry(deps),
|
|
4094
5241
|
messages: hints.message ? [{ role: "user", content: hints.message }, ask] : [ask],
|
|
4095
5242
|
permission: deps.permission,
|
|
@@ -4111,10 +5258,25 @@ ${describeDiff(diff)}` };
|
|
|
4111
5258
|
tests: testEvidence
|
|
4112
5259
|
};
|
|
4113
5260
|
}
|
|
4114
|
-
const byCriterion = new Map(checks.map((c) => [c.criterion
|
|
5261
|
+
const byCriterion = new Map(checks.map((c) => [normalizeCriterion(c.criterion), c]));
|
|
5262
|
+
const paired = /* @__PURE__ */ new Map();
|
|
5263
|
+
const usedChecks = /* @__PURE__ */ new Set();
|
|
5264
|
+
const unpairedCriteria = [];
|
|
5265
|
+
for (const c of card.acceptance) {
|
|
5266
|
+
const key2 = normalizeCriterion(c);
|
|
5267
|
+
const hit = byCriterion.get(key2) ?? checks.find((x) => normalizeCriterion(x.criterion).includes(key2.slice(0, 40)));
|
|
5268
|
+
if (hit) {
|
|
5269
|
+
paired.set(c, hit);
|
|
5270
|
+
usedChecks.add(hit);
|
|
5271
|
+
} else unpairedCriteria.push(c);
|
|
5272
|
+
}
|
|
5273
|
+
const unusedChecks = checks.filter((x) => !usedChecks.has(x));
|
|
5274
|
+
if (unpairedCriteria.length === 1 && unusedChecks.length === 1) {
|
|
5275
|
+
paired.set(unpairedCriteria[0], unusedChecks[0]);
|
|
5276
|
+
}
|
|
4115
5277
|
const unmet = [];
|
|
4116
5278
|
for (const c of card.acceptance) {
|
|
4117
|
-
const hit =
|
|
5279
|
+
const hit = paired.get(c);
|
|
4118
5280
|
if (!hit) unmet.push(`${c} (not reported by the acceptance gate)`);
|
|
4119
5281
|
else if (!hit.met) unmet.push(`${c} \u2014 ${hit.evidence}`);
|
|
4120
5282
|
}
|
|
@@ -4177,9 +5339,18 @@ async function runCycleWithRole(deps, board, taskId, cwd, role, git = defaultGit
|
|
|
4177
5339
|
deps.fitness?.ok(role, deps.roleRegistry.chainFor(role, rotation)[0] ?? "");
|
|
4178
5340
|
board.move(taskId, "REVIEW", role);
|
|
4179
5341
|
const card = board.get(taskId);
|
|
4180
|
-
const review = () => runCodeReview(
|
|
4181
|
-
|
|
4182
|
-
|
|
5342
|
+
const review = () => runCodeReview(
|
|
5343
|
+
rdeps,
|
|
5344
|
+
cwd,
|
|
5345
|
+
card.title,
|
|
5346
|
+
void 0,
|
|
5347
|
+
(ev) => {
|
|
5348
|
+
if (ev.kind === "note") deps.note?.(ev.text);
|
|
5349
|
+
},
|
|
5350
|
+
card.attempts,
|
|
5351
|
+
// Only what objected last time is asked again — see `runCodeReview`'s `cleared`.
|
|
5352
|
+
card.clearedLenses ?? []
|
|
5353
|
+
);
|
|
4183
5354
|
let v;
|
|
4184
5355
|
try {
|
|
4185
5356
|
v = await telemetry().span(
|
|
@@ -4202,6 +5373,8 @@ async function runCycleWithRole(deps, board, taskId, cwd, role, git = defaultGit
|
|
|
4202
5373
|
board.appendStage(taskId, { role: "code-reviewer", action: "reviewed:cancelled", note });
|
|
4203
5374
|
return { verdict: "fail", notes: [note] };
|
|
4204
5375
|
}
|
|
5376
|
+
const approved = v.approvedLenses ?? [];
|
|
5377
|
+
if (approved.length) board.markLensesCleared(taskId, approved);
|
|
4205
5378
|
if (v.verdict === "pass") {
|
|
4206
5379
|
const check = () => verifyAcceptance(rdeps, board.get(taskId), cwd, (ev) => {
|
|
4207
5380
|
if (ev.kind === "note") deps.note?.(ev.text);
|
|
@@ -4245,28 +5418,31 @@ async function runTaskCycle(deps, board, taskId, worktreePath, slot = 0) {
|
|
|
4245
5418
|
}
|
|
4246
5419
|
|
|
4247
5420
|
// src/board/board.ts
|
|
4248
|
-
import { z as
|
|
5421
|
+
import { z as z19 } from "zod";
|
|
4249
5422
|
var MAX_STAGE_EVENTS = 200;
|
|
4250
|
-
var stageEventSchema =
|
|
4251
|
-
role:
|
|
4252
|
-
action:
|
|
4253
|
-
note:
|
|
5423
|
+
var stageEventSchema = z19.object({
|
|
5424
|
+
role: z19.string(),
|
|
5425
|
+
action: z19.string(),
|
|
5426
|
+
note: z19.string().optional()
|
|
4254
5427
|
});
|
|
4255
|
-
var cardSchema =
|
|
4256
|
-
id:
|
|
4257
|
-
title:
|
|
4258
|
-
column:
|
|
4259
|
-
worktree:
|
|
4260
|
-
deps:
|
|
4261
|
-
acceptance:
|
|
5428
|
+
var cardSchema = z19.object({
|
|
5429
|
+
id: z19.string(),
|
|
5430
|
+
title: z19.string(),
|
|
5431
|
+
column: z19.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
|
|
5432
|
+
worktree: z19.string().optional(),
|
|
5433
|
+
deps: z19.array(z19.string()),
|
|
5434
|
+
acceptance: z19.array(z19.string()).default([]),
|
|
4262
5435
|
// default: boards persisted before the gate existed still load
|
|
4263
|
-
files:
|
|
5436
|
+
files: z19.array(z19.string()).default([]),
|
|
4264
5437
|
// ditto — a board written before file lists existed still loads
|
|
4265
|
-
reviewNotes:
|
|
4266
|
-
|
|
4267
|
-
|
|
5438
|
+
reviewNotes: z19.array(z19.string()),
|
|
5439
|
+
// Optional rather than defaulted: a board written before this existed must round-trip unchanged, and an
|
|
5440
|
+
// empty list is the same statement as no list at all.
|
|
5441
|
+
clearedLenses: z19.array(z19.string()).optional(),
|
|
5442
|
+
attempts: z19.number(),
|
|
5443
|
+
stageHistory: z19.array(stageEventSchema)
|
|
4268
5444
|
});
|
|
4269
|
-
var boardDataSchema =
|
|
5445
|
+
var boardDataSchema = z19.object({ version: z19.literal(1), cards: z19.array(cardSchema) });
|
|
4270
5446
|
function migrateDelivered(c) {
|
|
4271
5447
|
if (c.column !== "DONE") return c;
|
|
4272
5448
|
return c.stageHistory.some((e) => e.action === "merged") ? { ...c, column: "MERGED" } : c;
|
|
@@ -4278,6 +5454,7 @@ function cloneCard(c) {
|
|
|
4278
5454
|
acceptance: [...c.acceptance],
|
|
4279
5455
|
files: [...c.files],
|
|
4280
5456
|
reviewNotes: [...c.reviewNotes],
|
|
5457
|
+
...c.clearedLenses?.length ? { clearedLenses: [...c.clearedLenses] } : {},
|
|
4281
5458
|
stageHistory: c.stageHistory.map((e) => ({ ...e }))
|
|
4282
5459
|
};
|
|
4283
5460
|
}
|
|
@@ -4393,6 +5570,12 @@ var Board = class _Board {
|
|
|
4393
5570
|
this.require(id).reviewNotes = [];
|
|
4394
5571
|
this.onChange?.();
|
|
4395
5572
|
}
|
|
5573
|
+
/** Remember which lenses approved, so the next attempt does not ask them again. */
|
|
5574
|
+
markLensesCleared(id, lenses) {
|
|
5575
|
+
const c = this.require(id);
|
|
5576
|
+
c.clearedLenses = [.../* @__PURE__ */ new Set([...c.clearedLenses ?? [], ...lenses])];
|
|
5577
|
+
this.onChange?.();
|
|
5578
|
+
}
|
|
4396
5579
|
/**
|
|
4397
5580
|
* Starts the escalation ladder over for one card, keeping its history.
|
|
4398
5581
|
*
|
|
@@ -4427,19 +5610,18 @@ var Board = class _Board {
|
|
|
4427
5610
|
};
|
|
4428
5611
|
|
|
4429
5612
|
export {
|
|
5613
|
+
SHORT_CALL_MS,
|
|
5614
|
+
LONG_CALL_MS,
|
|
5615
|
+
ZAI_MODELS,
|
|
5616
|
+
modelsFor,
|
|
5617
|
+
cliCatalog,
|
|
5618
|
+
cliFor,
|
|
5619
|
+
CliProvider,
|
|
4430
5620
|
describeInherited,
|
|
4431
5621
|
describeTopUp,
|
|
4432
5622
|
toSlug,
|
|
4433
5623
|
mainWorktreeRoot,
|
|
4434
5624
|
WorktreeManager,
|
|
4435
|
-
applySkills,
|
|
4436
|
-
buildSkillTool,
|
|
4437
|
-
RoleRegistry,
|
|
4438
|
-
unfinishedSessions,
|
|
4439
|
-
describeUnfinished,
|
|
4440
|
-
gitTool,
|
|
4441
|
-
buildRememberTool,
|
|
4442
|
-
routeSkills,
|
|
4443
5625
|
REQUIRED_ROLES,
|
|
4444
5626
|
DEFAULT_ROLE_SKILLS,
|
|
4445
5627
|
DEFAULT_PROMPTS,
|
|
@@ -4448,6 +5630,26 @@ export {
|
|
|
4448
5630
|
CODE_TEAM,
|
|
4449
5631
|
DEFAULT_COUNCIL,
|
|
4450
5632
|
placedSkills,
|
|
5633
|
+
ROLE_PROFILES,
|
|
5634
|
+
filterModelsForRole,
|
|
5635
|
+
effortFor,
|
|
5636
|
+
isKnownModel,
|
|
5637
|
+
capabilityScore,
|
|
5638
|
+
mostCapable,
|
|
5639
|
+
modelBand,
|
|
5640
|
+
DURABLE_ROLES,
|
|
5641
|
+
strongestPrimary,
|
|
5642
|
+
newestPrimary,
|
|
5643
|
+
sourceOf,
|
|
5644
|
+
adjustRoleModels,
|
|
5645
|
+
applySkills,
|
|
5646
|
+
buildSkillTool,
|
|
5647
|
+
RoleRegistry,
|
|
5648
|
+
unfinishedSessions,
|
|
5649
|
+
describeUnfinished,
|
|
5650
|
+
gitTool,
|
|
5651
|
+
buildRememberTool,
|
|
5652
|
+
routeSkills,
|
|
4451
5653
|
specsDir,
|
|
4452
5654
|
constitutionPath,
|
|
4453
5655
|
verifyPaths,
|
|
@@ -4458,6 +5660,7 @@ export {
|
|
|
4458
5660
|
readOnlyRegistry,
|
|
4459
5661
|
deferMcp,
|
|
4460
5662
|
runReviewer,
|
|
5663
|
+
subjectOf,
|
|
4461
5664
|
asChoice,
|
|
4462
5665
|
buildTeamRegistry,
|
|
4463
5666
|
buildCouncilRegistry,
|
|
@@ -4469,8 +5672,11 @@ export {
|
|
|
4469
5672
|
createDefaultRegistry,
|
|
4470
5673
|
changedByMerge,
|
|
4471
5674
|
refreshAfterChange,
|
|
4472
|
-
LONG_CALL_MS,
|
|
4473
5675
|
routeTask,
|
|
5676
|
+
normalizeQuestion,
|
|
5677
|
+
extractChoicesFrom,
|
|
5678
|
+
buildAskUserTool,
|
|
5679
|
+
writerRegistry,
|
|
4474
5680
|
commitFile,
|
|
4475
5681
|
commitStep,
|
|
4476
5682
|
squashTask,
|