leos-agent 6.1.1 → 7.0.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/LICENSE +21 -0
- package/README.md +18 -11
- package/adapters/cursor/agents/executor.md +2 -2
- package/adapters/cursor/agents/implementer.md +2 -2
- package/adapters/cursor/agents/review-lens.md +22 -0
- package/adapters/cursor/agents/reviewer.md +3 -2
- package/adapters/opencode/agents.json +44 -5
- package/adapters/opencode/plugin.js +435 -47
- package/config/MCP_PINS.md +17 -0
- package/config/models.json +647 -33
- package/hooks/bash-guard.py +51 -9
- package/hooks/session-start.py +27 -0
- package/package.json +19 -8
- package/roles/executor.md +2 -2
- package/roles/implementer.md +2 -2
- package/roles/review-lens.md +20 -0
- package/roles/reviewer.md +3 -2
- package/scripts/doctor.py +520 -0
- package/scripts/ghreview.py +558 -0
- package/scripts/jsonc_bridge.cjs +23 -0
- package/scripts/memory.py +744 -0
- package/scripts/render_adapters.py +253 -121
- package/scripts/resolve_attach_target.py +389 -0
- package/scripts/setup.py +1753 -0
- package/skills/brainstorming/SKILL.md +3 -1
- package/skills/debugging/SKILL.md +4 -2
- package/skills/delegation/SKILL.md +10 -8
- package/skills/doctor/SKILL.md +124 -0
- package/skills/executing-plans/SKILL.md +2 -1
- package/skills/finishing-a-branch/SKILL.md +4 -2
- package/skills/freshness/SKILL.md +131 -0
- package/skills/memory/SKILL.md +154 -0
- package/skills/resolve-ticket/SKILL.md +275 -0
- package/skills/review-pr/SKILL.md +327 -0
- package/skills/setup/SKILL.md +199 -0
- package/skills/setup/agents/openai.yaml +5 -0
- package/skills/test-first/SKILL.md +3 -1
- package/skills/using-leo/SKILL.md +18 -6
- package/skills/using-leo/references/claude-mapping.md +23 -1
- package/skills/using-leo/references/codex-mapping.md +18 -9
- package/skills/using-leo/references/cursor-mapping.md +19 -6
- package/skills/using-leo/references/hermes-mapping.md +18 -7
- package/skills/using-leo/references/opencode-mapping.md +18 -9
- package/skills/verification/SKILL.md +9 -1
- package/skills/visual-verification/SKILL.md +115 -0
- package/skills/watch-review/SKILL.md +128 -0
- package/skills/watch-review/agents/openai.yaml +5 -0
- package/skills/worktrees/SKILL.md +3 -1
- package/skills/writing-plans/SKILL.md +2 -1
- package/skills/writing-skills/SKILL.md +141 -0
- package/vendor/jsonc-parser-3.3.1/LICENSE.md +21 -0
- package/vendor/jsonc-parser-3.3.1/README.md +26 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +201 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +275 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +682 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +456 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +42 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +351 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +194 -0
- package/vendor/jsonc-parser-3.3.1/package.json +37 -0
- package/workflows/cost-tiered-fix.js +32 -4
|
@@ -12,7 +12,74 @@ CONFIG_PATH = ROOT / "config" / "models.json"
|
|
|
12
12
|
ROLE_ROOT = ROOT / "roles"
|
|
13
13
|
CLAUDE_MANIFEST = ROOT / ".claude-plugin" / "plugin.json"
|
|
14
14
|
GENERATED = "<!-- Generated by scripts/render_adapters.py; do not edit. -->\n"
|
|
15
|
-
|
|
15
|
+
TIERS = ("fable", "opus", "sonnet", "haiku")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _roles(config):
|
|
19
|
+
"""(role, tier, access) triples, sorted.
|
|
20
|
+
|
|
21
|
+
`access` used to be a literal set here, duplicating a fact already stated
|
|
22
|
+
by each role prompt's own `tools:` line and duplicated a third time in the
|
|
23
|
+
OpenCode test, with nothing tying the three together.
|
|
24
|
+
"""
|
|
25
|
+
for role, spec in sorted(config["roles"].items()):
|
|
26
|
+
yield role, spec["tier"], spec["access"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cap(config, key, harness):
|
|
30
|
+
for row in config["capabilities"]:
|
|
31
|
+
if row["key"] == key:
|
|
32
|
+
return row["values"][harness]
|
|
33
|
+
raise KeyError(f"no capability row {key!r}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate(config):
|
|
37
|
+
"""Refuse to render an incoherent matrix.
|
|
38
|
+
|
|
39
|
+
This is the only place a contradiction can be caught before --check ships
|
|
40
|
+
it: a mapping that renders cleanly is indistinguishable from a true one.
|
|
41
|
+
"""
|
|
42
|
+
harnesses = set(config["harnesses"])
|
|
43
|
+
for harness, rows in config["harnesses"].items():
|
|
44
|
+
for tier in rows.get("absentTiers", ()):
|
|
45
|
+
# Two conditions, and collapse alone is not enough.
|
|
46
|
+
#
|
|
47
|
+
# Dropping a tier drops its roles. The ceiling tier is the only one
|
|
48
|
+
# whose role exists *solely* to be stronger than the rung below —
|
|
49
|
+
# `expert` cannot break a deadlock a collapsed Opus already lost, so
|
|
50
|
+
# when it collapses it has no remaining purpose. Every other role
|
|
51
|
+
# keeps its job through a collapse: "routing between collapsed rungs
|
|
52
|
+
# buys role, not power" is the policy's own line. Sonnet and Haiku
|
|
53
|
+
# share a model on two harnesses today, and declaring Sonnet absent
|
|
54
|
+
# there would silently delete `implementer` — a real rung with a
|
|
55
|
+
# real job — which is exactly the failure this whole rule replaced.
|
|
56
|
+
if tier != TIERS[0]:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"{harness}: tier {tier!r} is declared absent, but only the ceiling "
|
|
59
|
+
f"tier ({TIERS[0]!r}) can be — every other rung's roles keep their "
|
|
60
|
+
"job through a collapse"
|
|
61
|
+
)
|
|
62
|
+
twins = [
|
|
63
|
+
t for t in TIERS if t != tier and _tier_identity(rows[t]) == _tier_identity(rows[tier])
|
|
64
|
+
]
|
|
65
|
+
if not twins:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{harness}: tier {tier!r} is declared absent but has a model of its own — "
|
|
68
|
+
"absence is only ever justified by tier collapse"
|
|
69
|
+
)
|
|
70
|
+
for row in config["capabilities"]:
|
|
71
|
+
unanswered = harnesses - set(row["values"])
|
|
72
|
+
if unanswered:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"capability {row['key']!r} unanswered for {sorted(unanswered)} — every "
|
|
75
|
+
"harness answers every row, that is the whole point of the matrix"
|
|
76
|
+
)
|
|
77
|
+
for harness, value in row["values"].items():
|
|
78
|
+
if value["mode"] not in row["modes"]:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"capability {row['key']!r}/{harness}: mode {value['mode']!r} "
|
|
81
|
+
f"is not one of {row['modes']}"
|
|
82
|
+
)
|
|
16
83
|
|
|
17
84
|
|
|
18
85
|
def _split_role(text):
|
|
@@ -26,11 +93,20 @@ def _split_role(text):
|
|
|
26
93
|
return lines[1:end], "\n".join(lines[end + 1 :]).strip() + "\n"
|
|
27
94
|
|
|
28
95
|
|
|
96
|
+
def _tier_identity(row):
|
|
97
|
+
"""A tier is a model *and* its requested reasoning effort.
|
|
98
|
+
|
|
99
|
+
Codex deliberately maps Fable and Opus to the same model at different
|
|
100
|
+
efforts. Treating a model id alone as identity falsely removes Fable.
|
|
101
|
+
"""
|
|
102
|
+
return row["model"], row.get("effort")
|
|
103
|
+
|
|
104
|
+
|
|
29
105
|
def _without(frontmatter, keys):
|
|
30
106
|
return [line for line in frontmatter if not any(line.startswith(key + ":") for key in keys)]
|
|
31
107
|
|
|
32
108
|
|
|
33
|
-
def _agent_docs(role, tier, config):
|
|
109
|
+
def _agent_docs(role, tier, access, config):
|
|
34
110
|
source = (ROLE_ROOT / f"{role}.md").read_text(encoding="utf-8")
|
|
35
111
|
frontmatter, body = _split_role(source)
|
|
36
112
|
|
|
@@ -61,7 +137,7 @@ def _agent_docs(role, tier, config):
|
|
|
61
137
|
|
|
62
138
|
cursor_frontmatter = _without(frontmatter, {"model", "effort", "tools"})
|
|
63
139
|
cursor_frontmatter.append("model: inherit")
|
|
64
|
-
if
|
|
140
|
+
if access == "read-only" and _cap(config, "readOnlyRoles", "cursor")["mode"] == "frontmatter":
|
|
65
141
|
cursor_frontmatter.append("readonly: true")
|
|
66
142
|
cursor = (
|
|
67
143
|
"---\n"
|
|
@@ -79,18 +155,22 @@ def _opencode_agents(config):
|
|
|
79
155
|
role name, sourced from the same roles/*.md canonical prompts and the
|
|
80
156
|
same config/models.json tiers every other harness reads.
|
|
81
157
|
|
|
82
|
-
A role
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
158
|
+
A role is dropped only when its tier is *declared absent* on this harness
|
|
159
|
+
(`absentTiers`), rather than registering a fake rung. That is `expert`
|
|
160
|
+
here, whose fable tier collapses onto opus.
|
|
161
|
+
|
|
162
|
+
The rule this replaced dropped any role whose tier resolved to the same
|
|
163
|
+
model as opus — model identity, not intent. The day `sonnet` collapsed
|
|
164
|
+
onto the opus model, `implementer` would have silently vanished from the
|
|
165
|
+
roster, and the only test that noticed would have said "6 != 5".
|
|
87
166
|
"""
|
|
88
167
|
opencode = config["harnesses"]["opencode"]
|
|
89
|
-
|
|
168
|
+
absent = set(opencode.get("absentTiers", ()))
|
|
169
|
+
adapter = opencode["adapter"]
|
|
90
170
|
agents = {}
|
|
91
|
-
for role, tier in
|
|
171
|
+
for role, tier, access in _roles(config):
|
|
92
172
|
model = opencode[tier]["model"]
|
|
93
|
-
if
|
|
173
|
+
if tier in absent:
|
|
94
174
|
continue
|
|
95
175
|
source = (ROLE_ROOT / f"{role}.md").read_text(encoding="utf-8")
|
|
96
176
|
frontmatter, body = _split_role(source)
|
|
@@ -100,21 +180,20 @@ def _opencode_agents(config):
|
|
|
100
180
|
continue
|
|
101
181
|
key, _, value = line.partition(":")
|
|
102
182
|
fm[key.strip()] = value.strip()
|
|
103
|
-
if
|
|
104
|
-
permission =
|
|
183
|
+
if access == "read-only":
|
|
184
|
+
permission = dict(adapter["readOnlyPermission"])
|
|
105
185
|
else:
|
|
106
186
|
# Stopgap for opencode#5894 (unconfirmed whether
|
|
107
187
|
# tool.execute.before intercepts subagent bash): coarse denies
|
|
108
188
|
# on the catastrophic rm class for write-capable agents. The
|
|
109
189
|
# precise tripwire stays hooks/bash-guard.py.
|
|
110
|
-
permission = {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
190
|
+
permission = {"bash": {cmd: "deny" for cmd in adapter["writeBashDeny"]}}
|
|
191
|
+
# `permission.edit: deny` prevents native edits, not destructive shell
|
|
192
|
+
# commands. Apply the narrow catastrophic Bash denial to every role,
|
|
193
|
+
# including shell-capable read-only agents.
|
|
194
|
+
permission.setdefault("bash", {}).update(
|
|
195
|
+
{cmd: "deny" for cmd in adapter["writeBashDeny"]}
|
|
196
|
+
)
|
|
118
197
|
agents[role] = {
|
|
119
198
|
"description": fm.get("description", ""),
|
|
120
199
|
"mode": "subagent",
|
|
@@ -143,7 +222,7 @@ def _collapse_note(rows):
|
|
|
143
222
|
"""
|
|
144
223
|
by_model = {}
|
|
145
224
|
for tier in ("fable", "opus", "sonnet", "haiku"):
|
|
146
|
-
by_model.setdefault(rows[tier]
|
|
225
|
+
by_model.setdefault(_tier_identity(rows[tier]), []).append(tier)
|
|
147
226
|
collapsed = [tiers for tiers in by_model.values() if len(tiers) > 1]
|
|
148
227
|
if not collapsed:
|
|
149
228
|
return ""
|
|
@@ -170,100 +249,134 @@ def _skill_notes(config, harness):
|
|
|
170
249
|
token appears in a non-Claude mapping.
|
|
171
250
|
"""
|
|
172
251
|
skills = config.get("skills", {})
|
|
252
|
+
reasons = skills.get("reasons", {})
|
|
173
253
|
missing = set(skills.get("exclude", {}).get(harness, ()))
|
|
174
254
|
if harness != "claude":
|
|
175
255
|
missing |= set(skills.get("claudeOnly", ()))
|
|
176
256
|
if not missing:
|
|
177
|
-
|
|
178
|
-
|
|
257
|
+
# Claude's appendix used to end here, silent in both directions: it
|
|
258
|
+
# never listed what only it has, and the exclusion list renders only
|
|
259
|
+
# for the others. A Claude session could not tell from its own mapping
|
|
260
|
+
# which of its skills do not travel.
|
|
261
|
+
exclusive = sorted(set(skills.get("claudeOnly", ())))
|
|
262
|
+
if not exclusive:
|
|
263
|
+
return ""
|
|
264
|
+
lines = ["", "## Leo skills only available here", ""]
|
|
265
|
+
for name in exclusive:
|
|
266
|
+
lines.append(f"- `leo:{name}` — {reasons.get(name, 'not portable to other harnesses')}.")
|
|
267
|
+
lines.append("")
|
|
268
|
+
lines.append(
|
|
269
|
+
"Every other skill in the policy's Skill index is registered on every "
|
|
270
|
+
"harness and behaves the same. These are not, so a procedure that "
|
|
271
|
+
"leans on one of them does not transfer."
|
|
272
|
+
)
|
|
273
|
+
return "\n".join(lines) + "\n"
|
|
179
274
|
lines = ["", "## Leo skills not available here", ""]
|
|
180
275
|
for name in sorted(missing):
|
|
181
276
|
lines.append(f"- `leo:{name}` — {reasons.get(name, 'not portable to this harness')}.")
|
|
182
277
|
lines.append("")
|
|
183
278
|
lines.append(
|
|
184
279
|
"Every other skill in the policy's Skill index is registered here and "
|
|
185
|
-
"behaves the same
|
|
186
|
-
"
|
|
280
|
+
"behaves the same, and so are the operational skills — "
|
|
281
|
+
+ ", ".join(f"`leo:{name}`" for name in config["skills"].get("operational", ()))
|
|
282
|
+
+ " all run on this harness. "
|
|
283
|
+
"Where they name a capability the table above says is missing, take "
|
|
284
|
+
"the fallback each one documents."
|
|
187
285
|
)
|
|
188
286
|
return "\n".join(lines) + "\n"
|
|
189
287
|
|
|
190
288
|
|
|
289
|
+
def _capability_table(config, harness):
|
|
290
|
+
"""The declarative half of every appendix.
|
|
291
|
+
|
|
292
|
+
Every harness answers every row, so a *gap* is disclosed by exactly the
|
|
293
|
+
same mechanism as a capability. Hand-written prose is what let the
|
|
294
|
+
worktree tools and the workflow runner go unmentioned on three harnesses
|
|
295
|
+
each: nothing forces a paragraph that was never written, and nothing
|
|
296
|
+
notices when one goes stale.
|
|
297
|
+
|
|
298
|
+
The notes are still human-written English. The matrix does not make them
|
|
299
|
+
true; it makes them structurally complete and impossible to omit.
|
|
300
|
+
"""
|
|
301
|
+
lines = ["", "## Capabilities here", "", "| Capability | Here |", "|---|---|"]
|
|
302
|
+
for row in config["capabilities"]:
|
|
303
|
+
lines.append(f"| {row['label']} | {row['values'][harness]['note']} |")
|
|
304
|
+
return "\n".join(lines) + "\n"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _capability_notes(config, harness):
|
|
308
|
+
"""Per-harness visual-evidence rungs and memory projection target.
|
|
309
|
+
|
|
310
|
+
Both are things leo:visual-verification and leo:memory would otherwise have
|
|
311
|
+
to guess at from inside a session. Written as prose rather than a literal
|
|
312
|
+
plugin-root token: tests/test_harness_mappings.py fails the build if the
|
|
313
|
+
Claude placeholder leaks into another harness's mapping.
|
|
314
|
+
"""
|
|
315
|
+
visual = config.get("visual", {}).get(harness)
|
|
316
|
+
target = config.get("memoryTarget", {}).get(harness)
|
|
317
|
+
if not visual and not target:
|
|
318
|
+
return ""
|
|
319
|
+
out = []
|
|
320
|
+
if visual:
|
|
321
|
+
out.append(
|
|
322
|
+
f"\nVisual evidence here: {visual}. When no rung answers, "
|
|
323
|
+
"leo:visual-verification requires the unverified-change warning in "
|
|
324
|
+
"place of a done report.\n"
|
|
325
|
+
)
|
|
326
|
+
if target:
|
|
327
|
+
out.append(
|
|
328
|
+
f"\nMemory projection here writes to {target}. Only global-scope "
|
|
329
|
+
"facts are projected — every per-user surface loads in every "
|
|
330
|
+
"repository, so repo facts would leak across projects; they reach "
|
|
331
|
+
"the model through the session context block instead. Leo's block "
|
|
332
|
+
"is marker-delimited; the rest of the file is untouched.\n"
|
|
333
|
+
)
|
|
334
|
+
return "".join(out)
|
|
335
|
+
|
|
336
|
+
|
|
191
337
|
def _mapping_docs(config):
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
338
|
+
"""One loop, not five hand-written blocks.
|
|
339
|
+
|
|
340
|
+
Everything the per-harness prose used to say is now either a capability
|
|
341
|
+
note or a shared paragraph, so a new harness gets a complete appendix by
|
|
342
|
+
answering the matrix rather than by someone remembering to write one.
|
|
343
|
+
"""
|
|
344
|
+
out = {}
|
|
345
|
+
for harness, rows in sorted(config["harnesses"].items()):
|
|
346
|
+
parts = [GENERATED, f"# {rows['title']} mapping\n\n"]
|
|
347
|
+
if rows.get("provider"):
|
|
348
|
+
parts.append(f"Provider: `{rows['provider']}`\n\n")
|
|
349
|
+
parts.append(_table(rows))
|
|
350
|
+
parts.append("\n")
|
|
351
|
+
parts.append(_capability_table(config, harness))
|
|
352
|
+
parts.append(_capability_notes(config, harness))
|
|
353
|
+
parts.append(_collapse_note(rows))
|
|
354
|
+
if harness == "opencode":
|
|
355
|
+
parts.append("\n" + _absent_tier_sentence(config) + "\n")
|
|
356
|
+
parts.append(_skill_notes(config, harness))
|
|
357
|
+
out[harness] = "".join(parts)
|
|
358
|
+
return out
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _absent_tier_sentence(config):
|
|
362
|
+
"""Derived, not asserted: the roles OpenCode does not register.
|
|
363
|
+
|
|
364
|
+
Hand-written, this sentence named `expert` and Fable directly and would
|
|
365
|
+
have gone quietly wrong the first time the tier table changed.
|
|
366
|
+
"""
|
|
197
367
|
opencode = config["harnesses"]["opencode"]
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"`model` and `reasoning_effort` explicitly. A model override in the user's prompt or "
|
|
211
|
-
"native `AGENTS.md` wins over these defaults.\n"
|
|
212
|
-
"\nRead-only is prompt-enforced here, not harness-enforced: the judge roles "
|
|
213
|
-
"(planner, investigator, reviewer, explore) are pasted prompts, so nothing stops a "
|
|
214
|
-
"subagent that ignores them from editing. Treat their read-only contract as a "
|
|
215
|
-
"convention, and never route work here that depends on it being a guarantee.\n"
|
|
216
|
-
+ _collapse_note(codex)
|
|
217
|
-
+ _skill_notes(config, "codex"),
|
|
218
|
-
"cursor": GENERATED
|
|
219
|
-
+ "# Cursor mapping\n\n"
|
|
220
|
-
+ _table(cursor)
|
|
221
|
-
+ "\n\nCursor plugin agents use `model: inherit`. Select the mapped model in Cursor before "
|
|
222
|
-
"starting a homogeneous tier batch; the plugin does not claim to enforce arbitrary "
|
|
223
|
-
"per-agent model names.\n"
|
|
224
|
-
+ _collapse_note(cursor)
|
|
225
|
-
+ _skill_notes(config, "cursor"),
|
|
226
|
-
"hermes": GENERATED
|
|
227
|
-
+ "# Hermes mapping\n\n"
|
|
228
|
-
+ f"Provider: `{hermes['provider']}`\n\n"
|
|
229
|
-
+ _table(hermes_rows)
|
|
230
|
-
+ "\n\nHermes native `delegate_task` has one configured delegation model. Group work into "
|
|
231
|
-
"homogeneous Kimi or GLM batches, switch the parent with `/model`, and set "
|
|
232
|
-
"`delegation.provider: openrouter` plus the matching `delegation.model` before spawning.\n"
|
|
233
|
-
"\nThis policy is NOT injected automatically here. Hermes accepts a `pre_llm_call` "
|
|
234
|
-
"hook but its runtime never invokes one, so the plugin registers `leo:using-leo` as "
|
|
235
|
-
"an ordinary skill instead — read it at the start of a session to load the policy. "
|
|
236
|
-
"Read-only is prompt-enforced only: the judge roles are pasted prompts, so their "
|
|
237
|
-
"read-only contract is a convention here, not a guarantee.\n"
|
|
238
|
-
+ _collapse_note(hermes_rows)
|
|
239
|
-
+ _skill_notes(config, "hermes"),
|
|
240
|
-
"opencode": GENERATED
|
|
241
|
-
+ "# OpenCode mapping\n\n"
|
|
242
|
-
+ f"Provider: `{opencode['provider']}`\n\n"
|
|
243
|
-
+ _table(opencode_rows)
|
|
244
|
-
+ "\n\nRoles register as native OpenCode agents (from `adapters/opencode/agents.json`, "
|
|
245
|
-
"generated from `config/models.json` and `roles/*.md`) and are spawned via the task tool "
|
|
246
|
-
"as subagents. There is no per-spawn model override on this harness: each agent always "
|
|
247
|
-
"runs its registered model, so `reviewer` always runs the full Opus-tier model — the "
|
|
248
|
-
"trivial-diff Sonnet-tier downscale does not apply here; every diff gets the full review.\n"
|
|
249
|
-
"\nRead-only is harness-enforced here, unlike Codex and Cursor: read-only roles carry a "
|
|
250
|
-
"generated `permission.edit: deny`, so an off-policy write attempt is refused by OpenCode "
|
|
251
|
-
"itself, not merely discouraged by the prompt. Write-capable agents additionally carry "
|
|
252
|
-
"coarse `rm -rf` bash denies as a stopgap for opencode#5894 (unconfirmed whether "
|
|
253
|
-
"`tool.execute.before` also intercepts subagent bash); the precise tripwire stays "
|
|
254
|
-
"`hooks/bash-guard.py` on the primary agent.\n"
|
|
255
|
-
"\nSkills are invoked by bare name here — `brainstorming`, `verification` — not with the "
|
|
256
|
-
"`leo:` prefix the policy above uses. The plugin registers the skills directory by path, so "
|
|
257
|
-
"OpenCode names each skill from its own `name:` frontmatter and applies no plugin "
|
|
258
|
-
"namespace. Read `leo:<skill>` anywhere in the policy as `<skill>` on this harness.\n"
|
|
259
|
-
"\nNo `EnterWorktree` tool exists here — use the worktrees skill's raw `git worktree` fallback for "
|
|
260
|
-
"isolated branch work. State reads and writes go through `python3 <plugin-root>/scripts/state.py` "
|
|
261
|
-
"(`get` / `merge` / `path`), same contract as every other harness. There is no Workflow tool "
|
|
262
|
-
"and no `cost-tiered-fix.js` here — a batch of independent tasks is fanned out as manual "
|
|
263
|
-
"parallel task-tool subagent spawns instead.\n"
|
|
264
|
-
+ _collapse_note(opencode_rows)
|
|
265
|
-
+ _skill_notes(config, "opencode"),
|
|
266
|
-
}
|
|
368
|
+
absent = list(opencode.get("absentTiers", ()))
|
|
369
|
+
if not absent:
|
|
370
|
+
return "Every tier is a distinct rung here."
|
|
371
|
+
roles = [r for r, tier, _ in _roles(config) if tier in absent]
|
|
372
|
+
tiers = " and ".join(t.title() for t in absent)
|
|
373
|
+
verb = "is" if len(absent) == 1 else "are"
|
|
374
|
+
listed = ", ".join(f"`{r}`" for r in roles)
|
|
375
|
+
return (
|
|
376
|
+
f"{tiers} {verb} not a real rung here, so {listed} "
|
|
377
|
+
f"{'is' if len(roles) == 1 else 'are'} not registered as an agent and "
|
|
378
|
+
"escalation caps at Opus."
|
|
379
|
+
)
|
|
267
380
|
|
|
268
381
|
|
|
269
382
|
def _payload_readme(config):
|
|
@@ -282,38 +395,57 @@ def _payload_readme(config):
|
|
|
282
395
|
return (
|
|
283
396
|
GENERATED
|
|
284
397
|
+ "\n# Leo's Agent\n\n"
|
|
285
|
-
"Leo's Agent is a portable
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"## Install\n\n"
|
|
292
|
-
"
|
|
398
|
+
"Leo's Agent is a portable operating policy with cost-tiered routing, "
|
|
399
|
+
"specialist roles, process skills, review discipline, and a narrow command guard. "
|
|
400
|
+
"This npm package is the **OpenCode** distribution; use the "
|
|
401
|
+
"[repository](https://github.com/foxhatleo/leos-agent) for Claude Code, Codex, Cursor, "
|
|
402
|
+
"and Hermes instructions.\n\n"
|
|
403
|
+
"Supported hosts are macOS, Linux, and WSL with Python 3.9+; native Windows is unsupported.\n\n"
|
|
404
|
+
"## Install and update\n\n"
|
|
405
|
+
"```sh\nopencode plugin leos-agent --global\n```\n\n"
|
|
406
|
+
"On builds without that subcommand, add `leos-agent` to the `plugin` array in "
|
|
407
|
+
"`~/.config/opencode/opencode.json` or `opencode.jsonc`:\n\n"
|
|
293
408
|
'```json\n{ "$schema": "https://opencode.ai/config.json", "plugin": ["leos-agent"] }\n```\n\n'
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
|
|
409
|
+
"Run `opencode auth login` and choose OpenRouter before using the mapped models; Leo "
|
|
410
|
+
"never writes provider credentials. Update with "
|
|
411
|
+
"`opencode plugin leos-agent --global --force`, then start a new session. OpenCode "
|
|
412
|
+
"currently has no plugin removal command; remove the `leos-agent` configuration entry "
|
|
413
|
+
"to uninstall.\n\n"
|
|
414
|
+
"The plugin registers generated shadow skills (`leo-<name>`) and namespaced "
|
|
415
|
+
f"`leo-<role>` agents ({len(json.loads(_opencode_agents(config)))} generated definitions) "
|
|
416
|
+
"from `adapters/opencode/agents.json`, "
|
|
417
|
+
"then injects the operating policy through OpenCode's configuration. Invoke its shadow "
|
|
418
|
+
"skill as `leo-using-leo`. If a skill is absent, "
|
|
419
|
+
"run `opencode debug skill`; its `location` should be an `opencode-skills-<hash>/leo-<name>/` "
|
|
420
|
+
"directory under machine-local state, not a hand-written package path.\n\n"
|
|
421
|
+
"## MCP and durable state\n\n"
|
|
422
|
+
"Use `leo-setup` to inspect or explicitly configure MCP services: `connectors` reports "
|
|
423
|
+
"without writing, while `connect` and `apply` make only reviewed, harness-owned changes. "
|
|
424
|
+
"Vendor connectors are never installed automatically and OAuth stays in OpenCode. "
|
|
425
|
+
"Slack, Gmail, Drive, and providers without dynamic registration remain manual-only.\n\n"
|
|
426
|
+
"Uninstall preserves `${LEOS_AGENT_LOCAL_PATH:-$HOME/.leos-agent-local}`. Before a full "
|
|
427
|
+
"purge, export or copy that directory; only then explicitly remove it. For a 7.0 recovery, "
|
|
428
|
+
"move old `LEOS_AGENT_PATH/local/` data there, rename the variable, restart, and run "
|
|
429
|
+
"`leo-doctor`.\n\n"
|
|
300
430
|
"## Model tiers\n\n"
|
|
301
431
|
"Tier names describe the kind of work, not a fixed provider model.\n\n"
|
|
302
|
-
+ _table({tier: opencode[tier] for tier in
|
|
303
|
-
+ "\n\
|
|
304
|
-
|
|
305
|
-
"`scripts/render_adapters.py`.\n\n"
|
|
432
|
+
+ _table({tier: opencode[tier] for tier in TIERS})
|
|
433
|
+
+ "\n\n"
|
|
434
|
+
+ _absent_tier_sentence(config)
|
|
435
|
+
+ " Retier by editing `config/models.json` and re-running `scripts/render_adapters.py`.\n\n"
|
|
306
436
|
"## Links\n\n"
|
|
307
|
-
"- [Repository and
|
|
437
|
+
"- [Repository, contributing, and security policy](https://github.com/foxhatleo/leos-agent)\n"
|
|
438
|
+
"- [GitHub Releases](https://github.com/foxhatleo/leos-agent/releases)\n"
|
|
308
439
|
"- [Operating policy](https://github.com/foxhatleo/leos-agent/blob/main/plugins/leo/skills/using-leo/SKILL.md)\n\n"
|
|
309
440
|
"MIT licensed.\n"
|
|
310
441
|
)
|
|
311
442
|
|
|
312
443
|
|
|
313
444
|
def render(config):
|
|
445
|
+
_validate(config)
|
|
314
446
|
outputs = {}
|
|
315
|
-
for role, tier in
|
|
316
|
-
claude, cursor = _agent_docs(role, tier, config)
|
|
447
|
+
for role, tier, access in _roles(config):
|
|
448
|
+
claude, cursor = _agent_docs(role, tier, access, config)
|
|
317
449
|
# Claude Code agents MUST live at the conventional agents/ path: a
|
|
318
450
|
# manifest "agents" array of file paths validates but silently loads
|
|
319
451
|
# zero agents, and the conventional directory auto-loads.
|