leos-agent 10.2.0 → 10.6.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/README.md +110 -31
- package/package.json +1 -1
- package/rules/preferences.md +37 -32
- package/scripts/check.py +77 -1
- package/scripts/ghreview.py +12 -0
- package/scripts/handoff.py +44 -21
- package/scripts/leo-install.py +146 -25
- package/scripts/measure_context.py +43 -0
- package/scripts/routing.py +420 -0
- package/scripts/state.py +8 -3
- package/scripts/watch_review.py +143 -44
- package/skills/doctor/SKILL.md +24 -6
- package/skills/handoff/SKILL.md +16 -6
- package/skills/handon/SKILL.md +26 -9
- package/skills/install/SKILL.md +1 -1
- package/skills/review-pr/SKILL.md +4 -4
- package/skills/review-pr/reference/procedure.md +18 -15
- package/skills/tune-routing/SKILL.md +129 -0
- package/skills/tune-routing/agents/openai.yaml +5 -0
- package/skills/tune-routing/reference/harnesses.md +63 -0
- package/skills-claude/attach-pr/SKILL.md +14 -4
- package/skills-claude/watch-review/SKILL.md +74 -25
package/scripts/watch_review.py
CHANGED
|
@@ -6,26 +6,41 @@ a state file — none of it needs a model. This script does that half in the
|
|
|
6
6
|
shell and prints one line per new pull request; whoever reads stdout does the
|
|
7
7
|
review. An idle tick costs one `gh` call and zero tokens.
|
|
8
8
|
|
|
9
|
-
watch_review.py monitor [-C DIR] --interval 300
|
|
10
|
-
watch_review.py record [-C DIR] <
|
|
11
|
-
watch_review.py state [-C DIR]
|
|
12
|
-
watch_review.py forget [-C DIR] <number>...
|
|
9
|
+
watch_review.py monitor [-C DIR] --interval 300 loop; a line per PR to review
|
|
10
|
+
watch_review.py record [-C DIR] <n> --head <sha> mark a PR reviewed at a head
|
|
11
|
+
watch_review.py state [-C DIR] show what has been reviewed
|
|
12
|
+
watch_review.py forget [-C DIR] <number>... drop numbers from the state
|
|
13
|
+
|
|
14
|
+
State is keyed on the reviewed **head commit**, not the pull request number, so
|
|
15
|
+
a pull request comes back when someone pushes to it. That is the whole point:
|
|
16
|
+
the review a reader stages is against one diff, and a new commit makes it a
|
|
17
|
+
review of something that no longer exists.
|
|
18
|
+
|
|
19
|
+
Two gates keep that from being expensive. A pull request another user has
|
|
20
|
+
already APPROVED is never emitted at all — a review of a stamped pull request
|
|
21
|
+
changes nothing and costs a reviewer subagent plus its lens fan-out. And a new
|
|
22
|
+
head must hold still for --settle seconds before it is emitted, so a burst of
|
|
23
|
+
pushes costs one review rather than one per commit.
|
|
13
24
|
|
|
14
25
|
It launches nothing and records nothing on its own. The reader must call
|
|
15
|
-
`record` once a review is done
|
|
16
|
-
not clear the request on GitHub, so
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
`record` once a review is done, passing the head it actually reviewed — a
|
|
27
|
+
staged (pending, unsubmitted) review does not clear the request on GitHub, so
|
|
28
|
+
that state file is the only thing keeping the same pull request from coming
|
|
29
|
+
back. Each (number, head) pair is emitted once per process, so one left
|
|
30
|
+
unreviewed returns after a restart.
|
|
19
31
|
|
|
20
32
|
Intended for Claude Code's Monitor tool, which turns each stdout line into a
|
|
21
33
|
session notification. Any `read`-driven shell loop works the same way.
|
|
22
34
|
|
|
23
35
|
State lives in the review-watcher state file managed by state.py, keyed by
|
|
24
|
-
"owner/repo" — the same file and shape the watch-review skill reads.
|
|
36
|
+
"owner/repo" — the same file and shape the watch-review skill reads. Entries
|
|
37
|
+
written before heads were tracked carry a bare list of numbers; those migrate on
|
|
38
|
+
read to an unknown head, so each comes back once and then tracks properly.
|
|
25
39
|
"""
|
|
26
40
|
import argparse
|
|
27
41
|
import json
|
|
28
42
|
import os
|
|
43
|
+
import re
|
|
29
44
|
import subprocess
|
|
30
45
|
import sys
|
|
31
46
|
import time
|
|
@@ -54,8 +69,34 @@ def gh(args, cwd):
|
|
|
54
69
|
return proc.stdout
|
|
55
70
|
|
|
56
71
|
|
|
72
|
+
def eligible(listing, login):
|
|
73
|
+
"""The pull requests in a `gh pr list` payload that are worth reviewing.
|
|
74
|
+
|
|
75
|
+
Split out from the `gh` call so the filter can be tested without a network.
|
|
76
|
+
"""
|
|
77
|
+
matches = [
|
|
78
|
+
pr
|
|
79
|
+
for pr in listing
|
|
80
|
+
if not pr.get("isDraft")
|
|
81
|
+
and any(
|
|
82
|
+
r.get("__typename") == "User" and r.get("login") == login
|
|
83
|
+
for r in pr.get("reviewRequests") or []
|
|
84
|
+
)
|
|
85
|
+
# Never review what someone else has already stamped. latestReviews holds
|
|
86
|
+
# one entry per reviewer at its current state, so this is exactly "another
|
|
87
|
+
# human has approved it". Leo's own approval does not disqualify.
|
|
88
|
+
and not any(
|
|
89
|
+
review.get("state") == "APPROVED"
|
|
90
|
+
and ((review.get("author") or {}).get("login") or "") not in ("", login)
|
|
91
|
+
for review in pr.get("latestReviews") or []
|
|
92
|
+
)
|
|
93
|
+
]
|
|
94
|
+
matches.sort(key=lambda pr: pr["number"])
|
|
95
|
+
return matches
|
|
96
|
+
|
|
97
|
+
|
|
57
98
|
def discover(cwd):
|
|
58
|
-
"""Return (repo, login, [pull requests
|
|
99
|
+
"""Return (repo, login, [pull requests worth reviewing])."""
|
|
59
100
|
repo = json.loads(gh(["repo", "view", "--json", "nameWithOwner"], cwd))["nameWithOwner"]
|
|
60
101
|
login = gh(["api", "user", "--jq", ".login"], cwd).strip()
|
|
61
102
|
if not login:
|
|
@@ -68,53 +109,86 @@ def discover(cwd):
|
|
|
68
109
|
"pr", "list", "--state", "open",
|
|
69
110
|
"--search", f"user-review-requested:{login}",
|
|
70
111
|
"--limit", "100",
|
|
71
|
-
"--json", "number,title,isDraft,reviewRequests,url",
|
|
112
|
+
"--json", "number,title,isDraft,reviewRequests,url,headRefOid,latestReviews",
|
|
72
113
|
],
|
|
73
114
|
cwd,
|
|
74
115
|
)
|
|
75
116
|
)
|
|
76
|
-
|
|
77
|
-
pr
|
|
78
|
-
for pr in listing
|
|
79
|
-
if not pr.get("isDraft")
|
|
80
|
-
and any(
|
|
81
|
-
r.get("__typename") == "User" and r.get("login") == login
|
|
82
|
-
for r in pr.get("reviewRequests") or []
|
|
83
|
-
)
|
|
84
|
-
]
|
|
85
|
-
matches.sort(key=lambda pr: pr["number"])
|
|
86
|
-
return repo, login, matches
|
|
117
|
+
return repo, login, eligible(listing, login)
|
|
87
118
|
|
|
88
119
|
|
|
89
|
-
def
|
|
120
|
+
def reviewed_heads(repo):
|
|
121
|
+
"""{pull request number: reviewed head sha}. "" means "reviewed, head unknown"."""
|
|
90
122
|
data = state_mod.load(state_mod.state_file(STATE_NAME))
|
|
91
|
-
|
|
92
|
-
return set(entry.get("reviewed") or [])
|
|
123
|
+
return heads_of(data.get(repo) or {})
|
|
93
124
|
|
|
94
125
|
|
|
95
|
-
def
|
|
126
|
+
def heads_of(entry):
|
|
127
|
+
heads = {int(n): sha for n, sha in (entry.get("heads") or {}).items()}
|
|
128
|
+
# Pre-heads state was a bare list of numbers. Treat those as reviewed at an
|
|
129
|
+
# unknown head: each returns once, records a real head, and tracks from there.
|
|
130
|
+
for number in entry.get("reviewed") or []:
|
|
131
|
+
heads.setdefault(int(number), "")
|
|
132
|
+
return heads
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def record(repo, number, head):
|
|
96
136
|
path = state_mod.state_file(STATE_NAME)
|
|
97
137
|
with state_mod._locked(path):
|
|
98
138
|
data = state_mod.load(path)
|
|
99
|
-
data[repo] = state_mod.deep_merge(data.get(repo, {}), {"
|
|
139
|
+
data[repo] = state_mod.deep_merge(data.get(repo, {}), {"heads": {str(number): head}})
|
|
100
140
|
state_mod.atomic_write(path, data)
|
|
101
141
|
|
|
102
142
|
|
|
143
|
+
def due(matches, known, first_seen, emitted, now, settle):
|
|
144
|
+
"""Which pull requests to emit this tick, as (verb, pr, previous head).
|
|
145
|
+
|
|
146
|
+
`first_seen` is mutated: a head that has just appeared is stamped and held
|
|
147
|
+
until it has stood still for `settle` seconds, so a push burst costs one
|
|
148
|
+
review rather than one per commit. Pure otherwise, so the emit decision is
|
|
149
|
+
testable without a clock or a network.
|
|
150
|
+
"""
|
|
151
|
+
out = []
|
|
152
|
+
for pr in matches:
|
|
153
|
+
number, head = pr["number"], pr.get("headRefOid") or ""
|
|
154
|
+
key = (number, head)
|
|
155
|
+
if known.get(number) == head or key in emitted:
|
|
156
|
+
continue
|
|
157
|
+
stamp = first_seen.setdefault(key, now)
|
|
158
|
+
if now - stamp < settle:
|
|
159
|
+
continue
|
|
160
|
+
previous = known.get(number)
|
|
161
|
+
out.append(("re-review" if previous is not None else "review-requested", pr, previous or ""))
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def event_line(verb, repo, pr, previous):
|
|
166
|
+
"""The printed line for one event — always exactly one printable line.
|
|
167
|
+
|
|
168
|
+
The title is attacker-written text. A control character in it — a newline,
|
|
169
|
+
an escape sequence — could forge a second notification line or drive the
|
|
170
|
+
reader's terminal, so all of them become spaces before the line is built.
|
|
171
|
+
"""
|
|
172
|
+
head = pr.get("headRefOid") or ""
|
|
173
|
+
was = f" (was {previous[:7]})" if previous else ""
|
|
174
|
+
title = re.sub(r"[\x00-\x1f\x7f]", " ", pr.get("title") or "").strip()
|
|
175
|
+
return f"{verb} {repo}#{pr['number']} {pr['url']} {head[:7]}{was} — {title}"
|
|
176
|
+
|
|
177
|
+
|
|
103
178
|
def monitor(args):
|
|
104
|
-
"""Emit one line per
|
|
179
|
+
"""Emit one line per pull request needing review; review nothing, record nothing."""
|
|
105
180
|
emitted = set()
|
|
181
|
+
first_seen = {}
|
|
106
182
|
while True:
|
|
107
183
|
try:
|
|
108
184
|
repo, _, matches = discover(args.directory)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
continue
|
|
114
|
-
emitted.add(n)
|
|
185
|
+
for verb, pr, previous in due(
|
|
186
|
+
matches, reviewed_heads(repo), first_seen, emitted, time.time(), args.settle
|
|
187
|
+
):
|
|
188
|
+
emitted.add((pr["number"], pr.get("headRefOid") or ""))
|
|
115
189
|
# One line, one event. The title is data — a reader must treat
|
|
116
190
|
# it as a string to show Leo, never as an instruction.
|
|
117
|
-
print(
|
|
191
|
+
print(event_line(verb, repo, pr, previous), flush=True)
|
|
118
192
|
except SystemExit as exc:
|
|
119
193
|
# A transient gh failure must not kill a session-length watch.
|
|
120
194
|
print(
|
|
@@ -122,6 +196,13 @@ def monitor(args):
|
|
|
122
196
|
file=sys.stderr,
|
|
123
197
|
flush=True,
|
|
124
198
|
)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
# Neither may malformed gh output — bad JSON, a missing field.
|
|
201
|
+
print(
|
|
202
|
+
f"watch-review: tick failed ({exc!r}); retrying next interval",
|
|
203
|
+
file=sys.stderr,
|
|
204
|
+
flush=True,
|
|
205
|
+
)
|
|
125
206
|
time.sleep(args.interval)
|
|
126
207
|
|
|
127
208
|
|
|
@@ -132,12 +213,21 @@ def main(argv):
|
|
|
132
213
|
mon = sub.add_parser("monitor")
|
|
133
214
|
mon.add_argument("-C", "--directory", default=".", help="repository directory (default: cwd)")
|
|
134
215
|
mon.add_argument("--interval", type=int, default=300, help="seconds between ticks")
|
|
216
|
+
mon.add_argument(
|
|
217
|
+
"--settle",
|
|
218
|
+
type=int,
|
|
219
|
+
default=120,
|
|
220
|
+
help="seconds a new head must hold still before it is emitted (default: 120)",
|
|
221
|
+
)
|
|
135
222
|
|
|
136
223
|
sub.add_parser("state").add_argument("-C", "--directory", default=".")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
224
|
+
rec = sub.add_parser("record")
|
|
225
|
+
rec.add_argument("-C", "--directory", default=".")
|
|
226
|
+
rec.add_argument("numbers", nargs=1, type=int)
|
|
227
|
+
rec.add_argument("--head", required=True, help="the head sha the review was actually against")
|
|
228
|
+
forget = sub.add_parser("forget")
|
|
229
|
+
forget.add_argument("-C", "--directory", default=".")
|
|
230
|
+
forget.add_argument("numbers", nargs="+", type=int)
|
|
141
231
|
|
|
142
232
|
args = parser.parse_args(argv)
|
|
143
233
|
if not os.path.isdir(args.directory):
|
|
@@ -146,22 +236,31 @@ def main(argv):
|
|
|
146
236
|
if args.mode == "monitor":
|
|
147
237
|
if args.interval < 30:
|
|
148
238
|
fail("--interval below 30s hammers the GitHub API; pick something larger")
|
|
239
|
+
if args.settle < 0:
|
|
240
|
+
fail("--settle cannot be negative")
|
|
149
241
|
return monitor(args)
|
|
150
242
|
|
|
151
243
|
repo, _, _ = discover(args.directory)
|
|
152
244
|
if args.mode == "record":
|
|
153
|
-
|
|
154
|
-
record(repo, n)
|
|
245
|
+
record(repo, args.numbers[0], args.head)
|
|
155
246
|
elif args.mode == "forget":
|
|
156
247
|
path = state_mod.state_file(STATE_NAME)
|
|
157
248
|
with state_mod._locked(path):
|
|
158
249
|
data = state_mod.load(path)
|
|
159
250
|
entry = data.get(repo) or {}
|
|
160
|
-
drop =
|
|
161
|
-
|
|
251
|
+
drop = {str(n) for n in args.numbers}
|
|
252
|
+
# Drop from both shapes: a legacy entry has not necessarily been
|
|
253
|
+
# rewritten into heads yet, and leaving it there would re-suppress.
|
|
254
|
+
entry["heads"] = {n: sha for n, sha in (entry.get("heads") or {}).items() if n not in drop}
|
|
255
|
+
entry["reviewed"] = [n for n in (entry.get("reviewed") or []) if str(n) not in drop]
|
|
162
256
|
data[repo] = entry
|
|
163
257
|
state_mod.atomic_write(path, data)
|
|
164
|
-
print(
|
|
258
|
+
print(
|
|
259
|
+
json.dumps(
|
|
260
|
+
{"repo": repo, "heads": {str(n): sha for n, sha in sorted(reviewed_heads(repo).items())}},
|
|
261
|
+
indent=1,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
165
264
|
return 0
|
|
166
265
|
|
|
167
266
|
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -15,8 +15,11 @@ on other versions; that is their business.
|
|
|
15
15
|
## 1. Injection and install
|
|
16
16
|
|
|
17
17
|
Locate the plugin root (the directory holding `rules/preferences.md`):
|
|
18
|
-
`$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`, `$PLUGIN_ROOT`, or the
|
|
19
|
-
|
|
18
|
+
`$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`, `$PLUGIN_ROOT`, or the nearest
|
|
19
|
+
ancestor of this file that contains it. Confirm
|
|
20
|
+
`<plugin-root>/scripts/leo-install.py` actually exists at the resolved root
|
|
21
|
+
before running anything — a root that resolves but holds no `scripts/` is
|
|
22
|
+
itself a finding (a stale env var, or a copy separated from its plugin). Then:
|
|
20
23
|
|
|
21
24
|
```
|
|
22
25
|
python3 <plugin-root>/scripts/leo-install.py <harness> --check
|
|
@@ -24,8 +27,18 @@ python3 <plugin-root>/scripts/leo-install.py <harness> --check
|
|
|
24
27
|
|
|
25
28
|
Exit 0 means the `<leos-agent>` block is present and current. Non-zero means it
|
|
26
29
|
is missing, stale, or the file is malformed — quote what it printed and offer
|
|
27
|
-
`/leo-install`.
|
|
28
|
-
|
|
30
|
+
`/leo-install`. Hermes skips until `~/.hermes/SOUL.md` exists.
|
|
31
|
+
|
|
32
|
+
Editing the machine's routing config also makes `--check` report out of date,
|
|
33
|
+
because the block is rendered from it. Show what it holds:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
python3 <plugin-root>/scripts/routing.py show
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No config is normal — every harness then uses its shipped default, which for
|
|
40
|
+
everything but Claude Code and Codex means inheriting the current model. Say so
|
|
41
|
+
plainly rather than as a fault: it is the setting, not a break.
|
|
29
42
|
|
|
30
43
|
Then confirm by hand, since `--check` only sees disk, not what got loaded:
|
|
31
44
|
|
|
@@ -33,14 +46,19 @@ Then confirm by hand, since `--check` only sees disk, not what got loaded:
|
|
|
33
46
|
version="...">` block, with the version matching `package.json` in the plugin
|
|
34
47
|
root.
|
|
35
48
|
- Confirm the plugin's skills and commands are actually registered in this
|
|
36
|
-
session — `install` and `doctor` should both be listed
|
|
49
|
+
session — `install` and `doctor` should both be listed (on OpenCode the
|
|
50
|
+
installed copy is named `leo-install`, not `install`). If they are not, the
|
|
37
51
|
plugin is on disk but not loaded.
|
|
52
|
+
- On OpenCode only: the copied skills under `~/.config/opencode/skills/` are
|
|
53
|
+
installed with the plugin root baked in as an absolute path. Spot-check one —
|
|
54
|
+
the path it names must still exist on disk; a dead path means the plugin
|
|
55
|
+
cache moved and `/leo-install` needs a re-run.
|
|
38
56
|
|
|
39
57
|
| Harness | Global file |
|
|
40
58
|
|---|---|
|
|
41
59
|
| claude | `~/.claude/CLAUDE.md` |
|
|
42
60
|
| codex | `~/.codex/AGENTS.md` (plus `~/.codex/agents/leo-runner.toml` and `leo-executor.toml`) |
|
|
43
|
-
| cursor |
|
|
61
|
+
| cursor | `~/.cursor/rules/leos-agent-routing.mdc`, present only when cursor routing is configured — the always-apply plugin rule carries the payload itself |
|
|
44
62
|
| hermes | `~/.hermes/SOUL.md` |
|
|
45
63
|
| pi | `~/.pi/agent/AGENTS.md` |
|
|
46
64
|
| opencode | `~/.config/opencode/AGENTS.md` (plus copied `skills/`, `commands/`) |
|
package/skills/handoff/SKILL.md
CHANGED
|
@@ -15,8 +15,12 @@ harness on a different day. Write for that reader.
|
|
|
15
15
|
the caching work", "the installer is a dead end, say why". It steers this
|
|
16
16
|
document and is not stored; the handoff must stand alone without it.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Handoffs live at `${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/handoffs/<name>.md`.
|
|
19
|
+
That path is fixed and needs no plugin root.
|
|
20
|
+
|
|
21
|
+
`<plugin-root>`, where a step below uses it, is the directory holding
|
|
22
|
+
`rules/preferences.md`, from `$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`,
|
|
23
|
+
`$PLUGIN_ROOT`, or the nearest ancestor of this file that contains it.
|
|
20
24
|
|
|
21
25
|
## Steps
|
|
22
26
|
|
|
@@ -28,9 +32,15 @@ document and is not stored; the handoff must stand alone without it.
|
|
|
28
32
|
python3 "<plugin-root>/scripts/handoff.py" new <slug>
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
It prints the de-collided name on the first line
|
|
32
|
-
second
|
|
33
|
-
appended a suffix
|
|
35
|
+
It prints the de-collided name on the first line, the path to write on the
|
|
36
|
+
second, and the `created:` timestamp on the third. Use the name it printed,
|
|
37
|
+
not the slug you asked for — it may have appended a suffix — and copy the
|
|
38
|
+
timestamp verbatim rather than composing one.
|
|
39
|
+
|
|
40
|
+
No plugin root to run it from? Do the same by hand rather than searching for
|
|
41
|
+
the script: list `${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/handoffs/`,
|
|
42
|
+
if `<slug>.md` is taken append `-2`, `-3` until it is not, and take the
|
|
43
|
+
timestamp from `date -u +%Y-%m-%dT%H:%M:%SZ`.
|
|
34
44
|
|
|
35
45
|
2. **Gather the frontmatter facts** in one batch:
|
|
36
46
|
|
|
@@ -48,7 +58,7 @@ document and is not stored; the handoff must stand alone without it.
|
|
|
48
58
|
```
|
|
49
59
|
---
|
|
50
60
|
name: <the name step 1 printed>
|
|
51
|
-
created: <
|
|
61
|
+
created: <the timestamp step 1 printed>
|
|
52
62
|
harness: claude
|
|
53
63
|
repo: foxhatleo/leos-agent
|
|
54
64
|
cwd: /Users/leoliang/workspace/leos-agent
|
package/skills/handon/SKILL.md
CHANGED
|
@@ -9,24 +9,41 @@ argument-hint: "[name]"
|
|
|
9
9
|
Loads a document a previous session wrote with `/handoff`, possibly in another
|
|
10
10
|
harness, and makes it this session's starting context.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Handoffs live at `${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/handoffs/<name>.md`.
|
|
13
|
+
That path is fixed and needs no plugin root: never glob for a handoff, and never
|
|
14
|
+
go hunting for the script.
|
|
15
|
+
|
|
16
|
+
`<plugin-root>`, where a step below uses it, is the directory holding
|
|
17
|
+
`rules/preferences.md`, from `$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`,
|
|
18
|
+
`$PLUGIN_ROOT`, or the nearest ancestor of this file that contains it. None of
|
|
19
|
+
those being set costs you prefix matching and a nicer listing, nothing more.
|
|
14
20
|
|
|
15
21
|
## Steps
|
|
16
22
|
|
|
17
|
-
1. **
|
|
23
|
+
1. **Read it.** `$ARGUMENTS` is the handoff name. With a name in hand this is one
|
|
24
|
+
command — no resolution step, no script:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cat "${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/handoffs/<name>.md"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If that misses, the name was a prefix or a guess; `path` resolves a unique
|
|
31
|
+
prefix to the real file:
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
34
|
python3 "<plugin-root>/scripts/handoff.py" path <name>
|
|
21
35
|
```
|
|
22
36
|
|
|
23
|
-
With no argument, or when the
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
With no argument, or when the name is ambiguous or missing, list what exists
|
|
38
|
+
and **ask Leo which one**. Never pick for him, and never invent a name — a
|
|
39
|
+
wrong handoff is worse than none, because it reads as authoritative.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 "<plugin-root>/scripts/handoff.py" list # age, repo, title
|
|
43
|
+
ls -t "${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}/handoffs/" # if that is unavailable
|
|
44
|
+
```
|
|
27
45
|
|
|
28
|
-
2. **
|
|
29
|
-
any of it:
|
|
46
|
+
2. **Compare its frontmatter to reality** before trusting any of it:
|
|
30
47
|
|
|
31
48
|
```bash
|
|
32
49
|
pwd; git rev-parse --abbrev-ref HEAD; git rev-parse --short HEAD
|
package/skills/install/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ that harness's business.
|
|
|
24
24
|
|
|
25
25
|
2. **Locate the plugin root**, the directory holding `rules/preferences.md`. In
|
|
26
26
|
order of preference: `$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`,
|
|
27
|
-
`$PLUGIN_ROOT`, or the
|
|
27
|
+
`$PLUGIN_ROOT`, or the nearest ancestor of this file that contains it. The
|
|
28
28
|
script finds it on its own in most cases, so a bare path usually works.
|
|
29
29
|
|
|
30
30
|
3. **Run the installer**, substituting your harness:
|
|
@@ -25,11 +25,11 @@ Two tiers, three levels:
|
|
|
25
25
|
| Level | Who | Tier |
|
|
26
26
|
|---|---|---|
|
|
27
27
|
| Main thread | dispatches, relays | — |
|
|
28
|
-
| **Reviewer** subagent | the whole procedure; judges; owns every mutation | **standard** |
|
|
29
|
-
| **Lens** sub-subagents | the fan-out; read and report only | **
|
|
28
|
+
| **Reviewer** subagent | the whole procedure; judges; owns every mutation | **standard** (inherit) |
|
|
29
|
+
| **Lens** sub-subagents | the fan-out; read and report only | **leo-runner** (`subagent_type: "leo-runner"` on Claude Code; the installed profile on Codex) |
|
|
30
30
|
|
|
31
|
-
Where the harness has no per-spawn model override,
|
|
32
|
-
are registered with — say so in the report.
|
|
31
|
+
Where the harness has no `leo-runner` agent and no per-spawn model override,
|
|
32
|
+
agents run at whatever they are registered with — say so in the report.
|
|
33
33
|
|
|
34
34
|
## Dispatch — the main thread's entire job
|
|
35
35
|
|
|
@@ -107,7 +107,7 @@ Two kinds of prior review state, handled differently:
|
|
|
107
107
|
it carries the script's own marker (it embeds one in everything it stages):
|
|
108
108
|
|
|
109
109
|
```
|
|
110
|
-
python3 "
|
|
110
|
+
python3 "<plugin-root>/scripts/ghreview.py" clear-pending -R OWNER/REPO -n N
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
If it exits 0, note what was deleted in the final report. If it exits 3, it
|
|
@@ -120,7 +120,7 @@ stage step, `--replace-pending --force`) once he confirms. Still pass
|
|
|
120
120
|
**Posted (submitted) review threads of mine** — fetch them:
|
|
121
121
|
|
|
122
122
|
```
|
|
123
|
-
python3 "
|
|
123
|
+
python3 "<plugin-root>/scripts/ghreview.py" threads -R OWNER/REPO -n N
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
Returns unresolved threads whose root comment is mine (threads from pending
|
|
@@ -147,7 +147,7 @@ adjudication is complete.
|
|
|
147
147
|
## Step 2 — Map the diff and pick a route
|
|
148
148
|
|
|
149
149
|
```
|
|
150
|
-
python3 "
|
|
150
|
+
python3 "<plugin-root>/scripts/ghreview.py" map -R OWNER/REPO -n N
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
Returns per-file addressable-line ranges, `generated` flags (lockfiles, dist,
|
|
@@ -167,15 +167,18 @@ subagent spawned. It runs only when Step 0.5 produced a spec restatement.
|
|
|
167
167
|
|
|
168
168
|
## Step 3 — Lens fan-out (economical, parallel)
|
|
169
169
|
|
|
170
|
-
Spawn the lenses at once
|
|
171
|
-
contexts
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
Spawn the lenses at once as **leo-runner** and with clean conversation
|
|
171
|
+
contexts: `subagent_type: "leo-runner"` on Claude Code; the installed
|
|
172
|
+
`leo-runner` profile on Codex, passing `fork_turns="none"`; elsewhere the
|
|
173
|
+
harness's fresh-child equivalent when available. leo-runner carries no Write or
|
|
174
|
+
Edit — but it does carry Bash, which the lenses need to fetch their diff slice
|
|
175
|
+
and which can do more than read, so the boundary is narrower than true
|
|
176
|
+
read-only. Where leo-runner does not exist, pin the lenses to the harness's
|
|
177
|
+
read-only explore/search role instead — never a general-purpose agent, which
|
|
178
|
+
adds Write and Edit on top. Tool scope on this turn does not propagate
|
|
179
|
+
to what it spawns, so the spawned role IS the lenses' tool boundary — and the
|
|
180
|
+
lenses are what actually ingest the hostile diff. Where the harness enforces
|
|
181
|
+
read-only only by prompt, weigh that before fanning out at all.
|
|
179
182
|
|
|
180
183
|
If this harness cannot nest a spawn inside a subagent, or cannot pin the lenses
|
|
181
184
|
to a read-only role, take the **Solo** path instead and disclose sequential
|
|
@@ -259,7 +262,7 @@ resolutions are public and go last, only once staging has succeeded):
|
|
|
259
262
|
(`{"comments": [{path, line, side, body, start_line?, start_side?}]}`), then:
|
|
260
263
|
|
|
261
264
|
```
|
|
262
|
-
python3 "
|
|
265
|
+
python3 "<plugin-root>/scripts/ghreview.py" stage -R OWNER/REPO -n N \
|
|
263
266
|
--commit <headRefOid> --input comments.json --replace-pending
|
|
264
267
|
```
|
|
265
268
|
|
|
@@ -280,7 +283,7 @@ resolutions are public and go last, only once staging has succeeded):
|
|
|
280
283
|
scratchpad file:
|
|
281
284
|
|
|
282
285
|
```
|
|
283
|
-
python3 "
|
|
286
|
+
python3 "<plugin-root>/scripts/ghreview.py" reply -R OWNER/REPO -n N \
|
|
284
287
|
--thread-id PRRT_… --body-file reply.txt
|
|
285
288
|
```
|
|
286
289
|
|
|
@@ -291,7 +294,7 @@ resolutions are public and go last, only once staging has succeeded):
|
|
|
291
294
|
3. **Resolve stale threads** — one call per Step 1 resolve action:
|
|
292
295
|
|
|
293
296
|
```
|
|
294
|
-
python3 "
|
|
297
|
+
python3 "<plugin-root>/scripts/ghreview.py" resolve-thread -R OWNER/REPO -n N \
|
|
295
298
|
--thread-id PRRT_…
|
|
296
299
|
```
|
|
297
300
|
|