@hizliemre/horse-code 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{app-LAJN3TWC.js → app-5FXHE7GX.js} +354 -350
  2. package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
  3. package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
  4. package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
  5. package/dist/{chunk-VPAWRRHL.js → chunk-AE36LLL2.js} +31 -21
  6. package/dist/{chunk-4EWK7HWQ.js → chunk-EAF22QIG.js} +32 -0
  7. package/dist/chunk-G45RWL7S.js +289 -0
  8. package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
  9. package/dist/{chunk-DRZSUQ7Q.js → chunk-LLL7QWXB.js} +18 -7
  10. package/dist/{chunk-WYQBRCKY.js → chunk-LNW557IO.js} +2 -2
  11. package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
  12. package/dist/chunk-M2RKCIGV.js +11 -0
  13. package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
  14. package/dist/{chunk-RPVAIS3P.js → chunk-UEWVVN5L.js} +1511 -305
  15. package/dist/{chunk-EQX7BQYN.js → chunk-UGESK765.js} +1 -1
  16. package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
  17. package/dist/{chunk-K2VERI5Q.js → chunk-XYZVZPAY.js} +365 -666
  18. package/dist/{chunk-KOWMHL23.js → chunk-YPZP7LYL.js} +2 -2
  19. package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
  20. package/dist/cli.js +675 -95
  21. package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
  22. package/dist/fix-ONLA45HD.js +34 -0
  23. package/dist/git-QZTDZSJY.js +8 -0
  24. package/dist/{ongoing-XP6WXNI7.js → ongoing-WHYXPW24.js} +4 -4
  25. package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
  26. package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
  27. package/dist/{save-skills-NSLBU33X.js → save-skills-ZW5GY6KV.js} +2 -1
  28. package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
  29. package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
  30. package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
  31. package/dist/{triage-ES5OHZOS.js → triage-FCYHD2AQ.js} +8 -9
  32. package/dist/{verify-3R7DGUSI.js → verify-LC57A6H2.js} +19 -19
  33. package/package.json +1 -1
  34. package/dist/chunk-3UYA3KUG.js +0 -212
  35. package/dist/chunk-B67BK5GQ.js +0 -34
  36. package/dist/chunk-O74BDQKS.js +0 -28
  37. package/dist/fix-CMARU6JR.js +0 -34
  38. package/dist/git-VTSZALSR.js +0 -6
@@ -3,14 +3,20 @@ import {
3
3
  checkpointMtime,
4
4
  isContinuePrompt,
5
5
  readCheckpoint
6
- } from "./chunk-FGVJFMK5.js";
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-NNTIACT4.js";
15
+ } from "./chunk-MRZVA5JB.js";
11
16
  import {
12
- defaultGitRunner
13
- } from "./chunk-IW2KBAVZ.js";
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-DRZSUQ7Q.js";
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-VPAWRRHL.js";
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-FFYBY2NA.js";
47
+ } from "./chunk-KAGKX2YT.js";
42
48
  import {
43
49
  buildProjectGraph,
44
50
  loadGraphSync,
45
51
  pruneAreaNames
46
- } from "./chunk-PGOYDOI4.js";
52
+ } from "./chunk-XEGQT5EN.js";
47
53
  import {
48
54
  writableStateRoot
49
- } from "./chunk-SSDLHWSF.js";
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
- return available.length ? `skill not found: ${name}. Available: ${available.join(", ")}. Use one of these exactly, or carry on without a skill \u2014 do not guess another name.` : `skill not found: ${name}. This project has no skills installed, so carry on without one.`;
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(["diff", "diff-index", "diff-tree", "diff-files", "merge-base", "check-ignore"]);
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 narrow the range, or put \`--stat\` directly after the subcommand (git diff --stat <rest>), which git requires]` : out.text;
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 = 5 * 60 * 1e3;
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 is not unique (${count} matches) \u2014 replaceAll required`,
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 z13 } from "zod";
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 = z13.object({
2608
- role: z13.enum(["coder", "designer"]).describe(
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 z14 } from "zod";
2671
- var CommitSchema = z14.object({
2672
- message: z14.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
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.map((n) => `- ${n}`).join("\n")}` : `This is a NEW task: "${task.title}". Implement it.`) + (brief ? `
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(" ")} ${task.reviewNotes.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 systemPrompt = withSkills + law + projectToolsNote(tools.list(), !!loadGraphSync(cwd)) + BATCH_TOOLS_NOTE;
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
- provider: deps.provider,
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 z15 } from "zod";
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
- const k = c.length ? (slot % c.length + c.length) % c.length : 0;
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) => this.markExhausted(m, reason ?? "unavailable"),
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 = z15.object({
3334
- findings: z15.array(z15.object({
3335
- severity: z15.enum(["critical", "medium", "low"]).describe(
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: z15.string()
4341
+ note: z17.string()
3339
4342
  })).default([]),
3340
- recommendation: z15.enum(["approve", "revise"]).describe(
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 = z15.object({
3382
- vote: z15.enum(["pass", "revise"]).describe(
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: z15.string()
4388
+ rationale: z17.string()
3386
4389
  });
3387
- var JudgeSchema = z15.object({
3388
- decision: z15.enum(["pass", "revise", "ask-human"]).describe(
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: z15.array(z15.string()),
3392
- question: z15.string()
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 = 3 * 60 * 1e3;
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 team = lensesFor(deps.teams.code, diff);
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
- if (crit === 0) {
3913
- const deferred = nonBlockingNotes(assessments, "code");
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 no critical findings; ${deferred.length} medium/low note(s) deferred to the revision pass.` });
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 { verdict: "fail", notes: blocking.length ? blocking : votes.filter((v) => v.vote === "revise").map((v) => v.rationale) };
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 z16 } from "zod";
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 spawn2 } from "child_process";
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 = spawn2(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
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
- var AcceptanceSchema = z16.object({
4036
- checks: z16.array(z16.object({
4037
- criterion: z16.string(),
4038
- met: z16.boolean(),
4039
- evidence: z16.string().describe(
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 PROMPT = `You are the acceptance gate for one implementation task. You are given the task's acceptance criteria and read-only access to 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).
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: `${PROMPT}${deps.roleRegistry.ruleSuffix()}`,
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.trim().toLowerCase(), c]));
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 = byCriterion.get(c.trim().toLowerCase()) ?? checks.find((x) => x.criterion.toLowerCase().includes(c.trim().toLowerCase().slice(0, 40)));
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(rdeps, cwd, card.title, void 0, (ev) => {
4181
- if (ev.kind === "note") deps.note?.(ev.text);
4182
- }, card.attempts);
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 z17 } from "zod";
5421
+ import { z as z19 } from "zod";
4249
5422
  var MAX_STAGE_EVENTS = 200;
4250
- var stageEventSchema = z17.object({
4251
- role: z17.string(),
4252
- action: z17.string(),
4253
- note: z17.string().optional()
5423
+ var stageEventSchema = z19.object({
5424
+ role: z19.string(),
5425
+ action: z19.string(),
5426
+ note: z19.string().optional()
4254
5427
  });
4255
- var cardSchema = z17.object({
4256
- id: z17.string(),
4257
- title: z17.string(),
4258
- column: z17.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
4259
- worktree: z17.string().optional(),
4260
- deps: z17.array(z17.string()),
4261
- acceptance: z17.array(z17.string()).default([]),
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: z17.array(z17.string()).default([]),
5436
+ files: z19.array(z19.string()).default([]),
4264
5437
  // ditto — a board written before file lists existed still loads
4265
- reviewNotes: z17.array(z17.string()),
4266
- attempts: z17.number(),
4267
- stageHistory: z17.array(stageEventSchema)
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 = z17.object({ version: z17.literal(1), cards: z17.array(cardSchema) });
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,