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.
Files changed (35) hide show
  1. package/README.md +43 -0
  2. package/adapters/cursor/agents/executor.md +1 -1
  3. package/adapters/cursor/agents/implementer.md +1 -1
  4. package/adapters/cursor/agents/reviewer.md +1 -0
  5. package/adapters/opencode/agents.json +3 -3
  6. package/adapters/opencode/plugin.js +131 -29
  7. package/config/models.json +379 -33
  8. package/hooks/session-start.py +27 -0
  9. package/package.json +18 -4
  10. package/roles/executor.md +1 -1
  11. package/roles/implementer.md +1 -1
  12. package/roles/reviewer.md +1 -0
  13. package/scripts/doctor.py +284 -0
  14. package/scripts/ghreview.py +554 -0
  15. package/scripts/memory.py +705 -0
  16. package/scripts/render_adapters.py +244 -97
  17. package/scripts/resolve_attach_target.py +357 -0
  18. package/scripts/setup.py +161 -0
  19. package/skills/delegation/SKILL.md +1 -1
  20. package/skills/doctor/SKILL.md +105 -0
  21. package/skills/freshness/SKILL.md +118 -0
  22. package/skills/memory/SKILL.md +144 -0
  23. package/skills/resolve-ticket/SKILL.md +269 -0
  24. package/skills/review-pr/SKILL.md +317 -0
  25. package/skills/setup/SKILL.md +85 -0
  26. package/skills/using-leo/SKILL.md +8 -1
  27. package/skills/using-leo/references/claude-mapping.md +22 -1
  28. package/skills/using-leo/references/codex-mapping.md +17 -7
  29. package/skills/using-leo/references/cursor-mapping.md +18 -6
  30. package/skills/using-leo/references/hermes-mapping.md +17 -7
  31. package/skills/using-leo/references/opencode-mapping.md +16 -8
  32. package/skills/verification/SKILL.md +7 -0
  33. package/skills/visual-verification/SKILL.md +114 -0
  34. package/skills/watch-review/SKILL.md +125 -0
  35. 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:]))