leos-agent 7.0.0 → 10.2.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 (96) hide show
  1. package/README.md +544 -28
  2. package/commands/handoff.md +11 -0
  3. package/commands/handon.md +10 -0
  4. package/commands/review-pr.md +9 -0
  5. package/commands-claude/watch-review.md +9 -0
  6. package/index.js +12 -0
  7. package/package.json +29 -14
  8. package/payload/codex-agents/leo-executor.toml +36 -0
  9. package/payload/codex-agents/leo-runner.toml +28 -0
  10. package/rules/preferences.md +97 -0
  11. package/scripts/check.py +244 -0
  12. package/scripts/ghreview.py +17 -3
  13. package/scripts/handoff.py +183 -0
  14. package/scripts/leo-install.py +509 -0
  15. package/scripts/measure_context.py +113 -0
  16. package/scripts/publish-npm.py +138 -0
  17. package/scripts/watch_review.py +169 -0
  18. package/skills/doctor/SKILL.md +73 -115
  19. package/skills/doctor/agents/openai.yaml +5 -0
  20. package/skills/handoff/SKILL.md +99 -0
  21. package/skills/handoff/agents/openai.yaml +5 -0
  22. package/skills/handon/SKILL.md +61 -0
  23. package/skills/install/SKILL.md +79 -0
  24. package/skills/install/agents/openai.yaml +5 -0
  25. package/skills/review-pr/SKILL.md +59 -318
  26. package/skills/review-pr/reference/lenses.md +67 -0
  27. package/skills/review-pr/reference/procedure.md +348 -0
  28. package/skills-claude/attach-pr/SKILL.md +178 -0
  29. package/skills-claude/watch-review/SKILL.md +91 -0
  30. package/adapters/cursor/agents/executor.md +0 -17
  31. package/adapters/cursor/agents/expert.md +0 -70
  32. package/adapters/cursor/agents/explore.md +0 -16
  33. package/adapters/cursor/agents/implementer.md +0 -18
  34. package/adapters/cursor/agents/investigator.md +0 -18
  35. package/adapters/cursor/agents/planner.md +0 -28
  36. package/adapters/cursor/agents/review-lens.md +0 -22
  37. package/adapters/cursor/agents/reviewer.md +0 -34
  38. package/adapters/opencode/agents.json +0 -105
  39. package/adapters/opencode/plugin.js +0 -576
  40. package/config/MCP_PINS.md +0 -17
  41. package/config/models.json +0 -676
  42. package/hooks/bash-guard.py +0 -583
  43. package/hooks/cursor-guard.py +0 -84
  44. package/hooks/hooks-cursor.json +0 -11
  45. package/hooks/hooks.json +0 -20
  46. package/hooks/session-start.py +0 -148
  47. package/roles/executor.md +0 -15
  48. package/roles/expert.md +0 -67
  49. package/roles/explore.md +0 -13
  50. package/roles/implementer.md +0 -16
  51. package/roles/investigator.md +0 -15
  52. package/roles/planner.md +0 -25
  53. package/roles/review-lens.md +0 -20
  54. package/roles/reviewer.md +0 -31
  55. package/scripts/doctor.py +0 -520
  56. package/scripts/jsonc_bridge.cjs +0 -23
  57. package/scripts/memory.py +0 -744
  58. package/scripts/render_adapters.py +0 -508
  59. package/scripts/setup.py +0 -1753
  60. package/settings.json +0 -7
  61. package/skills/.gitkeep +0 -0
  62. package/skills/brainstorming/SKILL.md +0 -111
  63. package/skills/debugging/SKILL.md +0 -100
  64. package/skills/delegation/SKILL.md +0 -143
  65. package/skills/executing-plans/SKILL.md +0 -117
  66. package/skills/finishing-a-branch/SKILL.md +0 -125
  67. package/skills/freshness/SKILL.md +0 -131
  68. package/skills/memory/SKILL.md +0 -154
  69. package/skills/resolve-ticket/SKILL.md +0 -275
  70. package/skills/setup/SKILL.md +0 -199
  71. package/skills/setup/agents/openai.yaml +0 -5
  72. package/skills/test-first/SKILL.md +0 -92
  73. package/skills/using-leo/SKILL.md +0 -101
  74. package/skills/using-leo/references/claude-mapping.md +0 -33
  75. package/skills/using-leo/references/codex-mapping.md +0 -33
  76. package/skills/using-leo/references/cursor-mapping.md +0 -35
  77. package/skills/using-leo/references/hermes-mapping.md +0 -37
  78. package/skills/using-leo/references/opencode-mapping.md +0 -39
  79. package/skills/verification/SKILL.md +0 -110
  80. package/skills/visual-verification/SKILL.md +0 -115
  81. package/skills/watch-review/SKILL.md +0 -128
  82. package/skills/watch-review/agents/openai.yaml +0 -5
  83. package/skills/worktrees/SKILL.md +0 -131
  84. package/skills/writing-plans/SKILL.md +0 -97
  85. package/skills/writing-skills/SKILL.md +0 -141
  86. package/vendor/jsonc-parser-3.3.1/LICENSE.md +0 -21
  87. package/vendor/jsonc-parser-3.3.1/README.md +0 -26
  88. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +0 -201
  89. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +0 -275
  90. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +0 -682
  91. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +0 -456
  92. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +0 -42
  93. package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +0 -351
  94. package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +0 -194
  95. package/vendor/jsonc-parser-3.3.1/package.json +0 -37
  96. package/workflows/cost-tiered-fix.js +0 -287
package/scripts/doctor.py DELETED
@@ -1,520 +0,0 @@
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
- Most breadcrumb logs are reported as history with unknown provenance, never as
11
- a verdict about this session. Some legacy entries carry no timestamps and the
12
- test suite drives failure paths deliberately, so "the log has errors" usually
13
- proves nothing on its own. `opencode-skills.log` is the capability exception:
14
- its presence records a namespaced-skill registration failure and is reported
15
- as degraded until it is intentionally cleared after repair.
16
-
17
- doctor.py human-readable report
18
- doctor.py --json the same facts as JSON
19
- doctor.py --harness <name> state the harness instead of detecting it
20
-
21
- Exit code is nonzero when required payload/bootstrap artifacts are missing or
22
- invalid, or the running Python is unsupported. Degraded historical warnings
23
- remain report-only.
24
- """
25
- import importlib.util
26
- import json
27
- import os
28
- import re
29
- import sys
30
-
31
- _HERE = os.path.dirname(os.path.abspath(__file__))
32
- PAYLOAD = os.path.dirname(_HERE)
33
- TIERS = ("fable", "opus", "sonnet", "haiku")
34
- MIN_PYTHON = (3, 9)
35
- EXPECTED_HARNESSES = {"claude", "codex", "cursor", "hermes", "opencode"}
36
- SEMVER_RE = re.compile(
37
- r"^(0|[1-9]\d*)\."
38
- r"(0|[1-9]\d*)\."
39
- r"(0|[1-9]\d*)"
40
- r"(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\."
41
- r"(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?"
42
- r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$"
43
- )
44
-
45
-
46
- HARNESS_ENV = ("CURSOR_PLUGIN_ROOT", "CURSOR_VERSION", "PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT")
47
-
48
-
49
- def _known_harnesses():
50
- models = _read_json(os.path.join(PAYLOAD, "config", "models.json")) or {}
51
- harnesses = models.get("harnesses") if isinstance(models, dict) else None
52
- return set(harnesses) if isinstance(harnesses, dict) else set()
53
-
54
-
55
- def _detect_harness(argv=()):
56
- """Three signals, most explicit first; never a guess.
57
-
58
- The env-var rules still live in hooks/session-start.py and are reused
59
- rather than re-derived, because their ordering carries two subtleties a
60
- second implementation gets wrong: Cursor must be tested first because it
61
- sets more than one variable, and the absence of CLAUDE_PLUGIN_ROOT is not
62
- a Codex signal. The filename is hyphenated and therefore not importable by
63
- name, so it loads by path — the same technique hooks/cursor-guard.py uses.
64
-
65
- What is new is that the delegation is *gated*. That function's final branch
66
- returns "claude" as a default, which is right for a hook that only ever
67
- runs on the three hook harnesses. doctor ships to five. Hermes and OpenCode
68
- run no hook and export no plugin-root variable, so doctor inherited the
69
- default and reported `claude` on both — printing Claude's tier table and
70
- listing four Claude-only skills as available on harnesses that have none of
71
- them. Absence of every marker means unknown, and unknown is reported.
72
- """
73
- known = _known_harnesses()
74
-
75
- # 1. Stated outright. leo:doctor tells the agent to pass the harness it can
76
- # read off its own mapping heading. Validated against the config, so a
77
- # typo degrades to detection rather than inventing a harness.
78
- argv = list(argv)
79
- for index, arg in enumerate(argv):
80
- name = None
81
- if arg == "--harness" and index + 1 < len(argv):
82
- name = argv[index + 1]
83
- elif arg.startswith("--harness="):
84
- name = arg.split("=", 1)[1]
85
- if name and name in known:
86
- return name, "--harness"
87
-
88
- # 2. A positive signal the two hookless harnesses set for themselves at
89
- # registration (__init__.py and adapters/opencode/plugin.js).
90
- declared = os.environ.get("LEOS_AGENT_HARNESS")
91
- if declared and declared in known:
92
- return declared, "env (LEOS_AGENT_HARNESS)"
93
-
94
- # 3. The env-var rules, unchanged — but only when a marker actually exists.
95
- if not any(os.environ.get(var) for var in HARNESS_ENV):
96
- return "unknown", "no signal"
97
-
98
- path = os.path.join(PAYLOAD, "hooks", "session-start.py")
99
- try:
100
- spec = importlib.util.spec_from_file_location("leo_session_start", path)
101
- module = importlib.util.module_from_spec(spec)
102
- spec.loader.exec_module(module)
103
- return module._detect_harness(), "hooks/session-start.py"
104
- except Exception:
105
- if os.environ.get("CURSOR_PLUGIN_ROOT") or os.environ.get("CURSOR_VERSION"):
106
- return "cursor", "env"
107
- if os.environ.get("PLUGIN_ROOT"):
108
- return "codex", "env"
109
- return "claude", "env"
110
-
111
-
112
- def _read_json(path):
113
- try:
114
- with open(path, encoding="utf-8") as fh:
115
- return json.load(fh)
116
- except Exception:
117
- return None
118
-
119
-
120
- def _manifest_valid(manifest):
121
- """Validate the required portable manifest fields doctor reports."""
122
- if not isinstance(manifest, dict):
123
- return False
124
- for key in ("name", "description", "homepage", "repository", "license"):
125
- if not isinstance(manifest.get(key), str) or not manifest[key].strip():
126
- return False
127
- version = manifest.get("version")
128
- if not isinstance(version, str) or SEMVER_RE.fullmatch(version) is None:
129
- return False
130
- author = manifest.get("author")
131
- if not isinstance(author, dict) or not isinstance(author.get("name"), str) \
132
- or not author["name"].strip():
133
- return False
134
- skills = manifest.get("skills")
135
- return (isinstance(skills, list) and bool(skills) and
136
- all(isinstance(item, str) and item.strip() for item in skills))
137
-
138
-
139
- def _models_valid(models):
140
- """Apply structural checks and the canonical renderer validation.
141
-
142
- The renderer owns cross-field coherence, while these checks reject
143
- skeletal documents that happen not to exercise one of its relationships.
144
- Doctor must never call either shape healthy merely because it contains a
145
- ``harnesses`` object.
146
- """
147
- if not isinstance(models, dict) or models.get("schemaVersion") != 4:
148
- return False
149
- for key in ("roles", "harnesses", "skills", "mcp", "memoryTarget", "visual"):
150
- if not isinstance(models.get(key), dict) or not models[key]:
151
- return False
152
- capabilities = models.get("capabilities")
153
- if not isinstance(capabilities, list) or not capabilities:
154
- return False
155
- harnesses = models["harnesses"]
156
- if set(harnesses) != EXPECTED_HARNESSES:
157
- return False
158
- for rows in harnesses.values():
159
- if not isinstance(rows, dict):
160
- return False
161
- for tier in TIERS:
162
- mapping = rows.get(tier)
163
- if (not isinstance(mapping, dict) or
164
- not isinstance(mapping.get("model"), str) or
165
- not mapping["model"].strip()):
166
- return False
167
- if "effort" in mapping and (not isinstance(mapping["effort"], str) or
168
- not mapping["effort"].strip()):
169
- return False
170
- for spec in models["roles"].values():
171
- if (not isinstance(spec, dict) or spec.get("tier") not in TIERS or
172
- spec.get("access") not in ("read-only", "write")):
173
- return False
174
- for row in capabilities:
175
- if (not isinstance(row, dict) or not isinstance(row.get("key"), str) or
176
- not isinstance(row.get("modes"), list) or not row["modes"] or
177
- not isinstance(row.get("values"), dict)):
178
- return False
179
-
180
- validator_path = os.path.join(PAYLOAD, "scripts", "render_adapters.py")
181
- try:
182
- spec = importlib.util.spec_from_file_location("leo_doctor_models_validator",
183
- validator_path)
184
- validator = importlib.util.module_from_spec(spec)
185
- spec.loader.exec_module(validator)
186
- validator._validate(models)
187
- except Exception:
188
- return False
189
- return True
190
-
191
-
192
- def _local_root():
193
- return os.environ.get("LEOS_AGENT_LOCAL_PATH") or os.path.join(
194
- os.path.expanduser("~"), ".leos-agent-local"
195
- )
196
-
197
-
198
- def _memory_report():
199
- try:
200
- sys.path.insert(0, _HERE)
201
- import memory
202
-
203
- root = memory.memory_root()
204
- hermes_enabled = memory.hermes_enabled()
205
- hermes_target = os.path.join(memory.hermes_home(), "SOUL.md")
206
- hermes_projection = {
207
- "enabled": hermes_enabled,
208
- "path": hermes_target,
209
- "status": (
210
- "disabled" if not hermes_enabled else
211
- "projected" if (os.path.isfile(hermes_target) and
212
- memory.BEGIN in _slurp(hermes_target)) else
213
- "not projected"
214
- ),
215
- }
216
- if not os.path.isdir(root):
217
- return {"store": root, "facts": 0, "present": False, "targets": [],
218
- "hermes_projection": hermes_projection}
219
- index = memory._load_index() or {"facts": []}
220
- targets = [
221
- {"harness": h, "path": f, "present": os.path.exists(f),
222
- "projected": os.path.exists(f) and memory.BEGIN in _slurp(f)}
223
- for h, gate, f, _, _ in memory.projection_targets()
224
- if os.path.isdir(gate)
225
- ]
226
- return {"store": root, "facts": len(index["facts"]), "present": True,
227
- "targets": targets, "hermes_projection": hermes_projection}
228
- except Exception as exc:
229
- return {"store": _local_root(), "error": f"{type(exc).__name__}: {exc}"}
230
-
231
-
232
- def _slurp(path):
233
- try:
234
- with open(path, encoding="utf-8", errors="replace") as fh:
235
- return fh.read()
236
- except OSError:
237
- return ""
238
-
239
-
240
- def _breadcrumbs():
241
- out = {}
242
- for name in ("session-start.log", "hermes-policy.log", "opencode-guard.log",
243
- "opencode-skills.log"):
244
- path = os.path.join(_local_root(), name)
245
- if not os.path.exists(path):
246
- continue
247
- lines = [l for l in _slurp(path).splitlines() if l.strip()]
248
- if lines:
249
- out[name] = {"entries": len(lines), "newest": lines[-1][:120]}
250
- return out
251
-
252
-
253
- def _contains(path, needle):
254
- return needle in _slurp(path)
255
-
256
-
257
- def _hook_manifest_valid(data, harness):
258
- """Validate the session-start registration, not a stray filename mention."""
259
- if not isinstance(data, dict) or not isinstance(data.get("hooks"), dict):
260
- return False
261
- event = "sessionStart" if harness == "cursor" else "SessionStart"
262
- groups = data["hooks"].get(event)
263
- if not isinstance(groups, list):
264
- return False
265
- hooks = groups if harness == "cursor" else [
266
- hook
267
- for group in groups if isinstance(group, dict)
268
- for hook in (group.get("hooks") or []) if isinstance(hook, dict)
269
- ]
270
- return any(
271
- isinstance(hook, dict)
272
- and isinstance(hook.get("command"), str)
273
- and "hooks/session-start.py" in hook["command"]
274
- for hook in hooks
275
- )
276
-
277
-
278
- def _bootstrap(harness):
279
- """Describe the harness's actual policy delivery mechanism.
280
-
281
- Presence is intentionally a payload check, not a claim that the host ran
282
- it. Codex hook trust is visible only in its `/hooks` UI, and neither
283
- Hermes nor OpenCode exposes a host-side registration ledger to inspect.
284
- """
285
- hook = os.path.join(PAYLOAD, "hooks", "session-start.py")
286
- if harness in ("claude", "codex", "cursor"):
287
- manifest = os.path.join(
288
- PAYLOAD, "hooks", "hooks-cursor.json" if harness == "cursor" else "hooks.json"
289
- )
290
- manifest_data = _read_json(manifest)
291
- data = {"kind": "session hook", "hook": hook,
292
- "present": os.path.isfile(hook),
293
- "executable": os.access(hook, os.X_OK),
294
- "manifest": manifest,
295
- "manifest_valid": _hook_manifest_valid(manifest_data, harness),
296
- "required": ("present", "executable", "manifest_valid")}
297
- if harness == "codex":
298
- data["trust_instruction"] = (
299
- "Codex cannot be verified from disk; run /hooks and confirm this plugin is trusted."
300
- )
301
- return data
302
- if harness == "opencode":
303
- plugin = os.path.join(PAYLOAD, "adapters", "opencode", "plugin.js")
304
- return {"kind": "config.instructions", "plugin": plugin,
305
- "present": os.path.isfile(plugin),
306
- "instructions": _contains(plugin, "config.instructions"),
307
- "required": ("present", "instructions")}
308
- if harness == "hermes":
309
- entry = os.path.join(os.path.dirname(PAYLOAD), "..", "__init__.py")
310
- entry = os.path.abspath(entry)
311
- return {"kind": "registration + first-tool-result fallback", "entrypoint": entry,
312
- "present": os.path.isfile(entry),
313
- "registration": _contains(entry, "def register(ctx)"),
314
- "fallback": _contains(
315
- entry, 'register_hook("transform_tool_result", _on_transform_tool_result)'
316
- ),
317
- "required": ("present", "registration", "fallback")}
318
- return {"kind": "unknown", "present": True, "required": ()}
319
-
320
-
321
- def _bootstrap_valid(bootstrap):
322
- return all(bootstrap.get(key) for key in bootstrap.get("required", ("present",)))
323
-
324
-
325
- def _skills():
326
- shipped = {}
327
- for root in ("skills", "skills-claude"):
328
- directory = os.path.join(PAYLOAD, root)
329
- shipped[root] = sorted(
330
- name for name in os.listdir(directory)
331
- if os.path.isfile(os.path.join(directory, name, "SKILL.md"))
332
- ) if os.path.isdir(directory) else []
333
- return shipped
334
-
335
-
336
- def collect(argv=()):
337
- harness, source = _detect_harness(argv)
338
- manifest_path = os.path.join(PAYLOAD, ".claude-plugin", "plugin.json")
339
- models_path = os.path.join(PAYLOAD, "config", "models.json")
340
- manifest_raw = _read_json(manifest_path)
341
- models_raw = _read_json(models_path)
342
- manifest = manifest_raw if isinstance(manifest_raw, dict) else {}
343
- models = models_raw if isinstance(models_raw, dict) else {}
344
- harnesses = models.get("harnesses")
345
- harnesses = harnesses if isinstance(harnesses, dict) else {}
346
- config = harnesses.get(harness)
347
- config = config if isinstance(config, dict) else {}
348
- bootstrap = _bootstrap(harness)
349
- local = _local_root()
350
- skills = _skills()
351
- skill_config = models.get("skills")
352
- skill_config = skill_config if isinstance(skill_config, dict) else {}
353
- claude_only_raw = skill_config.get("claudeOnly")
354
- claude_only = ({item for item in claude_only_raw if isinstance(item, str)}
355
- if isinstance(claude_only_raw, list) else set())
356
- exclusions = skill_config.get("exclude")
357
- exclusions = exclusions if isinstance(exclusions, dict) else {}
358
- excluded_raw = exclusions.get(harness)
359
- excluded = ({item for item in excluded_raw if isinstance(item, str)}
360
- if isinstance(excluded_raw, list) else set())
361
-
362
- registered = [n for n in skills["skills"] if n not in excluded]
363
- if harness == "claude":
364
- registered += skills["skills-claude"]
365
- tiers = {}
366
- for tier in TIERS:
367
- mapping = config.get(tier)
368
- mapping = mapping if isinstance(mapping, dict) else {}
369
- tiers[tier] = {"model": mapping.get("model"), "effort": mapping.get("effort")}
370
- manifest_valid = _manifest_valid(manifest)
371
- models_valid = _models_valid(models)
372
- tier_mapping_valid = harness == "unknown" or all(
373
- isinstance(value["model"], str) and bool(value["model"])
374
- for value in tiers.values()
375
- )
376
- python_report = {
377
- "running": ".".join(str(v) for v in sys.version_info[:3]),
378
- "minimum": ".".join(str(v) for v in MIN_PYTHON),
379
- "supported": sys.version_info[:2] >= MIN_PYTHON,
380
- }
381
- breadcrumbs = _breadcrumbs()
382
- degraded_reasons = []
383
- if harness == "unknown":
384
- degraded_reasons.append("unknown harness")
385
- if "opencode-skills.log" in breadcrumbs:
386
- degraded_reasons.append("OpenCode namespaced skills previously failed to register")
387
- if not python_report["supported"]:
388
- degraded_reasons.append("unsupported Python")
389
- if not (manifest_valid and models_valid and tier_mapping_valid):
390
- degraded_reasons.append("invalid required payload")
391
- if harness != "unknown" and not _bootstrap_valid(bootstrap):
392
- degraded_reasons.append("missing or invalid required bootstrap")
393
-
394
- return {
395
- "harness": {"value": harness, "source": source},
396
- "payload": {
397
- "path": PAYLOAD,
398
- "version": manifest.get("version"),
399
- "manifest": manifest_path,
400
- "models": models_path,
401
- "valid": manifest_valid and models_valid and tier_mapping_valid,
402
- },
403
- "bootstrap": bootstrap,
404
- "python": python_report,
405
- "tiers": tiers,
406
- "local_state": {
407
- "path": local,
408
- "present": os.path.isdir(local),
409
- "writable": os.access(local, os.W_OK) if os.path.isdir(local) else None,
410
- },
411
- "memory": _memory_report(),
412
- "skills": {
413
- "shipped_portable": skills["skills"],
414
- "shipped_claude_only": skills["skills-claude"],
415
- "expected_here": sorted(registered),
416
- "excluded_here": sorted(excluded | (set() if harness == "claude" else claude_only)),
417
- },
418
- "breadcrumbs": breadcrumbs,
419
- # Most breadcrumbs are deliberately untrusted history. The OpenCode
420
- # shadow-skills log is different: it means this payload could not
421
- # register the namespaced skill tree, so its presence is a durable
422
- # degraded capability signal even though it cannot timestamp a turn.
423
- "status": "degraded" if degraded_reasons else "healthy",
424
- "degraded_reasons": degraded_reasons,
425
- }
426
-
427
-
428
- def _render(data):
429
- lines = ["leo doctor", ""]
430
-
431
- def row(label, value, source):
432
- # A long path must not shove the source column off the line: overflow
433
- # drops the source onto its own indented continuation instead.
434
- value = str(value)
435
- if len(value) > 44:
436
- lines.append(f" {label:<16}{value}")
437
- lines.append(f" {'':<16}{'':<44}{source}")
438
- else:
439
- lines.append(f" {label:<16}{value:<44}{source}")
440
-
441
- harness = data["harness"]
442
- row("harness", harness["value"], f"detected via {harness['source']}")
443
- row("payload", f"{data['payload']['path']}", "disk")
444
- row("version", data["payload"]["version"] or "unknown", "disk")
445
-
446
- boot = data["bootstrap"]
447
- state = "present" if _bootstrap_valid(boot) else "MISSING or invalid"
448
- row("bootstrap", f"{boot['kind']}: {state}", "disk")
449
- if boot.get("trust_instruction"):
450
- row("Codex hooks", boot["trust_instruction"], "manual check")
451
- python = data["python"]
452
- row("python", f"{python['running']} (minimum {python['minimum']})",
453
- "supported" if python["supported"] else "UNSUPPORTED")
454
-
455
- tiers = " · ".join(
456
- f"{t.capitalize()} {v['model']}" + (f"/{v['effort']}" if v["effort"] else "")
457
- for t, v in data["tiers"].items() if v["model"]
458
- )
459
- row("tiers", tiers or "no mapping for this harness", "config/models.json")
460
-
461
- local = data["local_state"]
462
- row("local state", local["path"],
463
- "disk: " + ("writable" if local["writable"] else
464
- "present" if local["present"] else "not created yet"))
465
-
466
- mem = data["memory"]
467
- if mem.get("error"):
468
- row("memory", mem["error"], "disk")
469
- elif not mem["present"]:
470
- row("memory", "no store yet", "disk")
471
- else:
472
- row("memory", f"{mem['facts']} facts", "disk")
473
- for target in mem["targets"]:
474
- mark = "projected" if target["projected"] else "not projected"
475
- row("", f" {target['harness']}: {mark}", target["path"])
476
- hermes = mem.get("hermes_projection")
477
- if hermes:
478
- row("Hermes memory", hermes["status"], hermes["path"])
479
-
480
- skills = data["skills"]
481
- row("skills shipped",
482
- f"{len(skills['shipped_portable'])} portable · "
483
- f"{len(skills['shipped_claude_only'])} claude-only", "disk")
484
- row("expected here", f"{len(skills['expected_here'])} skills", "disk + config")
485
- if skills["excluded_here"]:
486
- row("not available", ", ".join(skills["excluded_here"]), "config/models.json")
487
-
488
- if data["breadcrumbs"]:
489
- lines.append("")
490
- lines.append(" breadcrumbs (history, provenance unknown — not this session):")
491
- for name, info in data["breadcrumbs"].items():
492
- lines.append(f" {name}: {info['entries']} entries, newest: {info['newest']}")
493
-
494
- lines += [
495
- "",
496
- " This script proves what shipped to disk. It cannot prove the policy",
497
- " reached the model — answer the three context questions in leo:doctor.",
498
- ]
499
- return "\n".join(lines)
500
-
501
-
502
- def _exit_code(data):
503
- if not data["payload"].get("valid") or not data["python"].get("supported"):
504
- return 1
505
- if data["harness"]["value"] != "unknown" and not _bootstrap_valid(data["bootstrap"]):
506
- return 1
507
- return 0
508
-
509
-
510
- def main(argv):
511
- data = collect(argv)
512
- if "--json" in argv:
513
- print(json.dumps(data, indent=1, sort_keys=True))
514
- else:
515
- print(_render(data))
516
- return _exit_code(data)
517
-
518
-
519
- if __name__ == "__main__":
520
- sys.exit(main(sys.argv[1:]))
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- const parser = require(process.argv[2]);
4
- try {
5
- const request = JSON.parse(require("fs").readFileSync(0, "utf8"));
6
- const errors = [];
7
- const value = parser.parse(request.text, errors, { allowTrailingComma: true, disallowComments: false });
8
- if (errors.length) throw new Error("invalid JSONC (parse error " + errors[0].error + ")");
9
- if (request.mode === "parse") process.stdout.write(JSON.stringify({ value }));
10
- else {
11
- let text = request.text;
12
- const eol = text.includes("\r\n") ? "\r\n" : "\n";
13
- const indentation = text.match(/(?:\r?\n)([ \t]+)"/);
14
- const indent = indentation ? indentation[1] : " ";
15
- for (const edit of request.edits || []) {
16
- text = parser.applyEdits(text, parser.modify(text, edit.path, edit.value, {
17
- isArrayInsertion: false,
18
- formattingOptions: { insertSpaces: !indent.includes("\t"), tabSize: indent.includes("\t") ? 1 : Math.max(1, indent.length), eol, keepLines: true },
19
- }));
20
- }
21
- process.stdout.write(JSON.stringify({ text }));
22
- }
23
- } catch (error) { process.stderr.write(String(error && error.message || error)); process.exit(2); }