leos-agent 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/adapters/cursor/agents/executor.md +17 -0
  2. package/adapters/cursor/agents/expert.md +70 -0
  3. package/adapters/cursor/agents/explore.md +16 -0
  4. package/adapters/cursor/agents/implementer.md +18 -0
  5. package/adapters/cursor/agents/investigator.md +18 -0
  6. package/adapters/cursor/agents/planner.md +28 -0
  7. package/adapters/cursor/agents/reviewer.md +33 -0
  8. package/adapters/opencode/agents.json +66 -0
  9. package/adapters/opencode/plugin.js +186 -0
  10. package/config/models.json +62 -0
  11. package/hooks/bash-guard.py +541 -0
  12. package/hooks/cursor-guard.py +84 -0
  13. package/hooks/hooks-cursor.json +11 -0
  14. package/hooks/hooks.json +20 -0
  15. package/hooks/session-start.py +121 -0
  16. package/package.json +16 -0
  17. package/roles/executor.md +15 -0
  18. package/roles/expert.md +67 -0
  19. package/roles/explore.md +13 -0
  20. package/roles/implementer.md +16 -0
  21. package/roles/investigator.md +15 -0
  22. package/roles/planner.md +25 -0
  23. package/roles/reviewer.md +30 -0
  24. package/scripts/render_adapters.py +326 -0
  25. package/scripts/state.py +127 -0
  26. package/settings.json +7 -0
  27. package/skills/.gitkeep +0 -0
  28. package/skills/brainstorming/SKILL.md +109 -0
  29. package/skills/debugging/SKILL.md +98 -0
  30. package/skills/delegation/SKILL.md +141 -0
  31. package/skills/executing-plans/SKILL.md +116 -0
  32. package/skills/finishing-a-branch/SKILL.md +123 -0
  33. package/skills/test-first/SKILL.md +90 -0
  34. package/skills/using-leo/SKILL.md +89 -0
  35. package/skills/using-leo/references/claude-mapping.md +11 -0
  36. package/skills/using-leo/references/codex-mapping.md +24 -0
  37. package/skills/using-leo/references/cursor-mapping.md +22 -0
  38. package/skills/using-leo/references/hermes-mapping.md +26 -0
  39. package/skills/using-leo/references/opencode-mapping.md +28 -0
  40. package/skills/verification/SKILL.md +102 -0
  41. package/skills/worktrees/SKILL.md +129 -0
  42. package/skills/writing-plans/SKILL.md +96 -0
  43. package/workflows/cost-tiered-fix.js +259 -0
@@ -0,0 +1,28 @@
1
+ <!-- Generated by scripts/render_adapters.py; do not edit. -->
2
+ # OpenCode mapping
3
+
4
+ Provider: `openrouter`
5
+
6
+ | Tier | Model | Effort |
7
+ |---|---|---|
8
+ | Fable | `moonshotai/kimi-k3` | native default |
9
+ | Opus | `moonshotai/kimi-k3` | native default |
10
+ | Sonnet | `z-ai/glm-5.2` | native default |
11
+ | Haiku | `z-ai/glm-5.2` | native default |
12
+
13
+ Roles register as native OpenCode agents (from `adapters/opencode/agents.json`, generated from `config/models.json` and `roles/*.md`) and are spawned via the task tool as subagents. There is no per-spawn model override on this harness: each agent always runs its registered model, so `reviewer` always runs the full Opus-tier model — the trivial-diff Sonnet-tier downscale does not apply here; every diff gets the full review.
14
+
15
+ Read-only is harness-enforced here, unlike Codex and Cursor: read-only roles carry a generated `permission.edit: deny`, so an off-policy write attempt is refused by OpenCode itself, not merely discouraged by the prompt. Write-capable agents additionally carry coarse `rm -rf` bash denies as a stopgap for opencode#5894 (unconfirmed whether `tool.execute.before` also intercepts subagent bash); the precise tripwire stays `hooks/bash-guard.py` on the primary agent.
16
+
17
+ No `EnterWorktree` tool exists here — use leo:worktrees' raw `git worktree` fallback for isolated branch work. State reads and writes go through `python3 <plugin-root>/scripts/state.py` (`get` / `merge` / `path`), same contract as every other harness. There is no Workflow tool and no `cost-tiered-fix.js` here — a batch of independent tasks is fanned out as manual parallel task-tool subagent spawns instead.
18
+
19
+ Tier collapse here: Fable≡Opus (`moonshotai/kimi-k3`), Sonnet≡Haiku (`z-ai/glm-5.2`) — routing between collapsed rungs buys role, not power. Fable is not a real rung: `expert` cannot break a deadlock a collapsed Opus already lost, so cap escalation at Opus and report.
20
+
21
+ ## Leo skills not available here
22
+
23
+ - `leo:attach-pr` — its entire purpose is Claude Code Desktop's PR-card detector, which no other harness has, and it reaches its resolver through a Claude-only skill-directory placeholder.
24
+ - `leo:resolve-ticket` — needs plugin-path placeholders, a pinned Claude model, and Claude-only subagent, worktree, and question tools.
25
+ - `leo:review-pr` — its whole mechanism is a script reached through a Claude-only skill-directory placeholder, plus a pinned Claude model.
26
+ - `leo:watch-review` — pinned Claude model, and it drives review-pr through Claude Code's own skill-invocation tool.
27
+
28
+ Every other skill in the policy's Skill index is registered here and behaves the same. Reviewing a pull request on this harness means running the canonical reviewer role prompt against the diff by hand.
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: verification
3
+ description: >
4
+ Fresh-evidence gate before claiming done, fixed, or passing. Applies to
5
+ the main loop, implementer, executor, and anyone reporting completion:
6
+ a completion claim needs a proving command run in the current turn, whose
7
+ output was actually read — never a prior run, a "should pass now," or a
8
+ subagent's self-report relayed as fact.
9
+ when_to_use: >
10
+ Before writing any completion claim — "tests pass," "build is green,"
11
+ "bug fixed," "agent finished the task." Also applies when relaying a
12
+ subagent's own "done"/"success" report up the chain. NOT for routine
13
+ in-progress status updates that make no completion claim, and NOT a
14
+ substitute for the review phase itself — this gates the evidence behind
15
+ it, execute-then-review still owns the verdict.
16
+ ---
17
+
18
+ # verification
19
+
20
+ Core rule: no completion claim without a proving command run fresh, this
21
+ turn, with output actually read. A claim resting on memory, a prior run, or
22
+ someone else's word is not verification — it is a guess wearing the shape
23
+ of one.
24
+
25
+ ## When it fires
26
+
27
+ Any sentence of the form "X passes," "X is fixed," "X works now," "agent Y
28
+ finished." That sentence is a claim. A claim needs proof, and proof has an
29
+ expiration: the moment code changes again, prior proof is stale.
30
+
31
+ Does not fire for: status updates that don't assert completion ("still
32
+ running," "found the bug, fixing now"), or work that genuinely has no
33
+ runtime surface (docs/comment-only diffs — see the execute-then-review
34
+ exemptions).
35
+
36
+ ## The discipline
37
+
38
+ 1. **Name the command that would falsify the claim.** Not "does it look
39
+ right" — the specific test/build/repro that fails if the claim is false.
40
+ If no such command exists, the claim isn't verifiable yet; say so instead
41
+ of asserting it.
42
+ 2. **Run it fresh, this turn.** A green run from three edits ago proves
43
+ nothing about the code as it stands now.
44
+ 3. **Read the exit status and failure counts.** Not the last line of
45
+ scrollback, not a summary a subagent wrote — the actual output.
46
+ 4. **Only then claim, and state the evidence** — the command and what it
47
+ returned, not just "verified."
48
+
49
+ Skipping step 1 is how "should be fine" sneaks in. Skipping step 2 is how a
50
+ stale green run gets relayed as current. Skipping step 3 is how a nonzero
51
+ exit gets read as success because the output scrolled by fast.
52
+
53
+ ## Claim → proof
54
+
55
+ | Claim | Falsifying command |
56
+ |---|---|
57
+ | Tests pass | the actual test command, run now, exit code + failure count read |
58
+ | Build is green | the build command, run now, read for errors/warnings |
59
+ | Bug is fixed | the reproducer that showed the bug — now green, run fresh |
60
+ | Agent reports done | its diff and output, inspected directly — its "success" is a claim, not evidence |
61
+
62
+ ## Subagent reports are claims, not evidence
63
+
64
+ A subagent saying "success," "all tests pass," or "implemented as
65
+ requested" is exactly as unverified as your own untested assertion would be
66
+ — it is one more claim to check against artifacts. Inspect the diff it
67
+ produced. Run the check it says it ran. If it reports a test command, that
68
+ command's output belongs in your evidence, not its summary of the output.
69
+ Relaying a subagent's self-report upward without this check just moves the
70
+ gap in provenance one level up the chain.
71
+
72
+ ## Done is the three-line report
73
+
74
+ Writing code is not done. Done is the execute-then-review report:
75
+
76
+ - **what changed** — the diff, in one line
77
+ - **checks run** — the fresh commands from this gate, with results
78
+ - **review verdict** — clean per the execute-then-review policy, not
79
+ self-assessed
80
+
81
+ Verification is the evidence behind lines two and three. A report with line
82
+ one but not two and three is a status update, not a completion claim — label
83
+ it as such.
84
+
85
+ ## Self-talk to catch
86
+
87
+ - "It should pass now, I fixed the obvious thing" — run it.
88
+ - "The tests were green before this edit" — before this edit is not now.
89
+ - "The subagent said it's done" — done according to whom, checked how.
90
+ - "I read the code and it looks correct" — reading is not running.
91
+ - "Re-running is wasteful, nothing changed" — if nothing changed, the prior
92
+ run is fine to cite as fresh evidence; if anything did, it isn't.
93
+
94
+ ## Works with
95
+
96
+ - The execute-then-review policy (injected leo:using-leo) — that gate is the
97
+ outer loop this skill feeds into; the reviewer subagent judges the diff,
98
+ this skill governs the evidence claimed leading up to that judgment.
99
+ - reviewer — its verdict is itself a claim to relay accurately, not to
100
+ soften or summarize away.
101
+ - End-to-end exercise — when the falsifying command is "does the real flow
102
+ work," drive the actual app or flow, not just the test suite.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: worktrees
3
+ description: >
4
+ Worktree lifecycle mechanics for isolated branch work — detect, create,
5
+ and clean up a git worktree so implementation happens off the main
6
+ checkout. Shared by resolve-ticket, executing-plans, and delegation
7
+ fan-outs; not itself a workflow, just the plumbing they all call into.
8
+ when_to_use: >
9
+ Any skill or agent about to create or tear down a worktree for isolated
10
+ branch work. NOT for choosing whether isolation is needed in the first
11
+ place (that call belongs to the calling skill's plan/gate step) and NOT
12
+ for merging or cleaning up a finished branch's remnants after the PR
13
+ lands — that's leo:finishing-a-branch.
14
+ ---
15
+
16
+ # Worktrees
17
+
18
+ Core rule: **detect existing isolation before creating anything, and never
19
+ remove a worktree from inside it.**
20
+
21
+ ## When this fires
22
+
23
+ A calling skill has already decided it wants isolated branch work (a plan
24
+ was approved, a fan-out item needs its own tree) and needs the mechanics:
25
+ enter, verify, exit, clean up. This skill doesn't decide *whether* to
26
+ isolate — that's upstream. It also doesn't cover post-merge branch
27
+ deletion or remote cleanup; once the PR lands, hand off to
28
+ leo:finishing-a-branch.
29
+
30
+ ## Procedure
31
+
32
+ ### 1. Detect existing isolation first
33
+
34
+ Before creating anything, check whether the session is already inside a
35
+ worktree:
36
+
37
+ ```
38
+ git rev-parse --git-common-dir
39
+ git rev-parse --git-dir
40
+ ```
41
+
42
+ If they differ, the current checkout **is already a worktree** — the
43
+ session's own isolation. Never nest a worktree inside a worktree: do the
44
+ work here, or exit to the main checkout first if a *different* branch
45
+ needs its own tree. Nesting produces a git state no cleanup step can
46
+ untangle cleanly.
47
+
48
+ ### 2. Prefer the native tools
49
+
50
+ `EnterWorktree` / `ExitWorktree` are harness-managed: they track which
51
+ worktree belongs to which session and auto-clean on exit. Default to them.
52
+
53
+ ### 3. Raw-git fallback, only when the native tools are unavailable
54
+
55
+ Fixed location convention: `.claude/worktrees/<name>`. Before creating,
56
+ verify the location is actually ignored:
57
+
58
+ ```
59
+ git check-ignore .claude/worktrees/<name>
60
+ ```
61
+
62
+ No output (or a non-zero exit) means it isn't ignored — stop and fix
63
+ `.gitignore` first. A worktree directory that git tracks will fight every
64
+ subsequent commit in the main checkout. Only after `check-ignore` confirms
65
+ it, run:
66
+
67
+ ```
68
+ git worktree add -b <branch> .claude/worktrees/<name> <base-ref>
69
+ ```
70
+
71
+ ### 4. Cleanup is provenance-gated
72
+
73
+ Before removing any worktree, establish who created it:
74
+
75
+ - Path under `.claude/worktrees/<name>` (the convention dir) **and** this
76
+ system created it → safe to remove.
77
+ - Created via `EnterWorktree` → belongs to its `ExitWorktree`, not to raw
78
+ `git worktree remove`. Use the matching exit tool; don't hand-remove a
79
+ harness-tracked worktree, it loses the session-tracking state.
80
+ - Anything else — a path outside the convention dir, or one this system
81
+ didn't create — is the user's. Leave it alone; report it, don't touch it.
82
+
83
+ Provenance is the only gate. A worktree existing and looking abandoned is
84
+ not permission to remove it; confirm it's one this system made via the
85
+ convention path (or the matching Enter/Exit pairing) before it goes.
86
+
87
+ ### 5. Never remove a worktree from inside it
88
+
89
+ `cd` to the main checkout first — removing a worktree while it's the
90
+ current working directory leaves git in a state that needs manual repair.
91
+ Sequence:
92
+
93
+ ```
94
+ cd <main-checkout>
95
+ git worktree remove .claude/worktrees/<name>
96
+ git worktree prune
97
+ ```
98
+
99
+ `ExitWorktree` handles this ordering itself when used; the manual sequence
100
+ above is only for the raw-git fallback path.
101
+
102
+ ## Live config repo caveat
103
+
104
+ In Leo's own setup, files under this repo (`~/.leos-agent`) may be wired
105
+ into the running environment via symlinks or hooks — editing them in place
106
+ can break the very session doing the editing. Restructuring work on this
107
+ repo happens in a worktree so the live tree stays intact while the change
108
+ is built and reviewed. This skill's own file was written that way: this
109
+ migration is the example, not a hypothetical.
110
+
111
+ ## Self-talk to catch
112
+
113
+ - "It's probably fine to reuse the current checkout" — check
114
+ `--git-common-dir` vs `--git-dir` first; don't guess from vibes.
115
+ - "This worktree looks stale, I'll just remove it" — stale isn't
116
+ provenance. Confirm the convention path or the Enter/Exit pairing.
117
+ - "I'm already in the worktree, `git worktree remove .` should work" —
118
+ never remove a worktree from inside it; cd out first.
119
+ - "Skipping check-ignore, the convention dir is obviously gitignored" —
120
+ verify it every time; a missing `.gitignore` entry silently breaks the
121
+ main checkout's commits.
122
+
123
+ ## Works with
124
+
125
+ - resolve-ticket — Step 5 (Worktree) calls this for enter, Step 8 (Ship)
126
+ calls this for exit.
127
+ - executing-plans — isolates plan execution the same way.
128
+ - leo:finishing-a-branch — post-merge cleanup once the PR lands; out of
129
+ scope here.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: writing-plans
3
+ description: >
4
+ Quality bar for plans produced by the planner agent or in plan mode. A
5
+ plan is done when a Sonnet implementer can execute it without making a
6
+ single design decision — every step names exact files, shows literal
7
+ code or commands, and states how to verify it, anchored to a recorded
8
+ base ref.
9
+ when_to_use: >
10
+ Writing or reviewing a plan before handoff to leo:executing-plans —
11
+ planner-agent output, plan-mode output, or any multi-step change spec.
12
+ NOT for choosing the approach itself (that's leo:brainstorming) and NOT
13
+ for the implementation or review phases that consume the plan.
14
+ ---
15
+
16
+ # writing-plans
17
+
18
+ Core rule: a plan is done when a Sonnet implementer can execute it without
19
+ making a single design decision. If executing the plan requires judgment
20
+ calls, the plan isn't finished — it's a to-do list wearing a plan's clothes.
21
+
22
+ ## When this fires
23
+
24
+ Any time a plan is about to be handed off for execution: planner-agent
25
+ output before leo:executing-plans picks it up, plan-mode output before
26
+ approval, or a plan Leo asks you to review. Not for the design discussion
27
+ that precedes the plan — an unsettled approach means back up to
28
+ leo:brainstorming (rule 5 below), not push forward into more plan detail.
29
+
30
+ ## The five load-bearing rules
31
+
32
+ 1. **Exact files, literal code, stated verification.** Every step names the
33
+ file(s) it touches, shows the literal code or command to write/run — not
34
+ a description of what the code should do — and states how to verify the
35
+ step worked (a command, a test name, an expected output). "Add error
36
+ handling to the parser" is not a step. "In `src/parser.py`, wrap the
37
+ `json.loads(raw)` call on line 42 in a `try/except json.JSONDecodeError`
38
+ that raises `ParseError(f\"bad payload: {raw[:80]}\")`; verify with
39
+ `pytest tests/test_parser.py::test_malformed_json`" is a step.
40
+
41
+ 2. **Base ref in the header.** The plan header records the base ref —
42
+ `git rev-parse HEAD`, or literally "uncommitted working tree" if the
43
+ plan starts from dirty state. Without a shared base ref, implementer and
44
+ reviewer are diffing against different worlds and neither's output
45
+ means anything to the other.
46
+
47
+ 3. **No placeholders.** "TBD", "TODO", "handle edge cases", "add
48
+ validation", "similar to step N" are plan failures, not acceptable
49
+ shorthand — fix them before handoff, not during execution. A
50
+ placeholder in a plan just moves the design decision onto whichever
51
+ Sonnet implementer hits it first, which is exactly the failure mode
52
+ this skill exists to prevent. "Similar to step N" is the sneakiest
53
+ form: it looks concrete but hides a judgment call about what actually
54
+ differs — write the step out.
55
+
56
+ 4. **Steps sized to a reviewable boundary.** Each step should be the
57
+ smallest unit a reviewer could accept or reject on its own — one file's
58
+ worth of change, one migration, one function. Bundle unrelated changes
59
+ into a single step and the reviewer either rubber-stamps the whole
60
+ thing or blocks all of it over one bad line. If a step needs "and
61
+ also" to describe, it's two steps.
62
+
63
+ 5. **Unsettled approach → back up.** If writing the plan surfaces a real
64
+ design fork ("could go with polling or webhooks here") that the plan
65
+ author is resolving on the fly, stop — that's not plan-writing, that's
66
+ design happening inside a document meant to record decisions already
67
+ made. Route to leo:brainstorming to settle the approach, then come back
68
+ and write the plan. A plan for an unchosen design is waste: the
69
+ implementer either can't proceed or silently picks for you, and now
70
+ the review is judging a decision nobody signed off on.
71
+
72
+ ## Self-talk to catch
73
+
74
+ - "The implementer will know what I mean" — no placeholder survives
75
+ contact with a different model on a different day; write the literal
76
+ code.
77
+ - "This step is basically step 3 again" — then step 3's text belongs
78
+ here too, verbatim or adapted; "similar to step 3" is a placeholder.
79
+ - "I'll figure out the base ref when review starts" — the header needs it
80
+ now, or implementer and reviewer silently diff against different trees.
81
+ - "It's obviously going to be small, I don't need to size the step" —
82
+ size it anyway; "obviously small" is exactly the case where a bundled
83
+ step slips a real decision past review.
84
+ - "I'm not 100% sure webhooks vs. polling, I'll note it as a decision
85
+ point in the plan" — a decision point in a plan is a design fork that
86
+ belongs in leo:brainstorming, not a step for the implementer to guess
87
+ at.
88
+
89
+ ## Works with
90
+
91
+ - leo:brainstorming — resolve the approach before a plan gets written for it.
92
+ - leo:executing-plans — consumes a plan that passes this bar; if it can't
93
+ find exact files/commands or hits a placeholder, the plan should have
94
+ failed this checklist.
95
+ - reviewer — judges the diff the plan produced, using the same base ref
96
+ the plan recorded.
@@ -0,0 +1,259 @@
1
+ export const meta = {
2
+ name: 'cost-tiered-fix',
3
+ description: 'Fix a batch of independent tasks with tiered models: Opus plans and verifies, Haiku/Sonnet execute, low-confidence items escalate to Opus',
4
+ whenToUse: 'A list of independent, well-scoped fixes (many tickets, many files) — NOT one large stateful change, which belongs in a normal session with subagents',
5
+ phases: [
6
+ { title: 'Plan', detail: 'decompose the goal into tiered work items', model: 'opus' },
7
+ { title: 'Execute', detail: 'cheap executors, one isolated worktree per item' },
8
+ { title: 'Verify', detail: 'Opus reviews each branch diff', model: 'opus' },
9
+ ],
10
+ }
11
+
12
+ // Invoke with either:
13
+ // args: { goal: "...", runId?: "..." } -> Opus plans the decomposition
14
+ // args: { tasks: ["...", { task, tier }], runId?: "..." } -> skip planning, run your list
15
+ // runId (e.g. a ticket id or date string) namespaces branch names across runs;
16
+ // Date.now()/Math.random() are unavailable in workflow scripts, so it must come
17
+ // from the caller. Without it, executors resolve collisions by numeric suffix.
18
+ // Each work item ends up as a committed branch plus an Opus verdict.
19
+ // Merging approved branches is left to the main session.
20
+ //
21
+ // args.tiers optionally remaps the three rungs, e.g.
22
+ // { tiers: { cheap: 'haiku', normal: 'sonnet', judge: 'opus' } }
23
+ // Workflow scripts have no filesystem access, so the canonical matrix in
24
+ // config/models.json cannot be read here — the caller passes it through when
25
+ // this machine's mapping differs from the defaults below.
26
+
27
+ if (!args || (!args.goal && !Array.isArray(args.tasks))) {
28
+ throw new Error('cost-tiered-fix needs args: { goal: "..." } or { tasks: [...] }')
29
+ }
30
+
31
+ const BRANCH_PREFIX = args.runId ? `leos/fix-${args.runId}` : 'leos/fix'
32
+
33
+ // Bare model aliases only — this is the documented subagent `model:` shape.
34
+ // The 1m-extended-context suffix (square-bracket /model syntax) is a
35
+ // /model-command / SKILL-frontmatter thing, not a valid subagent model
36
+ // value; passing it here reaches the model selector verbatim and kills the
37
+ // spawn (see the outage this repo just had). Do not add that suffix back.
38
+ const TIERS = {
39
+ cheap: 'haiku',
40
+ normal: 'sonnet',
41
+ judge: 'opus',
42
+ ...(args.tiers || {}),
43
+ }
44
+
45
+ const PLAN_SCHEMA = {
46
+ type: 'object',
47
+ properties: {
48
+ items: {
49
+ type: 'array',
50
+ items: {
51
+ type: 'object',
52
+ properties: {
53
+ task: { type: 'string', description: 'self-contained instruction: exact file paths, expected behavior, how to check it' },
54
+ tier: { type: 'string', enum: [TIERS.cheap, TIERS.normal], description: `${TIERS.cheap} for mechanical work, ${TIERS.normal} for normal implementation` },
55
+ },
56
+ required: ['task', 'tier'],
57
+ },
58
+ },
59
+ },
60
+ required: ['items'],
61
+ }
62
+
63
+ const EXEC_SCHEMA = {
64
+ type: 'object',
65
+ properties: {
66
+ branch: { type: 'string', description: 'the branch actually created and committed to; omit if no branch was created' },
67
+ summary: { type: 'string' },
68
+ checks: { type: 'string', description: 'what was run to verify, and the result' },
69
+ confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
70
+ },
71
+ required: ['summary', 'confidence'],
72
+ }
73
+
74
+ const VERDICT_SCHEMA = {
75
+ type: 'object',
76
+ properties: {
77
+ approved: { type: 'boolean' },
78
+ issues: { type: 'array', items: { type: 'string' } },
79
+ },
80
+ required: ['approved', 'issues'],
81
+ }
82
+
83
+ function execPrompt(task, branch) {
84
+ return [
85
+ 'You are one executor in a fan-out. Work ONLY on this task; touch nothing else.',
86
+ `Task: ${task}`,
87
+ `You are in an isolated git worktree. Create and switch to branch ${branch} (if that name already exists, use the lowest free numeric suffix, e.g. ${branch}-2), implement the task, run the narrowest relevant check (tests/build for the touched files), and commit.`,
88
+ 'Report the branch name you actually used. If the task is ambiguous or you cannot make the check pass, commit only what is safe and report confidence: low with the blocker in summary. If you created no branch, omit the branch field entirely — never invent one.',
89
+ ].join('\n')
90
+ }
91
+
92
+ // Next tier up the escalation ladder. The judge tier is the ceiling: it has
93
+ // nowhere left to escalate to, so it maps to itself.
94
+ function nextTier(tier) {
95
+ if (tier === TIERS.cheap) return TIERS.normal
96
+ return TIERS.judge
97
+ }
98
+
99
+ // Escalation must buy more than a model swap: the normal rung gets a wider
100
+ // reasoning budget than the cheap rung it is replacing, or a cheap->normal
101
+ // escalation would spend more money for the same effort that already failed.
102
+ function effortFor(tier) {
103
+ return tier === TIERS.judge ? 'high' : tier === TIERS.normal ? 'medium' : 'low'
104
+ }
105
+
106
+ // Caller-supplied args.tasks bypasses the planning agent (and PLAN_SCHEMA's
107
+ // validation with it), so entries need their own gate here: a bad `tier`
108
+ // would flow straight into `model:` below, and a non-object/non-string entry
109
+ // (e.g. null) would silently produce `task: undefined` fed to execPrompt.
110
+ const ALLOWED_TIERS = new Set([TIERS.cheap, TIERS.normal])
111
+ function validateTask(t, i) {
112
+ if (typeof t === 'string') return { task: t, tier: TIERS.normal }
113
+ if (t && typeof t === 'object' && typeof t.task === 'string') {
114
+ if (t.tier !== undefined && !ALLOWED_TIERS.has(t.tier)) {
115
+ throw new Error(`cost-tiered-fix: args.tasks[${i}].tier must be one of ${[...ALLOWED_TIERS].join(', ')}, got ${JSON.stringify(t.tier)}`)
116
+ }
117
+ return { tier: TIERS.normal, ...t }
118
+ }
119
+ throw new Error(`cost-tiered-fix: args.tasks[${i}] must be a string or an object with a string "task", got ${JSON.stringify(t)}`)
120
+ }
121
+
122
+ phase('Plan')
123
+ let items
124
+ if (Array.isArray(args.tasks)) {
125
+ items = args.tasks.map(validateTask)
126
+ log(`Using ${items.length} caller-provided tasks (planning skipped)`)
127
+ } else {
128
+ const plan = await agent(
129
+ `Decompose this goal into independent, well-scoped work items that can each be done in an isolated worktree without touching the same files. For each item write a self-contained instruction (exact file paths, expected behavior, how to check it) and pick a tier: ${TIERS.cheap} for mechanical work, ${TIERS.normal} for normal implementation. At most 10 items — if the goal needs more, return the 10 highest-value and say so in the last item.\n\nGoal: ` + args.goal,
130
+ { label: 'plan', phase: 'Plan', model: TIERS.judge, effort: 'high', schema: PLAN_SCHEMA },
131
+ )
132
+ if (!plan || !Array.isArray(plan.items) || plan.items.length === 0) {
133
+ log('Planning agent failed or returned no items — aborting cleanly')
134
+ return { approved: [], rejected: [], note: 'planning agent died or produced no work items; nothing was run' }
135
+ }
136
+ items = plan.items
137
+ log(`Planned ${items.length} work items`)
138
+ }
139
+ if (items.length > 10) {
140
+ log(`Capping fan-out: running the first 10 of ${items.length} items`)
141
+ items = items.slice(0, 10)
142
+ }
143
+
144
+ // pipeline(): no barrier between stages — item 0 can be verifying while item 3
145
+ // is still executing. Wall-clock is the slowest single item, not the sum.
146
+ const results = await pipeline(
147
+ items,
148
+
149
+ // Stage 1 — execute cheap (haiku/sonnet, effort low: the cost levers)
150
+ (item, _orig, i) =>
151
+ agent(execPrompt(item.task, `${BRANCH_PREFIX}-${i}`), {
152
+ label: `exec-${i}:${item.tier}`,
153
+ phase: 'Execute',
154
+ model: item.tier,
155
+ effort: 'low',
156
+ isolation: 'worktree',
157
+ schema: EXEC_SCHEMA,
158
+ }),
159
+
160
+ // Stage 2 — escalation ladder:
161
+ // - confident result (non-null, confidence !== 'low') -> return as-is, no escalation.
162
+ // - null result -> ONE retry at the same tier (haiku retries at sonnet, since
163
+ // haiku already failed cheap); if that retry is also null/low, ONE escalation to
164
+ // the next tier up.
165
+ // - low-confidence result -> ONE escalation exactly one rung up.
166
+ // Stop at the first confident attempt. Every superseded attempt's branch is
167
+ // collected into supersededBranches so the tail can flag it as an orphan.
168
+ async (run, item, i) => {
169
+ if (run && run.confidence !== 'low') return run
170
+
171
+ const supersededBranches = []
172
+
173
+ async function attempt(tier, suffix, priorSummary) {
174
+ const branch = `${BRANCH_PREFIX}-${i}-${suffix}`
175
+ return agent(
176
+ execPrompt(item.task, branch) +
177
+ `\n\nA cheaper model already attempted this and reported: "${priorSummary}". Start from the task itself on a fresh branch off the same base as mainline — do NOT build on the earlier attempt's branch.`,
178
+ { label: `escalate-${i}-${suffix}`, phase: 'Execute', model: tier, effort: effortFor(tier), isolation: 'worktree', schema: EXEC_SCHEMA },
179
+ )
180
+ }
181
+
182
+ let result
183
+ let finalTier
184
+ if (!run) {
185
+ const retryTier = item.tier === TIERS.cheap ? TIERS.normal : item.tier
186
+ finalTier = retryTier
187
+ log(`Item ${i} produced no result — retrying at ${retryTier}`)
188
+ result = await attempt(retryTier, 'r2', 'no result (agent failed)')
189
+ if (!result || result.confidence === 'low') {
190
+ if (result && result.branch) supersededBranches.push(result.branch)
191
+ const escTier = nextTier(retryTier)
192
+ finalTier = escTier
193
+ log(`Item ${i} still ${result ? 'low confidence' : 'no result'} at ${retryTier} — escalating to ${escTier}`)
194
+ result = await attempt(escTier, 'r3', result ? result.summary : 'no result on retry')
195
+ }
196
+ } else {
197
+ if (run.branch) supersededBranches.push(run.branch)
198
+ const escTier = nextTier(item.tier)
199
+ finalTier = escTier
200
+ log(`Item ${i} low confidence — escalating to ${escTier}`)
201
+ result = await attempt(escTier, 'r2', run.summary)
202
+ }
203
+
204
+ // "escalated" means the work actually moved up a rung. A same-tier retry
205
+ // (a null result at a tier that is already the ceiling) is not one.
206
+ const escalated = finalTier !== item.tier
207
+
208
+ if (!result) {
209
+ // Every attempt failed, but earlier attempts may already have created
210
+ // branches. Returning null here would drop supersededBranches and leave
211
+ // those branches out of the orphan report — invisible litter in the repo.
212
+ return { summary: 'every attempt failed; no usable result', confidence: 'low', supersededBranches, escalated }
213
+ }
214
+ return { ...result, supersededBranches, escalated }
215
+ },
216
+
217
+ // Stage 3 — Opus verifies the actual diff, not the executor's self-report.
218
+ // Stage 2 always returns an object (never null — see its final `return`s
219
+ // above), so `run` here is never null; no null-guard needed.
220
+ async (run, item, i) => {
221
+ if (!run.branch) {
222
+ return { task: item.task, ...run, verdict: { approved: false, issues: ['executor reported no branch — nothing to review'] } }
223
+ }
224
+ // agentType pulls in the canonical reviewer rubric (roles/reviewer.md)
225
+ // instead of the weaker inline restatement this used to carry.
226
+ const verdict = await agent(
227
+ [
228
+ `Review branch ${run.branch} against this task: "${item.task}".`,
229
+ `Diff scope: git diff $(git merge-base HEAD ${run.branch}) ${run.branch}`,
230
+ `First check the branch is reviewable: git rev-parse --verify ${run.branch} and git diff --stat $(git merge-base HEAD ${run.branch}) ${run.branch}. If the branch is missing or the diff is empty, return approved: false with issue "no reviewable diff".`,
231
+ `Executor self-report (do not trust it, verify it): ${run.summary} — checks: ${run.checks || 'none reported'}`,
232
+ ].join('\n'),
233
+ { label: `verify-${i}`, phase: 'Verify', agentType: 'leo:reviewer', model: TIERS.judge, effort: 'medium', schema: VERDICT_SCHEMA },
234
+ )
235
+ return { task: item.task, ...run, verdict }
236
+ },
237
+ )
238
+
239
+ const done = results.filter(Boolean)
240
+ const approved = done.filter(r => r.verdict && r.verdict.approved)
241
+ const rejected = done.filter(r => !r.verdict || !r.verdict.approved)
242
+ log(`${approved.length} approved, ${rejected.length} rejected, ${items.length - done.length} failed to run`)
243
+
244
+ // Orphan tracking: only superseded retries (an earlier attempt's branch that
245
+ // got superseded by a later, kept attempt on the SAME item) are safe to
246
+ // delete — that work is duplicated by the branch that replaced it. A
247
+ // rejected branch is different: it may be the only copy of that item's
248
+ // work, just judged not good enough yet, so it is reported separately and
249
+ // never described as safe to delete — deleting it on the note's say-so
250
+ // would destroy the only copy.
251
+ const orphans = [...new Set(done.flatMap(r => r.supersededBranches || []).filter(Boolean))]
252
+ const kept = approved.map(r => r.branch).filter(Boolean)
253
+
254
+ return {
255
+ approved: approved.map(r => ({ task: r.task, branch: r.branch, escalated: !!r.escalated })),
256
+ rejected: rejected.map(r => ({ task: r.task, branch: r.branch || null, issues: r.verdict ? r.verdict.issues : ['agent failed, no verdict'] })),
257
+ orphans,
258
+ note: `Approved (merge these from the main session): ${kept.join(', ') || 'none'}. Orphaned (superseded retries — safe to delete): ${orphans.join(', ') || 'none'}. Rejected branches hold work that failed review but may still be worth salvaging — do NOT delete them without reviewing first: ${rejected.map(r => r.branch).filter(Boolean).join(', ') || 'none'}. To clean up an orphan: \`git worktree list\` to find its path, then \`git worktree remove <path>\` (prune does not remove live worktrees).`,
259
+ }