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.
- package/adapters/cursor/agents/executor.md +17 -0
- package/adapters/cursor/agents/expert.md +70 -0
- package/adapters/cursor/agents/explore.md +16 -0
- package/adapters/cursor/agents/implementer.md +18 -0
- package/adapters/cursor/agents/investigator.md +18 -0
- package/adapters/cursor/agents/planner.md +28 -0
- package/adapters/cursor/agents/reviewer.md +33 -0
- package/adapters/opencode/agents.json +66 -0
- package/adapters/opencode/plugin.js +186 -0
- package/config/models.json +62 -0
- package/hooks/bash-guard.py +541 -0
- package/hooks/cursor-guard.py +84 -0
- package/hooks/hooks-cursor.json +11 -0
- package/hooks/hooks.json +20 -0
- package/hooks/session-start.py +121 -0
- package/package.json +16 -0
- package/roles/executor.md +15 -0
- package/roles/expert.md +67 -0
- package/roles/explore.md +13 -0
- package/roles/implementer.md +16 -0
- package/roles/investigator.md +15 -0
- package/roles/planner.md +25 -0
- package/roles/reviewer.md +30 -0
- package/scripts/render_adapters.py +326 -0
- package/scripts/state.py +127 -0
- package/settings.json +7 -0
- package/skills/.gitkeep +0 -0
- package/skills/brainstorming/SKILL.md +109 -0
- package/skills/debugging/SKILL.md +98 -0
- package/skills/delegation/SKILL.md +141 -0
- package/skills/executing-plans/SKILL.md +116 -0
- package/skills/finishing-a-branch/SKILL.md +123 -0
- package/skills/test-first/SKILL.md +90 -0
- package/skills/using-leo/SKILL.md +89 -0
- package/skills/using-leo/references/claude-mapping.md +11 -0
- package/skills/using-leo/references/codex-mapping.md +24 -0
- package/skills/using-leo/references/cursor-mapping.md +22 -0
- package/skills/using-leo/references/hermes-mapping.md +26 -0
- package/skills/using-leo/references/opencode-mapping.md +28 -0
- package/skills/verification/SKILL.md +102 -0
- package/skills/worktrees/SKILL.md +129 -0
- package/skills/writing-plans/SKILL.md +96 -0
- 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
|
+
}
|