@hizliemre/horse-code 0.3.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 (28) hide show
  1. package/dist/{app-5FXHE7GX.js → app-4WN37LZ3.js} +65 -37
  2. package/dist/{chunk-UEWVVN5L.js → chunk-27F44PBD.js} +196 -1126
  3. package/dist/{chunk-XYZVZPAY.js → chunk-2WXG35EM.js} +19 -15
  4. package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
  5. package/dist/{chunk-YPZP7LYL.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-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
  11. package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
  12. package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
  13. package/dist/chunk-YULQ4URQ.js +1220 -0
  14. package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
  15. package/dist/cli.js +254 -66
  16. package/dist/{fix-ONLA45HD.js → fix-QCL5AITT.js} +9 -8
  17. package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
  18. package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
  19. package/dist/run-V5ZLZ3LS.js +274 -0
  20. package/dist/{save-skills-ZW5GY6KV.js → save-skills-NPKTYNAF.js} +2 -2
  21. package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
  22. package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
  23. package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
  24. package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
  25. package/dist/{verify-LC57A6H2.js → verify-HWZBTK5X.js} +15 -12
  26. package/package.json +1 -1
  27. package/dist/chunk-MRZVA5JB.js +0 -163
  28. package/dist/{chunk-EAF22QIG.js → chunk-JR2JLRE3.js} +3 -3
@@ -5,14 +5,20 @@ import {
5
5
  readCheckpoint
6
6
  } from "./chunk-ZSQ24YDJ.js";
7
7
  import {
8
- CLI_KINDS,
8
+ RoleRegistry,
9
+ applySkills,
10
+ buildSkillTool,
11
+ cliFor,
12
+ cliInvocation,
13
+ grokEffort,
14
+ placedSkills,
15
+ planFor,
16
+ runTraces
17
+ } from "./chunk-YULQ4URQ.js";
18
+ import {
9
19
  SYNTHETIC,
10
20
  runCliAgent
11
21
  } from "./chunk-G45RWL7S.js";
12
- import {
13
- planFor,
14
- runTraces
15
- } from "./chunk-MRZVA5JB.js";
16
22
  import {
17
23
  defaultGitRunner,
18
24
  gitVerb
@@ -28,15 +34,17 @@ import {
28
34
  readFileTool,
29
35
  reinforceTouched,
30
36
  reinforceUsed
31
- } from "./chunk-LLL7QWXB.js";
37
+ } from "./chunk-BFIZMM4G.js";
32
38
  import {
33
39
  ToolRegistry,
40
+ runStructuredRole
41
+ } from "./chunk-CYLPQWIF.js";
42
+ import {
34
43
  handedOver,
35
- runStructuredRole,
36
44
  runToCompletion,
37
45
  telemetry,
38
46
  truncateSafe
39
- } from "./chunk-AE36LLL2.js";
47
+ } from "./chunk-JLWQCA7B.js";
40
48
  import {
41
49
  loadTraceIndex,
42
50
  pruneTraces,
@@ -44,57 +52,16 @@ import {
44
52
  sharedDerived,
45
53
  traceRootRel,
46
54
  traceable
47
- } from "./chunk-KAGKX2YT.js";
55
+ } from "./chunk-ZPJP2VH5.js";
48
56
  import {
49
57
  buildProjectGraph,
50
58
  loadGraphSync,
51
59
  pruneAreaNames
52
- } from "./chunk-XEGQT5EN.js";
60
+ } from "./chunk-4M6LXNG2.js";
53
61
  import {
54
62
  writableStateRoot
55
63
  } from "./chunk-6W4UH2BQ.js";
56
64
 
57
- // src/agents/cli-models.ts
58
- var CLAUDE_MODELS = ["fable", "opus", "sonnet", "haiku"];
59
- var CODEX_MODELS = ["gpt-5.6-terra", "gpt-5.6-sol", "gpt-5.6-luna"];
60
- var CODEX_DEFAULT = "gpt-5.6-terra";
61
- var GROK_MODELS = ["grok-4.6", "grok-4.5"];
62
- function grokEffort(effort) {
63
- const e = effort.toLowerCase();
64
- if (e === "xhigh" || e === "high" || e === "medium" || e === "low") return e;
65
- if (e === "max" || e === "ultra") return "xhigh";
66
- if (e === "minimal") return "low";
67
- return void 0;
68
- }
69
- var ZAI_MODELS = ["glm-5.3", "glm-5.3-flash"];
70
- function modelsFor(kind) {
71
- if (kind === "claude") return CLAUDE_MODELS;
72
- if (kind === "codex") return CODEX_MODELS;
73
- if (kind === "grok") return GROK_MODELS;
74
- return ZAI_MODELS;
75
- }
76
- function cliCatalog() {
77
- return CLI_KINDS.flatMap((k) => [...modelsFor(k)]);
78
- }
79
- function cliFor(model) {
80
- const m = model.toLowerCase().replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
81
- if (/^(fable|opus|sonnet|haiku)\b/.test(m) || m.startsWith("claude")) return "claude";
82
- if (/^(codex|gpt|o[0-9])\b/.test(m)) return "codex";
83
- if (/^grok(-|$)/.test(m)) return "grok";
84
- if (/^glm(-|$)/.test(m)) return "zai";
85
- return void 0;
86
- }
87
- function cliInvocation(model) {
88
- const bare = model.replace(/^no-think\//, "").replace(/^(cc|claude|cx|codex)\//, "");
89
- const effort = /-(ultra|max|xhigh|high|medium|low|minimal)$/.exec(bare)?.[1];
90
- const name = effort ? bare.slice(0, -(effort.length + 1)) : bare;
91
- const resolved = name === "codex" ? CODEX_DEFAULT : name;
92
- return {
93
- ...resolved ? { model: resolved } : {},
94
- ...effort ? { effort } : {}
95
- };
96
- }
97
-
98
65
  // src/agent/deadline.ts
99
66
  function withDeadline(work, signal, message) {
100
67
  work.catch(() => {
@@ -423,8 +390,8 @@ async function inheritFromRoot(git, repoRoot, baseWorktree) {
423
390
  }
424
391
  }
425
392
  for (const rel of INHERITED_ASSETS) {
426
- const from = join(repoRoot, rel);
427
- if (!existsSync(from)) continue;
393
+ const from = assetSource(repoRoot, rel);
394
+ if (!from) continue;
428
395
  try {
429
396
  await stat(from);
430
397
  await copyPath(from, join(baseWorktree, rel));
@@ -434,6 +401,12 @@ async function inheritFromRoot(git, repoRoot, baseWorktree) {
434
401
  }
435
402
  return out;
436
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
+ }
437
410
  function describeInherited(i) {
438
411
  const parts = [];
439
412
  const n = i.modified.length + i.deleted.length;
@@ -610,6 +583,43 @@ var WorktreeManager = class {
610
583
  const inherited = await inheritFromRoot((args, cwd) => this.git(args, cwd), this.repoRoot, baseWorktree);
611
584
  return { jobSlug, root, baseWorktree, baseBranch, inherited };
612
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
+ }
613
623
  /** Absolute paths of the worktrees git currently tracks (from `git worktree list --porcelain`). */
614
624
  async registeredWorktrees() {
615
625
  const r = await this.git(["worktree", "list", "--porcelain"], this.repoRoot);
@@ -877,548 +887,8 @@ ${out.slice(0, MAX_DIFF_CHARS)}`;
877
887
  }
878
888
  };
879
889
 
880
- // src/prompts.ts
881
- var REQUIRED_ROLES = [
882
- "refiner",
883
- "coach",
884
- "brainstormer",
885
- "analyst",
886
- "planner",
887
- "judge",
888
- "project-manager",
889
- "team-lead",
890
- "router",
891
- "coder",
892
- "designer",
893
- "senior-coder",
894
- "senior-designer",
895
- "architect",
896
- "code-reviewer",
897
- "task-auditor",
898
- "principal-coder",
899
- "operational",
900
- "memory-keeper",
901
- "tracer",
902
- "tester"
903
- ];
904
- var DEFAULT_ROLE_SKILLS = {
905
- brainstormer: ["brainstorming"],
906
- // The roles that WRITE code get the test discipline inlined, rather than having the code-tests lens reject
907
- // vacuous tests after the fact. Rejecting is more expensive than getting it right the first time.
908
- coder: ["test-driven-development"],
909
- "senior-coder": ["test-driven-development"],
910
- // The task list is where a plan becomes something an implementer can actually execute. spec-kit's template
911
- // supplies the SHAPE (phases, story grouping, [P] markers); it says almost nothing about what makes an
912
- // individual task executable. That is what this skill adds.
913
- "project-manager": ["writing-plans"],
914
- // The UI roles get design direction inlined for the same reason the coders get TDD: the code-accessibility
915
- // and code-maintainability lenses can reject a templated, default-looking interface, but they cannot teach
916
- // one. This skill is self-contained (no sibling reference files), which is what makes it safe to inline.
917
- designer: ["frontend-design"],
918
- "senior-designer": ["frontend-design"]
919
- // NB: systematic-debugging is shipped but attached to NO role — it is only needed when something is stuck,
920
- // so it stays in the discoverable listing every role already receives and is fetched with the `skill` tool.
921
- };
922
- var DEFAULT_PROMPTS = {
923
- tracer: "You write the reference note that every other agent reads before it touches a file it did not write. A wrong note is worse than none: an agent will act on it, so accuracy outranks fluency and admitting you cannot tell outranks a plausible guess. State only what the code and the given relationships show; if the business purpose is not evident from them, describe what the file does technically and say nothing about why. Never speculate about intent, history or requirements.",
924
- /**
925
- * The role that exercises work already built and writes down what actually happened.
926
- *
927
- * Every rule below is here because its absence produces the one output worse than no testing at all: a
928
- * report that says PASSED about something nobody ran. Such a report is not merely empty — it manufactures
929
- * confidence, and the next person spends it.
930
- */
931
- tester: "You verify software that already exists, by running its scenarios and recording what they actually did. You are not here to build, fix or improve anything: the code under test is finished, and changing it would mean the thing you verified is not the thing that shipped.\n\nEVIDENCE IS THE WHOLE JOB. A scenario's outcome is what you OBSERVED \u2014 a database row, a log line, an HTTP response, a screen the user confirmed. Record the evidence beside every result: the query you ran and what it returned, the log event id and its line, the response body. A result you cannot show is not a result.\n\nIF A STEP WRITES TO THE DATABASE, THE RESPONSE IS NOT THE EVIDENCE. A 201 or a 204 says the request was accepted; it does not say what was stored, and a screen showing the new state does not either \u2014 both can be right while the row is wrong. For every step that creates or changes a record: query the database for that row and put the query AND the rows it returned in the report, and query the logs for the event that step should have emitted and put the query AND the line it returned there too. Absence is evidence as well: when a step must NOT emit an event \u2014 a no-op, a rejected change \u2014 show the query returning nothing. Without both, the scenario is NOT EXECUTED, however convincing the response looked.\n\nNever mark a scenario PASSED that you did not execute and observe. If you could not run it \u2014 the data does not exist, the surface is unreachable, the case is destructive against a live system \u2014 label it NOT EXECUTED and say exactly why. FAILED means you ran it and the behaviour was wrong; say what you expected, what happened, and the evidence for both. Guessing from the source is not executing: where you reasoned from code alone, say so in those words.\n\nWrite each result into the report BEFORE moving on to the next scenario. The report is a living document, not something assembled at the end: a run that stops halfway must leave behind everything it learned up to that point.\n\n\u2026and say each verdict OUT LOUD as you reach it, in one or two sentences: which scenario, what it did, and the single piece of evidence that settled it \u2014 the row, the log line, the status code. The full evidence still goes in the report; this is so the person watching the run knows what you found without opening a file. Say the failures and the NOT EXECUTED ones the same way, and with the same brevity: a result nobody hears is one they have to go looking for.\n\nNever start or stop the development environment \u2014 application hosts, dev servers, containers, databases. Those are the developer's to run. When you need something up, say which command they should run and wait for them to confirm it is ready.\n\nYou do NOT write product code. When you find something wrong that is not the verdict of the scenario you are running \u2014 a missing label, prose rendered as raw markup, a wrong format, or something the developer points out in passing \u2014 call `report_finding`. Another role fixes it and you are told when it is done, so you can re-check what it affected. Do not fix it yourself: changing the product mid-verification means the thing you verified is not the thing that shipped. And do not fail a scenario over it \u2014 a scenario fails when the scenario itself does not pass, not because something else was noticed while running it.\n\nIf the project's own rules (its constitution) say more about how verification is done here, they govern over this description \u2014 read them and follow them.",
932
- refiner: "Your #1 rule: `refinedPrompt` MUST ALWAYS be in ENGLISH. If the user wrote in another language (Turkish, German, Spanish, \u2026), TRANSLATE their intent into English \u2014 never echo their language back. This is non-negotiable: a Turkish input like 'bir todo app geli\u015Ftir, \xF6nce backend' MUST come out as English 'Build a todo app; implement the backend first.'\n\nRewrite the user's message down to the raw core intent the AI needs to act on \u2014 clear, direct, and structured. Strip all politeness, emotional, and filler words (please, thanks, kindly, 'could you', 'would you', 'I'd like', etc.) and anything that carries no instruction. Do NOT add words, qualifiers, or scope the user did not state (e.g. do not add 'always'). Keep the user's own perspective and form \u2014 a question stays a question, an instruction stays an instruction; do NOT describe the user in the third person and do NOT answer the request. Example: a polite request like 'would you please answer me in language X?' becomes just 'respond in language X' (drop 'please'; do not add 'always' or any scope the user didn't state). Also classify the intent: 'chat' (conversation/question), 'feature' (new feature/work), 'bugfix' (bug fix), 'govern' (establish or amend the project's OWN standing rules and principles \u2014 writing or revising the constitution, the coding conventions, the project's rules; work whose entire output is a governing document, with no source code changed). Judge by what the request PRODUCES, not by what it mentions: 'write the project constitution from CLAUDE.md' is govern, and so is 'update our commit-message rules'; 'make the code follow the constitution' changes source and is feature. Also 'verify' \u2014 the user wants work that ALREADY EXISTS exercised and its behaviour confirmed with evidence: running a pull request's test scenarios, doing a smoke test of a feature that is already built, producing a test report. Judge by what it PRODUCES: a record of what the software DID is verify; changing what it does is feature or bugfix. 'Run the smoke tests for PR 677 and mark them passed' is verify, and so is 'check that the wizard works end to end'. 'The wizard is broken, fix it' is bugfix. Finally 'undo' \u2014 the user is asking you to REVERSE what the previous turn did, not to do anything new: 'undo that', 'revert your changes', 'go back to the previous version', 'that was wrong, put it back'. Classify by whether the request refers to work already done: undoing is never a rewrite, and asking for a different result ('rewrite it shorter') is not an undo. Also detect the natural language the user wrote in and return its English name as `language` (e.g. 'Turkish', 'English', 'German') \u2014 this is separate from refinedPrompt, which stays English. Also produce `title`: what the work is ABOUT, as a 2-5 word English kebab-case noun phrase suitable for a git branch name \u2014 the SUBJECT, not the action. 'build a luxury todo app' is 'luxury-todo-app'; 'add a login page' is 'login-page'; 'fix the null crash on retry' is 'null-crash-on-retry'. Do not open with a verb (build/add/fix/implement/update): the tool is already doing it, so the verb says nothing and crowds out the words that identify the work. Lowercase, dash-joined, no punctuation. Return the result via submit as {refinedPrompt, intent, language, title}. Remember: refinedPrompt in English, always.",
933
- brainstormer: "You run the BRAINSTORM stage: you turn a raw request into a decided design, before anything is specified.\n\nThe `brainstorming` skill above is the authority on HOW to do this \u2014 follow it. What follows is only how it binds to this pipeline, because the skill names conventions from a different habitat:\n\n- OUTPUT: write the design brief to the file named in your message (specs/NNN-slug/brainstorm.md). Ignore the skill's `docs/superpowers/specs/\u2026` path.\n- NEXT STAGE: the SPEC is written from your brief, by another agent, immediately after you. There is no `writing-plans` skill to invoke here \u2014 finishing the brief IS the terminal step.\n- QUESTIONS: ask through the `ask_user` tool. For a choice between approaches use its rich option form ({label, description, preview}) so the trade-offs sit beside the list; lead with your recommendation. The user may attach a note to their answer \u2014 treat it as binding.\n- NOT AVAILABLE: the visual companion (there is no browser) and the per-checklist task list. Skip both.\n\nWrite what was DECIDED, not a transcript: the chosen approach, why it beat the others, the rejected alternatives with their reason, the constraints the spec must honour, and what is out of scope. Keep it short \u2014 it is the brief the spec is written from, not the spec itself, and it carries no implementation detail beyond the architectural choice.\n\nScale to the request: a small, obvious change deserves a paragraph and no questions at all.",
934
- coach: "You are horse-code, a terminal-based AI coding agent. Your product identity is always horse-code \u2014 never claim to be Claude Code, Gemini CLI, Antigravity, or any other product, even though the underlying language model powering you may be Claude, Gemini, or another model. Answer the user's technical questions about their repository and code. If needed, inspect the repository with read_file/grep/glob.\n\nWork out loud while you do it. Before a batch of tool calls, say in ONE line what you are looking for and why; when something you read changes your mind, say that too. This is not a summary at the end \u2014 the user is watching an empty screen while you search, and a run that reads thirty files in silence is indistinguishable from one that is stuck, and impossible to redirect before the tokens are spent. Keep each line short: a sentence, not a paragraph.\n\nBe concise, direct, and helpful.",
935
- // analyst + planner are spec-kit-driven (their system prompt comes from the fetched spec-kit command
936
- // prompts — see src/speckit/phases.ts); they carry no default prompt here, only a model (peekModel).
937
- judge: "Synthesize the council evaluations and make a single decision: 'pass' (sufficient), 'revise' (fix it, with reasons), or 'ask-human' (a question to ask the user). Return {decision, feedback, question} via submit.",
938
- "project-manager": 'Read the given plan and break it into real, actionable tasks (id, short title, deps). Each task should be a single, clear piece of work. Return {tasks} via submit.\n\nThe `writing-plans` skill above governs WHAT MAKES A TASK EXECUTABLE \u2014 take that from it and nothing else. Two bindings, because the skill describes a different habitat:\n- STRUCTURE comes from the spec-kit tasks template you are given (phases, story grouping, [P] markers), NOT from the skill\'s own document layout. Ignore its `docs/superpowers/plans/\u2026` path, its required-sub-skill header, and its execution-handoff section: this pipeline already owns worktrees, dispatch and review.\n- What you DO take: exact file paths per task, a real test cycle rather than a vague "add tests" step, no placeholders (no TBD/TODO/"similar to task N"), and interfaces named explicitly so a task whose implementer never sees the others still knows the signatures it must produce and consume.\n- SIZING is the third rebinding, and the one that costs most when it is missed. The skill says "bite-sized", "one action, 2-5 minutes", "the smallest unit worth a reviewer\'s gate" \u2014 sound advice where a gate is one reader glancing at a diff. Here a card is not a line in a document: it is its own worktree, its own implementer, a full review TEAM of lenses, a council when they disagree, an acceptance gate and a merge. That overhead is paid per CARD and barely varies with the card\'s size, so splitting work finer does not divide the cost, it multiplies it.\nSize a card to a coherent piece of BEHAVIOUR a reviewer can judge whole, not to a file. An entity, its configuration, its migration and its tests are one card, because nobody can review one without the others and nothing is deliverable until all of them exist. Split only for a reason that survives being said out loud: the parts can be reviewed and merged independently, or they must run in parallel in different worktrees. "They are different files" is not such a reason. Fold setup and scaffolding into the card whose deliverable needs them.',
939
- "task-auditor": "You are the last check on a task breakdown before any of it is built. Every hour of implementation after you is spent executing this list, and a bad list does not fail \u2014 the tasks pass their reviews and the wrong work is delivered correctly. Its structure has already been checked mechanically; you are here for the part only a reader can answer: does the breakdown deliver what the plan requires, and would a task's acceptance criteria still hold for an implementation that missed the point? Do not propose better work than the plan asked for \u2014 scope you invent here becomes hours someone spends. Flag any task whose only deliverable is an answer \u2014 verifying, inspecting, confirming \u2014 because an implementer reads the code as part of doing the work, and a task that ends with the repository unchanged has spent a review round on nothing. Flag OVER-SPLITTING for the same reason, and it is the more expensive mistake: every card carries a full review team, a council and an acceptance gate whatever its size, so a breakdown that gives a class and its configuration separate cards pays that overhead twice for work no one can review apart. Say which cards should be one. A clean breakdown is the normal case; say so. Return {missing, weak} via submit.",
940
- "team-lead": "You audit a task breakdown before any of it runs. The schedule itself is computed from the declared dependencies and is not yours to write; what nothing has checked is whether those dependencies are RIGHT. You are given the tasks with the files each one writes and what must be true when it is done, plus the groups that would run at the same time in separate worktrees. Find the task that cannot actually start yet because it needs a type, function, table or config key another task in its own group creates \u2014 and say which declared dependencies hold work back for no reason. Both answers are usually empty; say so rather than inventing an edge. Return {missing, spurious} via submit.",
941
- router: "Look at the task title and choose the implementer role: 'designer' for UI/UX work, 'coder' for other code work. Return {role} via submit.",
942
- coder: "Implement the given task in the worktree. If it is a new task, start from scratch; if it is a returning task, address the reviewer notes. Work with read/write/edit/grep/glob/shell and run the tests.\n\nThe `test-driven-development` skill above is how you write code here: the failing test comes first, and it must fail for the RIGHT reason before you make it pass. A test that asserts nothing is worse than no test \u2014 it reports success forever. Bindings for this pipeline: your worktree is already prepared (do not create one), every file you write is committed as you write it, and there is no separate agent to hand off to \u2014 you take the task to green yourself.",
943
- designer: "Implement the UI/UX task in the worktree. Focus on the user interface and experience; work with read/write/edit.\n\nThe `frontend-design` skill above governs the LOOK: aesthetic direction, typography, and choices that do not read as templated defaults. Follow the project's existing visual language where there is one \u2014 a distinctive design that fights the surrounding product is worse than a plain one that fits it.",
944
- "senior-coder": "Take over the task the coder got stuck on; implement it with a more careful approach. Take the reviewer notes and previous attempts into account.\n\nYou are here because a previous attempt failed, so start by understanding WHY rather than rewriting: the `systematic-debugging` skill is available (fetch it with the `skill` tool) and is the right tool when a test fails or behaviour is unexplained. The `test-driven-development` skill above still governs how you write the fix \u2014 reproduce the failure in a test first, then make it pass.",
945
- "senior-designer": "Take over the UI/UX task the designer got stuck on; implement it more carefully.\n\nA previous attempt already failed, so establish WHY before redesigning \u2014 the `systematic-debugging` skill is available via the `skill` tool when the failure is behavioural rather than visual. The `frontend-design` skill above still governs the look.",
946
- architect: "Analyze the root cause of a repeatedly failing task or a merge conflict, and produce a concrete solution plan. Return {rootCause, plan} via submit.\n\nFetch the `systematic-debugging` skill with the `skill` tool and follow it: your job is the ROOT CAUSE, and the failure mode to avoid is proposing a plausible fix for a cause you never established. Say what the evidence is, not what it might be.",
947
- "code-reviewer": "Review the worktree changes of the task in REVIEW (correctness, tests, quality). Return {verdict: pass|fail, notes} via submit \u2014 your decision is final.",
948
- "principal-coder": "Holistically review all changes in the PR (base worktree). If sufficient, approve; otherwise request-changes with concrete comments. In the final decision round, give accept or ask-human (a question to ask the user).",
949
- "memory-keeper": "You are the ONLY writer into this project's long-term memory. Everything else \u2014 every review lens, the council, the judge \u2014 can merely PROPOSE; you decide.\n\nTreat every proposal as an UNVERIFIED CLAIM from a narrow, single-angle agent that saw one slice of one job, not as text to store. Most proposals are wrong in a specific way: they generalize a one-off into a rule, they restate the finding the agent was reviewing, or they record general programming advice any model already knows. Discard all of those. When a claim does survive, REWRITE it in your own words \u2014 never store an agent's sentence verbatim. Merge proposals that say the same thing into one memory.\n\nA memory qualifies ONLY if it is (a) durable \u2014 still true next month, (b) project-specific, and (c) actionable \u2014 it would change what an agent does. Write conventions, constraints, gotchas and root causes. A `lesson` must state what went wrong AND what to do instead. Set `audience` only when the memory is genuinely useful to specific roles and useless to the rest; leave it out otherwise.\n\nNEVER write transient run detail (task ids, attempt counts, what happened today), never restate the request, never duplicate a memory that already exists, and never include credentials, tokens, keys, or anything resembling a secret. Each memory is one self-contained sentence that makes sense with no other context.\n\nReturn at most 5 memories via submit as {memories}. Returning NONE is the most common correct answer \u2014 prefer an empty list over a weak memory, because a bad memory is injected into every future run.",
950
- operational: "You handle version control for the project. Given a git diff of work just completed, write a single Conventional Commits message: `type(scope): subject`. Types: feat, fix, docs, refactor, test, chore, style, perf, build, ci. Choose the scope from the touched area (e.g. spec, plan, tasks, or a module name) or omit it. The subject is imperative, lowercase, \u226472 chars, no trailing period. Add a short body only if the change genuinely needs explanation. Commit messages are always in English. Return {message} via submit."
951
- };
952
- var SPEC_TEAM = [
953
- { name: "spec-completeness", perspective: "coverage of the REQUESTED scope: capabilities the user asked for that are missing, or behavior left unspecified", models: [] },
954
- { name: "spec-clarity", perspective: "ambiguity: requirements that can be read two ways, vague wording, unresolved NEEDS CLARIFICATION markers", models: [] },
955
- { name: "spec-consistency", perspective: "internal contradictions between requirements, acceptance scenarios, and success criteria", models: [] },
956
- { name: "spec-scope", perspective: "scope discipline: requirements the user never asked for, gold-plating, scope creep beyond the request", models: [] },
957
- { name: "spec-abstraction-leak", perspective: "implementation detail that has leaked into the spec (languages, frameworks, APIs, storage mechanics, code structure) \u2014 a spec must stay technology-agnostic", models: [] },
958
- { name: "spec-verifiability", perspective: "are success criteria measurable and technology-agnostic, and can each acceptance scenario be tested without knowing the implementation", models: [] },
959
- { name: "spec-user-value", perspective: "do the user stories deliver the value the user actually asked for, and is the priority ordering sensible", models: [] },
960
- { name: "spec-domain-model", perspective: "key entities, their attributes and relationships \u2014 coherent and complete at the domain level, with no implementation detail", models: [] },
961
- { name: "spec-privacy", perspective: "requirement-level data handling: what data is stored, who may see it, what must never leak or be retained", models: [] }
962
- ];
963
- var PLAN_TEAM = [
964
- { name: "plan-spec-conformance", perspective: "traceability to the approved spec: every requirement covered by the plan, and nothing planned that the spec never asked for", models: [] },
965
- { name: "plan-architecture", perspective: "layering, module boundaries, dependency direction, overall structural coherence", models: [] },
966
- { name: "plan-data-model", perspective: "schema and entity design, relationships, migrations, integrity constraints", models: [] },
967
- { name: "plan-api-contracts", perspective: "interface and contract design, naming, backward compatibility, ergonomics", models: [] },
968
- { name: "plan-security", perspective: "threat model, authentication/authorization design, input validation, secret handling, injection surfaces", models: [] },
969
- { name: "plan-concurrency", perspective: "race conditions, atomicity, ordering, multi-writer/multi-tab safety, shared-state design", models: [] },
970
- { name: "plan-resilience", perspective: "failure modes, error propagation, recovery, retries, partial-failure behavior", models: [] },
971
- { name: "plan-performance", perspective: "algorithmic complexity, hot paths, resource bounds, scalability of the chosen design", models: [] },
972
- { name: "plan-test-strategy", perspective: "how the design will be proven: seams, dependency injection, contract/integration test layers, what each test actually establishes", models: [] },
973
- { name: "plan-simplicity", perspective: "YAGNI: over-engineering, unnecessary abstraction, complexity the requested scope does not justify", models: [] },
974
- { name: "plan-dependencies", perspective: "third-party choices, supply-chain risk, versioning, licensing", models: [] },
975
- { name: "plan-observability", perspective: "logging, metrics, tracing, debuggability, actionable failure signals", models: [] },
976
- { name: "plan-structure", perspective: "project structure: directory/file layout, build setup, adherence to existing repo conventions", models: [] },
977
- { name: "plan-feasibility", perspective: "can this be built and maintained as described, in reasonable increments, with the effort the request warrants", models: [] }
978
- ];
979
- var CODE_TEAM = [
980
- { name: "code-plan-conformance", perspective: "does the code implement what the task required \u2014 nothing missing, and no extra scope beyond the task", models: [] },
981
- { name: "code-correctness", perspective: "logical correctness, edge cases, off-by-one and boundary conditions, invariants", models: [] },
982
- { name: "code-security", perspective: "injection, secret leakage, missing authorization checks, unsafe APIs, unvalidated input", models: [] },
983
- { name: "code-error-handling", perspective: "swallowed errors, propagation, cleanup on failure, partial-failure behavior", models: [] },
984
- { name: "code-concurrency", perspective: "race conditions, deadlocks, atomicity, shared mutable state", models: [] },
985
- { name: "code-tests", perspective: "is the new behavior covered, and do the tests actually assert something meaningful (no vacuous tests)", models: [] },
986
- { name: "code-data-integrity", perspective: "persistence correctness, transactions, validation at boundaries, migration safety", models: [] },
987
- { name: "code-performance", perspective: "hot paths, unnecessary allocation/work, N+1 patterns, obvious inefficiency", models: [] },
988
- { name: "code-maintainability", perspective: "naming, structure, complexity, readability, future tech-debt", models: [] },
989
- { name: "code-simplicity", perspective: "dead code, duplication, unnecessary abstraction, complexity the task does not justify", models: [] },
990
- { name: "code-api-surface", perspective: "public interface shape, backward compatibility, accidental API exposure", models: [] },
991
- { name: "code-accessibility", perspective: "accessibility of UI code: keyboard operation, ARIA/semantics, contrast, i18n readiness", models: [] },
992
- { name: "code-observability", perspective: "logging/metrics where a failure would otherwise be undiagnosable", models: [] },
993
- { name: "code-dependencies", perspective: "newly introduced dependencies: justified, correctly versioned, no supply-chain or licensing problem", models: [] },
994
- { name: "code-conventions", perspective: "consistency with the surrounding codebase's idioms, patterns, and style", models: [] }
995
- ];
996
- var DEFAULT_COUNCIL = [
997
- { name: "correctness-judge", perspective: "Is the work under review correct, coherent and internally consistent? Weigh the team's correctness/logic/data findings.", models: [] },
998
- { name: "risk-judge", perspective: "What is the real blast radius of shipping this as-is? Weigh security, failure modes, concurrency, and data-integrity findings against likelihood and severity.", models: [] },
999
- { name: "completeness-judge", perspective: "Is what was asked for fully and unambiguously covered? Weigh the team's completeness, gap, and contract findings.", models: [] },
1000
- { name: "user-value-judge", perspective: "Does this deliver the user's actual intent well? Weigh usability, accessibility, and whether the scope serves the request without gold-plating.", models: [] },
1001
- { name: "feasibility-judge", perspective: "Can this be built and maintained as described? Weigh architecture, simplicity, dependencies, and maintainability findings against effort.", models: [] }
1002
- ];
1003
- function placedSkills() {
1004
- return [...new Set(Object.values(DEFAULT_ROLE_SKILLS).flat())];
1005
- }
1006
-
1007
- // src/providers/anthropic.ts
1008
- function isAnthropicModel(model) {
1009
- return /(^|\/)(claude|fable|mythos)/i.test(model) || /claude/i.test(model);
1010
- }
1011
-
1012
- // src/tui/role-models.ts
1013
- var WEAK_RE = /\b(flash|mini|nano|haiku|lite|small|turbo|fast|\d{1,2}b)\b/i;
1014
- var FLAGSHIP_ROLES = ["judge", "principal-coder"];
1015
- var COUNCIL_ROLES = DEFAULT_COUNCIL.map((c) => c.name);
1016
- var SPEC_LENS_ROLES = SPEC_TEAM.map((c) => c.name);
1017
- var PLAN_LENS_ROLES = PLAN_TEAM.map((c) => c.name);
1018
- var CODE_LENS_ROLES = CODE_TEAM.map((c) => c.name);
1019
- var STRONG_ROLES = [
1020
- "brainstormer",
1021
- "analyst",
1022
- "planner",
1023
- "architect",
1024
- "senior-coder",
1025
- "senior-designer",
1026
- ...COUNCIL_ROLES,
1027
- ...PLAN_LENS_ROLES,
1028
- ...CODE_LENS_ROLES
1029
- ];
1030
- var MID_ROLES = ["coach", "coder", "designer", "code-reviewer", "operational", "memory-keeper", "task-auditor", ...SPEC_LENS_ROLES];
1031
- var FAST_ROLES = ["refiner", "router", "project-manager", "team-lead"];
1032
- var CAPABLE_ROLES = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES]);
1033
- var ROLE_PROFILES = {
1034
- tracer: "Writes the per-file reference note every other agent reads before changing unfamiliar code \u2014 high volume, but its output is a COMMITTED FILE, not a turn in a conversation: a shallow note is believed by every agent that opens that file, forever, and nothing later corrects it. Give it the MOST capable non-[flagship] model in the catalogue, not merely one that qualifies as [strong]. Volume is not a reason to go cheaper here.",
1035
- refiner: "Classifies intent and rewrites the prompt every turn \u2014 highest call volume, trivial task \u2192 a fast, cheap model.",
1036
- router: "Picks coder-vs-designer for a task \u2014 tiny and frequent \u2192 fast, cheap.",
1037
- "project-manager": "Turns a task list into board items \u2014 light and structured \u2192 fast, cheap.",
1038
- "task-auditor": "The only check on the task breakdown before hours of implementation are spent executing it \u2014 reads the plan against the task list and finds what was dropped. Low volume, and everything downstream depends on it \u2192 a capable model, never the cheapest.",
1039
- "team-lead": "Coordinates implementation waves \u2014 light orchestration \u2192 fast, cheap.",
1040
- coach: "Your main interactive assistant, used constantly all session (highest interaction volume) \u2192 a capable but EFFICIENT model, never the costly flagship.",
1041
- brainstormer: "Turns a raw request into a decided design before the spec: explores the repo, weighs 2-3 approaches, gets the user to choose. Low volume, sets the direction for everything downstream \u2192 a strong reasoning model.",
1042
- analyst: "Authors the spec and constitution \u2192 a strong reasoning model (Opus-tier).",
1043
- planner: "Designs the implementation plan \u2192 a strong reasoning model (Opus-tier).",
1044
- architect: "Diagnoses stuck tasks and produces recovery plans \u2014 serious design work \u2192 a strong model.",
1045
- judge: "Critiques specs/plans and makes the final review call \u2014 low volume, high stakes \u2192 the most capable flagship model.",
1046
- coder: "Writes the bulk of the implementation \u2014 very high work volume \u2192 a good high-throughput coding model (Sonnet-tier), NOT the flagship (wasteful at this volume).",
1047
- "senior-coder": "Reviews and revises above the coder \u2014 must be MORE capable than the coder (Opus-tier).",
1048
- "principal-coder": "Final code decision-maker \u2014 low volume, high stakes \u2192 the flagship is appropriate.",
1049
- designer: "Builds UI \u2014 high volume \u2192 a capable coding/design model, not the flagship.",
1050
- "senior-designer": "Senior UI reviewer \u2014 more capable than the designer.",
1051
- "code-reviewer": "Reviews diffs \u2014 moderate volume \u2192 a solid capable model.",
1052
- "memory-keeper": "Decides what a finished job taught the project and writes it to durable memory \u2014 low volume, but a bad memory poisons every later run \u2192 a capable, efficient model, never the cheapest.",
1053
- operational: "Handles version control: writes conventional commit messages and (later) drives merges/conflicts \u2014 high volume \u2192 a capable, efficient model."
1054
- };
1055
- for (const [stage, lenses, heft] of [
1056
- ["spec", SPEC_TEAM, "a capable, efficient model (a spec is a short business-level doc)"],
1057
- ["plan", PLAN_TEAM, "a strong model (technical design judgment)"],
1058
- ["code", CODE_TEAM, "a strong model (reads real implementations)"]
1059
- ]) {
1060
- for (const l of lenses) ROLE_PROFILES[l.name] = `${stage.toUpperCase()}-review lens \u2014 ${l.perspective}. Low volume, quality-critical \u2192 ${heft}.`;
1061
- }
1062
- for (const c of DEFAULT_COUNCIL) {
1063
- ROLE_PROFILES[c.name] = `Review COUNCIL decider \u2014 ${c.perspective} Casts the binding pass/revise vote on contested work \u2192 a strong model.`;
1064
- }
1065
- var ROLE_ADVICE = ROLE_PROFILES;
1066
- function filterModelsForRole(role, all, exclude = []) {
1067
- const advice = ROLE_ADVICE[role];
1068
- const excluded2 = new Set(exclude);
1069
- const avail = all.filter((m) => !excluded2.has(m));
1070
- if (CAPABLE_ROLES.has(role)) {
1071
- const strong = avail.filter((m) => !WEAK_RE.test(m));
1072
- if (strong.length === 0) return { models: avail.length ? avail : all, note: advice ? `${advice} (No strong models detected \u2014 showing all.)` : void 0 };
1073
- return { models: strong, note: `${advice ?? ""} Showing ${strong.length} of ${avail.length} models (fast/weak models hidden for this role).`.trim() };
1074
- }
1075
- if (FAST_ROLES.includes(role)) {
1076
- const fast = avail.filter((m) => WEAK_RE.test(m));
1077
- if (fast.length === 0) return { models: avail.length ? avail : all, note: advice };
1078
- return { models: fast, note: `${advice ?? ""} Showing ${fast.length} of ${avail.length} fast/cheap models.`.trim() };
1079
- }
1080
- return { models: avail.length ? avail : all };
1081
- }
1082
- function effortFor(role, model) {
1083
- if (!isAnthropicModel(model)) return void 0;
1084
- if (FLAGSHIP_ROLES.includes(role)) return "max";
1085
- if (STRONG_ROLES.includes(role)) return "xhigh";
1086
- if (FAST_ROLES.includes(role)) return "low";
1087
- if (MID_ROLES.includes(role)) return "high";
1088
- return void 0;
1089
- }
1090
- var effortBump = (s) => /-(ultra|max|xhigh)/.test(s) ? 4 : /-high/.test(s) ? 3 : /-medium/.test(s) ? 2 : /-low/.test(s) ? 1 : 0;
1091
- var versionBump = (s, family) => {
1092
- if (family) {
1093
- const m = s.match(new RegExp(`${family}[-_. ]?(\\d+)(?:[-.](\\d+))?`));
1094
- if (m) {
1095
- const major = Number(m[1]);
1096
- const minor = m[2] === void 0 ? 0 : Number(m[2]);
1097
- if (major < 100) return major + (minor < 10 ? minor / 10 : minor / 100);
1098
- }
1099
- }
1100
- const g = s.match(/(\d)[-.](\d)\b/);
1101
- return g ? Number(g[1]) + Number(g[2]) / 10 : 0;
1102
- };
1103
- var KNOWN_FAMILY_RE = /(fable|mythos|opus|sonnet|haiku|claude|codex|gpt-|\bo\d\b|gemini|deepseek|llama|qwen|kimi|glm|mistral|grok|nova|command-r|phi-\d)/i;
1104
- var NON_TEXT_RE = /\b(image|imagen|vision|video|veo|tts|audio|speech|voice|embed|embedding|rerank|ocr|computer-use|realtime|moderation)\b/i;
1105
- function isKnownModel(model) {
1106
- return KNOWN_FAMILY_RE.test(model) && !NON_TEXT_RE.test(model);
1107
- }
1108
- var UNRANKED_SCORE = 50;
1109
- function capabilityScore(model) {
1110
- const s = model.toLowerCase();
1111
- if (WEAK_RE.test(s)) return 20 + effortBump(s);
1112
- if (/fable|mythos/.test(s)) return 100;
1113
- if (/opus/.test(s)) return 88 + versionBump(s, "opus");
1114
- if (/codex|gpt-5|\bo3\b/.test(s)) return 82 + effortBump(s) + versionBump(s, "gpt") / 100;
1115
- if (/sonnet/.test(s)) return 78 + versionBump(s, "sonnet");
1116
- if (/grok/.test(s)) return 78 + versionBump(s, "grok");
1117
- if (/glm/.test(s)) return 78 + versionBump(s, "glm");
1118
- if (/gemini/.test(s) && /pro/.test(s)) return 76 + versionBump(s, "gemini") + effortBump(s);
1119
- if (/gpt-4/.test(s)) return 65;
1120
- if (/deepseek/.test(s)) return 55;
1121
- return UNRANKED_SCORE;
1122
- }
1123
- function mostCapable(models) {
1124
- return [...models].sort((a, b) => capabilityScore(b) - capabilityScore(a))[0] ?? "";
1125
- }
1126
- function modelBand(model) {
1127
- if (WEAK_RE.test(model)) return "fast";
1128
- const s = capabilityScore(model);
1129
- if (s >= 95) return "flagship";
1130
- if (s >= 84) return "strong";
1131
- if (s <= UNRANKED_SCORE) return "fast";
1132
- return "mid";
1133
- }
1134
- function baseModel(model) {
1135
- const segs = model.toLowerCase().split("/");
1136
- let s = segs[segs.length - 1];
1137
- s = s.replace(/-(ultra|max|xhigh|high|medium|low|free|thinking|preview)\b/g, "");
1138
- s = s.replace(/-\d{6,8}\b/g, "");
1139
- return s.replace(/-+$/, "");
1140
- }
1141
- function modelFamily(model) {
1142
- return baseModel(model).replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
1143
- }
1144
- function latestFirst(models) {
1145
- const best = /* @__PURE__ */ new Map();
1146
- for (const m of models) {
1147
- const key2 = modelFamily(m);
1148
- const cur = best.get(key2);
1149
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
1150
- }
1151
- const isLatest = (m) => best.get(modelFamily(m)) === m;
1152
- return [...models.filter(isLatest), ...models.filter((m) => !isLatest(m))];
1153
- }
1154
- function versionlessId(model) {
1155
- const cut = model.lastIndexOf("/");
1156
- const prefix = cut >= 0 ? model.slice(0, cut + 1) : "";
1157
- const name = model.slice(cut + 1).toLowerCase().replace(/-\d{6,8}\b/g, "").replace(/[-.]v?\d+(?:[-.]\d+)*(?=[-.]|$)/g, "").replace(/[-.]{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
1158
- return prefix + name;
1159
- }
1160
- var DURABLE_ROLES = ["tracer"];
1161
- function strongestPrimary(chain, pool) {
1162
- const head = chain[0];
1163
- if (!head) return chain;
1164
- let best = head;
1165
- for (const m of pool) {
1166
- if (modelBand(m) === "flagship" || !isKnownModel(m)) continue;
1167
- if (capabilityScore(m) > capabilityScore(best)) best = m;
1168
- }
1169
- if (best === head) return chain;
1170
- const at = chain.indexOf(best);
1171
- if (at > 0) {
1172
- const next = [...chain];
1173
- next[at] = head;
1174
- next[0] = best;
1175
- return next;
1176
- }
1177
- return [best, ...chain.slice(1)];
1178
- }
1179
- function newestPrimary(chain, pool) {
1180
- const head = chain[0];
1181
- if (!head) return chain;
1182
- const key2 = versionlessId(head);
1183
- let best = head;
1184
- for (const m of pool) {
1185
- if (versionlessId(m) !== key2) continue;
1186
- if (capabilityScore(m) > capabilityScore(best)) best = m;
1187
- }
1188
- if (best === head) return chain;
1189
- const at = chain.indexOf(best);
1190
- if (at > 0) {
1191
- const next = [...chain];
1192
- next[at] = head;
1193
- next[0] = best;
1194
- return next;
1195
- }
1196
- return [best, ...chain.slice(1)];
1197
- }
1198
- function dedupBest(models) {
1199
- const best = /* @__PURE__ */ new Map();
1200
- for (const m of models) {
1201
- const key2 = baseModel(m);
1202
- const cur = best.get(key2);
1203
- if (!cur || capabilityScore(m) > capabilityScore(cur)) best.set(key2, m);
1204
- }
1205
- return [...best.values()].sort((a, b) => capabilityScore(b) - capabilityScore(a));
1206
- }
1207
- function sourceOf(model) {
1208
- const s = model.toLowerCase().replace(/^no-think\//, "");
1209
- return cliFor(s) ?? s.split("/")[0];
1210
- }
1211
- function interleaveBySource(pool) {
1212
- const bySource = /* @__PURE__ */ new Map();
1213
- for (const m of pool) {
1214
- const s = sourceOf(m);
1215
- const q = bySource.get(s);
1216
- if (q) q.push(m);
1217
- else bySource.set(s, [m]);
1218
- }
1219
- const queues = [...bySource.values()];
1220
- const out = [];
1221
- for (let more = true; more; ) {
1222
- more = false;
1223
- for (const q of queues) {
1224
- const m = q.shift();
1225
- if (m !== void 0) {
1226
- out.push(m);
1227
- more = true;
1228
- }
1229
- }
1230
- }
1231
- return out;
1232
- }
1233
- var BAND_ORDER = { fast: 0, mid: 1, strong: 2, flagship: 3 };
1234
- function bandDistance(primary, candidate) {
1235
- const p = BAND_ORDER[modelBand(primary)];
1236
- const c = BAND_ORDER[modelBand(candidate)];
1237
- return Math.abs(c - p) * 2 + (c < p ? 1 : 0);
1238
- }
1239
- function pickFallbacks(primary, pool, n) {
1240
- const chosen = [];
1241
- const usedModels = /* @__PURE__ */ new Set([baseModel(primary)]);
1242
- const usedSources = /* @__PURE__ */ new Set([sourceOf(primary)]);
1243
- const byHeft = pool.map((m, i) => ({ m, i })).sort((a, b) => bandDistance(primary, a.m) - bandDistance(primary, b.m) || a.i - b.i).map((x) => x.m);
1244
- for (const m of byHeft) {
1245
- if (chosen.length >= n) break;
1246
- if (usedModels.has(baseModel(m)) || usedSources.has(sourceOf(m))) continue;
1247
- chosen.push(m);
1248
- usedModels.add(baseModel(m));
1249
- usedSources.add(sourceOf(m));
1250
- }
1251
- for (const m of byHeft) {
1252
- if (chosen.length >= n) break;
1253
- if (usedModels.has(baseModel(m))) continue;
1254
- chosen.push(m);
1255
- usedModels.add(baseModel(m));
1256
- }
1257
- return chosen;
1258
- }
1259
- var FALLBACK_COUNT = 2;
1260
- function adjustRoleModels(roles, models, unfit) {
1261
- if (models.length === 0) return [];
1262
- const recognised = models.filter(isKnownModel);
1263
- const pick = recognised.length ? recognised : models;
1264
- const capable = dedupBest(pick.filter((m) => !WEAK_RE.test(m)));
1265
- const fast = dedupBest(pick.filter((m) => WEAK_RE.test(m)));
1266
- const capablePool = capable.length ? capable : fast;
1267
- const fastPool = fast.length ? fast : capable;
1268
- const primaryPool = latestFirst(capablePool);
1269
- const primaryFast = latestFirst(fastPool);
1270
- const nonFlagship = primaryPool.filter((m) => modelBand(m) !== "flagship");
1271
- const strongPool = primaryPool.filter((m) => modelBand(m) === "strong");
1272
- const midPool = primaryPool.filter((m) => modelBand(m) === "mid");
1273
- const wanted = new Set(roles);
1274
- const forRole = (role, pool) => {
1275
- if (!unfit) return pool;
1276
- const fit = pool.filter((m) => !unfit(role, m));
1277
- return fit.length ? fit : pool;
1278
- };
1279
- const known = /* @__PURE__ */ new Set([...FLAGSHIP_ROLES, ...STRONG_ROLES, ...MID_ROLES, ...FAST_ROLES]);
1280
- const primary = /* @__PURE__ */ new Map();
1281
- const flagSrc = primaryPool;
1282
- FLAGSHIP_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1283
- const src = forRole(r, flagSrc);
1284
- primary.set(r, src[i % src.length]);
1285
- });
1286
- const strongSrc = interleaveBySource(strongPool.length ? strongPool : nonFlagship.length ? nonFlagship : primaryPool);
1287
- STRONG_ROLES.filter((r) => wanted.has(r)).concat(roles.filter((r) => !known.has(r))).forEach((r, i) => {
1288
- const src = forRole(r, strongSrc);
1289
- primary.set(r, src[i % src.length]);
1290
- });
1291
- const midSrc = interleaveBySource(midPool.length ? midPool : nonFlagship.length ? nonFlagship : primaryPool);
1292
- MID_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1293
- const src = forRole(r, midSrc);
1294
- primary.set(r, src[i % src.length]);
1295
- });
1296
- FAST_ROLES.filter((r) => wanted.has(r)).forEach((r, i) => {
1297
- const src = forRole(r, primaryFast);
1298
- primary.set(r, src[i % src.length]);
1299
- });
1300
- return roles.map((role) => {
1301
- const head = primary.get(role) ?? primaryPool[0];
1302
- const capForFb = MID_ROLES.includes(role) ? capablePool.filter((m) => modelBand(m) !== "flagship") : capablePool;
1303
- const pool = FAST_ROLES.includes(role) ? [...fastPool, ...capForFb] : [...capForFb, ...fastPool];
1304
- return { role, models: newestPrimary([head, ...pickFallbacks(head, forRole(role, pool), FALLBACK_COUNT)], models) };
1305
- });
1306
- }
1307
-
1308
- // src/skills/apply.ts
1309
- import { readFile } from "fs/promises";
1310
- import { readdirSync as readdirSync2 } from "fs";
1311
- import { resolve as resolve2, sep as sep3 } from "path";
1312
- import { z } from "zod";
1313
- function applySkills(basePrompt, mandatory, registry) {
1314
- const parts = [basePrompt];
1315
- if (mandatory.length) {
1316
- const sections = mandatory.map((name) => {
1317
- const skill = registry.get(name);
1318
- if (!skill) throw new Error(`applySkills: undefined skill: ${name}`);
1319
- const where = skill.dir ? `
1320
- _Skill base directory: ${skill.dir}_
1321
- ` : "";
1322
- return `## ${skill.name}${where}
1323
- ${skill.content}`;
1324
- });
1325
- parts.push(`# Mandatory Skills
1326
- ${sections.join("\n\n")}`);
1327
- }
1328
- const mandatorySet = new Set(mandatory);
1329
- const available = registry.list().filter((s) => !mandatorySet.has(s.name));
1330
- if (available.length) {
1331
- const lines = available.map((s) => `- ${s.name}: ${s.description}`);
1332
- parts.push(`# Discoverable Skills (call the skill tool to fetch its content)
1333
- ${lines.join("\n")}`);
1334
- }
1335
- return parts.join("\n\n");
1336
- }
1337
- var skillParams = z.object({
1338
- name: z.string().describe("The skill's name, exactly as it is listed."),
1339
- /**
1340
- * A supporting document inside the skill's own directory, e.g. "reference/critique.md".
1341
- *
1342
- * Described, because an undescribed optional string gets filled in. Measured: four consecutive calls sent
1343
- * `file: ""` and every one of them failed — the skill was there, its content was one branch away, and an
1344
- * empty string took the other branch.
1345
- */
1346
- 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.')
1347
- });
1348
- var DOCS_SHOWN = 12;
1349
- function docsIn(dir) {
1350
- try {
1351
- 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);
1352
- } catch {
1353
- return [];
1354
- }
1355
- }
1356
- var MAX_SKILL_DOC_CHARS = 3e4;
1357
- var MAX_SKILLS_LISTED = 12;
1358
- function noSuchSkill(name, available) {
1359
- const shape = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1360
- const same = available.filter((s) => shape(s) === shape(name));
1361
- if (same.length === 1) {
1362
- return `skill not found: ${name} \u2014 did you mean \`${same[0]}\`? Call it with that exact name.`;
1363
- }
1364
- if (!available.length) return `skill not found: ${name}. This project has no skills installed, so carry on without one.`;
1365
- const near = available.filter((s) => shape(s).includes(shape(name)) || shape(name).includes(shape(s)));
1366
- if (near.length && near.length <= MAX_SKILLS_LISTED) {
1367
- return `skill not found: ${name}. Closest by name: ${near.map((s) => `\`${s}\``).join(", ")}. Call one of those exactly if it is what you meant, or carry on without a skill.`;
1368
- }
1369
- const shown = available.slice(0, MAX_SKILLS_LISTED);
1370
- const rest = available.length - shown.length;
1371
- return `skill not found: ${name}. Available: ${shown.join(", ")}${rest > 0 ? `, and ${rest} more \u2014 the full list is in your system prompt` : ""}. Use one of these exactly, or carry on without a skill \u2014 do not guess another name.`;
1372
- }
1373
- function buildSkillTool(registry) {
1374
- return {
1375
- name: "skill",
1376
- 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.',
1377
- permissionLevel: "safe",
1378
- parameters: skillParams,
1379
- run: async (rawArgs) => {
1380
- const parsed = skillParams.safeParse(rawArgs);
1381
- if (!parsed.success) {
1382
- return { content: `skill: invalid args: ${parsed.error.issues.map((i) => i.message).join("; ")}`, isError: true };
1383
- }
1384
- const { name, file } = parsed.data;
1385
- const skill = registry.get(name);
1386
- if (!skill) return { content: noSuchSkill(name, registry.list().map((s) => s.name)), isError: true };
1387
- if (file === void 0 || !file.trim()) {
1388
- const where = skill.dir ? `_Skill base directory: ${skill.dir}_
1389
-
1390
- ` : "";
1391
- return { content: `${where}${skill.content}`, isError: false };
1392
- }
1393
- if (!skill.dir) return { content: `skill ${name}: has no supporting documents`, isError: true };
1394
- const target = resolve2(skill.dir, file);
1395
- const root = resolve2(skill.dir);
1396
- if (target !== root && !target.startsWith(root + sep3)) {
1397
- return { content: `skill ${name}: ${file} is outside the skill directory`, isError: true };
1398
- }
1399
- let raw;
1400
- try {
1401
- raw = await readFile(target, "utf8");
1402
- } catch {
1403
- const has = docsIn(skill.dir);
1404
- return {
1405
- content: `skill ${name}: no such document: ${file}` + (has.length ? `. It has: ${has.join(", ")}` : `. It has no supporting documents.`),
1406
- isError: true
1407
- };
1408
- }
1409
- if (raw.length <= MAX_SKILL_DOC_CHARS) return { content: raw, isError: false };
1410
- return {
1411
- content: `${raw.slice(0, MAX_SKILL_DOC_CHARS)}
1412
-
1413
- [skill ${name}/${file}: truncated at ${MAX_SKILL_DOC_CHARS} of ${raw.length} chars]`,
1414
- isError: false
1415
- };
1416
- }
1417
- };
1418
- }
1419
-
1420
890
  // src/engine/unfinished.ts
1421
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync, statSync } from "fs";
891
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync, statSync } from "fs";
1422
892
  import { join as join3 } from "path";
1423
893
  function boardCounts(dir) {
1424
894
  try {
@@ -1434,7 +904,7 @@ function unfinishedSessions(cwd, commitCount = () => 0) {
1434
904
  const root = join3(cwd, ".horsecode", "worktrees");
1435
905
  if (!existsSync3(root)) return [];
1436
906
  const out = [];
1437
- for (const id of readdirSync3(root)) {
907
+ for (const id of readdirSync2(root)) {
1438
908
  const dir = join3(root, id);
1439
909
  try {
1440
910
  if (!statSync(dir).isDirectory()) continue;
@@ -1471,7 +941,7 @@ function describeUnfinished(s) {
1471
941
 
1472
942
  // src/tools/git.ts
1473
943
  import { execFile } from "child_process";
1474
- import { z as z2 } from "zod";
944
+ import { z } from "zod";
1475
945
  var READ_ONLY = /* @__PURE__ */ new Set([
1476
946
  "status",
1477
947
  "log",
@@ -1542,8 +1012,8 @@ var READ_ONLY_PAIRS = /* @__PURE__ */ new Set([
1542
1012
  "stash show"
1543
1013
  ]);
1544
1014
  var REFUSED_ARG = /^(--output|-c$|--config-env|--exec-path|-C$|--git-dir|--work-tree|--upload-pack|--receive-pack)/;
1545
- var params = z2.object({
1546
- args: z2.array(z2.string()).min(1).describe(
1015
+ var params = z.object({
1016
+ args: z.array(z.string()).min(1).describe(
1547
1017
  'Git arguments as a list, without the leading "git" \u2014 e.g. ["status","--porcelain"] or ["log","-5","--oneline"].'
1548
1018
  )
1549
1019
  });
@@ -1712,7 +1182,7 @@ var gitTool = {
1712
1182
  const args = parsed.data.args;
1713
1183
  const why = refuse(args);
1714
1184
  if (why) return { content: why, isError: true, settled: true };
1715
- const out = await new Promise((resolve6) => {
1185
+ const out = await new Promise((resolve5) => {
1716
1186
  const child = execFile("git", args, {
1717
1187
  cwd: ctx.cwd,
1718
1188
  timeout: GIT_TIMEOUT_MS,
@@ -1722,7 +1192,7 @@ var gitTool = {
1722
1192
  env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
1723
1193
  }, (err, stdout, stderr) => {
1724
1194
  const text = `${stdout}${stderr}`.trim();
1725
- resolve6({ code: err?.code ?? (err ? 1 : 0), text });
1195
+ resolve5({ code: err?.code ?? (err ? 1 : 0), text });
1726
1196
  });
1727
1197
  ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1728
1198
  });
@@ -1783,7 +1253,7 @@ var gitWriteTool = {
1783
1253
  const args = parsed.data.args;
1784
1254
  const why = refuseWrite(args);
1785
1255
  if (why) return { content: why, isError: true, settled: true };
1786
- const out = await new Promise((resolve6) => {
1256
+ const out = await new Promise((resolve5) => {
1787
1257
  const child = execFile("git", args, {
1788
1258
  cwd: ctx.cwd,
1789
1259
  // A push talks to a server: the read tool's 30s is a reasonable ceiling for a local query and a
@@ -1794,7 +1264,7 @@ var gitWriteTool = {
1794
1264
  // prompt no one can see — the TUI owns the terminal, so the agent would simply hang.
1795
1265
  env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" }
1796
1266
  }, (err, stdout, stderr) => {
1797
- resolve6({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
1267
+ resolve5({ code: err ? 1 : 0, text: `${stdout}${stderr}`.trim() });
1798
1268
  });
1799
1269
  ctx.signal?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1800
1270
  });
@@ -1806,9 +1276,9 @@ var gitWriteTool = {
1806
1276
  };
1807
1277
 
1808
1278
  // src/tools/remember.ts
1809
- import { z as z3 } from "zod";
1810
- var params2 = z3.object({
1811
- fact: z3.string().describe(
1279
+ import { z as z2 } from "zod";
1280
+ var params2 = z2.object({
1281
+ fact: z2.string().describe(
1812
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."
1813
1283
  )
1814
1284
  });
@@ -1833,7 +1303,7 @@ function buildRememberTool(sink) {
1833
1303
  var rememberFactTool = buildRememberTool();
1834
1304
 
1835
1305
  // src/speckit/layout.ts
1836
- import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync4 } from "fs";
1306
+ import { existsSync as existsSync4, mkdirSync, readdirSync as readdirSync3 } from "fs";
1837
1307
  import { join as join4 } from "path";
1838
1308
  function specsDir(workdir) {
1839
1309
  return join4(workdir, "specs");
@@ -1878,7 +1348,7 @@ function featureSlugFor(workdir, title) {
1878
1348
  const want = toSlug(title);
1879
1349
  const dir = specsDir(workdir);
1880
1350
  if (existsSync4(dir)) {
1881
- const names = readdirSync4(dir);
1351
+ const names = readdirSync3(dir);
1882
1352
  for (const name of names) {
1883
1353
  if (name.replace(/^\d+-/, "") === want) return name;
1884
1354
  }
@@ -1896,7 +1366,7 @@ function nextFeatureSlug(workdir, title) {
1896
1366
  const dir = specsDir(workdir);
1897
1367
  let max = 0;
1898
1368
  if (existsSync4(dir)) {
1899
- for (const name of readdirSync4(dir)) {
1369
+ for (const name of readdirSync3(dir)) {
1900
1370
  const m = name.match(/^(\d+)-/);
1901
1371
  if (m) max = Math.max(max, Number(m[1]));
1902
1372
  }
@@ -1914,7 +1384,7 @@ function scaffoldFeature(workdir, slug) {
1914
1384
  import { createHash } from "crypto";
1915
1385
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1916
1386
  import { join as join5 } from "path";
1917
- import { z as z4 } from "zod";
1387
+ import { z as z3 } from "zod";
1918
1388
 
1919
1389
  // src/engine/constitution.ts
1920
1390
  var SCOPES = ["always", "backend", "frontend", "data", "infra", "docs", "review", "spec", "test", "govern"];
@@ -2050,10 +1520,10 @@ function labellingLooksWrong(scoped) {
2050
1520
  }
2051
1521
 
2052
1522
  // src/engine/constitution-store.ts
2053
- var LabelsSchema = z4.object({
2054
- labels: z4.array(z4.object({
2055
- index: z4.number().int().describe("The rule's number, exactly as given to you."),
2056
- 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(
2057
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.`
2058
1528
  )
2059
1529
  }))
@@ -2141,12 +1611,12 @@ async function constitutionNote(deps, cwd, work) {
2141
1611
  }
2142
1612
 
2143
1613
  // src/engine/reviewer.ts
2144
- import { z as z8 } from "zod";
1614
+ import { z as z7 } from "zod";
2145
1615
 
2146
1616
  // src/tools/find-tool.ts
2147
- import { z as z5 } from "zod";
2148
- var params3 = z5.object({
2149
- query: z5.string().describe(
1617
+ import { z as z4 } from "zod";
1618
+ var params3 = z4.object({
1619
+ query: z4.string().describe(
2150
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.'
2151
1621
  )
2152
1622
  });
@@ -2234,8 +1704,8 @@ ${rows.join("\n")}`,
2234
1704
  // src/tools/unfinished-tool.ts
2235
1705
  import { execFileSync } from "child_process";
2236
1706
  import { join as join6 } from "path";
2237
- import { z as z6 } from "zod";
2238
- var params4 = z6.object({});
1707
+ import { z as z5 } from "zod";
1708
+ var params4 = z5.object({});
2239
1709
  function commitsAhead(cwd, branch) {
2240
1710
  try {
2241
1711
  const out = execFileSync(
@@ -2284,10 +1754,10 @@ To continue one of these, the user says **continue** \u2014 that reopens the ses
2284
1754
  };
2285
1755
 
2286
1756
  // src/tools/propose-memory.ts
2287
- import { z as z7 } from "zod";
2288
- var params5 = z7.object({
2289
- text: z7.string(),
2290
- 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(
2291
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."
2292
1762
  )
2293
1763
  });
@@ -2543,11 +2013,11 @@ async function diffSince(cwd, sinceRef, git = defaultGitRunner) {
2543
2013
  // src/engine/reviewer.ts
2544
2014
  var CODE_REVIEW_MAX_TURNS = 25;
2545
2015
  var CODE_REVIEW_TIMEOUT_MS = 10 * 60 * 1e3;
2546
- var VerdictSchema = z8.object({
2547
- verdict: z8.enum(["pass", "fail"]).describe(
2016
+ var VerdictSchema = z7.object({
2017
+ verdict: z7.enum(["pass", "fail"]).describe(
2548
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."
2549
2019
  ),
2550
- notes: z8.array(z8.string())
2020
+ notes: z7.array(z7.string())
2551
2021
  });
2552
2022
  function readOnlyRegistry(deps, opts = {}) {
2553
2023
  const r = new ToolRegistry();
@@ -2615,9 +2085,9 @@ ${describeDiff(diff)}` };
2615
2085
  // src/tools/write.ts
2616
2086
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
2617
2087
  import { existsSync as existsSync6 } from "fs";
2618
- import { dirname as dirname3, resolve as resolve3, sep as sep4 } from "path";
2619
- import { z as z9 } from "zod";
2620
- 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() });
2621
2091
  var writeFileTool = {
2622
2092
  name: "write_file",
2623
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.",
@@ -2636,9 +2106,9 @@ var writeFileTool = {
2636
2106
  };
2637
2107
  }
2638
2108
  const a = parsed.data;
2639
- const target = resolve3(ctx.cwd, a.path);
2640
- const cwdResolved = resolve3(ctx.cwd);
2641
- 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)) {
2642
2112
  return { content: `write_file: path is outside cwd: ${a.path}`, isError: true };
2643
2113
  }
2644
2114
  if (ctx.readFiles && existsSync6(target) && !ctx.readFiles.has(target)) {
@@ -2666,21 +2136,21 @@ var writeFileTool = {
2666
2136
  };
2667
2137
 
2668
2138
  // src/tools/edit.ts
2669
- import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
2670
- import { resolve as resolve4, sep as sep5 } from "path";
2671
- import { z as z10 } from "zod";
2672
- var params7 = z10.object({
2673
- path: z10.string(),
2674
- oldString: z10.string(),
2675
- newString: z10.string(),
2676
- 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()
2677
2147
  });
2678
2148
  var NEAR_MISS_CHARS = 600;
2679
2149
  var MAX_MATCH_LINES = 5;
2680
2150
  var norm = (t) => t.replace(/[ \t]+/g, " ").replace(/[ \t]+$/gm, "").trim();
2681
2151
  function shortPath(path, cwd) {
2682
- const abs = resolve4(cwd, path);
2683
- 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;
2684
2154
  }
2685
2155
  function whyNotFound(content, oldString) {
2686
2156
  if (/^\s*\d+\t/m.test(oldString)) {
@@ -2721,14 +2191,14 @@ var editFileTool = {
2721
2191
  };
2722
2192
  }
2723
2193
  const a = parsed.data;
2724
- const target = resolve4(ctx.cwd, a.path);
2725
- const cwdResolved = resolve4(ctx.cwd);
2726
- 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)) {
2727
2197
  return { content: `edit_file: path is outside cwd: ${a.path}`, isError: true };
2728
2198
  }
2729
2199
  let content;
2730
2200
  try {
2731
- content = await readFile2(target, "utf8");
2201
+ content = await readFile(target, "utf8");
2732
2202
  } catch (e) {
2733
2203
  return {
2734
2204
  content: `edit_file error: ${e instanceof Error ? e.message : String(e)}`,
@@ -2776,12 +2246,12 @@ var editFileTool = {
2776
2246
 
2777
2247
  // src/tools/shell.ts
2778
2248
  import { spawn } from "child_process";
2779
- import { resolve as resolve5, sep as sep6 } from "path";
2780
- import { z as z11 } from "zod";
2781
- var params8 = z11.object({
2782
- command: z11.string(),
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(),
2783
2253
  /** Milliseconds before the command is killed. Defaults to DEFAULT_TIMEOUT_MS, capped at MAX_TIMEOUT_MS. */
2784
- timeout: z11.number().int().positive().optional()
2254
+ timeout: z10.number().int().positive().optional()
2785
2255
  });
2786
2256
  var DEFAULT_TIMEOUT_MS = 12e4;
2787
2257
  var MAX_TIMEOUT_MS = 6e5;
@@ -2816,7 +2286,7 @@ var REWRITES = [
2816
2286
  ];
2817
2287
  var REDIRECT = /(?:^|[^0-9<>&])>>?\s*(?!\/dev\/|\/tmp\/|&)([A-Za-z0-9_./-]*\.[A-Za-z0-9]+)/;
2818
2288
  function leavesWorkdir(command, cwd) {
2819
- const base = resolve5(cwd);
2289
+ const base = resolve4(cwd);
2820
2290
  let at = base;
2821
2291
  for (const seg of command.split(/&&|\|\||;|\|/)) {
2822
2292
  const m = /^\s*(?:cd|pushd)(?:\s+(.*))?$/.exec(seg.trim());
@@ -2824,8 +2294,8 @@ function leavesWorkdir(command, cwd) {
2824
2294
  const raw = (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
2825
2295
  if (!raw || raw === "~" || raw === "$HOME" || raw.startsWith("~/")) return raw || "~";
2826
2296
  if (raw === "-") return "-";
2827
- at = resolve5(at, raw);
2828
- if (at !== base && !at.startsWith(base + sep6)) return raw;
2297
+ at = resolve4(at, raw);
2298
+ if (at !== base && !at.startsWith(base + sep5)) return raw;
2829
2299
  }
2830
2300
  return void 0;
2831
2301
  }
@@ -2956,8 +2426,8 @@ ${body}${tail}`, isError: timedOut || code !== 0 });
2956
2426
  };
2957
2427
 
2958
2428
  // src/tools/web.ts
2959
- import { z as z12 } from "zod";
2960
- var params9 = z12.object({ url: z12.string().url() });
2429
+ import { z as z11 } from "zod";
2430
+ var params9 = z11.object({ url: z11.string().url() });
2961
2431
  var MAX_CHARS = 1e5;
2962
2432
  function createWebFetchTool(fetchFn = globalThis.fetch) {
2963
2433
  return {
@@ -3032,8 +2502,8 @@ async function refreshTraces(opts) {
3032
2502
  }
3033
2503
  const targets = candidates.filter((f) => !gone.includes(f));
3034
2504
  if (!targets.length) return out;
3035
- const model = opts.models.find(Boolean);
3036
- if (!model) return out;
2505
+ const chain = opts.models.filter(Boolean);
2506
+ if (!chain.length) return out;
3037
2507
  try {
3038
2508
  const g = await buildProjectGraph(opts.cwd);
3039
2509
  out.graph = g.message;
@@ -3046,7 +2516,7 @@ async function refreshTraces(opts) {
3046
2516
  const res = await runTraces({
3047
2517
  cwd: opts.cwd,
3048
2518
  provider: opts.provider,
3049
- model,
2519
+ models: chain,
3050
2520
  plan,
3051
2521
  // No liveFiles: this run knows only the files one task changed, and a pruner given that list would
3052
2522
  // read every OTHER trace in the project as orphaned and delete it.
@@ -3080,14 +2550,14 @@ async function commitRefreshed(git, baseWorktree, traceRootRel2) {
3080
2550
  }
3081
2551
 
3082
2552
  // src/engine/writer-registry.ts
3083
- import { z as z14 } from "zod";
2553
+ import { z as z13 } from "zod";
3084
2554
 
3085
2555
  // src/engine/normalize-question.ts
3086
- import { z as z13 } from "zod";
3087
- var NormalizedQuestionSchema = z13.object({
3088
- question: z13.string().describe("The core question, concise, WITHOUT the embedded options table/list."),
3089
- options: z13.array(z13.string()).describe("Each selectable choice as a SHORT label; the recommended one first, suffixed ' (recommended)'. Empty when the question is genuinely open-ended."),
3090
- multiSelect: z13.boolean().describe("true only if the user may pick more than one.")
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.")
3091
2561
  });
3092
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.";
3093
2563
  function looksLikeChoices(text) {
@@ -3154,21 +2624,21 @@ var clipLabel = (body) => {
3154
2624
  };
3155
2625
 
3156
2626
  // src/engine/writer-registry.ts
3157
- var askUserParams = z14.object({
3158
- question: z14.string(),
2627
+ var askUserParams = z13.object({
2628
+ question: z13.string(),
3159
2629
  // For a multiple-choice question, list the choices here → the UI shows a selectable checkbox/radio list
3160
2630
  // (arrow keys + Enter) instead of a free-text box. Omit for an open-ended question.
3161
2631
  //
3162
2632
  // A choice may be a plain string, or an object carrying what the label alone cannot say: a one-line
3163
2633
  // `description`, and a `preview` rendered in a panel beside the list while that option is focused. Use the
3164
2634
  // rich form when the decision turns on the trade-offs rather than the name (e.g. "which approach?").
3165
- options: z14.array(z14.union([
3166
- z14.string(),
3167
- z14.object({ label: z14.string(), description: z14.string().optional(), preview: z14.string().optional() })
2635
+ options: z13.array(z13.union([
2636
+ z13.string(),
2637
+ z13.object({ label: z13.string(), description: z13.string().optional(), preview: z13.string().optional() })
3168
2638
  ])).optional().describe(
3169
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."
3170
2640
  ),
3171
- multiSelect: z14.boolean().optional().describe(
2641
+ multiSelect: z13.boolean().optional().describe(
3172
2642
  "True when the user may pick more than one (checkboxes); omitted means pick exactly one (radio)."
3173
2643
  ),
3174
2644
  /**
@@ -3177,7 +2647,7 @@ var askUserParams = z14.object({
3177
2647
  * Present ⇒ this is a hand-off, not a question: the run has stopped because only a person can carry the
3178
2648
  * next step, and the UI says so rather than showing a bare "? Question".
3179
2649
  */
3180
- steps: z14.array(z14.string()).optional().describe(
2650
+ steps: z13.array(z13.string()).optional().describe(
3181
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.'
3182
2652
  )
3183
2653
  });
@@ -3376,7 +2846,7 @@ var RoleFitness = class {
3376
2846
  };
3377
2847
 
3378
2848
  // src/engine/routing.ts
3379
- import { z as z15 } from "zod";
2849
+ import { z as z14 } from "zod";
3380
2850
 
3381
2851
  // src/engine/route-role.ts
3382
2852
  var STYLE_EXT = [".css", ".scss", ".sass", ".less", ".styl"];
@@ -3443,8 +2913,8 @@ function routeByEvidence(card) {
3443
2913
  }
3444
2914
 
3445
2915
  // src/engine/routing.ts
3446
- var RouteSchema = z15.object({
3447
- role: z15.enum(["coder", "designer"]).describe(
2916
+ var RouteSchema = z14.object({
2917
+ role: z14.enum(["coder", "designer"]).describe(
3448
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."
3449
2919
  )
3450
2920
  });
@@ -3506,9 +2976,9 @@ function createDefaultRegistry() {
3506
2976
  }
3507
2977
 
3508
2978
  // src/engine/operational.ts
3509
- import { z as z16 } from "zod";
3510
- var CommitSchema = z16.object({
3511
- message: z16.string().describe("A Conventional Commits message: `type(scope): subject`, English, imperative.")
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.")
3512
2982
  });
3513
2983
  var MAX_DIFF = 12e3;
3514
2984
  var OPERATIONAL_MAX_TURNS = 3;
@@ -3754,14 +3224,14 @@ function deadlineWarning(elapsedMs, budgetMs) {
3754
3224
  }
3755
3225
  var MAX_WRITTEN_CHARS = 6e4;
3756
3226
  async function writtenText(cwd, touched) {
3757
- const { readFile: readFile4 } = await import("fs/promises");
3227
+ const { readFile: readFile3 } = await import("fs/promises");
3758
3228
  const { join: join10 } = await import("path");
3759
3229
  const parts = [];
3760
3230
  let used = 0;
3761
3231
  for (const p of [...new Set(touched)]) {
3762
3232
  if (used >= MAX_WRITTEN_CHARS) break;
3763
3233
  try {
3764
- const t = await readFile4(join10(cwd, p), "utf8");
3234
+ const t = await readFile3(join10(cwd, p), "utf8");
3765
3235
  parts.push(t.slice(0, MAX_WRITTEN_CHARS - used));
3766
3236
  used += t.length;
3767
3237
  } catch {
@@ -3956,391 +3426,18 @@ ${handOver}`;
3956
3426
  // src/engine/review.ts
3957
3427
  import { existsSync as existsSync8 } from "fs";
3958
3428
  import { isAbsolute, join as join8 } from "path";
3959
- import { z as z17 } from "zod";
3960
-
3961
- // src/agent/roles.ts
3962
- function isTransientFailure(reason) {
3963
- const r = reason.toLowerCase();
3964
- if (/\b(429|rate.?limit|quota|exhaust|insufficient|billing|credit)\b/.test(r)) return false;
3965
- return /overload|529|50[0234]|timeout|timed out|deadline|econnreset|epipe|socket hang up|stream ended|temporar|unavailable|try again/.test(r);
3966
- }
3967
- function isSourceCapacity(reason) {
3968
- return /capacity is (?:temporarily unavailable|busy)/i.test(reason);
3969
- }
3970
- function sourcePrefix(model) {
3971
- const s = model.replace(/^no-think\//, "");
3972
- const cli = cliFor(s);
3973
- if (cli) return cli;
3974
- const i = s.indexOf("/");
3975
- return i > 0 ? s.slice(0, i) : void 0;
3976
- }
3977
- function weightedCycle(sources, weights) {
3978
- const queues = sources.map((s) => Array(Math.max(1, weights[s] ?? 1)).fill(s));
3979
- const out = [];
3980
- for (let more = true; more; ) {
3981
- more = false;
3982
- for (const q of queues) {
3983
- const m = q.shift();
3984
- if (m !== void 0) {
3985
- out.push(m);
3986
- more = true;
3987
- }
3988
- }
3989
- }
3990
- return out;
3991
- }
3992
- function canonicalSource(name) {
3993
- const s = name.toLowerCase().replace(/^no-think\//, "");
3994
- if (s === "cc" || s === "claude") return "claude";
3995
- if (s === "cx") return "codex";
3996
- return sourcePrefix(s) ?? s;
3997
- }
3998
- function providerOutage(reason) {
3999
- return /no active credentials for provider:?\s*([\w.-]+)/i.exec(reason)?.[1] ?? /provider\s+'?([\w.-]+)'?\s+is not configured/i.exec(reason)?.[1] ?? /all\s+([\w.-]+)\s+accounts have exhausted their quota/i.exec(reason)?.[1] ?? /shared egress ip quota exhausted\s*\(([\w.-]+)\)/i.exec(reason)?.[1] ?? /^\s*(claude|codex|grok|zai)\s+CLI:\s*rejected\b/i.exec(reason)?.[1]?.toLowerCase();
4000
- }
4001
- function quotaResetAt(reason) {
4002
- const iso = /\(resets\s+([0-9T:.\-]+Z)\)/i.exec(reason)?.[1];
4003
- const t = iso ? Date.parse(iso) : NaN;
4004
- return Number.isFinite(t) ? t : void 0;
4005
- }
4006
- var RoleRegistry = class _RoleRegistry {
4007
- // durable behavioral rules → appended to EVERY role's prompt
4008
- constructor(roles, defaultPrompts = {}, skillRegistry) {
4009
- this.roles = roles;
4010
- this.defaultPrompts = defaultPrompts;
4011
- this.skillRegistry = skillRegistry;
4012
- }
4013
- roles;
4014
- defaultPrompts;
4015
- skillRegistry;
4016
- modelOverride;
4017
- roleOverrides = /* @__PURE__ */ new Map();
4018
- // per-role model CHAIN override (highest priority)
4019
- effortOverrides = /* @__PURE__ */ new Map();
4020
- // Models that failed retryably (429/5xx/quota) → skipped in every chain until released. Kept WITH the
4021
- // reason and the time so a coordinator can report them and later re-probe whether the limit has reset.
4022
- quarantine = /* @__PURE__ */ new Map();
4023
- notify;
4024
- // fallback UI note sink (wired once the controller exists)
4025
- onQuarantine;
4026
- /** What each model has actually managed to do in each ROLE — see setFitness. */
4027
- fitness;
4028
- // Models that answered in prose instead of calling the submit tool. Not a transport error, so nothing ever
4029
- // benched them: the chain quietly slid to the fallback on EVERY call, forever, in every role that held them.
4030
- strikes = /* @__PURE__ */ new Map();
4031
- rulesProvider;
4032
- /** Every configured role name — used to validate a role reference produced by a model (memory audiences). */
4033
- names() {
4034
- return [.../* @__PURE__ */ new Set([...Object.keys(this.roles), ...Object.keys(this.defaultPrompts)])];
4035
- }
4036
- /** Wire the fallback-note sink (called after the controller exists). */
4037
- setNotify(fn) {
4038
- this.notify = fn;
4039
- }
4040
- /** Wire the durable-rules source (memory). Rules are appended to every role's system prompt (always honored). */
4041
- setRules(fn) {
4042
- this.rulesProvider = fn;
4043
- }
4044
- /** The rule block to append to a role's prompt — empty when there are no rules. Public so prompt-supplying
4045
- * callers (spec-kit phases build their own prompt) can append it too. */
4046
- ruleSuffix() {
4047
- const rules = this.rulesProvider?.() ?? [];
4048
- return rules.length ? `
4049
-
4050
- User rules (ALWAYS honor these):
4051
- ${rules.map((r) => `- ${r}`).join("\n")}` : "";
4052
- }
4053
- /** Live-swap the model used by every role (session-only; clears on undefined/empty). */
4054
- setModelOverride(model) {
4055
- this.modelOverride = model && model.length > 0 ? model : void 0;
4056
- }
4057
- /** Live-swap the model CHAIN of ONE role (session-only; wins over the global override). Clears on empty. */
4058
- setRoleModel(roleName, models) {
4059
- const chain = (typeof models === "string" ? [models] : models ?? []).filter((m) => m.length > 0);
4060
- if (chain.length) this.roleOverrides.set(roleName, chain);
4061
- else this.roleOverrides.delete(roleName);
4062
- }
4063
- /**
4064
- * How hard this role should work, set alongside its chain.
4065
- *
4066
- * An override on the live registry rather than a config re-read, for the same reason `setRoleModel` is one:
4067
- * `/roles adjust` has to take effect in the session that ran it, not only in the next one.
4068
- *
4069
- * `undefined` REMOVES it — a role reassigned from a Claude model to one whose effort cannot be set must
4070
- * stop carrying a level, or the config keeps a number that no longer applies to anything.
4071
- */
4072
- setRoleEffort(roleName, effort) {
4073
- if (effort) this.effortOverrides.set(roleName, effort);
4074
- else this.effortOverrides.delete(roleName);
4075
- }
4076
- /**
4077
- * Wire the record of what each model has actually managed to do in each role.
4078
- *
4079
- * Without it a chain is only a list of names from a catalogue. With it, a model that has twice answered
4080
- * this role in prose instead of doing its work stops being offered to this role — while staying available
4081
- * to every other role, where it may be perfectly good.
4082
- */
4083
- setFitness(f) {
4084
- this.fitness = f;
4085
- }
4086
- /** Wire the quarantine hook: whatever benches a model, every role still holding it must be re-assigned. */
4087
- setOnQuarantine(fn) {
4088
- this.onQuarantine = fn;
4089
- }
4090
- /** Mark a model spent — every chain skips it from now on, until it is released. */
4091
- markExhausted(model, reason = "unavailable", now = Date.now(), until) {
4092
- if (!model || this.isQuarantined(model)) return;
4093
- const ends = until ?? (isTransientFailure(reason) ? now + _RoleRegistry.TRANSIENT_BENCH_MS : void 0);
4094
- this.quarantine.set(model, { at: now, reason, ...ends !== void 0 && { until: ends } });
4095
- this.onQuarantine?.(model, reason, ends);
4096
- }
4097
- /** Every model any role's chain names — the pool this registry can actually reach for. */
4098
- knownModels() {
4099
- return [...new Set(Object.values(this.roles).flatMap((r) => r.models ?? []))];
4100
- }
4101
- /**
4102
- * Benches every model of one provider, for a failure that is about the provider itself.
4103
- *
4104
- * Returns what it took out, so the caller can say so once instead of six times. Falls back to benching the
4105
- * single model when the pool names none of that provider — an unknown provider is still a real failure.
4106
- */
4107
- markProviderExhausted(provider, model, reason, now = Date.now()) {
4108
- const want = canonicalSource(provider);
4109
- const hit = this.knownModels().filter((m) => sourcePrefix(m) === want);
4110
- const until = quotaResetAt(reason);
4111
- for (const m of hit) this.markExhausted(m, reason, now, until);
4112
- if (!hit.length) {
4113
- this.markExhausted(model, reason, now);
4114
- return [model];
4115
- }
4116
- return hit;
4117
- }
4118
- /**
4119
- * How long a BEHAVIOURAL bench lasts before the model is tried again.
4120
- *
4121
- * A model that is out of quota is out until the quota returns, and nothing here can shorten that. A model
4122
- * that answered in prose is a different case entirely: the transport was fine, and the next prompt may not
4123
- * be the one it stumbled on. Benching it for the rest of a multi-hour run costs every role that held it —
4124
- * measured live, one such bench re-assigned SIXTEEN roles away from the best model available.
4125
- */
4126
- static STRUCTURAL_BENCH_MS = 10 * 6e4;
4127
- /**
4128
- * How long a TRANSPORT bench lasts — the busy server, not the spent subscription.
4129
- *
4130
- * The argument above, one door over. A model that answered in prose gets ten minutes because the transport
4131
- * was fine; a model whose transport said "Overloaded" for one second is the same case in its purest form,
4132
- * and it was the one getting benched for the whole session.
4133
- *
4134
- * Measured live, in the middle of a feature run: `cc/claude-opus-5` served five calls in the preceding two
4135
- * 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
4136
- * off the best model in the fleet for the rest of the session. A 529 is the textbook transient condition;
4137
- * the API's own guidance for it is to retry with backoff.
4138
- *
4139
- * Two minutes: long enough that a genuinely struggling gateway is not hammered, short enough that a
4140
- * one-second blip costs a couple of turns rather than an afternoon.
4141
- */
4142
- static TRANSIENT_BENCH_MS = 2 * 6e4;
4143
- /**
4144
- * How many structured failures a model gets before it is benched. One miss can be a genuinely hard prompt;
4145
- * a pattern is the model. Low, because every strike costs a full wasted pass in every role that holds it.
4146
- */
4147
- static STRUCTURAL_STRIKES = 2;
4148
- /**
4149
- * Records that a model finished a turn WITHOUT producing the structured result it was asked for (prose
4150
- * instead of a tool call). This is not "unavailable" — the transport was fine — so it never reached the
4151
- * retryable path that benches a model, and the chain slid to the fallback on every single call instead.
4152
- * Returns the strike count; at the threshold the model is quarantined like any other spent one.
4153
- */
4154
- markStructuralFailure(model, reason = "no valid structured result", role) {
4155
- if (!model) return 0;
4156
- const key2 = role ? `${model}::${role}` : model;
4157
- const n = (this.strikes.get(key2) ?? 0) + 1;
4158
- this.strikes.set(key2, n);
4159
- if (n < _RoleRegistry.STRUCTURAL_STRIKES) return n;
4160
- if (role) {
4161
- this.fitness?.record?.(role, model, reason);
4162
- const rolesFailed = [...this.strikes.entries()].filter(([k, v]) => k.startsWith(`${model}::`) && v >= _RoleRegistry.STRUCTURAL_STRIKES).length;
4163
- if (rolesFailed >= _RoleRegistry.STRUCTURAL_ROLES_BEFORE_BENCH) {
4164
- this.markExhausted(
4165
- model,
4166
- `${reason} (in ${rolesFailed} roles)`,
4167
- Date.now(),
4168
- Date.now() + _RoleRegistry.STRUCTURAL_BENCH_MS
4169
- );
4170
- }
4171
- return n;
4172
- }
4173
- this.markExhausted(model, reason, Date.now(), Date.now() + _RoleRegistry.STRUCTURAL_BENCH_MS);
4174
- return n;
4175
- }
4176
- /**
4177
- * How many DISTINCT roles must reject a model this way before it is benched outright.
4178
- *
4179
- * Two, because one role can have a prompt that a good model reads badly — and the fitness record already
4180
- * takes it out of that role. A second, unrelated role failing the same way is the first evidence that the
4181
- * model, not the prompt, is the problem.
4182
- */
4183
- static STRUCTURAL_ROLES_BEFORE_BENCH = 2;
4184
- /** Models currently quarantined, with why and when — surfaced to the user and re-probed before an adjust. */
4185
- quarantined() {
4186
- return [...this.quarantine].map(([model, q]) => ({ model, ...q }));
4187
- }
4188
- isQuarantined(model, now = Date.now()) {
4189
- const q = this.quarantine.get(model);
4190
- if (!q) return false;
4191
- if (q.until !== void 0 && now >= q.until) {
4192
- this.quarantine.delete(model);
4193
- this.strikes.clear();
4194
- return false;
4195
- }
4196
- return true;
4197
- }
4198
- /** Put a model back in play (its quota reset, or the user forced it). */
4199
- release(model) {
4200
- this.strikes.delete(model);
4201
- return this.quarantine.delete(model);
4202
- }
4203
- /**
4204
- * Roles whose CURRENT chain still contains `model`. When a model is quarantined these are the roles that
4205
- * would otherwise keep resolving to a dead chain, so they are exactly the ones to re-assign.
4206
- */
4207
- rolesUsing(model) {
4208
- return this.names().filter((r) => this.rawChain(r).includes(model));
4209
- }
4210
- /**
4211
- * The role's chain BEFORE quarantine filtering — what was actually assigned to it.
4212
- *
4213
- * The order is the documented one and nothing precedes it: per-role override, then the session model, then
4214
- * the config. It used to bail on an empty CONFIG chain before either override was consulted, which made a
4215
- * role the config had never heard of impossible to assign — the one case where assigning is the whole
4216
- * point. Measured with `tester`, added in a version the user's config predated: `/roles adjust` set the
4217
- * override, the override was skipped, and the role stayed broken for the rest of the session while the
4218
- * error message recommended running `/roles adjust`.
4219
- */
4220
- rawChain(roleName) {
4221
- const perRole = this.roleOverrides.get(roleName);
4222
- if (perRole && perRole.length) return perRole;
4223
- if (this.modelOverride && roleName !== "refiner") return [this.modelOverride];
4224
- return this.roles[roleName]?.models ?? [];
4225
- }
4226
- /** True when every model assigned to this role is quarantined — the chain has collapsed and needs replacing. */
4227
- chainCollapsed(roleName) {
4228
- const raw = this.rawChain(roleName);
4229
- return raw.length > 0 && raw.every((m) => this.isQuarantined(m));
4230
- }
4231
- /** The full model chain for a role by priority: per-role override → global override (non-refiner) → config. */
4232
- chain(roleName) {
4233
- const base = this.rawChain(roleName);
4234
- if (!base.length) return [];
4235
- const live = base.filter((m) => !this.isQuarantined(m));
4236
- const usable = live.length ? live : base;
4237
- const fit = this.fitness ? usable.filter((m) => !this.fitness.unfit(roleName, m)) : usable;
4238
- return fit.length ? fit : usable;
4239
- }
4240
- /**
4241
- * The role's chain ROTATED by `slot`. Parallel workers share one role — five implementers in a wave are all
4242
- * `coder` — so every one of them resolved to the same chain head and hammered a single subscription until it
4243
- * rate-limited. Rotating gives each worker a different lead model while keeping its FULL fallback set, so
4244
- * spreading the load costs no resilience.
4245
- */
4246
- chainFor(roleName, slot = 0) {
4247
- const c = this.chain(roleName);
4248
- if (c.length < 2) return c;
4249
- const order = [];
4250
- for (const m of c) {
4251
- const s = sourceOf(m);
4252
- if (!order.includes(s)) order.push(s);
4253
- }
4254
- const cycle = weightedCycle(order, this.sourceWeights?.() ?? {});
4255
- if (cycle.length) {
4256
- const want = cycle[(slot % cycle.length + cycle.length) % cycle.length];
4257
- const i = c.findIndex((m) => sourceOf(m) === want);
4258
- if (i > 0) return [c[i], ...c.filter((_, j) => j !== i)];
4259
- if (i === 0) return c;
4260
- }
4261
- const k = (slot % c.length + c.length) % c.length;
4262
- return k === 0 ? c : [...c.slice(k), ...c.slice(0, k)];
4263
- }
4264
- /** How many accounts each source has connected — set at the composition root; equal weights without it. */
4265
- sourceWeights;
4266
- /** Wire the account weights (called once the pool exists). */
4267
- setSourceWeights(fn) {
4268
- this.sourceWeights = fn;
4269
- }
4270
- /** The model a role would use next (chain head), for UI display only. */
4271
- peekModel(roleName) {
4272
- return this.chain(roleName)[0] ?? "";
4273
- }
4274
- /**
4275
- * The chain (primary + fallbacks) and session-fallback hooks for a role, WITHOUT its system prompt —
4276
- * for callers that supply their own prompt (e.g. spec-kit phases). resolve() layers the prompt on top.
4277
- */
4278
- fallbackOpts(roleName) {
4279
- const chain = this.chain(roleName);
4280
- const notify = this.notify;
4281
- const effort = this.effortOverrides.get(roleName) ?? this.roles[roleName]?.effort;
4282
- return {
4283
- // Travels with the chain, not with the prompt: the seven callers that take only the chain are exactly
4284
- // the ones whose work is heaviest (the tester, the analyst, the spec-kit phases).
4285
- ...effort ? { effort } : {},
4286
- /**
4287
- * The name belongs to the CHAIN, not to the prompt.
4288
- *
4289
- * It was put on `resolve()` alone, on the assumption that every caller spreads a resolved role. Seven
4290
- * do not: they take the chain from here and supply their own prompt (the spec-kit phases, the tester,
4291
- * the analyst, the question normalizer). Measured on the first run after the change — 19 tool calls,
4292
- * one of them attributed. Everything the attribution was for happens in those seven.
4293
- */
4294
- role: roleName,
4295
- model: chain[0] ?? "",
4296
- fallbacks: chain.slice(1),
4297
- onExhausted: (m, reason) => {
4298
- const why = reason ?? "unavailable";
4299
- const source = providerOutage(why) ?? (isSourceCapacity(why) ? sourcePrefix(m) : void 0);
4300
- if (source) this.markProviderExhausted(source, m, why);
4301
- else this.markExhausted(m, why);
4302
- },
4303
- onStructuralFailure: (m, reason) => this.markStructuralFailure(m, reason, roleName),
4304
- onFallback: notify ? (from, to, reason) => notify(`\u2935 \`${from}\` \u2192 \`${to}\` \u2014 ${reason}`) : void 0
4305
- };
4306
- }
4307
- /** The skills already attached to a role — what task-level routing must not inline a second time. */
4308
- skillsFor(roleName) {
4309
- return this.roles[roleName]?.skills ?? [];
4310
- }
4311
- resolve(roleName) {
4312
- const role = this.roles[roleName];
4313
- if (!role) throw new Error(`undefined role: ${roleName}`);
4314
- if (!this.rawChain(roleName).length) {
4315
- throw new Error(
4316
- `role '${roleName}' has no model defined \u2014 set one with \`/roles setmodel\`, run \`/roles adjust\`, or choose a session model with \`/model\`.`
4317
- );
4318
- }
4319
- let systemPrompt = role.systemPrompt ?? this.defaultPrompts[roleName];
4320
- if (systemPrompt === void 0) throw new Error(`role '${roleName}' has no systemPrompt`);
4321
- if (this.skillRegistry) {
4322
- try {
4323
- systemPrompt = applySkills(systemPrompt, role.skills ?? [], this.skillRegistry);
4324
- } catch (e) {
4325
- throw new Error(`role '${roleName}' skill error: ${e instanceof Error ? e.message : String(e)}`);
4326
- }
4327
- }
4328
- return { ...this.fallbackOpts(roleName), systemPrompt: systemPrompt + this.ruleSuffix() };
4329
- }
4330
- };
4331
-
4332
- // src/engine/review.ts
3429
+ import { z as z16 } from "zod";
4333
3430
  function asChoice(o) {
4334
3431
  return typeof o === "string" ? { label: o } : o;
4335
3432
  }
4336
- var AssessmentSchema = z17.object({
4337
- findings: z17.array(z17.object({
4338
- 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(
4339
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."
4340
3437
  ),
4341
- note: z17.string()
3438
+ note: z16.string()
4342
3439
  })).default([]),
4343
- recommendation: z17.enum(["approve", "revise"]).describe(
3440
+ recommendation: z16.enum(["approve", "revise"]).describe(
4344
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."
4345
3442
  )
4346
3443
  });
@@ -4381,18 +3478,18 @@ function coverage(assessments) {
4381
3478
  const verified = assessments.length - unverified;
4382
3479
  return { verified, unverified, enough: !assessments.length || verified / assessments.length >= TEAM_MIN_COVERAGE };
4383
3480
  }
4384
- var CouncilVoteSchema = z17.object({
4385
- vote: z17.enum(["pass", "revise"]).describe(
3481
+ var CouncilVoteSchema = z16.object({
3482
+ vote: z16.enum(["pass", "revise"]).describe(
4386
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."
4387
3484
  ),
4388
- rationale: z17.string()
3485
+ rationale: z16.string()
4389
3486
  });
4390
- var JudgeSchema = z17.object({
4391
- decision: z17.enum(["pass", "revise", "ask-human"]).describe(
3487
+ var JudgeSchema = z16.object({
3488
+ decision: z16.enum(["pass", "revise", "ask-human"]).describe(
4392
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."
4393
3490
  ),
4394
- feedback: z17.array(z17.string()),
4395
- question: z17.string()
3491
+ feedback: z16.array(z16.string()),
3492
+ question: z16.string()
4396
3493
  });
4397
3494
  var STAGE_FRAMING = {
4398
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.
@@ -4977,7 +4074,7 @@ async function runCodeReview(deps, workdir, taskTitle, request, emit = () => {
4977
4074
  }
4978
4075
 
4979
4076
  // src/engine/acceptance.ts
4980
- import { z as z18 } from "zod";
4077
+ import { z as z17 } from "zod";
4981
4078
 
4982
4079
  // src/engine/criterion-commands.ts
4983
4080
  import { spawn as spawn2 } from "child_process";
@@ -5013,12 +4110,12 @@ function commandsIn(criterion) {
5013
4110
  }
5014
4111
  async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
5015
4112
  const [bin, ...args] = argv;
5016
- return new Promise((resolve6) => {
4113
+ return new Promise((resolve5) => {
5017
4114
  let child;
5018
4115
  try {
5019
4116
  child = spawn2(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
5020
4117
  } catch (e) {
5021
- 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) });
5022
4119
  return;
5023
4120
  }
5024
4121
  let out = "";
@@ -5035,11 +4132,11 @@ async function runCommand(cwd, argv, timeoutMs = CRITERION_TIMEOUT_MS) {
5035
4132
  }, timeoutMs);
5036
4133
  child.on("error", (e) => {
5037
4134
  clearTimeout(timer);
5038
- resolve6({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
4135
+ resolve5({ argv, passed: false, exitCode: null, timedOut: false, output: e.message });
5039
4136
  });
5040
4137
  child.on("close", (code) => {
5041
4138
  clearTimeout(timer);
5042
- 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) });
5043
4140
  });
5044
4141
  });
5045
4142
  }
@@ -5073,7 +4170,7 @@ ${r.output.slice(-800)}
5073
4170
  }
5074
4171
 
5075
4172
  // src/engine/test-runner.ts
5076
- import { readFile as readFile3 } from "fs/promises";
4173
+ import { readFile as readFile2 } from "fs/promises";
5077
4174
  import { existsSync as existsSync9 } from "fs";
5078
4175
  import { spawn as spawn3 } from "child_process";
5079
4176
  import { join as join9 } from "path";
@@ -5084,7 +4181,7 @@ async function detectTestCommand(cwd) {
5084
4181
  const pkgPath = join9(cwd, "package.json");
5085
4182
  if (existsSync9(pkgPath)) {
5086
4183
  try {
5087
- const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
4184
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
5088
4185
  const script = pkg.scripts?.test;
5089
4186
  if (script && !PLACEHOLDER.test(script)) {
5090
4187
  const runner = existsSync9(join9(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync9(join9(cwd, "yarn.lock")) ? "yarn" : existsSync9(join9(cwd, "bun.lockb")) ? "bun" : "npm";
@@ -5106,7 +4203,7 @@ async function runProjectTests(cwd, cmd) {
5106
4203
  const command = cmd ?? await detectTestCommand(cwd);
5107
4204
  if (!command) return { skipped: true, passed: true, output: "", timedOut: false };
5108
4205
  const [bin, ...args] = command.argv;
5109
- return new Promise((resolve6) => {
4206
+ return new Promise((resolve5) => {
5110
4207
  const child = spawn3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, CI: "1" } });
5111
4208
  let out = "";
5112
4209
  const take = (d) => {
@@ -5122,7 +4219,7 @@ async function runProjectTests(cwd, cmd) {
5122
4219
  }, TEST_TIMEOUT_MS);
5123
4220
  const done = (code, extra = "") => {
5124
4221
  clearTimeout(timer);
5125
- resolve6({
4222
+ resolve5({
5126
4223
  skipped: false,
5127
4224
  passed: code === 0 && !timedOut,
5128
4225
  command: command.argv.join(" "),
@@ -5133,7 +4230,7 @@ async function runProjectTests(cwd, cmd) {
5133
4230
  };
5134
4231
  child.on("error", (e) => {
5135
4232
  clearTimeout(timer);
5136
- resolve6({
4233
+ resolve5({
5137
4234
  skipped: true,
5138
4235
  passed: true,
5139
4236
  command: command.argv.join(" "),
@@ -5164,13 +4261,13 @@ ${run.output}`;
5164
4261
  function normalizeCriterion(s) {
5165
4262
  return s.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().replace(/[.,;:!?…]+$/, "");
5166
4263
  }
5167
- var AcceptanceSchema = z18.object({
5168
- checks: z18.array(z18.object({
5169
- criterion: z18.string().describe(
4264
+ var AcceptanceSchema = z17.object({
4265
+ checks: z17.array(z17.object({
4266
+ criterion: z17.string().describe(
5170
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."
5171
4268
  ),
5172
- met: z18.boolean(),
5173
- evidence: z18.string().describe(
4269
+ met: z17.boolean(),
4270
+ evidence: z17.string().describe(
5174
4271
  'Where you SAW it: a file path and what it contains, a symbol, a test name. "It looks fine" is not evidence.'
5175
4272
  )
5176
4273
  }))
@@ -5418,31 +4515,31 @@ async function runTaskCycle(deps, board, taskId, worktreePath, slot = 0) {
5418
4515
  }
5419
4516
 
5420
4517
  // src/board/board.ts
5421
- import { z as z19 } from "zod";
4518
+ import { z as z18 } from "zod";
5422
4519
  var MAX_STAGE_EVENTS = 200;
5423
- var stageEventSchema = z19.object({
5424
- role: z19.string(),
5425
- action: z19.string(),
5426
- note: z19.string().optional()
4520
+ var stageEventSchema = z18.object({
4521
+ role: z18.string(),
4522
+ action: z18.string(),
4523
+ note: z18.string().optional()
5427
4524
  });
5428
- var cardSchema = z19.object({
5429
- id: z19.string(),
5430
- title: z19.string(),
5431
- column: z19.enum(["TODO", "IN-PROGRESS", "REVIEW", "DONE", "MERGED", "PARKED", "ABANDONED"]),
5432
- worktree: z19.string().optional(),
5433
- deps: z19.array(z19.string()),
5434
- acceptance: z19.array(z19.string()).default([]),
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([]),
5435
4532
  // default: boards persisted before the gate existed still load
5436
- files: z19.array(z19.string()).default([]),
4533
+ files: z18.array(z18.string()).default([]),
5437
4534
  // ditto — a board written before file lists existed still loads
5438
- reviewNotes: z19.array(z19.string()),
4535
+ reviewNotes: z18.array(z18.string()),
5439
4536
  // Optional rather than defaulted: a board written before this existed must round-trip unchanged, and an
5440
4537
  // empty list is the same statement as no list at all.
5441
- clearedLenses: z19.array(z19.string()).optional(),
5442
- attempts: z19.number(),
5443
- stageHistory: z19.array(stageEventSchema)
4538
+ clearedLenses: z18.array(z18.string()).optional(),
4539
+ attempts: z18.number(),
4540
+ stageHistory: z18.array(stageEventSchema)
5444
4541
  });
5445
- 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) });
5446
4543
  function migrateDelivered(c) {
5447
4544
  if (c.column !== "DONE") return c;
5448
4545
  return c.stageHistory.some((e) => e.action === "merged") ? { ...c, column: "MERGED" } : c;
@@ -5612,39 +4709,12 @@ var Board = class _Board {
5612
4709
  export {
5613
4710
  SHORT_CALL_MS,
5614
4711
  LONG_CALL_MS,
5615
- ZAI_MODELS,
5616
- modelsFor,
5617
- cliCatalog,
5618
- cliFor,
5619
4712
  CliProvider,
5620
4713
  describeInherited,
5621
4714
  describeTopUp,
5622
4715
  toSlug,
5623
4716
  mainWorktreeRoot,
5624
4717
  WorktreeManager,
5625
- REQUIRED_ROLES,
5626
- DEFAULT_ROLE_SKILLS,
5627
- DEFAULT_PROMPTS,
5628
- SPEC_TEAM,
5629
- PLAN_TEAM,
5630
- CODE_TEAM,
5631
- DEFAULT_COUNCIL,
5632
- placedSkills,
5633
- ROLE_PROFILES,
5634
- filterModelsForRole,
5635
- effortFor,
5636
- isKnownModel,
5637
- capabilityScore,
5638
- mostCapable,
5639
- modelBand,
5640
- DURABLE_ROLES,
5641
- strongestPrimary,
5642
- newestPrimary,
5643
- sourceOf,
5644
- adjustRoleModels,
5645
- applySkills,
5646
- buildSkillTool,
5647
- RoleRegistry,
5648
4718
  unfinishedSessions,
5649
4719
  describeUnfinished,
5650
4720
  gitTool,