@hizliemre/horse-code 0.2.0 → 0.3.1

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 (29) hide show
  1. package/dist/{app-2GPGCDX6.js → app-4WN37LZ3.js} +282 -57
  2. package/dist/{chunk-7JMWPTJ5.js → chunk-27F44PBD.js} +221 -1363
  3. package/dist/{chunk-23CLQ2KO.js → chunk-2WXG35EM.js} +19 -15
  4. package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
  5. package/dist/{chunk-5ZV42XGJ.js → chunk-3ACDNDCG.js} +1 -1
  6. package/dist/{chunk-XEGQT5EN.js → chunk-4M6LXNG2.js} +1 -1
  7. package/dist/{chunk-6OSEQOYY.js → chunk-6S4WWQMN.js} +2 -2
  8. package/dist/{chunk-LLL7QWXB.js → chunk-BFIZMM4G.js} +6 -6
  9. package/dist/chunk-CYLPQWIF.js +214 -0
  10. package/dist/chunk-G45RWL7S.js +289 -0
  11. package/dist/{chunk-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
  12. package/dist/{chunk-KKWZBZYK.js → chunk-JR2JLRE3.js} +26 -4
  13. package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
  14. package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
  15. package/dist/chunk-YULQ4URQ.js +1220 -0
  16. package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
  17. package/dist/cli.js +586 -104
  18. package/dist/{fix-JOIXQFVP.js → fix-QCL5AITT.js} +10 -8
  19. package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
  20. package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
  21. package/dist/run-V5ZLZ3LS.js +274 -0
  22. package/dist/{save-skills-X7U3KCPU.js → save-skills-NPKTYNAF.js} +2 -1
  23. package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
  24. package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
  25. package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
  26. package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
  27. package/dist/{verify-6SC4I77M.js → verify-HWZBTK5X.js} +16 -12
  28. package/package.json +1 -1
  29. package/dist/chunk-MRZVA5JB.js +0 -163
@@ -5,9 +5,20 @@ import {
5
5
  readCheckpoint
6
6
  } from "./chunk-ZSQ24YDJ.js";
7
7
  import {
8
+ RoleRegistry,
9
+ applySkills,
10
+ buildSkillTool,
11
+ cliFor,
12
+ cliInvocation,
13
+ grokEffort,
14
+ placedSkills,
8
15
  planFor,
9
16
  runTraces
10
- } from "./chunk-MRZVA5JB.js";
17
+ } from "./chunk-YULQ4URQ.js";
18
+ import {
19
+ SYNTHETIC,
20
+ runCliAgent
21
+ } from "./chunk-G45RWL7S.js";
11
22
  import {
12
23
  defaultGitRunner,
13
24
  gitVerb
@@ -23,15 +34,17 @@ import {
23
34
  readFileTool,
24
35
  reinforceTouched,
25
36
  reinforceUsed
26
- } from "./chunk-LLL7QWXB.js";
37
+ } from "./chunk-BFIZMM4G.js";
27
38
  import {
28
39
  ToolRegistry,
40
+ runStructuredRole
41
+ } from "./chunk-CYLPQWIF.js";
42
+ import {
29
43
  handedOver,
30
- runStructuredRole,
31
44
  runToCompletion,
32
45
  telemetry,
33
46
  truncateSafe
34
- } from "./chunk-AE36LLL2.js";
47
+ } from "./chunk-JLWQCA7B.js";
35
48
  import {
36
49
  loadTraceIndex,
37
50
  pruneTraces,
@@ -39,291 +52,16 @@ import {
39
52
  sharedDerived,
40
53
  traceRootRel,
41
54
  traceable
42
- } from "./chunk-KAGKX2YT.js";
55
+ } from "./chunk-ZPJP2VH5.js";
43
56
  import {
44
57
  buildProjectGraph,
45
58
  loadGraphSync,
46
59
  pruneAreaNames
47
- } from "./chunk-XEGQT5EN.js";
60
+ } from "./chunk-4M6LXNG2.js";
48
61
  import {
49
62
  writableStateRoot
50
63
  } from "./chunk-6W4UH2BQ.js";
51
64
 
52
- // src/agents/cli-auth.ts
53
- import { spawnSync } from "child_process";
54
- function profileEnv(kind, configDir) {
55
- if (!configDir) return {};
56
- return kind === "claude" ? { CLAUDE_CONFIG_DIR: configDir } : { CODEX_HOME: configDir };
57
- }
58
- function readAuthStatus(kind, out) {
59
- if (kind === "codex") {
60
- const m = /logged in(?: using (.+))?/i.exec(out);
61
- if (!m || /not logged in/i.test(out)) return { loggedIn: false };
62
- const plan = m[1]?.trim();
63
- return { loggedIn: true, ...plan ? { plan } : {} };
64
- }
65
- try {
66
- const j = JSON.parse(out);
67
- if (!j.loggedIn) return { loggedIn: false };
68
- return {
69
- loggedIn: true,
70
- ...j.email ? { email: j.email } : {},
71
- ...j.subscriptionType ? { plan: j.subscriptionType } : {}
72
- };
73
- } catch {
74
- return { loggedIn: false };
75
- }
76
- }
77
- function statusArgs(kind) {
78
- return kind === "claude" ? ["auth", "status"] : ["login", "status"];
79
- }
80
- function loginArgs(kind) {
81
- return kind === "claude" ? ["auth", "login"] : ["login"];
82
- }
83
- function checkProfile(kind, configDir) {
84
- const r = spawnSync(kind, statusArgs(kind), {
85
- env: { ...process.env, ...profileEnv(kind, configDir) },
86
- encoding: "utf8",
87
- // A status check that hangs must not hang the startup summary with it.
88
- timeout: 2e4
89
- });
90
- if (r.error) return { loggedIn: false };
91
- return readAuthStatus(kind, `${r.stdout ?? ""}${r.stderr ?? ""}`);
92
- }
93
- function runLogin(kind, configDir) {
94
- const r = spawnSync(kind, loginArgs(kind), {
95
- env: { ...process.env, ...profileEnv(kind, configDir) },
96
- stdio: "inherit"
97
- });
98
- if (r.error) {
99
- const e = r.error;
100
- return e.code === "ENOENT" ? { ok: false, error: `\`${kind}\` is not installed, or not on PATH` } : { ok: false, error: e.message };
101
- }
102
- return { ok: r.status === 0 };
103
- }
104
-
105
- // src/agents/cli-models.ts
106
- var CLAUDE_MODELS = ["fable", "opus", "sonnet", "haiku"];
107
- var CODEX_MODELS = ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-5.6-luna"];
108
- var CODEX_DEFAULT = "gpt-5.6-terra";
109
- function cliCatalog() {
110
- return [...CLAUDE_MODELS, ...CODEX_MODELS];
111
- }
112
- function cliFor(model) {
113
- const m = model.toLowerCase().replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
114
- if (/^(fable|opus|sonnet|haiku)\b/.test(m) || m.startsWith("claude")) return "claude";
115
- if (/^(codex|gpt|o[0-9])\b/.test(m)) return "codex";
116
- return void 0;
117
- }
118
- function cliInvocation(model) {
119
- const bare = model.replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
120
- const effort = /-(ultra|max|xhigh|high|medium|low|minimal)$/.exec(bare)?.[1];
121
- const name = effort ? bare.slice(0, -(effort.length + 1)) : bare;
122
- const resolved = name === "codex" ? CODEX_DEFAULT : name;
123
- return {
124
- ...resolved ? { model: resolved } : {},
125
- ...effort ? { effort } : {}
126
- };
127
- }
128
-
129
- // src/agents/cli-agent.ts
130
- import { spawn } from "child_process";
131
- function cliArgs(kind, prompt, extra = []) {
132
- return kind === "claude" ? ["--output-format", "stream-json", "--verbose", ...extra, "-p", "--", prompt] : ["exec", "--json", "--skip-git-repo-check", ...extra, "--", prompt];
133
- }
134
- function decodeClaudeEvent(line) {
135
- let e;
136
- try {
137
- e = JSON.parse(line);
138
- } catch {
139
- return void 0;
140
- }
141
- const type = e.type;
142
- if (type === "rate_limit_event") {
143
- const info = e.rate_limit_info ?? {};
144
- const status = String(info.status ?? "unknown");
145
- const raw = info.unifiedWindows ?? {};
146
- const windows = {};
147
- for (const [name, w] of Object.entries(raw)) windows[name] = w?.utilization ?? 0;
148
- const quota = {
149
- status,
150
- windows,
151
- ...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {}
152
- };
153
- return status.startsWith("allowed") ? { quota } : {
154
- quota,
155
- /**
156
- * The reset time rides along, as an ISO instant rather than prose.
157
- *
158
- * A spent five-hour window reopens; without saying when, the only safe bench is "the rest of the
159
- * run", which on a ten-hour board writes off a subscription for hours after it recovered. The
160
- * gateway's wordings said "reset after 4h" and nothing ever parsed them — see `quotaResetAt`.
161
- */
162
- rateLimited: `${status} \u2014 ${describeWindows(windows)}` + (quota.resetsAt ? ` (resets ${new Date(quota.resetsAt * 1e3).toISOString()})` : "")
163
- };
164
- }
165
- if (type === "assistant") {
166
- const msg = e.message;
167
- const parts = Array.isArray(msg?.content) ? msg.content : [];
168
- const text = parts.filter((b) => typeof b === "object" && b !== null && b.type === "text").map((b) => b.text).join("");
169
- const tool = parts.find((b) => typeof b === "object" && b !== null && b.type === "tool_use");
170
- return {
171
- ...text ? { text } : {},
172
- ...msg?.model ? { served: msg.model } : {},
173
- ...tool ? { tool: { name: tool.name, ...targetOf(tool.input) ? { target: targetOf(tool.input) } : {} } } : {}
174
- };
175
- }
176
- if (type === "user") {
177
- const parts = e.message?.content;
178
- const failed = (Array.isArray(parts) ? parts : []).find(
179
- (b) => typeof b === "object" && b !== null && b.type === "tool_result" && b.is_error === true
180
- );
181
- return failed ? { tool: { name: "tool", ok: false } } : void 0;
182
- }
183
- if (type === "result") {
184
- const u = e.usage ?? {};
185
- const cost = e.total_cost_usd;
186
- return {
187
- usage: {
188
- freshTokens: u.input_tokens ?? 0,
189
- cachedTokens: u.cache_read_input_tokens ?? 0,
190
- cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
191
- outputTokens: u.output_tokens ?? 0,
192
- ...cost !== void 0 ? { costUsd: cost } : {}
193
- },
194
- ...e.subtype === "error_during_execution" ? { error: String(e.result ?? "the CLI reported an error") } : {}
195
- };
196
- }
197
- return void 0;
198
- }
199
- function decodeCodexEvent(line) {
200
- let e;
201
- try {
202
- e = JSON.parse(line);
203
- } catch {
204
- return void 0;
205
- }
206
- const type = String(e.type ?? "");
207
- if (/rate.?limit/i.test(type)) return { rateLimited: String(e.message ?? "rate limited by the CLI") };
208
- if (type === "item.completed") {
209
- const item = e.item;
210
- if (item?.type === "agent_message" && item.text) return { text: item.text };
211
- if (item?.type && item.type !== "agent_message") {
212
- const changes = item.changes;
213
- const first = Array.isArray(changes) ? changes.find((c) => typeof c?.path === "string")?.path : void 0;
214
- const more = Array.isArray(changes) && changes.length > 1 ? ` +${changes.length - 1}` : "";
215
- return {
216
- tool: {
217
- name: item.name ?? item.type,
218
- ...first ? { target: `${first}${more}` } : {}
219
- }
220
- };
221
- }
222
- return void 0;
223
- }
224
- if (type === "turn.completed") {
225
- const u = e.usage ?? {};
226
- return {
227
- usage: {
228
- freshTokens: u.input_tokens ?? 0,
229
- cachedTokens: u.cached_input_tokens ?? 0,
230
- cacheWriteTokens: u.cache_write_input_tokens ?? 0,
231
- outputTokens: u.output_tokens ?? 0
232
- }
233
- };
234
- }
235
- if (type === "turn.failed" || type === "error") {
236
- return { error: String(e.message ?? "codex reported an error") };
237
- }
238
- return void 0;
239
- }
240
- function describeWindows(windows) {
241
- const parts = Object.entries(windows).map(([k, v]) => `${k} ${Math.round(v * 100)}%`);
242
- return parts.length ? parts.join(", ") : "no window reported";
243
- }
244
- var SYNTHETIC = "<synthetic>";
245
- function targetOf(input) {
246
- for (const k of ["file_path", "path", "filePath", "notebook_path"]) {
247
- const v = input?.[k];
248
- if (typeof v === "string" && v) return v;
249
- }
250
- return void 0;
251
- }
252
- function makeStreamReader(decode, onEvent) {
253
- let pending = "";
254
- const drain = (upToNewline) => {
255
- const lines = pending.split("\n");
256
- pending = upToNewline ? lines.pop() ?? "" : "";
257
- for (const line of lines) {
258
- if (!line.trim()) continue;
259
- const ev = decode(line);
260
- if (ev) onEvent(ev);
261
- }
262
- };
263
- return {
264
- push(chunk) {
265
- pending += chunk;
266
- drain(true);
267
- },
268
- end() {
269
- if (pending.trim()) drain(false);
270
- }
271
- };
272
- }
273
- async function runCliAgent(run) {
274
- const decode = run.kind === "claude" ? decodeClaudeEvent : decodeCodexEvent;
275
- const args = cliArgs(run.kind, run.prompt, run.args ?? []);
276
- return new Promise((resolve6) => {
277
- let child;
278
- try {
279
- child = spawn(run.kind, args, {
280
- cwd: run.cwd,
281
- signal: run.signal,
282
- stdio: ["ignore", "pipe", "pipe"],
283
- ...run.configDir ? { env: { ...process.env, ...profileEnv(run.kind, run.configDir) } } : {}
284
- });
285
- } catch (e) {
286
- resolve6({ text: "", error: e instanceof Error ? e.message : String(e), exitCode: -1 });
287
- return;
288
- }
289
- let text = "";
290
- let usage;
291
- let rateLimited;
292
- let served;
293
- let quota;
294
- let error;
295
- let stderr = "";
296
- const reader = makeStreamReader(decode, (ev) => {
297
- if (ev.text) text += ev.text;
298
- if (ev.usage) usage = ev.usage;
299
- if (ev.rateLimited) rateLimited = ev.rateLimited;
300
- if (ev.served) served = ev.served;
301
- if (ev.quota) quota = ev.quota;
302
- if (ev.error) error = ev.error;
303
- run.onEvent?.(ev);
304
- });
305
- child.stdout?.on("data", (d) => reader.push(d.toString()));
306
- child.stderr?.on("data", (d) => {
307
- stderr += d.toString();
308
- });
309
- child.on("error", (e) => resolve6({ text, ...usage ? { usage } : {}, error: e.message, exitCode: -1 }));
310
- child.on("close", (code) => {
311
- reader.end();
312
- resolve6({
313
- text,
314
- ...usage ? { usage } : {},
315
- ...rateLimited ? { rateLimited } : {},
316
- ...quota ? { quota } : {},
317
- ...served ? { served } : {},
318
- // stderr only becomes the error when nothing better was said — a CLI that warns on stderr and
319
- // succeeds must not be read as having failed.
320
- ...error ?? (code !== 0 && stderr.trim()) ? { error: error ?? stderr.trim().slice(0, 500) } : {},
321
- exitCode: code ?? -1
322
- });
323
- });
324
- });
325
- }
326
-
327
65
  // src/agent/deadline.ts
328
66
  function withDeadline(work, signal, message) {
329
67
  work.catch(() => {
@@ -369,7 +107,7 @@ ${m.content}` : m.content);
369
107
  return parts.join("\n\n");
370
108
  }
371
109
  function isLoggedOut(text) {
372
- return /not logged in|please run \/login/i.test(text);
110
+ return /not logged in|not signed in|please run \/login/i.test(text);
373
111
  }
374
112
  async function* streamWhileRunning(start) {
375
113
  const queue = [];
@@ -427,11 +165,17 @@ var CliProvider = class {
427
165
  const { model, effort: named } = cliInvocation(req.model);
428
166
  if (model) args.push("--model", model);
429
167
  const effort = req.effort ?? named;
430
- if (effort && kind === "claude") args.push("--effort", effort);
431
- if (this.readOnly && kind === "claude") args.push("--disallowed-tools", "Write", "Edit", "NotebookEdit");
168
+ if (effort && (kind === "claude" || kind === "zai")) args.push("--effort", effort);
169
+ if (effort && kind === "grok") {
170
+ const level = grokEffort(effort);
171
+ if (level) args.push("--reasoning-effort", level);
172
+ }
173
+ if (this.readOnly && (kind === "claude" || kind === "zai")) args.push("--disallowed-tools", "Write", "Edit", "NotebookEdit");
432
174
  if (this.readOnly && kind === "codex") args.push("--sandbox", "read-only");
433
- if (!this.readOnly && kind === "claude") args.push("--permission-mode", "acceptEdits");
175
+ if (this.readOnly && kind === "grok") args.push("--disallowed-tools", "write,search_replace");
176
+ if (!this.readOnly && (kind === "claude" || kind === "zai")) args.push("--permission-mode", "acceptEdits");
434
177
  if (!this.readOnly && kind === "codex") args.push("--sandbox", "workspace-write");
178
+ if (!this.readOnly && kind === "grok") args.push("--permission-mode", "acceptEdits");
435
179
  const account = this.accounts?.pick(kind);
436
180
  let res;
437
181
  yield* streamWhileRunning((push) => runCliAgent({
@@ -471,6 +215,14 @@ var CliProvider = class {
471
215
  return;
472
216
  }
473
217
  if (res.error && !res.text.trim()) {
218
+ if (isLoggedOut(res.error)) {
219
+ yield {
220
+ type: "error",
221
+ retryable: true,
222
+ message: `${kind} CLI is not logged in${account ? ` under profile "${account.name}"` : ""} \u2014 run \`hcode add-provider ${kind}\` to sign it in again`
223
+ };
224
+ return;
225
+ }
474
226
  yield { type: "error", message: `${kind} CLI: ${res.error}`, retryable: res.exitCode !== 0 };
475
227
  return;
476
228
  }
@@ -638,8 +390,8 @@ async function inheritFromRoot(git, repoRoot, baseWorktree) {
638
390
  }
639
391
  }
640
392
  for (const rel of INHERITED_ASSETS) {
641
- const from = join(repoRoot, rel);
642
- if (!existsSync(from)) continue;
393
+ const from = assetSource(repoRoot, rel);
394
+ if (!from) continue;
643
395
  try {
644
396
  await stat(from);
645
397
  await copyPath(from, join(baseWorktree, rel));
@@ -649,6 +401,12 @@ async function inheritFromRoot(git, repoRoot, baseWorktree) {
649
401
  }
650
402
  return out;
651
403
  }
404
+ function assetSource(repoRoot, rel) {
405
+ const atRoot = join(repoRoot, rel);
406
+ if (existsSync(atRoot)) return atRoot;
407
+ const standing = join(repoRoot, ".horsecode", "worktrees", "traces", "base", rel);
408
+ return existsSync(standing) ? standing : void 0;
409
+ }
652
410
  function describeInherited(i) {
653
411
  const parts = [];
654
412
  const n = i.modified.length + i.deleted.length;
@@ -825,6 +583,43 @@ var WorktreeManager = class {
825
583
  const inherited = await inheritFromRoot((args, cwd) => this.git(args, cwd), this.repoRoot, baseWorktree);
826
584
  return { jobSlug, root, baseWorktree, baseBranch, inherited };
827
585
  }
586
+ /**
587
+ * Opens — or re-enters — a worktree with a FIXED name, for the standing work that is not one job.
588
+ *
589
+ * `openSession` mints a fresh dated slug every call, which is right for a job: two runs of "add login" are
590
+ * two pieces of work and must not share a branch. Tracing is the opposite. It is one long-lived artefact
591
+ * the project keeps, its index is checkpointed so an interrupted run resumes, and a new worktree per
592
+ * invocation would both lose that resumption and pile up full checkouts — measured on the project this was
593
+ * written for, a checkout is not small.
594
+ *
595
+ * So the slug is the caller's, and running it twice re-enters the same place. Re-entry is decided by git
596
+ * rather than by the directory existing: a leftover directory git no longer tracks is not a worktree, and
597
+ * treating one as resumable is how a run ends up writing into a checkout that no longer has a branch.
598
+ */
599
+ async openFixed(fromBranch, slug) {
600
+ await this.ensureBaseCommit();
601
+ const worktreesDir = join2(this.worktreeHome, ".horsecode", "worktrees");
602
+ await mkdir2(worktreesDir, { recursive: true });
603
+ await writeFile(join2(worktreesDir, ".gitignore"), "*\n", "utf8");
604
+ const root = join2(worktreesDir, slug);
605
+ const baseWorktree = join2(root, "base");
606
+ const baseBranch = `hc/${slug}/base`;
607
+ let real;
608
+ try {
609
+ real = realpathSync(baseWorktree);
610
+ } catch {
611
+ }
612
+ if (real && (await this.registeredWorktrees()).has(real)) {
613
+ return { jobSlug: slug, root, baseWorktree, baseBranch, resumed: true };
614
+ }
615
+ const base = await this.resolveBase(fromBranch);
616
+ await mkdir2(join2(root, "tasks"), { recursive: true });
617
+ const listed = await this.git(["for-each-ref", "--format=%(refname:short)", `refs/heads/${baseBranch}`], this.repoRoot);
618
+ const exists = listed.stdout.trim() === baseBranch;
619
+ await this.run(exists ? ["worktree", "add", baseWorktree, baseBranch] : ["worktree", "add", "-b", baseBranch, baseWorktree, base], this.repoRoot);
620
+ const inherited = await inheritFromRoot((args, cwd) => this.git(args, cwd), this.repoRoot, baseWorktree);
621
+ return { jobSlug: slug, root, baseWorktree, baseBranch, inherited };
622
+ }
828
623
  /** Absolute paths of the worktrees git currently tracks (from `git worktree list --porcelain`). */
829
624
  async registeredWorktrees() {
830
625
  const r = await this.git(["worktree", "list", "--porcelain"], this.repoRoot);
@@ -1092,546 +887,8 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
1092
887
  }
1093
888
  };
1094
889
 
1095
- // src/prompts.ts
1096
- var REQUIRED_ROLES = [
1097
- "refiner",
1098
- "coach",
1099
- "brainstormer",
1100
- "analyst",
1101
- "planner",
1102
- "judge",
1103
- "project-manager",
1104
- "team-lead",
1105
- "router",
1106
- "coder",
1107
- "designer",
1108
- "senior-coder",
1109
- "senior-designer",
1110
- "architect",
1111
- "code-reviewer",
1112
- "task-auditor",
1113
- "principal-coder",
1114
- "operational",
1115
- "memory-keeper",
1116
- "tracer",
1117
- "tester"
1118
- ];
1119
- var DEFAULT_ROLE_SKILLS = {
1120
- brainstormer: ["brainstorming"],
1121
- // The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
1122
- // vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
1123
- coder: ["test-driven-development"],
1124
- "senior-coder": ["test-driven-development"],
1125
- // The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
1126
- // supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
1127
- // individual task executable. That is what this skill adds.
1128
- "project-manager": ["writing-plans"],
1129
- // The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
1130
- // and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
1131
- // one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
1132
- designer: ["frontend-design"],
1133
- "senior-designer": ["frontend-design"]
1134
- // NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
1135
- // so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
1136
- };
1137
- var DEFAULT_PROMPTS = {
1138
- tracer: "You write the reference note that every other agent reads before it touches a file it did not write. A wrong note is worse than none: an agent will act on it, so accuracy outranks fluency and admitting you cannot tell outranks a plausible guess. State only what the code and the given relationships show; if the business purpose is not evident from them, describe what the file does technically and say nothing about why. Never speculate about intent, history or requirements.",
1139
- /**
1140
- * The role that exercises work already built and writes down what actually happened.
1141
- *
1142
- * Every rule below is here because its absence produces the one output worse than no testing at all: a
1143
- * report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
1144
- * confidence, and the next person spends it.
1145
- */
1146
- tester: "You verify software that already exists, by running its scenarios and recording what they actually did. You are not here to build, fix or improve anything: the code under test is finished, and changing it would mean the thing you verified is not the thing that shipped.\n\nEVIDENCE IS THE WHOLE JOB. A scenario's outcome is what you OBSERVED \u2014 a database row, a log line, an HTTP response, a screen the user confirmed. Record the evidence beside every result: the query you ran and what it returned, the log event id and its line, the response body. A result you cannot show is not a result.\n\nIF A STEP WRITES TO THE DATABASE, THE RESPONSE IS NOT THE EVIDENCE. A 201 or a 204 says the request was accepted; it does not say what was stored, and a screen showing the new state does not either \u2014 both can be right while the row is wrong. For every step that creates or changes a record: query the database for that row and put the query AND the rows it returned in the report, and query the logs for the event that step should have emitted and put the query AND the line it returned there too. Absence is evidence as well: when a step must NOT emit an event \u2014 a no-op, a rejected change \u2014 show the query returning nothing. Without both, the scenario is NOT EXECUTED, however convincing the response looked.\n\nNever mark a scenario PASSED that you did not execute and observe. If you could not run it \u2014 the data does not exist, the surface is unreachable, the case is destructive against a live system \u2014 label it NOT EXECUTED and say exactly why. FAILED means you ran it and the behaviour was wrong; say what you expected, what happened, and the evidence for both. Guessing from the source is not executing: where you reasoned from code alone, say so in those words.\n\nWrite each result into the report BEFORE moving on to the next scenario. The report is a living document, not something assembled at the end: a run that stops halfway must leave behind everything it learned up to that point.\n\n\u2026and say each verdict OUT LOUD as you reach it, in one or two sentences: which scenario, what it did, and the single piece of evidence that settled it \u2014 the row, the log line, the status code. The full evidence still goes in the report; this is so the person watching the run knows what you found without opening a file. Say the failures and the NOT EXECUTED ones the same way, and with the same brevity: a result nobody hears is one they have to go looking for.\n\nNever start or stop the development environment \u2014 application hosts, dev servers, containers, databases. Those are the developer's to run. When you need something up, say which command they should run and wait for them to confirm it is ready.\n\nYou do NOT write product code. When you find something wrong that is not the verdict of the scenario you are running \u2014 a missing label, prose rendered as raw markup, a wrong format, or something the developer points out in passing \u2014 call `report_finding`. Another role fixes it and you are told when it is done, so you can re-check what it affected. Do not fix it yourself: changing the product mid-verification means the thing you verified is not the thing that shipped. And do not fail a scenario over it \u2014 a scenario fails when the scenario itself does not pass, not because something else was noticed while running it.\n\nIf the project's own rules (its constitution) say more about how verification is done here, they govern over this description \u2014 read them and follow them.",
1147
- refiner: "Your #1 rule: `refinedPrompt` MUST ALWAYS be in ENGLISH. If the user wrote in another language (Turkish, German, Spanish, \u2026), TRANSLATE their intent into English \u2014 never echo their language back. This is non-negotiable: a Turkish input like 'bir todo app geli\u015Ftir, \xF6nce backend' MUST come out as English 'Build a todo app; implement the backend first.'\n\nRewrite the user's message down to the raw core intent the AI needs to act on \u2014 clear, direct, and structured. Strip all politeness, emotional, and filler words (please, thanks, kindly, 'could you', 'would you', 'I'd like', etc.) and anything that carries no instruction. Do NOT add words, qualifiers, or scope the user did not state (e.g. do not add 'always'). Keep the user's own perspective and form \u2014 a question stays a question, an instruction stays an instruction; do NOT describe the user in the third person and do NOT answer the request. Example: a polite request like 'would you please answer me in language X?' becomes just 'respond in language X' (drop 'please'; do not add 'always' or any scope the user didn't state). Also classify the intent: 'chat' (conversation/question), 'feature' (new feature/work), 'bugfix' (bug fix), 'govern' (establish or amend the project's OWN standing rules and principles \u2014 writing or revising the constitution, the coding conventions, the project's rules; work whose entire output is a governing document, with no source code changed). Judge by what the request PRODUCES, not by what it mentions: 'write the project constitution from CLAUDE.md' is govern, and so is 'update our commit-message rules'; 'make the code follow the constitution' changes source and is feature. Also 'verify' \u2014 the user wants work that ALREADY EXISTS exercised and its behaviour confirmed with evidence: running a pull request's test scenarios, doing a smoke test of a feature that is already built, producing a test report. Judge by what it PRODUCES: a record of what the software DID is verify; changing what it does is feature or bugfix. 'Run the smoke tests for PR 677 and mark them passed' is verify, and so is 'check that the wizard works end to end'. 'The wizard is broken, fix it' is bugfix. Finally 'undo' \u2014 the user is asking you to REVERSE what the previous turn did, not to do anything new: 'undo that', 'revert your changes', 'go back to the previous version', 'that was wrong, put it back'. Classify by whether the request refers to work already done: undoing is never a rewrite, and asking for a different result ('rewrite it shorter') is not an undo. Also detect the natural language the user wrote in and return its English name as `language` (e.g. 'Turkish', 'English', 'German') \u2014 this is separate from refinedPrompt, which stays English. Also produce `title`: what the work is ABOUT, as a 2-5 word English kebab-case noun phrase suitable for a git branch name \u2014 the SUBJECT, not the action. 'build a luxury todo app' is 'luxury-todo-app'; 'add a login page' is 'login-page'; 'fix the null crash on retry' is 'null-crash-on-retry'. Do not open with a verb (build/add/fix/implement/update): the tool is already doing it, so the verb says nothing and crowds out the words that identify the work. Lowercase, dash-joined, no punctuation. Return the result via submit as {refinedPrompt, intent, language, title}. Remember: refinedPrompt in English, always.",
1148
- brainstormer: "You run the BRAINSTORM stage: you turn a raw request into a decided design, before anything is specified.\n\nThe `brainstorming` skill above is the authority on HOW to do this \u2014 follow it. What follows is only how it binds to this pipeline, because the skill names conventions from a different habitat:\n\n- OUTPUT: write the design brief to the file named in your message (specs/NNN-slug/brainstorm.md). Ignore the skill's `docs/superpowers/specs/\u2026` path.\n- NEXT STAGE: the SPEC is written from your brief, by another agent, immediately after you. There is no `writing-plans` skill to invoke here \u2014 finishing the brief IS the terminal step.\n- QUESTIONS: ask through the `ask_user` tool. For a choice between approaches use its rich option form ({label, description, preview}) so the trade-offs sit beside the list; lead with your recommendation. The user may attach a note to their answer \u2014 treat it as binding.\n- NOT AVAILABLE: the visual companion (there is no browser) and the per-checklist task list. Skip both.\n\nWrite what was DECIDED, not a transcript: the chosen approach, why it beat the others, the rejected alternatives with their reason, the constraints the spec must honour, and what is out of scope. Keep it short \u2014 it is the brief the spec is written from, not the spec itself, and it carries no implementation detail beyond the architectural choice.\n\nScale to the request: a small, obvious change deserves a paragraph and no questions at all.",
1149
- coach: "You are horse-code, a terminal-based AI coding agent. Your product identity is always horse-code \u2014 never claim to be Claude Code, Gemini CLI, Antigravity, or any other product, even though the underlying language model powering you may be Claude, Gemini, or another model. Answer the user's technical questions about their repository and code. If needed, inspect the repository with read_file/grep/glob.\n\nWork out loud while you do it. Before a batch of tool calls, say in ONE line what you are looking for and why; when something you read changes your mind, say that too. This is not a summary at the end \u2014 the user is watching an empty screen while you search, and a run that reads thirty files in silence is indistinguishable from one that is stuck, and impossible to redirect before the tokens are spent. Keep each line short: a sentence, not a paragraph.\n\nBe concise, direct, and helpful.",
1150
- // analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
1151
- // prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
1152
- judge: "Synthesize the council evaluations and make a single decision: 'pass' (sufficient), 'revise' (fix it, with reasons), or 'ask-human' (a question to ask the user). Return {decision, feedback, question} via submit.",
1153
- "project-manager": 'Read the given plan and break it into real, actionable tasks (id, short title, deps). Each task should be a single, clear piece of work. Return {tasks} via submit.\n\nThe `writing-plans` skill above governs WHAT MAKES A TASK EXECUTABLE \u2014 take that from it and nothing else. Two bindings, because the skill describes a different habitat:\n- STRUCTURE comes from the spec-kit tasks template you are given (phases, story grouping, [P] markers), NOT from the skill\'s own document layout. Ignore its `docs/superpowers/plans/\u2026` path, its required-sub-skill header, and its execution-handoff section: this pipeline already owns worktrees, dispatch and review.\n- What you DO take: exact file paths per task, a real test cycle rather than a vague "add tests" step, no placeholders (no TBD/TODO/"similar to task N"), and interfaces named explicitly so a task whose implementer never sees the others still knows the signatures it must produce and consume.\n- SIZING is the third rebinding, and the one that costs most when it is missed. The skill says "bite-sized", "one action, 2-5 minutes", "the smallest unit worth a reviewer\'s gate" \u2014 sound advice where a gate is one reader glancing at a diff. Here a card is not a line in a document: it is its own worktree, its own implementer, a full review TEAM of lenses, a council when they disagree, an acceptance gate and a merge. That overhead is paid per CARD and barely varies with the card\'s size, so splitting work finer does not divide the cost, it multiplies it.\nSize a card to a coherent piece of BEHAVIOUR a reviewer can judge whole, not to a file. An entity, its configuration, its migration and its tests are one card, because nobody can review one without the others and nothing is deliverable until all of them exist. Split only for a reason that survives being said out loud: the parts can be reviewed and merged independently, or they must run in parallel in different worktrees. "They are different files" is not such a reason. Fold setup and scaffolding into the card whose deliverable needs them.',
1154
- "task-auditor": "You are the last check on a task breakdown before any of it is built. Every hour of implementation after you is spent executing this list, and a bad list does not fail \u2014 the tasks pass their reviews and the wrong work is delivered correctly. Its structure has already been checked mechanically; you are here for the part only a reader can answer: does the breakdown deliver what the plan requires, and would a task's acceptance criteria still hold for an implementation that missed the point? Do not propose better work than the plan asked for \u2014 scope you invent here becomes hours someone spends. Flag any task whose only deliverable is an answer \u2014 verifying, inspecting, confirming \u2014 because an implementer reads the code as part of doing the work, and a task that ends with the repository unchanged has spent a review round on nothing. Flag OVER-SPLITTING for the same reason, and it is the more expensive mistake: every card carries a full review team, a council and an acceptance gate whatever its size, so a breakdown that gives a class and its configuration separate cards pays that overhead twice for work no one can review apart. Say which cards should be one. A clean breakdown is the normal case; say so. Return {missing, weak} via submit.",
1155
- "team-lead": "You audit a task breakdown before any of it runs. The schedule itself is computed from the declared dependencies and is not yours to write; what nothing has checked is whether those dependencies are RIGHT. You are given the tasks with the files each one writes and what must be true when it is done, plus the groups that would run at the same time in separate worktrees. Find the task that cannot actually start yet because it needs a type, function, table or config key another task in its own group creates \u2014 and say which declared dependencies hold work back for no reason. Both answers are usually empty; say so rather than inventing an edge. Return {missing, spurious} via submit.",
1156
- router: "Look at the task title and choose the implementer role: 'designer' for UI/UX work, 'coder' for other code work. Return {role} via submit.",
1157
- coder: "Implement the given task in the worktree. If it is a new task, start from scratch; if it is a returning task, address the reviewer notes. Work with read/write/edit/grep/glob/shell and run the tests.\n\nThe `test-driven-development` skill above is how you write code here: the failing test comes first, and it must fail for the RIGHT reason before you make it pass. A test that asserts nothing is worse than no test \u2014 it reports success forever. Bindings for this pipeline: your worktree is already prepared (do not create one), every file you write is committed as you write it, and there is no separate agent to hand off to \u2014 you take the task to green yourself.",
1158
- designer: "Implement the UI/UX task in the worktree. Focus on the user interface and experience; work with read/write/edit.\n\nThe `frontend-design` skill above governs the LOOK: aesthetic direction, typography, and choices that do not read as templated defaults. Follow the project's existing visual language where there is one \u2014 a distinctive design that fights the surrounding product is worse than a plain one that fits it.",
1159
- "senior-coder": "Take over the task the coder got stuck on; implement it with a more careful approach. Take the reviewer notes and previous attempts into account.\n\nYou are here because a previous attempt failed, so start by understanding WHY rather than rewriting: the `systematic-debugging` skill is available (fetch it with the `skill` tool) and is the right tool when a test fails or behaviour is unexplained. The `test-driven-development` skill above still governs how you write the fix \u2014 reproduce the failure in a test first, then make it pass.",
1160
- "senior-designer": "Take over the UI/UX task the designer got stuck on; implement it more carefully.\n\nA previous attempt already failed, so establish WHY before redesigning \u2014 the `systematic-debugging` skill is available via the `skill` tool when the failure is behavioural rather than visual. The `frontend-design` skill above still governs the look.",
1161
- architect: "Analyze the root cause of a repeatedly failing task or a merge conflict, and produce a concrete solution plan. Return {rootCause, plan} via submit.\n\nFetch the `systematic-debugging` skill with the `skill` tool and follow it: your job is the ROOT CAUSE, and the failure mode to avoid is proposing a plausible fix for a cause you never established. Say what the evidence is, not what it might be.",
1162
- "code-reviewer": "Review the worktree changes of the task in REVIEW (correctness, tests, quality). Return {verdict: pass|fail, notes} via submit \u2014 your decision is final.",
1163
- "principal-coder": "Holistically review all changes in the PR (base worktree). If sufficient, approve; otherwise request-changes with concrete comments. In the final decision round, give accept or ask-human (a question to ask the user).",
1164
- "memory-keeper": "You are the ONLY writer into this project's long-term memory. Everything else \u2014 every review lens, the council, the judge \u2014 can merely PROPOSE; you decide.\n\nTreat every proposal as an UNVERIFIED CLAIM from a narrow, single-angle agent that saw one slice of one job, not as text to store. Most proposals are wrong in a specific way: they generalize a one-off into a rule, they restate the finding the agent was reviewing, or they record general programming advice any model already knows. Discard all of those. When a claim does survive, REWRITE it in your own words \u2014 never store an agent's sentence verbatim. Merge proposals that say the same thing into one memory.\n\nA memory qualifies ONLY if it is (a) durable \u2014 still true next month, (b) project-specific, and (c) actionable \u2014 it would change what an agent does. Write conventions, constraints, gotchas and root causes. A `lesson` must state what went wrong AND what to do instead. Set `audience` only when the memory is genuinely useful to specific roles and useless to the rest; leave it out otherwise.\n\nNEVER write transient run detail (task ids, attempt counts, what happened today), never restate the request, never duplicate a memory that already exists, and never include credentials, tokens, keys, or anything resembling a secret. Each memory is one self-contained sentence that makes sense with no other context.\n\nReturn at most 5 memories via submit as {memories}. Returning NONE is the most common correct answer \u2014 prefer an empty list over a weak memory, because a bad memory is injected into every future run.",
1165
- operational: "You handle version control for the project. Given a git diff of work just completed, write a single Conventional Commits message: `type(scope): subject`. Types: feat, fix, docs, refactor, test, chore, style, perf, build, ci. Choose the scope from the touched area (e.g. spec, plan, tasks, or a module name) or omit it. The subject is imperative, lowercase, \u226472 chars, no trailing period. Add a short body only if the change genuinely needs explanation. Commit messages are always in English. Return {message} via submit."
1166
- };
1167
- var SPEC_TEAM = [
1168
- { name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
1169
- { name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
1170
- { name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
1171
- { name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
1172
- { name: "spec-abstraction-leak", perspective: "implementation detail that has leaked into the spec (languages, frameworks, APIs, storage mechanics, code structure) \u2014 a spec must stay technology-agnostic", models: [] },
1173
- { name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
1174
- { name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
1175
- { name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
1176
- { name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
1177
- ];
1178
- var PLAN_TEAM = [
1179
- { name: "plan-spec-conformance", perspective: "traceability to the approved spec: every requirement covered by the plan, and nothing planned that the spec never asked for", models: [] },
1180
- { name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
1181
- { name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
1182
- { name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
1183
- { name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
1184
- { name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
1185
- { name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
1186
- { name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
1187
- { name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
1188
- { name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
1189
- { name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
1190
- { name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
1191
- { name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
1192
- { name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
1193
- ];
1194
- var CODE_TEAM = [
1195
- { name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
1196
- { name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
1197
- { name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
1198
- { name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
1199
- { name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
1200
- { name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
1201
- { name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
1202
- { name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
1203
- { name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
1204
- { name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
1205
- { name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
1206
- { name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
1207
- { name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
1208
- { name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
1209
- { name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
1210
- ];
1211
- var DEFAULT_COUNCIL = [
1212
- { name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
1213
- { name: "risk-judge", perspective: "What is the real blast radius of shipping this as-is? Weigh security, failure modes, concurrency, and data-integrity findings against likelihood and severity.", models: [] },
1214
- { name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
1215
- { name: "user-value-judge", perspective: "Does this deliver the user's actual intent well? Weigh usability, accessibility, and whether the scope serves the request without gold-plating.", models: [] },
1216
- { name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
1217
- ];
1218
- function placedSkills() {
1219
- return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
1220
- }
1221
-
1222
- // src/providers/anthropic.ts
1223
- function isAnthropicModel(model) {
1224
- return /(^|\/)(claude|fable|mythos)/i.test(model) || /claude/i.test(model);
1225
- }
1226
-
1227
- // src/tui/role-models.ts
1228
- var WEAK_RE = /\b(flash|mini|nano|haiku|lite|small|turbo|fast|\d{1,2}b)\b/i;
1229
- var FLAGSHIP_ROLES = ["judge", "principal-coder"];
1230
- var COUNCIL_ROLES = DEFAULT_COUNCIL.map((c) => c.name);
1231
- var SPEC_LENS_ROLES = SPEC_TEAM.map((c) => c.name);
1232
- var PLAN_LENS_ROLES = PLAN_TEAM.map((c) => c.name);
1233
- var CODE_LENS_ROLES = CODE_TEAM.map((c) => c.name);
1234
- var STRONG_ROLES = [
1235
- "brainstormer",
1236
- "analyst",
1237
- "planner",
1238
- "architect",
1239
- "senior-coder",
1240
- "senior-designer",
1241
- ...COUNCIL_ROLES,
1242
- ...PLAN_LENS_ROLES,
1243
- ...CODE_LENS_ROLES
1244
- ];
1245
- var MID_ROLES = ["coach", "coder", "designer", "code-reviewer", "operational", "memory-keeper", "task-auditor", ...SPEC_LENS_ROLES];
1246
- var FAST_ROLES = ["refiner", "router", "project-manager", "team-lead"];
1247
- var CAPABLE_ROLES = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES]);
1248
- var ROLE_PROFILES = {
1249
- tracer: "Writes the per-file reference note every other agent reads before changing unfamiliar code \u2014 high volume, but its output is a COMMITTED FILE, not a turn in a conversation: a shallow note is believed by every agent that opens that file, forever, and nothing later corrects it. Give it the MOST capable non-[flagship] model in the catalogue, not merely one that qualifies as [strong]. Volume is not a reason to go cheaper here.",
1250
- refiner: "Classifies intent and rewrites the prompt every turn \u2014 highest call volume, trivial task \u2192 a fast, cheap model.",
1251
- router: "Picks coder-vs-designer for a task \u2014 tiny and frequent \u2192 fast, cheap.",
1252
- "project-manager": "Turns a task list into board items \u2014 light and structured \u2192 fast, cheap.",
1253
- "task-auditor": "The only check on the task breakdown before hours of implementation are spent executing it \u2014 reads the plan against the task list and finds what was dropped. Low volume, and everything downstream depends on it \u2192 a capable model, never the cheapest.",
1254
- "team-lead": "Coordinates implementation waves \u2014 light orchestration \u2192 fast, cheap.",
1255
- coach: "Your main interactive assistant, used constantly all session (highest interaction volume) \u2192 a capable but EFFICIENT model, never the costly flagship.",
1256
- brainstormer: "Turns a raw request into a decided design before the spec: explores the repo, weighs 2-3 approaches, gets the user to choose. Low volume, sets the direction for everything downstream \u2192 a strong reasoning model.",
1257
- analyst: "Authors the spec and constitution \u2192 a strong reasoning model (Opus-tier).",
1258
- planner: "Designs the implementation plan \u2192 a strong reasoning model (Opus-tier).",
1259
- architect: "Diagnoses stuck tasks and produces recovery plans \u2014 serious design work \u2192 a strong model.",
1260
- judge: "Critiques specs/plans and makes the final review call \u2014 low volume, high stakes \u2192 the most capable flagship model.",
1261
- coder: "Writes the bulk of the implementation \u2014 very high work volume \u2192 a good high-throughput coding model (Sonnet-tier), NOT the flagship (wasteful at this volume).",
1262
- "senior-coder": "Reviews and revises above the coder \u2014 must be MORE capable than the coder (Opus-tier).",
1263
- "principal-coder": "Final code decision-maker \u2014 low volume, high stakes \u2192 the flagship is appropriate.",
1264
- designer: "Builds UI \u2014 high volume \u2192 a capable coding/design model, not the flagship.",
1265
- "senior-designer": "Senior UI reviewer \u2014 more capable than the designer.",
1266
- "code-reviewer": "Reviews diffs \u2014 moderate volume \u2192 a solid capable model.",
1267
- "memory-keeper": "Decides what a finished job taught the project and writes it to durable memory \u2014 low volume, but a bad memory poisons every later run \u2192 a capable, efficient model, never the cheapest.",
1268
- operational: "Handles version control: writes conventional commit messages and (later) drives merges/conflicts \u2014 high volume \u2192 a capable, efficient model."
1269
- };
1270
- for (const [stage, lenses, heft] of [
1271
- ["spec", SPEC_TEAM, "a capable, efficient model (a spec is a short business-level doc)"],
1272
- ["plan", PLAN_TEAM, "a strong model (technical design judgment)"],
1273
- ["code", CODE_TEAM, "a strong model (reads real implementations)"]
1274
- ]) {
1275
- for (const l of lenses) ROLE_PROFILES[l.name] = `${stage.toUpperCase()}-review lens \u2014 ${l.perspective}. Low volume, quality-critical \u2192 ${heft}.`;
1276
- }
1277
- for (const c of DEFAULT_COUNCIL) {
1278
- ROLE_PROFILES[c.name] = `Review COUNCIL decider \u2014 ${c.perspective} Casts the binding pass/revise vote on contested work \u2192 a strong model.`;
1279
- }
1280
- var ROLE_ADVICE = ROLE_PROFILES;
1281
- function filterModelsForRole(role, all, exclude = []) {
1282
- const advice = ROLE_ADVICE[role];
1283
- const excluded2 = new Set(exclude);
1284
- const avail = all.filter((m) => !excluded2.has(m));
1285
- if (CAPABLE_ROLES.has(role)) {
1286
- const strong = avail.filter((m) => !WEAK_RE.test(m));
1287
- if (strong.length === 0) return { models: avail.length ? avail : all, note: advice ? `${advice} (No strong models detected \u2014 showing all.)` : void 0 };
1288
- return { models: strong, note: `${advice ?? ""} Showing ${strong.length} of ${avail.length} models (fast/weak models hidden for this role).`.trim() };
1289
- }
1290
- if (FAST_ROLES.includes(role)) {
1291
- const fast = avail.filter((m) => WEAK_RE.test(m));
1292
- if (fast.length === 0) return { models: avail.length ? avail : all, note: advice };
1293
- return { models: fast, note: `${advice ?? ""} Showing ${fast.length} of ${avail.length} fast/cheap models.`.trim() };
1294
- }
1295
- return { models: avail.length ? avail : all };
1296
- }
1297
- function effortFor(role, model) {
1298
- if (!isAnthropicModel(model)) return void 0;
1299
- if (FLAGSHIP_ROLES.includes(role)) return "max";
1300
- if (STRONG_ROLES.includes(role)) return "xhigh";
1301
- if (FAST_ROLES.includes(role)) return "low";
1302
- if (MID_ROLES.includes(role)) return "high";
1303
- return void 0;
1304
- }
1305
- var effortBump = (s) => /-(ultra|max|xhigh)/.test(s) ? 4 : /-high/.test(s) ? 3 : /-medium/.test(s) ? 2 : /-low/.test(s) ? 1 : 0;
1306
- var versionBump = (s, family) => {
1307
- if (family) {
1308
- const m = s.match(new RegExp(`${family}[-_. ]?(\\d+)(?:[-.](\\d+))?`));
1309
- if (m) {
1310
- const major = Number(m[1]);
1311
- const minor = m[2] === void 0 ? 0 : Number(m[2]);
1312
- if (major < 100) return major + (minor < 10 ? minor / 10 : minor / 100);
1313
- }
1314
- }
1315
- const g = s.match(/(\d)[-.](\d)\b/);
1316
- return g ? Number(g[1]) + Number(g[2]) / 10 : 0;
1317
- };
1318
- var KNOWN_FAMILY_RE = /(fable|mythos|opus|sonnet|haiku|claude|codex|gpt-|\bo\d\b|gemini|deepseek|llama|qwen|kimi|glm|mistral|grok|nova|command-r|phi-\d)/i;
1319
- var NON_TEXT_RE = /\b(image|imagen|vision|video|veo|tts|audio|speech|voice|embed|embedding|rerank|ocr|computer-use|realtime|moderation)\b/i;
1320
- function isKnownModel(model) {
1321
- return KNOWN_FAMILY_RE.test(model) && !NON_TEXT_RE.test(model);
1322
- }
1323
- var UNRANKED_SCORE = 50;
1324
- function capabilityScore(model) {
1325
- const s = model.toLowerCase();
1326
- if (WEAK_RE.test(s)) return 20 + effortBump(s);
1327
- if (/fable|mythos/.test(s)) return 100;
1328
- if (/opus/.test(s)) return 88 + versionBump(s, "opus");
1329
- if (/codex|gpt-5|\bo3\b/.test(s)) return 82 + effortBump(s) + versionBump(s, "gpt") / 100;
1330
- if (/sonnet/.test(s)) return 78 + versionBump(s, "sonnet");
1331
- if (/gemini/.test(s) && /pro/.test(s)) return 76 + versionBump(s, "gemini") + effortBump(s);
1332
- if (/gpt-4/.test(s)) return 65;
1333
- if (/deepseek/.test(s)) return 55;
1334
- return UNRANKED_SCORE;
1335
- }
1336
- function mostCapable(models) {
1337
- return [...models].sort((a, b) => capabilityScore(b) - capabilityScore(a))[0] ?? "";
1338
- }
1339
- function modelBand(model) {
1340
- if (WEAK_RE.test(model)) return "fast";
1341
- const s = capabilityScore(model);
1342
- if (s >= 95) return "flagship";
1343
- if (s >= 84) return "strong";
1344
- if (s <= UNRANKED_SCORE) return "fast";
1345
- return "mid";
1346
- }
1347
- function baseModel(model) {
1348
- const segs = model.toLowerCase().split("/");
1349
- let s = segs[segs.length - 1];
1350
- s = s.replace(/-(ultra|max|xhigh|high|medium|low|free|thinking|preview)\b/g, "");
1351
- s = s.replace(/-\d{6,8}\b/g, "");
1352
- return s.replace(/-+$/, "");
1353
- }
1354
- function modelFamily(model) {
1355
- return baseModel(model).replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
1356
- }
1357
- function latestFirst(models) {
1358
- const best = /* @__PURE__ */ new Map();
1359
- for (const m of models) {
1360
- const key2 = modelFamily(m);
1361
- const cur = best.get(key2);
1362
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
1363
- }
1364
- const isLatest = (m) => best.get(modelFamily(m)) === m;
1365
- return [...models.filter(isLatest), ...models.filter((m) => !isLatest(m))];
1366
- }
1367
- function versionlessId(model) {
1368
- const cut = model.lastIndexOf("/");
1369
- const prefix = cut >= 0 ? model.slice(0, cut + 1) : "";
1370
- const name = model.slice(cut + 1).toLowerCase().replace(/-\d{6,8}\b/g, "").replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
1371
- return prefix + name;
1372
- }
1373
- var DURABLE_ROLES = ["tracer"];
1374
- function strongestPrimary(chain, pool) {
1375
- const head = chain[0];
1376
- if (!head) return chain;
1377
- let best = head;
1378
- for (const m of pool) {
1379
- if (modelBand(m) === "flagship" || !isKnownModel(m)) continue;
1380
- if (capabilityScore(m) > capabilityScore(best)) best = m;
1381
- }
1382
- if (best === head) return chain;
1383
- const at = chain.indexOf(best);
1384
- if (at > 0) {
1385
- const next = [...chain];
1386
- next[at] = head;
1387
- next[0] = best;
1388
- return next;
1389
- }
1390
- return [best, ...chain.slice(1)];
1391
- }
1392
- function newestPrimary(chain, pool) {
1393
- const head = chain[0];
1394
- if (!head) return chain;
1395
- const key2 = versionlessId(head);
1396
- let best = head;
1397
- for (const m of pool) {
1398
- if (versionlessId(m) !== key2) continue;
1399
- if (capabilityScore(m) > capabilityScore(best)) best = m;
1400
- }
1401
- if (best === head) return chain;
1402
- const at = chain.indexOf(best);
1403
- if (at > 0) {
1404
- const next = [...chain];
1405
- next[at] = head;
1406
- next[0] = best;
1407
- return next;
1408
- }
1409
- return [best, ...chain.slice(1)];
1410
- }
1411
- function dedupBest(models) {
1412
- const best = /* @__PURE__ */ new Map();
1413
- for (const m of models) {
1414
- const key2 = baseModel(m);
1415
- const cur = best.get(key2);
1416
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
1417
- }
1418
- return [...best.values()].sort((a, b) => capabilityScore(b) - capabilityScore(a));
1419
- }
1420
- function sourceOf(model) {
1421
- const s = model.toLowerCase().replace(/^no-think\//, "");
1422
- return cliFor(s) ?? s.split("/")[0];
1423
- }
1424
- function interleaveBySource(pool) {
1425
- const bySource = /* @__PURE__ */ new Map();
1426
- for (const m of pool) {
1427
- const s = sourceOf(m);
1428
- const q = bySource.get(s);
1429
- if (q) q.push(m);
1430
- else bySource.set(s, [m]);
1431
- }
1432
- const queues = [...bySource.values()];
1433
- const out = [];
1434
- for (let more = true; more; ) {
1435
- more = false;
1436
- for (const q of queues) {
1437
- const m = q.shift();
1438
- if (m !== void 0) {
1439
- out.push(m);
1440
- more = true;
1441
- }
1442
- }
1443
- }
1444
- return out;
1445
- }
1446
- var BAND_ORDER = { fast: 0, mid: 1, strong: 2, flagship: 3 };
1447
- function bandDistance(primary, candidate) {
1448
- const p = BAND_ORDER[modelBand(primary)];
1449
- const c = BAND_ORDER[modelBand(candidate)];
1450
- return Math.abs(c - p) * 2 + (c < p ? 1 : 0);
1451
- }
1452
- function pickFallbacks(primary, pool, n) {
1453
- const chosen = [];
1454
- const usedModels = /* @__PURE__ */ new Set([baseModel(primary)]);
1455
- const usedSources = /* @__PURE__ */ new Set([sourceOf(primary)]);
1456
- const byHeft = pool.map((m, i) => ({ m, i })).sort((a, b) => bandDistance(primary, a.m) - bandDistance(primary, b.m) || a.i - b.i).map((x) => x.m);
1457
- for (const m of byHeft) {
1458
- if (chosen.length >= n) break;
1459
- if (usedModels.has(baseModel(m)) || usedSources.has(sourceOf(m))) continue;
1460
- chosen.push(m);
1461
- usedModels.add(baseModel(m));
1462
- usedSources.add(sourceOf(m));
1463
- }
1464
- for (const m of byHeft) {
1465
- if (chosen.length >= n) break;
1466
- if (usedModels.has(baseModel(m))) continue;
1467
- chosen.push(m);
1468
- usedModels.add(baseModel(m));
1469
- }
1470
- return chosen;
1471
- }
1472
- var FALLBACK_COUNT = 2;
1473
- function adjustRoleModels(roles, models, unfit) {
1474
- if (models.length === 0) return [];
1475
- const recognised = models.filter(isKnownModel);
1476
- const pick = recognised.length ? recognised : models;
1477
- const capable = dedupBest(pick.filter((m) => !WEAK_RE.test(m)));
1478
- const fast = dedupBest(pick.filter((m) => WEAK_RE.test(m)));
1479
- const capablePool = capable.length ? capable : fast;
1480
- const fastPool = fast.length ? fast : capable;
1481
- const primaryPool = latestFirst(capablePool);
1482
- const primaryFast = latestFirst(fastPool);
1483
- const nonFlagship = primaryPool.filter((m) => modelBand(m) !== "flagship");
1484
- const strongPool = primaryPool.filter((m) => modelBand(m) === "strong");
1485
- const midPool = primaryPool.filter((m) => modelBand(m) === "mid");
1486
- const wanted = new Set(roles);
1487
- const forRole = (role, pool) => {
1488
- if (!unfit) return pool;
1489
- const fit = pool.filter((m) => !unfit(role, m));
1490
- return fit.length ? fit : pool;
1491
- };
1492
- const known = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES, ...FAST_ROLES]);
1493
- const primary = /* @__PURE__ */ new Map();
1494
- const flagSrc = primaryPool;
1495
- FLAGSHIP_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1496
- const src = forRole(r, flagSrc);
1497
- primary.set(r, src[i % src.length]);
1498
- });
1499
- const strongSrc = interleaveBySource(strongPool.length ? strongPool : nonFlagship.length ? nonFlagship : primaryPool);
1500
- STRONG_ROLES.filter((r) => wanted.has(r)).concat(roles.filter((r) => !known.has(r))).forEach((r, i) => {
1501
- const src = forRole(r, strongSrc);
1502
- primary.set(r, src[i % src.length]);
1503
- });
1504
- const midSrc = interleaveBySource(midPool.length ? midPool : nonFlagship.length ? nonFlagship : primaryPool);
1505
- MID_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1506
- const src = forRole(r, midSrc);
1507
- primary.set(r, src[i % src.length]);
1508
- });
1509
- FAST_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1510
- const src = forRole(r, primaryFast);
1511
- primary.set(r, src[i % src.length]);
1512
- });
1513
- return roles.map((role) => {
1514
- const head = primary.get(role) ?? primaryPool[0];
1515
- const capForFb = MID_ROLES.includes(role) ? capablePool.filter((m) => modelBand(m) !== "flagship") : capablePool;
1516
- const pool = FAST_ROLES.includes(role) ? [...fastPool, ...capForFb] : [...capForFb, ...fastPool];
1517
- return { role, models: newestPrimary([head, ...pickFallbacks(head, forRole(role, pool), FALLBACK_COUNT)], models) };
1518
- });
1519
- }
1520
-
1521
- // src/skills/apply.ts
1522
- import { readFile } from "fs/promises";
1523
- import { readdirSync as readdirSync2 } from "fs";
1524
- import { resolve as resolve2, sep as sep3 } from "path";
1525
- import { z } from "zod";
1526
- function applySkills(basePrompt, mandatory, registry) {
1527
- const parts = [basePrompt];
1528
- if (mandatory.length) {
1529
- const sections = mandatory.map((name) => {
1530
- const skill = registry.get(name);
1531
- if (!skill) throw new Error(`applySkills: undefined skill: ${name}`);
1532
- const where = skill.dir ? `
1533
- _Skill base directory: ${skill.dir}_
1534
- ` : "";
1535
- return `## ${skill.name}${where}
1536
- ${skill.content}`;
1537
- });
1538
- parts.push(`# Mandatory Skills
1539
- ${sections.join("\n\n")}`);
1540
- }
1541
- const mandatorySet = new Set(mandatory);
1542
- const available = registry.list().filter((s) => !mandatorySet.has(s.name));
1543
- if (available.length) {
1544
- const lines = available.map((s) => `- ${s.name}: ${s.description}`);
1545
- parts.push(`# Discoverable Skills (call the skill tool to fetch its content)
1546
- ${lines.join("\n")}`);
1547
- }
1548
- return parts.join("\n\n");
1549
- }
1550
- var skillParams = z.object({
1551
- name: z.string().describe("The skill's name, exactly as it is listed."),
1552
- /**
1553
- * A supporting document inside the skill's own directory, e.g. "reference/critique.md".
1554
- *
1555
- * Described, because an undescribed optional string gets filled in. Measured: four consecutive calls sent
1556
- * `file: ""` and every one of them failed — the skill was there, its content was one branch away, and an
1557
- * empty string took the other branch.
1558
- */
1559
- file: z.string().optional().describe('Optional. A supporting document inside the skill, e.g. "reference/critique.md". Omit it to read the skill itself \u2014 do not pass an empty string.')
1560
- });
1561
- var DOCS_SHOWN = 12;
1562
- function docsIn(dir) {
1563
- try {
1564
- return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.name !== "SKILL.md" && !e.name.startsWith(".")).map((e) => e.isDirectory() ? `${e.name}/` : e.name).sort().slice(0, DOCS_SHOWN);
1565
- } catch {
1566
- return [];
1567
- }
1568
- }
1569
- var MAX_SKILL_DOC_CHARS = 3e4;
1570
- var MAX_SKILLS_LISTED = 12;
1571
- function noSuchSkill(name, available) {
1572
- const shape = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1573
- const same = available.filter((s) => shape(s) === shape(name));
1574
- if (same.length === 1) {
1575
- return `skill not found: ${name} \u2014 did you mean \`${same[0]}\`? Call it with that exact name.`;
1576
- }
1577
- if (!available.length) return `skill not found: ${name}. This project has no skills installed, so carry on without one.`;
1578
- const near = available.filter((s) => shape(s).includes(shape(name)) || shape(name).includes(shape(s)));
1579
- if (near.length && near.length <= MAX_SKILLS_LISTED) {
1580
- return `skill not found: ${name}. Closest by name: ${near.map((s) => `\`${s}\``).join(", ")}. Call one of those exactly if it is what you meant, or carry on without a skill.`;
1581
- }
1582
- const shown = available.slice(0, MAX_SKILLS_LISTED);
1583
- const rest = available.length - shown.length;
1584
- return `skill not found: ${name}. Available: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more \u2014 the full list is in your system prompt` : ""}. Use one of these exactly, or carry on without a skill \u2014 do not guess another name.`;
1585
- }
1586
- function buildSkillTool(registry) {
1587
- return {
1588
- name: "skill",
1589
- description: 'Fetch a skill\'s content by name. Some skills are dispatchers whose SKILL.md points at supporting documents (e.g. "see reference/critique.md"); pass `file` with that relative path to read one. Fetch a document only when the skill actually sends you to it.',
1590
- permissionLevel: "safe",
1591
- parameters: skillParams,
1592
- run: async (rawArgs) => {
1593
- const parsed = skillParams.safeParse(rawArgs);
1594
- if (!parsed.success) {
1595
- return { content: `skill: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
1596
- }
1597
- const { name, file } = parsed.data;
1598
- const skill = registry.get(name);
1599
- if (!skill) return { content: noSuchSkill(name, registry.list().map((s) => s.name)), isError: true };
1600
- if (file === void 0 || !file.trim()) {
1601
- const where = skill.dir ? `_Skill base directory: ${skill.dir}_
1602
-
1603
- ` : "";
1604
- return { content: `${where}${skill.content}`, isError: false };
1605
- }
1606
- if (!skill.dir) return { content: `skill ${name}: has no supporting documents`, isError: true };
1607
- const target = resolve2(skill.dir, file);
1608
- const root = resolve2(skill.dir);
1609
- if (target !== root && !target.startsWith(root + sep3)) {
1610
- return { content: `skill ${name}: ${file} is outside the skill directory`, isError: true };
1611
- }
1612
- let raw;
1613
- try {
1614
- raw = await readFile(target, "utf8");
1615
- } catch {
1616
- const has = docsIn(skill.dir);
1617
- return {
1618
- content: `skill ${name}: no such document: ${file}` + (has.length ? `. It has: ${has.join(", ")}` : `. It has no supporting documents.`),
1619
- isError: true
1620
- };
1621
- }
1622
- if (raw.length <= MAX_SKILL_DOC_CHARS) return { content: raw, isError: false };
1623
- return {
1624
- content: `${raw.slice(0, MAX_SKILL_DOC_CHARS)}
1625
-
1626
- [skill ${name}/${file}: truncated at ${MAX_SKILL_DOC_CHARS} of ${raw.length} chars]`,
1627
- isError: false
1628
- };
1629
- }
1630
- };
1631
- }
1632
-
1633
890
  // src/engine/unfinished.ts
1634
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync, statSync } from "fs";
891
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync, statSync } from "fs";
1635
892
  import { join as join3 } from "path";
1636
893
  function boardCounts(dir) {
1637
894
  try {
@@ -1647,7 +904,7 @@ function unfinishedSessions(cwd, commitCount = () => 0) {
1647
904
  const root = join3(cwd, ".horsecode", "worktrees");
1648
905
  if (!existsSync3(root)) return [];
1649
906
  const out = [];
1650
- for (const id of readdirSync3(root)) {
907
+ for (const id of readdirSync2(root)) {
1651
908
  const dir = join3(root, id);
1652
909
  try {
1653
910
  if (!statSync(dir).isDirectory()) continue;
@@ -1684,7 +941,7 @@ function describeUnfinished(s) {
1684
941
 
1685
942
  // src/tools/git.ts
1686
943
  import { execFile } from "child_process";
1687
- import { z as z2 } from "zod";
944
+ import { z } from "zod";
1688
945
  var READ_ONLY = /* @__PURE__ */ new Set([
1689
946
  "status",
1690
947
  "log",
@@ -1755,8 +1012,8 @@ var READ_ONLY_PAIRS = /* @__PURE__ */ new Set([
1755
1012
  "stash show"
1756
1013
  ]);
1757
1014
  var REFUSED_ARG = /^(--output|-c$|--config-env|--exec-path|-C$|--git-dir|--work-tree|--upload-pack|--receive-pack)/;
1758
- var params = z2.object({
1759
- args: z2.array(z2.string()).min(1).describe(
1015
+ var params = z.object({
1016
+ args: z.array(z.string()).min(1).describe(
1760
1017
  'Git arguments as a list, without the leading "git" \u2014 e.g. ["status","--porcelain"] or ["log","-5","--oneline"].'
1761
1018
  )
1762
1019
  });
@@ -1925,7 +1182,7 @@ var gitTool = {
1925
1182
  const args = parsed.data.args;
1926
1183
  const why = refuse(args);
1927
1184
  if (why) return { content: why, isError: true, settled: true };
1928
- const out = await new Promise((resolve6) => {
1185
+ const out = await new Promise((resolve5) => {
1929
1186
  const child = execFile("git", args, {
1930
1187
  cwd: ctx.cwd,
1931
1188
  timeout: GIT_TIMEOUT_MS,
@@ -1935,7 +1192,7 @@ var gitTool = {
1935
1192
  env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
1936
1193
  }, (err, stdout, stderr) => {
1937
1194
  const text = `${stdout}${stderr}`.trim();
1938
- resolve6({ code: err?.code ?? (err ? 1 : 0), text });
1195
+ resolve5({ code: err?.code ?? (err ? 1 : 0), text });
1939
1196
  });
1940
1197
  ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1941
1198
  });
@@ -1996,7 +1253,7 @@ var gitWriteTool = {
1996
1253
  const args = parsed.data.args;
1997
1254
  const why = refuseWrite(args);
1998
1255
  if (why) return { content: why, isError: true, settled: true };
1999
- const out = await new Promise((resolve6) => {
1256
+ const out = await new Promise((resolve5) => {
2000
1257
  const child = execFile("git", args, {
2001
1258
  cwd: ctx.cwd,
2002
1259
  // A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
@@ -2007,7 +1264,7 @@ var gitWriteTool = {
2007
1264
  // prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
2008
1265
  env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
2009
1266
  }, (err, stdout, stderr) => {
2010
- resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
1267
+ resolve5({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
2011
1268
  });
2012
1269
  ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
2013
1270
  });
@@ -2019,9 +1276,9 @@ var gitWriteTool = {
2019
1276
  };
2020
1277
 
2021
1278
  // src/tools/remember.ts
2022
- import { z as z3 } from "zod";
2023
- var params2 = z3.object({
2024
- fact: z3.string().describe(
1279
+ import { z as z2 } from "zod";
1280
+ var params2 = z2.object({
1281
+ fact: z2.string().describe(
2025
1282
  "One short sentence, durable and project-specific: where something lives, which command builds it, a convention this codebase follows, a schema detail that cost you a search. Not what you did, not what is true of the language in general \u2014 something the next agent would otherwise have to rediscover."
2026
1283
  )
2027
1284
  });
@@ -2046,7 +1303,7 @@ function buildRememberTool(sink) {
2046
1303
  var rememberFactTool = buildRememberTool();
2047
1304
 
2048
1305
  // src/speckit/layout.ts
2049
- import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync4 } from "fs";
1306
+ import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync3 } from "fs";
2050
1307
  import { join as join4 } from "path";
2051
1308
  function specsDir(workdir) {
2052
1309
  return join4(workdir, "specs");
@@ -2091,7 +1348,7 @@ function featureSlugFor(workdir, title) {
2091
1348
  const want = toSlug(title);
2092
1349
  const dir = specsDir(workdir);
2093
1350
  if (existsSync4(dir)) {
2094
- const names = readdirSync4(dir);
1351
+ const names = readdirSync3(dir);
2095
1352
  for (const name of names) {
2096
1353
  if (name.replace(/^\d+-/, "") === want) return name;
2097
1354
  }
@@ -2109,7 +1366,7 @@ function nextFeatureSlug(workdir, title) {
2109
1366
  const dir = specsDir(workdir);
2110
1367
  let max = 0;
2111
1368
  if (existsSync4(dir)) {
2112
- for (const name of readdirSync4(dir)) {
1369
+ for (const name of readdirSync3(dir)) {
2113
1370
  const m = name.match(/^(\d+)-/);
2114
1371
  if (m) max = Math.max(max, Number(m[1]));
2115
1372
  }
@@ -2127,7 +1384,7 @@ function scaffoldFeature(workdir, slug) {
2127
1384
  import { createHash } from "crypto";
2128
1385
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
2129
1386
  import { join as join5 } from "path";
2130
- import { z as z4 } from "zod";
1387
+ import { z as z3 } from "zod";
2131
1388
 
2132
1389
  // src/engine/constitution.ts
2133
1390
  var SCOPES = ["always", "backend", "frontend", "data", "infra", "docs", "review", "spec", "test", "govern"];
@@ -2263,10 +1520,10 @@ function labellingLooksWrong(scoped) {
2263
1520
  }
2264
1521
 
2265
1522
  // src/engine/constitution-store.ts
2266
- var LabelsSchema = z4.object({
2267
- labels: z4.array(z4.object({
2268
- index: z4.number().int().describe("The rule's number, exactly as given to you."),
2269
- scopes: z4.array(z4.enum(SCOPES)).describe(
1523
+ var LabelsSchema = z3.object({
1524
+ labels: z3.array(z3.object({
1525
+ index: z3.number().int().describe("The rule's number, exactly as given to you."),
1526
+ scopes: z3.array(z3.enum(SCOPES)).describe(
2270
1527
  `Which kinds of work this rule actually binds. Only the ones it really governs: a rule that reaches everyone is carried into every agent's prompt, so a scope added "to be safe" is paid for on every call that will never use it.`
2271
1528
  )
2272
1529
  }))
@@ -2354,12 +1611,12 @@ async function constitutionNote(deps, cwd, work) {
2354
1611
  }
2355
1612
 
2356
1613
  // src/engine/reviewer.ts
2357
- import { z as z8 } from "zod";
1614
+ import { z as z7 } from "zod";
2358
1615
 
2359
1616
  // src/tools/find-tool.ts
2360
- import { z as z5 } from "zod";
2361
- var params3 = z5.object({
2362
- query: z5.string().describe(
1617
+ import { z as z4 } from "zod";
1618
+ var params3 = z4.object({
1619
+ query: z4.string().describe(
2363
1620
  'What you need a tool for, in a few words \u2014 e.g. "pull request comments", "list angular projects". Or an exact tool name to fetch just that one.'
2364
1621
  )
2365
1622
  });
@@ -2447,8 +1704,8 @@ ${rows.join("\n")}`,
2447
1704
  // src/tools/unfinished-tool.ts
2448
1705
  import { execFileSync } from "child_process";
2449
1706
  import { join as join6 } from "path";
2450
- import { z as z6 } from "zod";
2451
- var params4 = z6.object({});
1707
+ import { z as z5 } from "zod";
1708
+ var params4 = z5.object({});
2452
1709
  function commitsAhead(cwd, branch) {
2453
1710
  try {
2454
1711
  const out = execFileSync(
@@ -2497,10 +1754,10 @@ To continue one of these, the user says **continue** \u2014 that reopens the ses
2497
1754
  };
2498
1755
 
2499
1756
  // src/tools/propose-memory.ts
2500
- import { z as z7 } from "zod";
2501
- var params5 = z7.object({
2502
- text: z7.string(),
2503
- kind: z7.enum(["fact", "lesson"]).optional().describe(
1757
+ import { z as z6 } from "zod";
1758
+ var params5 = z6.object({
1759
+ text: z6.string(),
1760
+ kind: z6.enum(["fact", "lesson"]).optional().describe(
2504
1761
  "`fact`: something true about this project that a later run would otherwise have to rediscover (where something lives, which command builds it). `lesson`: something learned the hard way \u2014 an approach that failed and what to do instead."
2505
1762
  )
2506
1763
  });
@@ -2756,11 +2013,11 @@ async function diffSince(cwd, sinceRef, git = defaultGitRunner) {
2756
2013
  // src/engine/reviewer.ts
2757
2014
  var CODE_REVIEW_MAX_TURNS = 25;
2758
2015
  var CODE_REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
2759
- var VerdictSchema = z8.object({
2760
- verdict: z8.enum(["pass", "fail"]).describe(
2016
+ var VerdictSchema = z7.object({
2017
+ verdict: z7.enum(["pass", "fail"]).describe(
2761
2018
  "`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."
2762
2019
  ),
2763
- notes: z8.array(z8.string())
2020
+ notes: z7.array(z7.string())
2764
2021
  });
2765
2022
  function readOnlyRegistry(deps, opts = {}) {
2766
2023
  const r = new ToolRegistry();
@@ -2828,9 +2085,9 @@ ${describeDiff(diff)}` };
2828
2085
  // src/tools/write.ts
2829
2086
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2830
2087
  import { existsSync as existsSync6 } from "fs";
2831
- import { dirname as dirname3, resolve as resolve3, sep as sep4 } from "path";
2832
- import { z as z9 } from "zod";
2833
- var params6 = z9.object({ path: z9.string(), content: z9.string() });
2088
+ import { dirname as dirname3, resolve as resolve2, sep as sep3 } from "path";
2089
+ import { z as z8 } from "zod";
2090
+ var params6 = z8.object({ path: z8.string(), content: z8.string() });
2834
2091
  var writeFileTool = {
2835
2092
  name: "write_file",
2836
2093
  description: "Writes content to a file (creates parent directories). Creating a NEW file is always allowed; to OVERWRITE an existing file you must read_file it first in this run \u2014 otherwise the write is refused.",
@@ -2849,9 +2106,9 @@ var writeFileTool = {
2849
2106
  };
2850
2107
  }
2851
2108
  const a = parsed.data;
2852
- const target = resolve3(ctx.cwd, a.path);
2853
- const cwdResolved = resolve3(ctx.cwd);
2854
- if (target !== cwdResolved && !target.startsWith(cwdResolved + sep4)) {
2109
+ const target = resolve2(ctx.cwd, a.path);
2110
+ const cwdResolved = resolve2(ctx.cwd);
2111
+ if (target !== cwdResolved && !target.startsWith(cwdResolved + sep3)) {
2855
2112
  return { content: `write_file: path is outside cwd: ${a.path}`, isError: true };
2856
2113
  }
2857
2114
  if (ctx.readFiles && existsSync6(target) && !ctx.readFiles.has(target)) {
@@ -2879,21 +2136,21 @@ var writeFileTool = {
2879
2136
  };
2880
2137
 
2881
2138
  // src/tools/edit.ts
2882
- import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
2883
- import { resolve as resolve4, sep as sep5 } from "path";
2884
- import { z as z10 } from "zod";
2885
- var params7 = z10.object({
2886
- path: z10.string(),
2887
- oldString: z10.string(),
2888
- newString: z10.string(),
2889
- replaceAll: z10.boolean().optional()
2139
+ import { readFile, writeFile as writeFile3 } from "fs/promises";
2140
+ import { resolve as resolve3, sep as sep4 } from "path";
2141
+ import { z as z9 } from "zod";
2142
+ var params7 = z9.object({
2143
+ path: z9.string(),
2144
+ oldString: z9.string(),
2145
+ newString: z9.string(),
2146
+ replaceAll: z9.boolean().optional()
2890
2147
  });
2891
2148
  var NEAR_MISS_CHARS = 600;
2892
2149
  var MAX_MATCH_LINES = 5;
2893
2150
  var norm = (t) => t.replace(/[ \t]+/g, " ").replace(/[ \t]+$/gm, "").trim();
2894
2151
  function shortPath(path, cwd) {
2895
- const abs = resolve4(cwd, path);
2896
- return abs === cwd ? "." : abs.startsWith(cwd + sep5) ? abs.slice(cwd.length + 1) : path;
2152
+ const abs = resolve3(cwd, path);
2153
+ return abs === cwd ? "." : abs.startsWith(cwd + sep4) ? abs.slice(cwd.length + 1) : path;
2897
2154
  }
2898
2155
  function whyNotFound(content, oldString) {
2899
2156
  if (/^\s*\d+\t/m.test(oldString)) {
@@ -2934,14 +2191,14 @@ var editFileTool = {
2934
2191
  };
2935
2192
  }
2936
2193
  const a = parsed.data;
2937
- const target = resolve4(ctx.cwd, a.path);
2938
- const cwdResolved = resolve4(ctx.cwd);
2939
- if (target !== cwdResolved && !target.startsWith(cwdResolved + sep5)) {
2194
+ const target = resolve3(ctx.cwd, a.path);
2195
+ const cwdResolved = resolve3(ctx.cwd);
2196
+ if (target !== cwdResolved && !target.startsWith(cwdResolved + sep4)) {
2940
2197
  return { content: `edit_file: path is outside cwd: ${a.path}`, isError: true };
2941
2198
  }
2942
2199
  let content;
2943
2200
  try {
2944
- content = await readFile2(target, "utf8");
2201
+ content = await readFile(target, "utf8");
2945
2202
  } catch (e) {
2946
2203
  return {
2947
2204
  content: `edit_file error: ${e instanceof Error ? e.message : String(e)}`,
@@ -2988,13 +2245,13 @@ var editFileTool = {
2988
2245
  };
2989
2246
 
2990
2247
  // src/tools/shell.ts
2991
- import { spawn as spawn2 } from "child_process";
2992
- import { resolve as resolve5, sep as sep6 } from "path";
2993
- import { z as z11 } from "zod";
2994
- var params8 = z11.object({
2995
- command: z11.string(),
2248
+ import { spawn } from "child_process";
2249
+ import { resolve as resolve4, sep as sep5 } from "path";
2250
+ import { z as z10 } from "zod";
2251
+ var params8 = z10.object({
2252
+ command: z10.string(),
2996
2253
  /** Milliseconds before the command is killed. Defaults to DEFAULT_TIMEOUT_MS, capped at MAX_TIMEOUT_MS. */
2997
- timeout: z11.number().int().positive().optional()
2254
+ timeout: z10.number().int().positive().optional()
2998
2255
  });
2999
2256
  var DEFAULT_TIMEOUT_MS = 12e4;
3000
2257
  var MAX_TIMEOUT_MS = 6e5;
@@ -3029,7 +2286,7 @@ var REWRITES = [
3029
2286
  ];
3030
2287
  var REDIRECT = /(?:^|[^0-9<>&])>>?\s*(?!\/dev\/|\/tmp\/|&)([A-Za-z0-9_./-]*\.[A-Za-z0-9]+)/;
3031
2288
  function leavesWorkdir(command, cwd) {
3032
- const base = resolve5(cwd);
2289
+ const base = resolve4(cwd);
3033
2290
  let at = base;
3034
2291
  for (const seg of command.split(/&&|\|\||;|\|/)) {
3035
2292
  const m = /^\s*(?:cd|pushd)(?:\s+(.*))?$/.exec(seg.trim());
@@ -3037,8 +2294,8 @@ function leavesWorkdir(command, cwd) {
3037
2294
  const raw = (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
3038
2295
  if (!raw || raw === "~" || raw === "$HOME" || raw.startsWith("~/")) return raw || "~";
3039
2296
  if (raw === "-") return "-";
3040
- at = resolve5(at, raw);
3041
- if (at !== base && !at.startsWith(base + sep6)) return raw;
2297
+ at = resolve4(at, raw);
2298
+ if (at !== base && !at.startsWith(base + sep5)) return raw;
3042
2299
  }
3043
2300
  return void 0;
3044
2301
  }
@@ -3105,7 +2362,7 @@ var shellTool = {
3105
2362
  return new Promise((resolvePromise) => {
3106
2363
  let child;
3107
2364
  try {
3108
- child = spawn2(a.command, {
2365
+ child = spawn(a.command, {
3109
2366
  cwd: ctx.cwd,
3110
2367
  shell: true,
3111
2368
  signal: ctx.signal,
@@ -3169,8 +2426,8 @@ ${body}${tail}`, isError: timedOut || code !== 0 });
3169
2426
  };
3170
2427
 
3171
2428
  // src/tools/web.ts
3172
- import { z as z12 } from "zod";
3173
- var params9 = z12.object({ url: z12.string().url() });
2429
+ import { z as z11 } from "zod";
2430
+ var params9 = z11.object({ url: z11.string().url() });
3174
2431
  var MAX_CHARS = 1e5;
3175
2432
  function createWebFetchTool(fetchFn = globalThis.fetch) {
3176
2433
  return {
@@ -3245,8 +2502,8 @@ async function refreshTraces(opts) {
3245
2502
  }
3246
2503
  const targets = candidates.filter((f) => !gone.includes(f));
3247
2504
  if (!targets.length) return out;
3248
- const model = opts.models.find(Boolean);
3249
- if (!model) return out;
2505
+ const chain = opts.models.filter(Boolean);
2506
+ if (!chain.length) return out;
3250
2507
  try {
3251
2508
  const g = await buildProjectGraph(opts.cwd);
3252
2509
  out.graph = g.message;
@@ -3259,7 +2516,7 @@ async function refreshTraces(opts) {
3259
2516
  const res = await runTraces({
3260
2517
  cwd: opts.cwd,
3261
2518
  provider: opts.provider,
3262
- model,
2519
+ models: chain,
3263
2520
  plan,
3264
2521
  // No liveFiles: this run knows only the files one task changed, and a pruner given that list would
3265
2522
  // read every OTHER trace in the project as orphaned and delete it.
@@ -3293,14 +2550,14 @@ async function commitRefreshed(git, baseWorktree, traceRootRel2) {
3293
2550
  }
3294
2551
 
3295
2552
  // src/engine/writer-registry.ts
3296
- import { z as z14 } from "zod";
2553
+ import { z as z13 } from "zod";
3297
2554
 
3298
2555
  // src/engine/normalize-question.ts
3299
- import { z as z13 } from "zod";
3300
- var NormalizedQuestionSchema = z13.object({
3301
- question: z13.string().describe("The core question, concise, WITHOUT the embedded options table/list."),
3302
- options: z13.array(z13.string()).describe("Each selectable choice as a SHORT label; the recommended one first, suffixed ' (recommended)'. Empty when the question is genuinely open-ended."),
3303
- multiSelect: z13.boolean().describe("true only if the user may pick more than one.")
2556
+ import { z as z12 } from "zod";
2557
+ var NormalizedQuestionSchema = z12.object({
2558
+ question: z12.string().describe("The core question, concise, WITHOUT the embedded options table/list."),
2559
+ options: z12.array(z12.string()).describe("Each selectable choice as a SHORT label; the recommended one first, suffixed ' (recommended)'. Empty when the question is genuinely open-ended."),
2560
+ multiSelect: z12.boolean().describe("true only if the user may pick more than one.")
3304
2561
  });
3305
2562
  var PROMPT = "You reformat an agent's question for a terminal UI that renders selectable options (arrow keys + Enter). Given the raw question text \u2014 which may embed choices as a markdown table, an A/B/C/D list, or a 'recommended' suggestion \u2014 extract exactly:\n- `question`: the core question, concise, WITHOUT the embedded options table/list.\n- `options`: each selectable choice as a SHORT label. If one choice is recommended, list it FIRST and append ' (recommended)'. Do NOT add an 'other' / free-text / 'answer in your own words' option \u2014 the UI already provides that.\n- `multiSelect`: true only if the user may pick several.\nIf the text is genuinely open-ended (no discrete choices), return options: []. Preserve the user's language. Return the result via submit.";
3306
2563
  function looksLikeChoices(text) {
@@ -3367,21 +2624,21 @@ var clipLabel = (body) => {
3367
2624
  };
3368
2625
 
3369
2626
  // src/engine/writer-registry.ts
3370
- var askUserParams = z14.object({
3371
- question: z14.string(),
2627
+ var askUserParams = z13.object({
2628
+ question: z13.string(),
3372
2629
  // For a multiple-choice question, list the choices here → the UI shows a selectable checkbox/radio list
3373
2630
  // (arrow keys + Enter) instead of a free-text box. Omit for an open-ended question.
3374
2631
  //
3375
2632
  // A choice may be a plain string, or an object carrying what the label alone cannot say: a one-line
3376
2633
  // `description`, and a `preview` rendered in a panel beside the list while that option is focused. Use the
3377
2634
  // rich form when the decision turns on the trade-offs rather than the name (e.g. "which approach?").
3378
- options: z14.array(z14.union([
3379
- z14.string(),
3380
- z14.object({ label: z14.string(), description: z14.string().optional(), preview: z14.string().optional() })
2635
+ options: z13.array(z13.union([
2636
+ z13.string(),
2637
+ z13.object({ label: z13.string(), description: z13.string().optional(), preview: z13.string().optional() })
3381
2638
  ])).optional().describe(
3382
2639
  "The choices, when the question has discrete answers \u2014 the UI renders a selectable list instead of a free-text box. Omit for an open-ended question. A choice may be a plain string, or an object with a one-line `description` and a `preview` shown beside the list; use the rich form when the decision turns on trade-offs rather than on the name."
3383
2640
  ),
3384
- multiSelect: z14.boolean().optional().describe(
2641
+ multiSelect: z13.boolean().optional().describe(
3385
2642
  "True when the user may pick more than one (checkboxes); omitted means pick exactly one (radio)."
3386
2643
  ),
3387
2644
  /**
@@ -3390,7 +2647,7 @@ var askUserParams = z14.object({
3390
2647
  * Present ⇒ this is a hand-off, not a question: the run has stopped because only a person can carry the
3391
2648
  * next step, and the UI says so rather than showing a bare "? Question".
3392
2649
  */
3393
- steps: z14.array(z14.string()).optional().describe(
2650
+ steps: z13.array(z13.string()).optional().describe(
3394
2651
  'What the user has to DO before they can answer \u2014 one action per entry. Supplying this makes it a HAND-OFF rather than a question: the run has stopped because only a person can carry the next step, and the UI says so instead of showing a bare "? Question". Use it whenever you are asking someone to go and perform something and report back; leave it out when you only want an answer.'
3395
2652
  )
3396
2653
  });
@@ -3589,7 +2846,7 @@ var RoleFitness = class {
3589
2846
  };
3590
2847
 
3591
2848
  // src/engine/routing.ts
3592
- import { z as z15 } from "zod";
2849
+ import { z as z14 } from "zod";
3593
2850
 
3594
2851
  // src/engine/route-role.ts
3595
2852
  var STYLE_EXT = [".css", ".scss", ".sass", ".less", ".styl"];
@@ -3656,8 +2913,8 @@ function routeByEvidence(card) {
3656
2913
  }
3657
2914
 
3658
2915
  // src/engine/routing.ts
3659
- var RouteSchema = z15.object({
3660
- role: z15.enum(["coder", "designer"]).describe(
2916
+ var RouteSchema = z14.object({
2917
+ role: z14.enum(["coder", "designer"]).describe(
3661
2918
  "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."
3662
2919
  )
3663
2920
  });
@@ -3719,9 +2976,9 @@ function createDefaultRegistry() {
3719
2976
  }
3720
2977
 
3721
2978
  // src/engine/operational.ts
3722
- import { z as z16 } from "zod";
3723
- var CommitSchema = z16.object({
3724
- message: z16.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
2979
+ import { z as z15 } from "zod";
2980
+ var CommitSchema = z15.object({
2981
+ message: z15.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
3725
2982
  });
3726
2983
  var MAX_DIFF = 12e3;
3727
2984
  var OPERATIONAL_MAX_TURNS = 3;
@@ -3967,14 +3224,14 @@ function deadlineWarning(elapsedMs, budgetMs) {
3967
3224
  }
3968
3225
  var MAX_WRITTEN_CHARS = 6e4;
3969
3226
  async function writtenText(cwd, touched) {
3970
- const { readFile: readFile4 } = await import("fs/promises");
3227
+ const { readFile: readFile3 } = await import("fs/promises");
3971
3228
  const { join: join10 } = await import("path");
3972
3229
  const parts = [];
3973
3230
  let used = 0;
3974
3231
  for (const p of [...new Set(touched)]) {
3975
3232
  if (used >= MAX_WRITTEN_CHARS) break;
3976
3233
  try {
3977
- const t = await readFile4(join10(cwd, p), "utf8");
3234
+ const t = await readFile3(join10(cwd, p), "utf8");
3978
3235
  parts.push(t.slice(0, MAX_WRITTEN_CHARS - used));
3979
3236
  used += t.length;
3980
3237
  } catch {
@@ -4169,391 +3426,18 @@ ${handOver}`;
4169
3426
  // src/engine/review.ts
4170
3427
  import { existsSync as existsSync8 } from "fs";
4171
3428
  import { isAbsolute, join as join8 } from "path";
4172
- import { z as z17 } from "zod";
4173
-
4174
- // src/agent/roles.ts
4175
- function isTransientFailure(reason) {
4176
- const r = reason.toLowerCase();
4177
- if (/\b(429|rate.?limit|quota|exhaust|insufficient|billing|credit)\b/.test(r)) return false;
4178
- return /overload|529|50[0234]|timeout|timed out|deadline|econnreset|epipe|socket hang up|stream ended|temporar|unavailable|try again/.test(r);
4179
- }
4180
- function isSourceCapacity(reason) {
4181
- return /capacity is (?:temporarily unavailable|busy)/i.test(reason);
4182
- }
4183
- function sourcePrefix(model) {
4184
- const s = model.replace(/^no-think\//, "");
4185
- const cli = cliFor(s);
4186
- if (cli) return cli;
4187
- const i = s.indexOf("/");
4188
- return i > 0 ? s.slice(0, i) : void 0;
4189
- }
4190
- function weightedCycle(sources, weights) {
4191
- const queues = sources.map((s) => Array(Math.max(1, weights[s] ?? 1)).fill(s));
4192
- const out = [];
4193
- for (let more = true; more; ) {
4194
- more = false;
4195
- for (const q of queues) {
4196
- const m = q.shift();
4197
- if (m !== void 0) {
4198
- out.push(m);
4199
- more = true;
4200
- }
4201
- }
4202
- }
4203
- return out;
4204
- }
4205
- function canonicalSource(name) {
4206
- const s = name.toLowerCase().replace(/^no-think\//, "");
4207
- if (s === "cc" || s === "claude") return "claude";
4208
- if (s === "cx") return "codex";
4209
- return sourcePrefix(s) ?? s;
4210
- }
4211
- function providerOutage(reason) {
4212
- return /no active credentials for provider:?\s*([\w.-]+)/i.exec(reason)?.[1] ?? /provider\s+'?([\w.-]+)'?\s+is not configured/i.exec(reason)?.[1] ?? /all\s+([\w.-]+)\s+accounts have exhausted their quota/i.exec(reason)?.[1] ?? /shared egress ip quota exhausted\s*\(([\w.-]+)\)/i.exec(reason)?.[1] ?? /^\s*(claude|codex)\s+CLI:\s*rejected\b/i.exec(reason)?.[1]?.toLowerCase();
4213
- }
4214
- function quotaResetAt(reason) {
4215
- const iso = /\(resets\s+([0-9T:.\-]+Z)\)/i.exec(reason)?.[1];
4216
- const t = iso ? Date.parse(iso) : NaN;
4217
- return Number.isFinite(t) ? t : void 0;
4218
- }
4219
- var RoleRegistry = class _RoleRegistry {
4220
- // durable behavioral rules → appended to EVERY role's prompt
4221
- constructor(roles, defaultPrompts = {}, skillRegistry) {
4222
- this.roles = roles;
4223
- this.defaultPrompts = defaultPrompts;
4224
- this.skillRegistry = skillRegistry;
4225
- }
4226
- roles;
4227
- defaultPrompts;
4228
- skillRegistry;
4229
- modelOverride;
4230
- roleOverrides = /* @__PURE__ */ new Map();
4231
- // per-role model CHAIN override (highest priority)
4232
- effortOverrides = /* @__PURE__ */ new Map();
4233
- // Models that failed retryably (429/5xx/quota) → skipped in every chain until released. Kept WITH the
4234
- // reason and the time so a coordinator can report them and later re-probe whether the limit has reset.
4235
- quarantine = /* @__PURE__ */ new Map();
4236
- notify;
4237
- // fallback UI note sink (wired once the controller exists)
4238
- onQuarantine;
4239
- /** What each model has actually managed to do in each ROLE — see setFitness. */
4240
- fitness;
4241
- // Models that answered in prose instead of calling the submit tool. Not a transport error, so nothing ever
4242
- // benched them: the chain quietly slid to the fallback on EVERY call, forever, in every role that held them.
4243
- strikes = /* @__PURE__ */ new Map();
4244
- rulesProvider;
4245
- /** Every configured role name — used to validate a role reference produced by a model (memory audiences). */
4246
- names() {
4247
- return [.../* @__PURE__ */ new Set([...Object.keys(this.roles), ...Object.keys(this.defaultPrompts)])];
4248
- }
4249
- /** Wire the fallback-note sink (called after the controller exists). */
4250
- setNotify(fn) {
4251
- this.notify = fn;
4252
- }
4253
- /** Wire the durable-rules source (memory). Rules are appended to every role's system prompt (always honored). */
4254
- setRules(fn) {
4255
- this.rulesProvider = fn;
4256
- }
4257
- /** The rule block to append to a role's prompt — empty when there are no rules. Public so prompt-supplying
4258
- * callers (spec-kit phases build their own prompt) can append it too. */
4259
- ruleSuffix() {
4260
- const rules = this.rulesProvider?.() ?? [];
4261
- return rules.length ? `
4262
-
4263
- User rules (ALWAYS honor these):
4264
- ${rules.map((r) => `- ${r}`).join("\n")}` : "";
4265
- }
4266
- /** Live-swap the model used by every role (session-only; clears on undefined/empty). */
4267
- setModelOverride(model) {
4268
- this.modelOverride = model && model.length > 0 ? model : void 0;
4269
- }
4270
- /** Live-swap the model CHAIN of ONE role (session-only; wins over the global override). Clears on empty. */
4271
- setRoleModel(roleName, models) {
4272
- const chain = (typeof models === "string" ? [models] : models ?? []).filter((m) => m.length > 0);
4273
- if (chain.length) this.roleOverrides.set(roleName, chain);
4274
- else this.roleOverrides.delete(roleName);
4275
- }
4276
- /**
4277
- * How hard this role should work, set alongside its chain.
4278
- *
4279
- * An override on the live registry rather than a config re-read, for the same reason `setRoleModel` is one:
4280
- * `/roles adjust` has to take effect in the session that ran it, not only in the next one.
4281
- *
4282
- * `undefined` REMOVES it — a role reassigned from a Claude model to one whose effort cannot be set must
4283
- * stop carrying a level, or the config keeps a number that no longer applies to anything.
4284
- */
4285
- setRoleEffort(roleName, effort) {
4286
- if (effort) this.effortOverrides.set(roleName, effort);
4287
- else this.effortOverrides.delete(roleName);
4288
- }
4289
- /**
4290
- * Wire the record of what each model has actually managed to do in each role.
4291
- *
4292
- * Without it a chain is only a list of names from a catalogue. With it, a model that has twice answered
4293
- * this role in prose instead of doing its work stops being offered to this role — while staying available
4294
- * to every other role, where it may be perfectly good.
4295
- */
4296
- setFitness(f) {
4297
- this.fitness = f;
4298
- }
4299
- /** Wire the quarantine hook: whatever benches a model, every role still holding it must be re-assigned. */
4300
- setOnQuarantine(fn) {
4301
- this.onQuarantine = fn;
4302
- }
4303
- /** Mark a model spent — every chain skips it from now on, until it is released. */
4304
- markExhausted(model, reason = "unavailable", now = Date.now(), until) {
4305
- if (!model || this.isQuarantined(model)) return;
4306
- const ends = until ?? (isTransientFailure(reason) ? now + _RoleRegistry.TRANSIENT_BENCH_MS : void 0);
4307
- this.quarantine.set(model, { at: now, reason, ...ends !== void 0 && { until: ends } });
4308
- this.onQuarantine?.(model, reason, ends);
4309
- }
4310
- /** Every model any role's chain names — the pool this registry can actually reach for. */
4311
- knownModels() {
4312
- return [...new Set(Object.values(this.roles).flatMap((r) => r.models ?? []))];
4313
- }
4314
- /**
4315
- * Benches every model of one provider, for a failure that is about the provider itself.
4316
- *
4317
- * Returns what it took out, so the caller can say so once instead of six times. Falls back to benching the
4318
- * single model when the pool names none of that provider — an unknown provider is still a real failure.
4319
- */
4320
- markProviderExhausted(provider, model, reason, now = Date.now()) {
4321
- const want = canonicalSource(provider);
4322
- const hit = this.knownModels().filter((m) => sourcePrefix(m) === want);
4323
- const until = quotaResetAt(reason);
4324
- for (const m of hit) this.markExhausted(m, reason, now, until);
4325
- if (!hit.length) {
4326
- this.markExhausted(model, reason, now);
4327
- return [model];
4328
- }
4329
- return hit;
4330
- }
4331
- /**
4332
- * How long a BEHAVIOURAL bench lasts before the model is tried again.
4333
- *
4334
- * A model that is out of quota is out until the quota returns, and nothing here can shorten that. A model
4335
- * that answered in prose is a different case entirely: the transport was fine, and the next prompt may not
4336
- * be the one it stumbled on. Benching it for the rest of a multi-hour run costs every role that held it —
4337
- * measured live, one such bench re-assigned SIXTEEN roles away from the best model available.
4338
- */
4339
- static STRUCTURAL_BENCH_MS = 10 * 6e4;
4340
- /**
4341
- * How long a TRANSPORT bench lasts — the busy server, not the spent subscription.
4342
- *
4343
- * The argument above, one door over. A model that answered in prose gets ten minutes because the transport
4344
- * was fine; a model whose transport said "Overloaded" for one second is the same case in its purest form,
4345
- * and it was the one getting benched for the whole session.
4346
- *
4347
- * Measured live, in the middle of a feature run: `cc/claude-opus-5` served five calls in the preceding two
4348
- * minutes (23.8s, 2.9s, 3.1s, 25.3s, 38.7s, all ok), then one 529 in 1.7 seconds — and 18 roles were moved
4349
- * off the best model in the fleet for the rest of the session. A 529 is the textbook transient condition;
4350
- * the API's own guidance for it is to retry with backoff.
4351
- *
4352
- * Two minutes: long enough that a genuinely struggling gateway is not hammered, short enough that a
4353
- * one-second blip costs a couple of turns rather than an afternoon.
4354
- */
4355
- static TRANSIENT_BENCH_MS = 2 * 6e4;
4356
- /**
4357
- * How many structured failures a model gets before it is benched. One miss can be a genuinely hard prompt;
4358
- * a pattern is the model. Low, because every strike costs a full wasted pass in every role that holds it.
4359
- */
4360
- static STRUCTURAL_STRIKES = 2;
4361
- /**
4362
- * Records that a model finished a turn WITHOUT producing the structured result it was asked for (prose
4363
- * instead of a tool call). This is not "unavailable" — the transport was fine — so it never reached the
4364
- * retryable path that benches a model, and the chain slid to the fallback on every single call instead.
4365
- * Returns the strike count; at the threshold the model is quarantined like any other spent one.
4366
- */
4367
- markStructuralFailure(model, reason = "no valid structured result", role) {
4368
- if (!model) return 0;
4369
- const key2 = role ? `${model}::${role}` : model;
4370
- const n = (this.strikes.get(key2) ?? 0) + 1;
4371
- this.strikes.set(key2, n);
4372
- if (n < _RoleRegistry.STRUCTURAL_STRIKES) return n;
4373
- if (role) {
4374
- this.fitness?.record?.(role, model, reason);
4375
- const rolesFailed = [...this.strikes.entries()].filter(([k, v]) => k.startsWith(`${model}::`) && v >= _RoleRegistry.STRUCTURAL_STRIKES).length;
4376
- if (rolesFailed >= _RoleRegistry.STRUCTURAL_ROLES_BEFORE_BENCH) {
4377
- this.markExhausted(
4378
- model,
4379
- `${reason} (in ${rolesFailed} roles)`,
4380
- Date.now(),
4381
- Date.now() + _RoleRegistry.STRUCTURAL_BENCH_MS
4382
- );
4383
- }
4384
- return n;
4385
- }
4386
- this.markExhausted(model, reason, Date.now(), Date.now() + _RoleRegistry.STRUCTURAL_BENCH_MS);
4387
- return n;
4388
- }
4389
- /**
4390
- * How many DISTINCT roles must reject a model this way before it is benched outright.
4391
- *
4392
- * Two, because one role can have a prompt that a good model reads badly — and the fitness record already
4393
- * takes it out of that role. A second, unrelated role failing the same way is the first evidence that the
4394
- * model, not the prompt, is the problem.
4395
- */
4396
- static STRUCTURAL_ROLES_BEFORE_BENCH = 2;
4397
- /** Models currently quarantined, with why and when — surfaced to the user and re-probed before an adjust. */
4398
- quarantined() {
4399
- return [...this.quarantine].map(([model, q]) => ({ model, ...q }));
4400
- }
4401
- isQuarantined(model, now = Date.now()) {
4402
- const q = this.quarantine.get(model);
4403
- if (!q) return false;
4404
- if (q.until !== void 0 && now >= q.until) {
4405
- this.quarantine.delete(model);
4406
- this.strikes.clear();
4407
- return false;
4408
- }
4409
- return true;
4410
- }
4411
- /** Put a model back in play (its quota reset, or the user forced it). */
4412
- release(model) {
4413
- this.strikes.delete(model);
4414
- return this.quarantine.delete(model);
4415
- }
4416
- /**
4417
- * Roles whose CURRENT chain still contains `model`. When a model is quarantined these are the roles that
4418
- * would otherwise keep resolving to a dead chain, so they are exactly the ones to re-assign.
4419
- */
4420
- rolesUsing(model) {
4421
- return this.names().filter((r) => this.rawChain(r).includes(model));
4422
- }
4423
- /**
4424
- * The role's chain BEFORE quarantine filtering — what was actually assigned to it.
4425
- *
4426
- * The order is the documented one and nothing precedes it: per-role override, then the session model, then
4427
- * the config. It used to bail on an empty CONFIG chain before either override was consulted, which made a
4428
- * role the config had never heard of impossible to assign — the one case where assigning is the whole
4429
- * point. Measured with `tester`, added in a version the user's config predated: `/roles adjust` set the
4430
- * override, the override was skipped, and the role stayed broken for the rest of the session while the
4431
- * error message recommended running `/roles adjust`.
4432
- */
4433
- rawChain(roleName) {
4434
- const perRole = this.roleOverrides.get(roleName);
4435
- if (perRole && perRole.length) return perRole;
4436
- if (this.modelOverride && roleName !== "refiner") return [this.modelOverride];
4437
- return this.roles[roleName]?.models ?? [];
4438
- }
4439
- /** True when every model assigned to this role is quarantined — the chain has collapsed and needs replacing. */
4440
- chainCollapsed(roleName) {
4441
- const raw = this.rawChain(roleName);
4442
- return raw.length > 0 && raw.every((m) => this.isQuarantined(m));
4443
- }
4444
- /** The full model chain for a role by priority: per-role override → global override (non-refiner) → config. */
4445
- chain(roleName) {
4446
- const base = this.rawChain(roleName);
4447
- if (!base.length) return [];
4448
- const live = base.filter((m) => !this.isQuarantined(m));
4449
- const usable = live.length ? live : base;
4450
- const fit = this.fitness ? usable.filter((m) => !this.fitness.unfit(roleName, m)) : usable;
4451
- return fit.length ? fit : usable;
4452
- }
4453
- /**
4454
- * The role's chain ROTATED by `slot`. Parallel workers share one role — five implementers in a wave are all
4455
- * `coder` — so every one of them resolved to the same chain head and hammered a single subscription until it
4456
- * rate-limited. Rotating gives each worker a different lead model while keeping its FULL fallback set, so
4457
- * spreading the load costs no resilience.
4458
- */
4459
- chainFor(roleName, slot = 0) {
4460
- const c = this.chain(roleName);
4461
- if (c.length < 2) return c;
4462
- const order = [];
4463
- for (const m of c) {
4464
- const s = sourceOf(m);
4465
- if (!order.includes(s)) order.push(s);
4466
- }
4467
- const cycle = weightedCycle(order, this.sourceWeights?.() ?? {});
4468
- if (cycle.length) {
4469
- const want = cycle[(slot % cycle.length + cycle.length) % cycle.length];
4470
- const i = c.findIndex((m) => sourceOf(m) === want);
4471
- if (i > 0) return [c[i], ...c.filter((_, j) => j !== i)];
4472
- if (i === 0) return c;
4473
- }
4474
- const k = (slot % c.length + c.length) % c.length;
4475
- return k === 0 ? c : [...c.slice(k), ...c.slice(0, k)];
4476
- }
4477
- /** How many accounts each source has connected — set at the composition root; equal weights without it. */
4478
- sourceWeights;
4479
- /** Wire the account weights (called once the pool exists). */
4480
- setSourceWeights(fn) {
4481
- this.sourceWeights = fn;
4482
- }
4483
- /** The model a role would use next (chain head), for UI display only. */
4484
- peekModel(roleName) {
4485
- return this.chain(roleName)[0] ?? "";
4486
- }
4487
- /**
4488
- * The chain (primary + fallbacks) and session-fallback hooks for a role, WITHOUT its system prompt —
4489
- * for callers that supply their own prompt (e.g. spec-kit phases). resolve() layers the prompt on top.
4490
- */
4491
- fallbackOpts(roleName) {
4492
- const chain = this.chain(roleName);
4493
- const notify = this.notify;
4494
- const effort = this.effortOverrides.get(roleName) ?? this.roles[roleName]?.effort;
4495
- return {
4496
- // Travels with the chain, not with the prompt: the seven callers that take only the chain are exactly
4497
- // the ones whose work is heaviest (the tester, the analyst, the spec-kit phases).
4498
- ...effort ? { effort } : {},
4499
- /**
4500
- * The name belongs to the CHAIN, not to the prompt.
4501
- *
4502
- * It was put on `resolve()` alone, on the assumption that every caller spreads a resolved role. Seven
4503
- * do not: they take the chain from here and supply their own prompt (the spec-kit phases, the tester,
4504
- * the analyst, the question normalizer). Measured on the first run after the change — 19 tool calls,
4505
- * one of them attributed. Everything the attribution was for happens in those seven.
4506
- */
4507
- role: roleName,
4508
- model: chain[0] ?? "",
4509
- fallbacks: chain.slice(1),
4510
- onExhausted: (m, reason) => {
4511
- const why = reason ?? "unavailable";
4512
- const source = providerOutage(why) ?? (isSourceCapacity(why) ? sourcePrefix(m) : void 0);
4513
- if (source) this.markProviderExhausted(source, m, why);
4514
- else this.markExhausted(m, why);
4515
- },
4516
- onStructuralFailure: (m, reason) => this.markStructuralFailure(m, reason, roleName),
4517
- onFallback: notify ? (from, to, reason) => notify(`\u2935 \`${from}\` \u2192 \`${to}\` \u2014 ${reason}`) : void 0
4518
- };
4519
- }
4520
- /** The skills already attached to a role — what task-level routing must not inline a second time. */
4521
- skillsFor(roleName) {
4522
- return this.roles[roleName]?.skills ?? [];
4523
- }
4524
- resolve(roleName) {
4525
- const role = this.roles[roleName];
4526
- if (!role) throw new Error(`undefined role: ${roleName}`);
4527
- if (!this.rawChain(roleName).length) {
4528
- throw new Error(
4529
- `role '${roleName}' has no model defined \u2014 set one with \`/roles setmodel\`, run \`/roles adjust\`, or choose a session model with \`/model\`.`
4530
- );
4531
- }
4532
- let systemPrompt = role.systemPrompt ?? this.defaultPrompts[roleName];
4533
- if (systemPrompt === void 0) throw new Error(`role '${roleName}' has no systemPrompt`);
4534
- if (this.skillRegistry) {
4535
- try {
4536
- systemPrompt = applySkills(systemPrompt, role.skills ?? [], this.skillRegistry);
4537
- } catch (e) {
4538
- throw new Error(`role '${roleName}' skill error: ${e instanceof Error ? e.message : String(e)}`);
4539
- }
4540
- }
4541
- return { ...this.fallbackOpts(roleName), systemPrompt: systemPrompt + this.ruleSuffix() };
4542
- }
4543
- };
4544
-
4545
- // src/engine/review.ts
3429
+ import { z as z16 } from "zod";
4546
3430
  function asChoice(o) {
4547
3431
  return typeof o === "string" ? { label: o } : o;
4548
3432
  }
4549
- var AssessmentSchema = z17.object({
4550
- findings: z17.array(z17.object({
4551
- severity: z17.enum(["critical", "medium", "low"]).describe(
3433
+ var AssessmentSchema = z16.object({
3434
+ findings: z16.array(z16.object({
3435
+ severity: z16.enum(["critical", "medium", "low"]).describe(
4552
3436
  "`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."
4553
3437
  ),
4554
- note: z17.string()
3438
+ note: z16.string()
4555
3439
  })).default([]),
4556
- recommendation: z17.enum(["approve", "revise"]).describe(
3440
+ recommendation: z16.enum(["approve", "revise"]).describe(
4557
3441
  "`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."
4558
3442
  )
4559
3443
  });
@@ -4594,18 +3478,18 @@ function coverage(assessments) {
4594
3478
  const verified = assessments.length - unverified;
4595
3479
  return { verified, unverified, enough: !assessments.length || verified / assessments.length >= TEAM_MIN_COVERAGE };
4596
3480
  }
4597
- var CouncilVoteSchema = z17.object({
4598
- vote: z17.enum(["pass", "revise"]).describe(
3481
+ var CouncilVoteSchema = z16.object({
3482
+ vote: z16.enum(["pass", "revise"]).describe(
4599
3483
  "`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."
4600
3484
  ),
4601
- rationale: z17.string()
3485
+ rationale: z16.string()
4602
3486
  });
4603
- var JudgeSchema = z17.object({
4604
- decision: z17.enum(["pass", "revise", "ask-human"]).describe(
3487
+ var JudgeSchema = z16.object({
3488
+ decision: z16.enum(["pass", "revise", "ask-human"]).describe(
4605
3489
  "`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."
4606
3490
  ),
4607
- feedback: z17.array(z17.string()),
4608
- question: z17.string()
3491
+ feedback: z16.array(z16.string()),
3492
+ question: z16.string()
4609
3493
  });
4610
3494
  var STAGE_FRAMING = {
4611
3495
  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.
@@ -5190,10 +4074,10 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
5190
4074
  }
5191
4075
 
5192
4076
  // src/engine/acceptance.ts
5193
- import { z as z18 } from "zod";
4077
+ import { z as z17 } from "zod";
5194
4078
 
5195
4079
  // src/engine/criterion-commands.ts
5196
- import { spawn as spawn3 } from "child_process";
4080
+ import { spawn as spawn2 } from "child_process";
5197
4081
  var RUNNABLE_COMMANDS = [
5198
4082
  "dotnet",
5199
4083
  "npm",
@@ -5226,12 +4110,12 @@ function commandsIn(criterion) {
5226
4110
  }
5227
4111
  async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
5228
4112
  const [bin, ...args] = argv;
5229
- return new Promise((resolve6) => {
4113
+ return new Promise((resolve5) => {
5230
4114
  let child;
5231
4115
  try {
5232
- child = spawn3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
4116
+ child = spawn2(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
5233
4117
  } catch (e) {
5234
- resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e instanceof Error ? e.message : String(e) });
4118
+ resolve5({ argv, passed: false, exitCode: null, timedOut: false, output: e instanceof Error ? e.message : String(e) });
5235
4119
  return;
5236
4120
  }
5237
4121
  let out = "";
@@ -5248,11 +4132,11 @@ async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
5248
4132
  }, timeoutMs);
5249
4133
  child.on("error", (e) => {
5250
4134
  clearTimeout(timer);
5251
- resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
4135
+ resolve5({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
5252
4136
  });
5253
4137
  child.on("close", (code) => {
5254
4138
  clearTimeout(timer);
5255
- resolve6({ argv, passed: !timedOut && code === 0, exitCode: code, timedOut, output: out.slice(-MAX_OUTPUT) });
4139
+ resolve5({ argv, passed: !timedOut && code === 0, exitCode: code, timedOut, output: out.slice(-MAX_OUTPUT) });
5256
4140
  });
5257
4141
  });
5258
4142
  }
@@ -5286,9 +4170,9 @@ ${r.output.slice(-800)}
5286
4170
  }
5287
4171
 
5288
4172
  // src/engine/test-runner.ts
5289
- import { readFile as readFile3 } from "fs/promises";
4173
+ import { readFile as readFile2 } from "fs/promises";
5290
4174
  import { existsSync as existsSync9 } from "fs";
5291
- import { spawn as spawn4 } from "child_process";
4175
+ import { spawn as spawn3 } from "child_process";
5292
4176
  import { join as join9 } from "path";
5293
4177
  var TEST_TIMEOUT_MS = 6e5;
5294
4178
  var MAX_TEST_OUTPUT = 12e3;
@@ -5297,7 +4181,7 @@ async function detectTestCommand(cwd) {
5297
4181
  const pkgPath = join9(cwd, "package.json");
5298
4182
  if (existsSync9(pkgPath)) {
5299
4183
  try {
5300
- const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
4184
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
5301
4185
  const script = pkg.scripts?.test;
5302
4186
  if (script && !PLACEHOLDER.test(script)) {
5303
4187
  const runner = existsSync9(join9(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync9(join9(cwd, "yarn.lock")) ? "yarn" : existsSync9(join9(cwd, "bun.lockb")) ? "bun" : "npm";
@@ -5319,8 +4203,8 @@ async function runProjectTests(cwd, cmd) {
5319
4203
  const command = cmd ?? await detectTestCommand(cwd);
5320
4204
  if (!command) return { skipped: true, passed: true, output: "", timedOut: false };
5321
4205
  const [bin, ...args] = command.argv;
5322
- return new Promise((resolve6) => {
5323
- const child = spawn4(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
4206
+ return new Promise((resolve5) => {
4207
+ const child = spawn3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
5324
4208
  let out = "";
5325
4209
  const take = (d) => {
5326
4210
  out += d.toString();
@@ -5335,7 +4219,7 @@ async function runProjectTests(cwd, cmd) {
5335
4219
  }, TEST_TIMEOUT_MS);
5336
4220
  const done = (code, extra = "") => {
5337
4221
  clearTimeout(timer);
5338
- resolve6({
4222
+ resolve5({
5339
4223
  skipped: false,
5340
4224
  passed: code === 0 && !timedOut,
5341
4225
  command: command.argv.join(" "),
@@ -5346,7 +4230,7 @@ async function runProjectTests(cwd, cmd) {
5346
4230
  };
5347
4231
  child.on("error", (e) => {
5348
4232
  clearTimeout(timer);
5349
- resolve6({
4233
+ resolve5({
5350
4234
  skipped: true,
5351
4235
  passed: true,
5352
4236
  command: command.argv.join(" "),
@@ -5377,13 +4261,13 @@ ${run.output}`;
5377
4261
  function normalizeCriterion(s) {
5378
4262
  return s.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().replace(/[.,;:!?…]+$/, "");
5379
4263
  }
5380
- var AcceptanceSchema = z18.object({
5381
- checks: z18.array(z18.object({
5382
- criterion: z18.string().describe(
4264
+ var AcceptanceSchema = z17.object({
4265
+ checks: z17.array(z17.object({
4266
+ criterion: z17.string().describe(
5383
4267
  "Copy the criterion VERBATIM from the numbered list you were given, including any backticks and punctuation. Do not paraphrase, renumber or reformat it \u2014 it is matched back to the task by text."
5384
4268
  ),
5385
- met: z18.boolean(),
5386
- evidence: z18.string().describe(
4269
+ met: z17.boolean(),
4270
+ evidence: z17.string().describe(
5387
4271
  'Where you SAW it: a file path and what it contains, a symbol, a test name. "It looks fine" is not evidence.'
5388
4272
  )
5389
4273
  }))
@@ -5631,31 +4515,31 @@ async function runTaskCycle(deps, board, taskId, worktreePath, slot = 0) {
5631
4515
  }
5632
4516
 
5633
4517
  // src/board/board.ts
5634
- import { z as z19 } from "zod";
4518
+ import { z as z18 } from "zod";
5635
4519
  var MAX_STAGE_EVENTS = 200;
5636
- var stageEventSchema = z19.object({
5637
- role: z19.string(),
5638
- action: z19.string(),
5639
- note: z19.string().optional()
4520
+ var stageEventSchema = z18.object({
4521
+ role: z18.string(),
4522
+ action: z18.string(),
4523
+ note: z18.string().optional()
5640
4524
  });
5641
- var cardSchema = z19.object({
5642
- id: z19.string(),
5643
- title: z19.string(),
5644
- column: z19.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
5645
- worktree: z19.string().optional(),
5646
- deps: z19.array(z19.string()),
5647
- acceptance: z19.array(z19.string()).default([]),
4525
+ var cardSchema = z18.object({
4526
+ id: z18.string(),
4527
+ title: z18.string(),
4528
+ column: z18.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
4529
+ worktree: z18.string().optional(),
4530
+ deps: z18.array(z18.string()),
4531
+ acceptance: z18.array(z18.string()).default([]),
5648
4532
  // default: boards persisted before the gate existed still load
5649
- files: z19.array(z19.string()).default([]),
4533
+ files: z18.array(z18.string()).default([]),
5650
4534
  // ditto — a board written before file lists existed still loads
5651
- reviewNotes: z19.array(z19.string()),
4535
+ reviewNotes: z18.array(z18.string()),
5652
4536
  // Optional rather than defaulted: a board written before this existed must round-trip unchanged, and an
5653
4537
  // empty list is the same statement as no list at all.
5654
- clearedLenses: z19.array(z19.string()).optional(),
5655
- attempts: z19.number(),
5656
- stageHistory: z19.array(stageEventSchema)
4538
+ clearedLenses: z18.array(z18.string()).optional(),
4539
+ attempts: z18.number(),
4540
+ stageHistory: z18.array(stageEventSchema)
5657
4541
  });
5658
- var boardDataSchema = z19.object({ version: z19.literal(1), cards: z19.array(cardSchema) });
4542
+ var boardDataSchema = z18.object({ version: z18.literal(1), cards: z18.array(cardSchema) });
5659
4543
  function migrateDelivered(c) {
5660
4544
  if (c.column !== "DONE") return c;
5661
4545
  return c.stageHistory.some((e) => e.action === "merged") ? { ...c, column: "MERGED" } : c;
@@ -5823,40 +4707,14 @@ var Board = class _Board {
5823
4707
  };
5824
4708
 
5825
4709
  export {
5826
- checkProfile,
5827
- runLogin,
5828
4710
  SHORT_CALL_MS,
5829
4711
  LONG_CALL_MS,
5830
- cliCatalog,
5831
4712
  CliProvider,
5832
4713
  describeInherited,
5833
4714
  describeTopUp,
5834
4715
  toSlug,
5835
4716
  mainWorktreeRoot,
5836
4717
  WorktreeManager,
5837
- REQUIRED_ROLES,
5838
- DEFAULT_ROLE_SKILLS,
5839
- DEFAULT_PROMPTS,
5840
- SPEC_TEAM,
5841
- PLAN_TEAM,
5842
- CODE_TEAM,
5843
- DEFAULT_COUNCIL,
5844
- placedSkills,
5845
- ROLE_PROFILES,
5846
- filterModelsForRole,
5847
- effortFor,
5848
- isKnownModel,
5849
- capabilityScore,
5850
- mostCapable,
5851
- modelBand,
5852
- DURABLE_ROLES,
5853
- strongestPrimary,
5854
- newestPrimary,
5855
- sourceOf,
5856
- adjustRoleModels,
5857
- applySkills,
5858
- buildSkillTool,
5859
- RoleRegistry,
5860
4718
  unfinishedSessions,
5861
4719
  describeUnfinished,
5862
4720
  gitTool,