leos-agent 6.1.0 → 6.3.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 +43 -0
- package/adapters/cursor/agents/executor.md +1 -1
- package/adapters/cursor/agents/implementer.md +1 -1
- package/adapters/cursor/agents/reviewer.md +1 -0
- package/adapters/opencode/agents.json +3 -3
- package/adapters/opencode/plugin.js +131 -29
- package/config/models.json +379 -33
- package/hooks/session-start.py +27 -0
- package/package.json +18 -4
- package/roles/executor.md +1 -1
- package/roles/implementer.md +1 -1
- package/roles/reviewer.md +1 -0
- package/scripts/doctor.py +284 -0
- package/scripts/ghreview.py +554 -0
- package/scripts/memory.py +705 -0
- package/scripts/render_adapters.py +244 -97
- package/scripts/resolve_attach_target.py +357 -0
- package/scripts/setup.py +161 -0
- package/skills/delegation/SKILL.md +1 -1
- package/skills/doctor/SKILL.md +105 -0
- package/skills/freshness/SKILL.md +118 -0
- package/skills/memory/SKILL.md +144 -0
- package/skills/resolve-ticket/SKILL.md +269 -0
- package/skills/review-pr/SKILL.md +317 -0
- package/skills/setup/SKILL.md +85 -0
- package/skills/using-leo/SKILL.md +8 -1
- package/skills/using-leo/references/claude-mapping.md +22 -1
- package/skills/using-leo/references/codex-mapping.md +17 -7
- package/skills/using-leo/references/cursor-mapping.md +18 -6
- package/skills/using-leo/references/hermes-mapping.md +17 -7
- package/skills/using-leo/references/opencode-mapping.md +16 -8
- package/skills/verification/SKILL.md +7 -0
- package/skills/visual-verification/SKILL.md +114 -0
- package/skills/watch-review/SKILL.md +125 -0
- package/skills/writing-skills/SKILL.md +134 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""doctor: report how Leo's Agent is wired on this machine.
|
|
3
|
+
|
|
4
|
+
This script answers only what disk and environment can prove. It deliberately
|
|
5
|
+
does NOT claim the policy reached the model: a hook can be present, executable,
|
|
6
|
+
and correctly listed, and still have failed open this session. Only the running
|
|
7
|
+
agent can see its own context, so leo:doctor pairs this output with three
|
|
8
|
+
questions the model answers itself.
|
|
9
|
+
|
|
10
|
+
The breadcrumb logs are reported as history with unknown provenance, never as a
|
|
11
|
+
verdict about this session. They carry no timestamps and the test suite drives
|
|
12
|
+
the failure paths deliberately, so "the log has errors" proves nothing on its
|
|
13
|
+
own.
|
|
14
|
+
|
|
15
|
+
doctor.py human-readable report
|
|
16
|
+
doctor.py --json the same facts as JSON
|
|
17
|
+
doctor.py --harness <name> state the harness instead of detecting it
|
|
18
|
+
|
|
19
|
+
Exit code is 0 unless the payload itself cannot be found.
|
|
20
|
+
"""
|
|
21
|
+
import importlib.util
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
PAYLOAD = os.path.dirname(_HERE)
|
|
28
|
+
TIERS = ("fable", "opus", "sonnet", "haiku")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
HARNESS_ENV = ("CURSOR_PLUGIN_ROOT", "CURSOR_VERSION", "PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _known_harnesses():
|
|
35
|
+
models = _read_json(os.path.join(PAYLOAD, "config", "models.json")) or {}
|
|
36
|
+
return set(models.get("harnesses") or ())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _detect_harness(argv=()):
|
|
40
|
+
"""Three signals, most explicit first; never a guess.
|
|
41
|
+
|
|
42
|
+
The env-var rules still live in hooks/session-start.py and are reused
|
|
43
|
+
rather than re-derived, because their ordering carries two subtleties a
|
|
44
|
+
second implementation gets wrong: Cursor must be tested first because it
|
|
45
|
+
sets more than one variable, and the absence of CLAUDE_PLUGIN_ROOT is not
|
|
46
|
+
a Codex signal. The filename is hyphenated and therefore not importable by
|
|
47
|
+
name, so it loads by path — the same technique hooks/cursor-guard.py uses.
|
|
48
|
+
|
|
49
|
+
What is new is that the delegation is *gated*. That function's final branch
|
|
50
|
+
returns "claude" as a default, which is right for a hook that only ever
|
|
51
|
+
runs on the three hook harnesses. doctor ships to five. Hermes and OpenCode
|
|
52
|
+
run no hook and export no plugin-root variable, so doctor inherited the
|
|
53
|
+
default and reported `claude` on both — printing Claude's tier table and
|
|
54
|
+
listing four Claude-only skills as available on harnesses that have none of
|
|
55
|
+
them. Absence of every marker means unknown, and unknown is reported.
|
|
56
|
+
"""
|
|
57
|
+
known = _known_harnesses()
|
|
58
|
+
|
|
59
|
+
# 1. Stated outright. leo:doctor tells the agent to pass the harness it can
|
|
60
|
+
# read off its own mapping heading. Validated against the config, so a
|
|
61
|
+
# typo degrades to detection rather than inventing a harness.
|
|
62
|
+
argv = list(argv)
|
|
63
|
+
for index, arg in enumerate(argv):
|
|
64
|
+
name = None
|
|
65
|
+
if arg == "--harness" and index + 1 < len(argv):
|
|
66
|
+
name = argv[index + 1]
|
|
67
|
+
elif arg.startswith("--harness="):
|
|
68
|
+
name = arg.split("=", 1)[1]
|
|
69
|
+
if name and name in known:
|
|
70
|
+
return name, "--harness"
|
|
71
|
+
|
|
72
|
+
# 2. A positive signal the two hookless harnesses set for themselves at
|
|
73
|
+
# registration (__init__.py and adapters/opencode/plugin.js).
|
|
74
|
+
declared = os.environ.get("LEOS_AGENT_HARNESS")
|
|
75
|
+
if declared and declared in known:
|
|
76
|
+
return declared, "env (LEOS_AGENT_HARNESS)"
|
|
77
|
+
|
|
78
|
+
# 3. The env-var rules, unchanged — but only when a marker actually exists.
|
|
79
|
+
if not any(os.environ.get(var) for var in HARNESS_ENV):
|
|
80
|
+
return "unknown", "no signal"
|
|
81
|
+
|
|
82
|
+
path = os.path.join(PAYLOAD, "hooks", "session-start.py")
|
|
83
|
+
try:
|
|
84
|
+
spec = importlib.util.spec_from_file_location("leo_session_start", path)
|
|
85
|
+
module = importlib.util.module_from_spec(spec)
|
|
86
|
+
spec.loader.exec_module(module)
|
|
87
|
+
return module._detect_harness(), "hooks/session-start.py"
|
|
88
|
+
except Exception:
|
|
89
|
+
if os.environ.get("CURSOR_PLUGIN_ROOT") or os.environ.get("CURSOR_VERSION"):
|
|
90
|
+
return "cursor", "env"
|
|
91
|
+
if os.environ.get("PLUGIN_ROOT"):
|
|
92
|
+
return "codex", "env"
|
|
93
|
+
return "claude", "env"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_json(path):
|
|
97
|
+
try:
|
|
98
|
+
with open(path, encoding="utf-8") as fh:
|
|
99
|
+
return json.load(fh)
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _local_root():
|
|
105
|
+
return os.environ.get("LEOS_AGENT_LOCAL_PATH") or os.path.join(
|
|
106
|
+
os.path.expanduser("~"), ".leos-agent-local"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _memory_report():
|
|
111
|
+
try:
|
|
112
|
+
sys.path.insert(0, _HERE)
|
|
113
|
+
import memory
|
|
114
|
+
|
|
115
|
+
root = memory.memory_root()
|
|
116
|
+
if not os.path.isdir(root):
|
|
117
|
+
return {"store": root, "facts": 0, "present": False, "targets": []}
|
|
118
|
+
index = memory._load_index() or {"facts": []}
|
|
119
|
+
targets = [
|
|
120
|
+
{"harness": h, "path": f, "present": os.path.exists(f),
|
|
121
|
+
"projected": os.path.exists(f) and memory.BEGIN in _slurp(f)}
|
|
122
|
+
for h, gate, f, _, _ in memory.projection_targets()
|
|
123
|
+
if os.path.isdir(gate)
|
|
124
|
+
]
|
|
125
|
+
return {"store": root, "facts": len(index["facts"]), "present": True,
|
|
126
|
+
"targets": targets}
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
return {"store": _local_root(), "error": f"{type(exc).__name__}: {exc}"}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _slurp(path):
|
|
132
|
+
try:
|
|
133
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
134
|
+
return fh.read()
|
|
135
|
+
except OSError:
|
|
136
|
+
return ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _breadcrumbs():
|
|
140
|
+
out = {}
|
|
141
|
+
for name in ("session-start.log", "hermes-policy.log", "opencode-guard.log"):
|
|
142
|
+
path = os.path.join(_local_root(), name)
|
|
143
|
+
if not os.path.exists(path):
|
|
144
|
+
continue
|
|
145
|
+
lines = [l for l in _slurp(path).splitlines() if l.strip()]
|
|
146
|
+
if lines:
|
|
147
|
+
out[name] = {"entries": len(lines), "newest": lines[-1][:120]}
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _skills():
|
|
152
|
+
shipped = {}
|
|
153
|
+
for root in ("skills", "skills-claude"):
|
|
154
|
+
directory = os.path.join(PAYLOAD, root)
|
|
155
|
+
shipped[root] = sorted(
|
|
156
|
+
name for name in os.listdir(directory)
|
|
157
|
+
if os.path.isfile(os.path.join(directory, name, "SKILL.md"))
|
|
158
|
+
) if os.path.isdir(directory) else []
|
|
159
|
+
return shipped
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def collect(argv=()):
|
|
163
|
+
harness, source = _detect_harness(argv)
|
|
164
|
+
manifest = _read_json(os.path.join(PAYLOAD, ".claude-plugin", "plugin.json")) or {}
|
|
165
|
+
models = _read_json(os.path.join(PAYLOAD, "config", "models.json")) or {}
|
|
166
|
+
config = (models.get("harnesses") or {}).get(harness) or {}
|
|
167
|
+
hook = os.path.join(PAYLOAD, "hooks", "session-start.py")
|
|
168
|
+
local = _local_root()
|
|
169
|
+
skills = _skills()
|
|
170
|
+
claude_only = set((models.get("skills") or {}).get("claudeOnly") or ())
|
|
171
|
+
excluded = set(((models.get("skills") or {}).get("exclude") or {}).get(harness) or ())
|
|
172
|
+
|
|
173
|
+
registered = [n for n in skills["skills"] if n not in excluded]
|
|
174
|
+
if harness == "claude":
|
|
175
|
+
registered += skills["skills-claude"]
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"harness": {"value": harness, "source": source},
|
|
179
|
+
"payload": {"path": PAYLOAD, "version": manifest.get("version")},
|
|
180
|
+
"bootstrap": {
|
|
181
|
+
"hook": hook,
|
|
182
|
+
"present": os.path.isfile(hook),
|
|
183
|
+
"executable": os.access(hook, os.X_OK),
|
|
184
|
+
},
|
|
185
|
+
"tiers": {
|
|
186
|
+
tier: {"model": (config.get(tier) or {}).get("model"),
|
|
187
|
+
"effort": (config.get(tier) or {}).get("effort")}
|
|
188
|
+
for tier in TIERS
|
|
189
|
+
},
|
|
190
|
+
"local_state": {
|
|
191
|
+
"path": local,
|
|
192
|
+
"present": os.path.isdir(local),
|
|
193
|
+
"writable": os.access(local, os.W_OK) if os.path.isdir(local) else None,
|
|
194
|
+
},
|
|
195
|
+
"memory": _memory_report(),
|
|
196
|
+
"skills": {
|
|
197
|
+
"shipped_portable": skills["skills"],
|
|
198
|
+
"shipped_claude_only": skills["skills-claude"],
|
|
199
|
+
"expected_here": sorted(registered),
|
|
200
|
+
"excluded_here": sorted(excluded | (set() if harness == "claude" else claude_only)),
|
|
201
|
+
},
|
|
202
|
+
"breadcrumbs": _breadcrumbs(),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _render(data):
|
|
207
|
+
lines = ["leo doctor", ""]
|
|
208
|
+
|
|
209
|
+
def row(label, value, source):
|
|
210
|
+
# A long path must not shove the source column off the line: overflow
|
|
211
|
+
# drops the source onto its own indented continuation instead.
|
|
212
|
+
value = str(value)
|
|
213
|
+
if len(value) > 44:
|
|
214
|
+
lines.append(f" {label:<16}{value}")
|
|
215
|
+
lines.append(f" {'':<16}{'':<44}{source}")
|
|
216
|
+
else:
|
|
217
|
+
lines.append(f" {label:<16}{value:<44}{source}")
|
|
218
|
+
|
|
219
|
+
harness = data["harness"]
|
|
220
|
+
row("harness", harness["value"], f"detected via {harness['source']}")
|
|
221
|
+
row("payload", f"{data['payload']['path']}", "disk")
|
|
222
|
+
row("version", data["payload"]["version"] or "unknown", "disk")
|
|
223
|
+
|
|
224
|
+
boot = data["bootstrap"]
|
|
225
|
+
state = "present" if boot["present"] else "MISSING"
|
|
226
|
+
if boot["present"] and not boot["executable"]:
|
|
227
|
+
state += ", not executable"
|
|
228
|
+
row("bootstrap", state, "disk")
|
|
229
|
+
|
|
230
|
+
tiers = " · ".join(
|
|
231
|
+
f"{t.capitalize()} {v['model']}" + (f"/{v['effort']}" if v["effort"] else "")
|
|
232
|
+
for t, v in data["tiers"].items() if v["model"]
|
|
233
|
+
)
|
|
234
|
+
row("tiers", tiers or "no mapping for this harness", "config/models.json")
|
|
235
|
+
|
|
236
|
+
local = data["local_state"]
|
|
237
|
+
row("local state", local["path"],
|
|
238
|
+
"disk: " + ("writable" if local["writable"] else
|
|
239
|
+
"present" if local["present"] else "not created yet"))
|
|
240
|
+
|
|
241
|
+
mem = data["memory"]
|
|
242
|
+
if mem.get("error"):
|
|
243
|
+
row("memory", mem["error"], "disk")
|
|
244
|
+
elif not mem["present"]:
|
|
245
|
+
row("memory", "no store yet", "disk")
|
|
246
|
+
else:
|
|
247
|
+
row("memory", f"{mem['facts']} facts", "disk")
|
|
248
|
+
for target in mem["targets"]:
|
|
249
|
+
mark = "projected" if target["projected"] else "not projected"
|
|
250
|
+
row("", f" {target['harness']}: {mark}", target["path"])
|
|
251
|
+
|
|
252
|
+
skills = data["skills"]
|
|
253
|
+
row("skills shipped",
|
|
254
|
+
f"{len(skills['shipped_portable'])} portable · "
|
|
255
|
+
f"{len(skills['shipped_claude_only'])} claude-only", "disk")
|
|
256
|
+
row("expected here", f"{len(skills['expected_here'])} skills", "disk + config")
|
|
257
|
+
if skills["excluded_here"]:
|
|
258
|
+
row("not available", ", ".join(skills["excluded_here"]), "config/models.json")
|
|
259
|
+
|
|
260
|
+
if data["breadcrumbs"]:
|
|
261
|
+
lines.append("")
|
|
262
|
+
lines.append(" breadcrumbs (history, provenance unknown — not this session):")
|
|
263
|
+
for name, info in data["breadcrumbs"].items():
|
|
264
|
+
lines.append(f" {name}: {info['entries']} entries, newest: {info['newest']}")
|
|
265
|
+
|
|
266
|
+
lines += [
|
|
267
|
+
"",
|
|
268
|
+
" This script proves what shipped to disk. It cannot prove the policy",
|
|
269
|
+
" reached the model — answer the three context questions in leo:doctor.",
|
|
270
|
+
]
|
|
271
|
+
return "\n".join(lines)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(argv):
|
|
275
|
+
data = collect(argv)
|
|
276
|
+
if "--json" in argv:
|
|
277
|
+
print(json.dumps(data, indent=1, sort_keys=True))
|
|
278
|
+
else:
|
|
279
|
+
print(_render(data))
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
sys.exit(main(sys.argv[1:]))
|