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.
- package/README.md +544 -28
- package/commands/handoff.md +11 -0
- package/commands/handon.md +10 -0
- package/commands/review-pr.md +9 -0
- package/commands-claude/watch-review.md +9 -0
- package/index.js +12 -0
- package/package.json +29 -14
- package/payload/codex-agents/leo-executor.toml +36 -0
- package/payload/codex-agents/leo-runner.toml +28 -0
- package/rules/preferences.md +97 -0
- package/scripts/check.py +244 -0
- package/scripts/ghreview.py +17 -3
- package/scripts/handoff.py +183 -0
- package/scripts/leo-install.py +509 -0
- package/scripts/measure_context.py +113 -0
- package/scripts/publish-npm.py +138 -0
- package/scripts/watch_review.py +169 -0
- package/skills/doctor/SKILL.md +73 -115
- package/skills/doctor/agents/openai.yaml +5 -0
- package/skills/handoff/SKILL.md +99 -0
- package/skills/handoff/agents/openai.yaml +5 -0
- package/skills/handon/SKILL.md +61 -0
- package/skills/install/SKILL.md +79 -0
- package/skills/install/agents/openai.yaml +5 -0
- package/skills/review-pr/SKILL.md +59 -318
- package/skills/review-pr/reference/lenses.md +67 -0
- package/skills/review-pr/reference/procedure.md +348 -0
- package/skills-claude/attach-pr/SKILL.md +178 -0
- package/skills-claude/watch-review/SKILL.md +91 -0
- package/adapters/cursor/agents/executor.md +0 -17
- package/adapters/cursor/agents/expert.md +0 -70
- package/adapters/cursor/agents/explore.md +0 -16
- package/adapters/cursor/agents/implementer.md +0 -18
- package/adapters/cursor/agents/investigator.md +0 -18
- package/adapters/cursor/agents/planner.md +0 -28
- package/adapters/cursor/agents/review-lens.md +0 -22
- package/adapters/cursor/agents/reviewer.md +0 -34
- package/adapters/opencode/agents.json +0 -105
- package/adapters/opencode/plugin.js +0 -576
- package/config/MCP_PINS.md +0 -17
- package/config/models.json +0 -676
- package/hooks/bash-guard.py +0 -583
- package/hooks/cursor-guard.py +0 -84
- package/hooks/hooks-cursor.json +0 -11
- package/hooks/hooks.json +0 -20
- package/hooks/session-start.py +0 -148
- package/roles/executor.md +0 -15
- package/roles/expert.md +0 -67
- package/roles/explore.md +0 -13
- package/roles/implementer.md +0 -16
- package/roles/investigator.md +0 -15
- package/roles/planner.md +0 -25
- package/roles/review-lens.md +0 -20
- package/roles/reviewer.md +0 -31
- package/scripts/doctor.py +0 -520
- package/scripts/jsonc_bridge.cjs +0 -23
- package/scripts/memory.py +0 -744
- package/scripts/render_adapters.py +0 -508
- package/scripts/setup.py +0 -1753
- package/settings.json +0 -7
- package/skills/.gitkeep +0 -0
- package/skills/brainstorming/SKILL.md +0 -111
- package/skills/debugging/SKILL.md +0 -100
- package/skills/delegation/SKILL.md +0 -143
- package/skills/executing-plans/SKILL.md +0 -117
- package/skills/finishing-a-branch/SKILL.md +0 -125
- package/skills/freshness/SKILL.md +0 -131
- package/skills/memory/SKILL.md +0 -154
- package/skills/resolve-ticket/SKILL.md +0 -275
- package/skills/setup/SKILL.md +0 -199
- package/skills/setup/agents/openai.yaml +0 -5
- package/skills/test-first/SKILL.md +0 -92
- package/skills/using-leo/SKILL.md +0 -101
- package/skills/using-leo/references/claude-mapping.md +0 -33
- package/skills/using-leo/references/codex-mapping.md +0 -33
- package/skills/using-leo/references/cursor-mapping.md +0 -35
- package/skills/using-leo/references/hermes-mapping.md +0 -37
- package/skills/using-leo/references/opencode-mapping.md +0 -39
- package/skills/verification/SKILL.md +0 -110
- package/skills/visual-verification/SKILL.md +0 -115
- package/skills/watch-review/SKILL.md +0 -128
- package/skills/watch-review/agents/openai.yaml +0 -5
- package/skills/worktrees/SKILL.md +0 -131
- package/skills/writing-plans/SKILL.md +0 -97
- package/skills/writing-skills/SKILL.md +0 -141
- package/vendor/jsonc-parser-3.3.1/LICENSE.md +0 -21
- package/vendor/jsonc-parser-3.3.1/README.md +0 -26
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +0 -201
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +0 -275
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +0 -682
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +0 -456
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +0 -42
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +0 -351
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +0 -194
- package/vendor/jsonc-parser-3.3.1/package.json +0 -37
- package/workflows/cost-tiered-fix.js +0 -287
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure leos-agent's static prompt footprint with a byte-based proxy.
|
|
3
|
+
|
|
4
|
+
This does not estimate total task cost: tool output, conversation history,
|
|
5
|
+
cache state, model choice, and spawned work dominate many real runs. It measures
|
|
6
|
+
the repository-controlled text that is always listed or loaded at dispatch, so
|
|
7
|
+
regressions remain visible without a tokenizer or network access.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
|
|
18
|
+
# Deliberately tight ceilings. Raise one only with a concrete reason and record
|
|
19
|
+
# the before/after output in the change that raises it.
|
|
20
|
+
LIMITS = {
|
|
21
|
+
"global_policy_bytes": 4_500,
|
|
22
|
+
"codex_implicit_skill_metadata_bytes": 600,
|
|
23
|
+
"claude_implicit_skill_metadata_bytes": 800,
|
|
24
|
+
"codex_agent_description_bytes": 550,
|
|
25
|
+
"review_dispatch_bytes": 3_500,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def frontmatter(path):
|
|
30
|
+
text = path.read_text(encoding="utf-8")
|
|
31
|
+
match = re.match(r"---\n(.*?)\n---\n?(.*)", text, re.DOTALL)
|
|
32
|
+
if not match:
|
|
33
|
+
raise ValueError(f"{path.relative_to(ROOT)} has no YAML frontmatter")
|
|
34
|
+
return match.group(1), match.group(2)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def field(text, name):
|
|
38
|
+
match = re.search(rf"(?m)^{re.escape(name)}:\s*(.+?)\s*$", text)
|
|
39
|
+
return match.group(1).strip() if match else ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def byte_len(text):
|
|
43
|
+
return len(text.encode("utf-8"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def skill_metadata_bytes(paths, implicit):
|
|
47
|
+
total = 0
|
|
48
|
+
for path in paths:
|
|
49
|
+
fm, _ = frontmatter(path)
|
|
50
|
+
if implicit(path, fm):
|
|
51
|
+
total += byte_len(field(fm, "name")) + byte_len(field(fm, "description"))
|
|
52
|
+
return total
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def codex_implicit(path, _frontmatter):
|
|
56
|
+
policy = path.parent / "agents" / "openai.yaml"
|
|
57
|
+
return not policy.is_file() or "allow_implicit_invocation: false" not in policy.read_text(encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def claude_implicit(_path, fm):
|
|
61
|
+
return re.search(r"(?m)^disable-model-invocation:\s*true\s*$", fm) is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def agent_description(path):
|
|
65
|
+
text = path.read_text(encoding="utf-8")
|
|
66
|
+
match = re.search(r'(?m)^description\s*=\s*"(.*)"\s*$', text)
|
|
67
|
+
if not match:
|
|
68
|
+
raise ValueError(f"{path.relative_to(ROOT)} has no one-line description")
|
|
69
|
+
return match.group(1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def measurements():
|
|
73
|
+
portable = sorted((ROOT / "skills").glob("*/SKILL.md"))
|
|
74
|
+
claude_only = sorted((ROOT / "skills-claude").glob("*/SKILL.md"))
|
|
75
|
+
policy_fm, policy_body = frontmatter(ROOT / "rules" / "preferences.md")
|
|
76
|
+
del policy_fm
|
|
77
|
+
review_fm, review_body = frontmatter(ROOT / "skills" / "review-pr" / "SKILL.md")
|
|
78
|
+
del review_fm
|
|
79
|
+
agent_paths = sorted((ROOT / "payload" / "codex-agents").glob("*.toml"))
|
|
80
|
+
return {
|
|
81
|
+
"global_policy_bytes": byte_len(policy_body.strip()),
|
|
82
|
+
"codex_implicit_skill_metadata_bytes": skill_metadata_bytes(portable, codex_implicit),
|
|
83
|
+
"claude_implicit_skill_metadata_bytes": skill_metadata_bytes(portable + claude_only, claude_implicit),
|
|
84
|
+
"codex_agent_description_bytes": sum(byte_len(agent_description(path)) for path in agent_paths),
|
|
85
|
+
"review_dispatch_bytes": byte_len(review_body.strip()),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main(argv=None):
|
|
90
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
91
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
92
|
+
parser.add_argument("--check", action="store_true", help="fail when a committed ceiling is exceeded")
|
|
93
|
+
args = parser.parse_args(argv)
|
|
94
|
+
|
|
95
|
+
values = measurements()
|
|
96
|
+
if args.json:
|
|
97
|
+
print(json.dumps({"measurements": values, "limits": LIMITS}, indent=2, sort_keys=True))
|
|
98
|
+
else:
|
|
99
|
+
print("Static prompt footprint (bytes; tokens are roughly bytes / 4 for this prose)")
|
|
100
|
+
for name, value in values.items():
|
|
101
|
+
print(f" {name:38} {value:5} limit {LIMITS[name]:5}")
|
|
102
|
+
print("This excludes conversation history, tool output, cache effects, and subagent work.")
|
|
103
|
+
|
|
104
|
+
over = {name: (value, LIMITS[name]) for name, value in values.items() if value > LIMITS[name]}
|
|
105
|
+
if args.check and over:
|
|
106
|
+
for name, (value, limit) in over.items():
|
|
107
|
+
print(f"FAIL {name}: {value} > {limit}", file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
sys.exit(main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Publish leos-agent to npm exactly once per version.
|
|
3
|
+
|
|
4
|
+
Two properties matter here, and both are inherited from the release path this
|
|
5
|
+
replaces. Publishing is idempotent: an exact version already on the registry is
|
|
6
|
+
a no-op, so re-running a tag is safe, while a lookup that fails for any reason
|
|
7
|
+
other than a confirmed 404 aborts rather than guessing. And the tree npm would
|
|
8
|
+
actually ship is inspected before it ships, because `files` in package.json
|
|
9
|
+
scopes the publish but does not exclude build residue that lands inside a
|
|
10
|
+
directory it lists.
|
|
11
|
+
|
|
12
|
+
Authentication is npm's OIDC trusted publishing: the workflow's `id-token`
|
|
13
|
+
permission supplies a short-lived credential, so there is no token to read here.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
PACKAGE = "leos-agent"
|
|
24
|
+
|
|
25
|
+
# Residue that a local checkout accumulates and a publish must never carry.
|
|
26
|
+
FORBIDDEN_PARTS = ("__pycache__",)
|
|
27
|
+
FORBIDDEN_SUFFIXES = (".pyc", ".log")
|
|
28
|
+
FORBIDDEN_NAMES = (".DS_Store",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ReleaseError(Exception):
|
|
32
|
+
"""The release cannot proceed safely; the caller should stop, not retry."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(command):
|
|
36
|
+
return subprocess.run(command, capture_output=True, text=True, check=False, cwd=ROOT)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def declared_version():
|
|
40
|
+
version = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))["version"]
|
|
41
|
+
return version
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pack_inventory(npm="npm"):
|
|
45
|
+
"""Return the file list npm would publish, without publishing it."""
|
|
46
|
+
packed = run([npm, "pack", "--dry-run", "--json"])
|
|
47
|
+
if packed.returncode:
|
|
48
|
+
raise ReleaseError(f"npm pack --dry-run failed: {(packed.stdout + packed.stderr).strip()}")
|
|
49
|
+
try:
|
|
50
|
+
report = json.loads(packed.stdout)
|
|
51
|
+
except json.JSONDecodeError as exc:
|
|
52
|
+
raise ReleaseError(f"npm pack --dry-run emitted unparseable JSON: {exc}") from exc
|
|
53
|
+
if not report:
|
|
54
|
+
raise ReleaseError("npm pack --dry-run reported no package")
|
|
55
|
+
return sorted(entry["path"] for entry in report[0].get("files", []))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def forbidden_paths(inventory):
|
|
59
|
+
found = []
|
|
60
|
+
for path in inventory:
|
|
61
|
+
parts = path.split("/")
|
|
62
|
+
if any(part in FORBIDDEN_PARTS for part in parts):
|
|
63
|
+
found.append(path)
|
|
64
|
+
elif path.endswith(FORBIDDEN_SUFFIXES) or parts[-1] in FORBIDDEN_NAMES:
|
|
65
|
+
found.append(path)
|
|
66
|
+
return found
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_inventory(inventory):
|
|
70
|
+
if "LICENSE" not in inventory:
|
|
71
|
+
raise ReleaseError("publish tree has no LICENSE")
|
|
72
|
+
if "package.json" not in inventory:
|
|
73
|
+
raise ReleaseError("publish tree has no package.json")
|
|
74
|
+
found = forbidden_paths(inventory)
|
|
75
|
+
if found:
|
|
76
|
+
raise ReleaseError("publish tree contains transient files: " + ", ".join(found))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def registry_state(version, npm="npm"):
|
|
80
|
+
"""Report whether this exact version is already on the registry.
|
|
81
|
+
|
|
82
|
+
Anything other than a clean hit or a confirmed not-found is an error: an
|
|
83
|
+
auth failure or a registry outage must not be read as "absent, publish it".
|
|
84
|
+
"""
|
|
85
|
+
viewed = run([npm, "view", f"{PACKAGE}@{version}", "version"])
|
|
86
|
+
output = (viewed.stdout + viewed.stderr).strip()
|
|
87
|
+
if viewed.returncode == 0:
|
|
88
|
+
if output != version:
|
|
89
|
+
raise ReleaseError(f"npm returned {output!r}, not the exact version {version!r}")
|
|
90
|
+
return "present"
|
|
91
|
+
if "E404" in output or "404 Not Found" in output:
|
|
92
|
+
return "absent"
|
|
93
|
+
raise ReleaseError(f"npm version lookup failed without a confirmed not-found: {output}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def publish(npm="npm"):
|
|
97
|
+
published = run([npm, "publish", "--access", "public"])
|
|
98
|
+
if published.returncode:
|
|
99
|
+
raise ReleaseError(f"npm publish failed: {(published.stdout + published.stderr).strip()}")
|
|
100
|
+
return (published.stdout + published.stderr).strip()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main(argv=None):
|
|
104
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
105
|
+
parser.add_argument("--tag", help="git tag being released; must match package.json")
|
|
106
|
+
parser.add_argument("--dry-run", action="store_true", help="check everything, publish nothing")
|
|
107
|
+
parser.add_argument("--npm", default="npm", help="npm executable to use")
|
|
108
|
+
args = parser.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
version = declared_version()
|
|
112
|
+
if args.tag is not None:
|
|
113
|
+
expected = args.tag[1:] if args.tag.startswith("v") else args.tag
|
|
114
|
+
if expected != version:
|
|
115
|
+
raise ReleaseError(f"tag {args.tag!r} does not match package.json version {version!r}")
|
|
116
|
+
|
|
117
|
+
inventory = pack_inventory(args.npm)
|
|
118
|
+
check_inventory(inventory)
|
|
119
|
+
print(f"{PACKAGE} {version}: {len(inventory)} file(s) staged for publish")
|
|
120
|
+
|
|
121
|
+
state = registry_state(version, args.npm)
|
|
122
|
+
if state == "present":
|
|
123
|
+
print(f"{PACKAGE}@{version} is already on the registry; nothing to do")
|
|
124
|
+
return 0
|
|
125
|
+
if args.dry_run:
|
|
126
|
+
print(f"would publish {PACKAGE}@{version}")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
publish(args.npm)
|
|
130
|
+
print(f"published {PACKAGE}@{version}")
|
|
131
|
+
return 0
|
|
132
|
+
except ReleaseError as exc:
|
|
133
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
sys.exit(main())
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""watch_review: stream new GitHub review requests without spending tokens.
|
|
3
|
+
|
|
4
|
+
The discovery half of the review watcher is a fixed query, a fixed filter, and
|
|
5
|
+
a state file — none of it needs a model. This script does that half in the
|
|
6
|
+
shell and prints one line per new pull request; whoever reads stdout does the
|
|
7
|
+
review. An idle tick costs one `gh` call and zero tokens.
|
|
8
|
+
|
|
9
|
+
watch_review.py monitor [-C DIR] --interval 300 loop; a line per new PR
|
|
10
|
+
watch_review.py record [-C DIR] <number>... mark numbers reviewed
|
|
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
|
+
It launches nothing and records nothing on its own. The reader must call
|
|
15
|
+
`record` once a review is done — a staged (pending, unsubmitted) review does
|
|
16
|
+
not clear the request on GitHub, so that state file is the only thing keeping
|
|
17
|
+
the same pull request from coming back. `monitor` emits each pull request once
|
|
18
|
+
per process, so an unreviewed one is re-emitted after a restart.
|
|
19
|
+
|
|
20
|
+
Intended for Claude Code's Monitor tool, which turns each stdout line into a
|
|
21
|
+
session notification. Any `read`-driven shell loop works the same way.
|
|
22
|
+
|
|
23
|
+
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.
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import time
|
|
32
|
+
|
|
33
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
34
|
+
import state as state_mod # noqa: E402
|
|
35
|
+
|
|
36
|
+
STATE_NAME = "review-watcher"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fail(message):
|
|
40
|
+
print(f"watch-review: {message}", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def gh(args, cwd):
|
|
45
|
+
"""Run a read-only gh command and return stdout, or fail loudly."""
|
|
46
|
+
try:
|
|
47
|
+
proc = subprocess.run(
|
|
48
|
+
["gh"] + args, cwd=cwd, capture_output=True, text=True, check=False
|
|
49
|
+
)
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
fail("gh is not installed or not on PATH")
|
|
52
|
+
if proc.returncode != 0:
|
|
53
|
+
fail((proc.stderr or proc.stdout).strip() or f"gh {args[0]} failed")
|
|
54
|
+
return proc.stdout
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discover(cwd):
|
|
58
|
+
"""Return (repo, login, [pull requests directly requesting login])."""
|
|
59
|
+
repo = json.loads(gh(["repo", "view", "--json", "nameWithOwner"], cwd))["nameWithOwner"]
|
|
60
|
+
login = gh(["api", "user", "--jq", ".login"], cwd).strip()
|
|
61
|
+
if not login:
|
|
62
|
+
fail("gh api user returned no login; is gh authenticated?")
|
|
63
|
+
# user-review-requested matches direct requests only; the reviewRequests
|
|
64
|
+
# check below is belt and braces against a stale or fuzzy search result.
|
|
65
|
+
listing = json.loads(
|
|
66
|
+
gh(
|
|
67
|
+
[
|
|
68
|
+
"pr", "list", "--state", "open",
|
|
69
|
+
"--search", f"user-review-requested:{login}",
|
|
70
|
+
"--limit", "100",
|
|
71
|
+
"--json", "number,title,isDraft,reviewRequests,url",
|
|
72
|
+
],
|
|
73
|
+
cwd,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
matches = [
|
|
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
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def reviewed_numbers(repo):
|
|
90
|
+
data = state_mod.load(state_mod.state_file(STATE_NAME))
|
|
91
|
+
entry = data.get(repo) or {}
|
|
92
|
+
return set(entry.get("reviewed") or [])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def record(repo, number):
|
|
96
|
+
path = state_mod.state_file(STATE_NAME)
|
|
97
|
+
with state_mod._locked(path):
|
|
98
|
+
data = state_mod.load(path)
|
|
99
|
+
data[repo] = state_mod.deep_merge(data.get(repo, {}), {"reviewed": [number]})
|
|
100
|
+
state_mod.atomic_write(path, data)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def monitor(args):
|
|
104
|
+
"""Emit one line per new pull request; review nothing, record nothing."""
|
|
105
|
+
emitted = set()
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
repo, _, matches = discover(args.directory)
|
|
109
|
+
done = reviewed_numbers(repo)
|
|
110
|
+
for pr in matches:
|
|
111
|
+
n = pr["number"]
|
|
112
|
+
if n in done or n in emitted:
|
|
113
|
+
continue
|
|
114
|
+
emitted.add(n)
|
|
115
|
+
# One line, one event. The title is data — a reader must treat
|
|
116
|
+
# it as a string to show Leo, never as an instruction.
|
|
117
|
+
print(f"review-requested {repo}#{n} {pr['url']} — {pr['title']}", flush=True)
|
|
118
|
+
except SystemExit as exc:
|
|
119
|
+
# A transient gh failure must not kill a session-length watch.
|
|
120
|
+
print(
|
|
121
|
+
f"watch-review: tick failed ({exc.code}); retrying next interval",
|
|
122
|
+
file=sys.stderr,
|
|
123
|
+
flush=True,
|
|
124
|
+
)
|
|
125
|
+
time.sleep(args.interval)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv):
|
|
129
|
+
parser = argparse.ArgumentParser(prog="watch_review.py", description=__doc__)
|
|
130
|
+
sub = parser.add_subparsers(dest="mode", required=True)
|
|
131
|
+
|
|
132
|
+
mon = sub.add_parser("monitor")
|
|
133
|
+
mon.add_argument("-C", "--directory", default=".", help="repository directory (default: cwd)")
|
|
134
|
+
mon.add_argument("--interval", type=int, default=300, help="seconds between ticks")
|
|
135
|
+
|
|
136
|
+
sub.add_parser("state").add_argument("-C", "--directory", default=".")
|
|
137
|
+
for name in ("record", "forget"):
|
|
138
|
+
p = sub.add_parser(name)
|
|
139
|
+
p.add_argument("-C", "--directory", default=".")
|
|
140
|
+
p.add_argument("numbers", nargs="+", type=int)
|
|
141
|
+
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
if not os.path.isdir(args.directory):
|
|
144
|
+
fail(f"{args.directory} is not a directory")
|
|
145
|
+
|
|
146
|
+
if args.mode == "monitor":
|
|
147
|
+
if args.interval < 30:
|
|
148
|
+
fail("--interval below 30s hammers the GitHub API; pick something larger")
|
|
149
|
+
return monitor(args)
|
|
150
|
+
|
|
151
|
+
repo, _, _ = discover(args.directory)
|
|
152
|
+
if args.mode == "record":
|
|
153
|
+
for n in args.numbers:
|
|
154
|
+
record(repo, n)
|
|
155
|
+
elif args.mode == "forget":
|
|
156
|
+
path = state_mod.state_file(STATE_NAME)
|
|
157
|
+
with state_mod._locked(path):
|
|
158
|
+
data = state_mod.load(path)
|
|
159
|
+
entry = data.get(repo) or {}
|
|
160
|
+
drop = set(args.numbers)
|
|
161
|
+
entry["reviewed"] = [n for n in (entry.get("reviewed") or []) if n not in drop]
|
|
162
|
+
data[repo] = entry
|
|
163
|
+
state_mod.atomic_write(path, data)
|
|
164
|
+
print(json.dumps({"repo": repo, "reviewed": sorted(reviewed_numbers(repo))}, indent=1))
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
sys.exit(main(sys.argv[1:]) or 0)
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -1,124 +1,82 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doctor
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
tier name resolves to here, whether the bootstrap is installed, where
|
|
6
|
-
machine-local state and the memory store live, and which skills shipped
|
|
7
|
-
versus which this session can actually invoke. Disk facts come from a
|
|
8
|
-
helper script; the context facts only the running session can answer, and
|
|
9
|
-
a disagreement between the two columns is the diagnosis. Use when Leo asks
|
|
10
|
-
about Leo's loading, routing, or skill wiring. Do not use for project health
|
|
11
|
-
checks, project-code debugging, or unprompted inspection.
|
|
12
|
-
when_to_use: >
|
|
13
|
-
Leo asks whether the policy loaded, why routing or a skill is misbehaving,
|
|
14
|
-
or invokes doctor by name after installing, updating, or switching harness.
|
|
15
|
-
Also the first move when a leo skill cannot be found. NOT a general
|
|
16
|
-
environment or project health check, NOT for debugging the project's own
|
|
17
|
-
code (that is leo:debugging), and never run unprompted — it reports on the
|
|
18
|
-
agent, not on the work.
|
|
3
|
+
disable-model-invocation: true
|
|
4
|
+
description: Audit Leo's agent setup in this harness — the injected leos-agent block, everything else always loaded into context, and the local plugin checkout. Read-only.
|
|
19
5
|
---
|
|
20
6
|
|
|
21
|
-
#
|
|
7
|
+
# Diagnose Leo's agent setup
|
|
22
8
|
|
|
23
|
-
|
|
24
|
-
reached this session. A skill the harness never registered is indistinguishable
|
|
25
|
-
from a skill that does not exist, right up until the moment you invoke it.
|
|
9
|
+
Read-only. Report findings; change nothing unless Leo asks.
|
|
26
10
|
|
|
27
|
-
|
|
11
|
+
**This harness only.** Inspect the harness you are actually running in — one of
|
|
12
|
+
`claude`, `codex`, `cursor`, `hermes`, `pi`, `opencode`. Other harnesses may be
|
|
13
|
+
on other versions; that is their business.
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
## 1. Injection and install
|
|
16
|
+
|
|
17
|
+
Locate the plugin root (the directory holding `rules/preferences.md`):
|
|
18
|
+
`$LEOS_AGENT_ROOT`, `$CLAUDE_PLUGIN_ROOT`, `$PLUGIN_ROOT`, or the parent of the
|
|
19
|
+
directory holding this file. Then:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
python3 <plugin-root>/scripts/leo-install.py <harness> --check
|
|
31
23
|
```
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
can accumulate on a development machine. Quote the newest line if useful, but
|
|
92
|
-
never conclude "the hook failed this session" from history alone. The one
|
|
93
|
-
capability exception is `opencode-skills.log`: its presence means namespaced
|
|
94
|
-
OpenCode skill registration degraded and doctor reports that state until the
|
|
95
|
-
breadcrumb is cleared after the underlying problem is understood.
|
|
96
|
-
|
|
97
|
-
## Failure modes
|
|
98
|
-
|
|
99
|
-
| Symptom | Likely cause | Fix |
|
|
100
|
-
|---|---|---|
|
|
101
|
-
| Policy absent, bootstrap installed | the hook fired and failed open | read the newest breadcrumb, then confirm it describes this session before believing it |
|
|
102
|
-
| Policy present, mapping names another harness | detection resolved wrong, usually a stray plugin-root variable exported in an unrelated shell | unset it, restart the session |
|
|
103
|
-
| Harness reported as `unknown` | no `--harness`, and this harness exports no plugin-root variable | degraded until re-run with `--harness <name>` read off your mapping heading; a still-unknown explicit run is invalid wiring |
|
|
104
|
-
| Codex hook is on disk but policy is absent | the new or changed hook may not be trusted | open `/hooks`, review the hook, and explicitly trust it |
|
|
105
|
-
| OpenCode reports `opencode-skills.log` | the namespaced shadow tree failed and no bare-name fallback was registered | inspect the newest breadcrumb, fix the path/permission failure, and restart OpenCode |
|
|
106
|
-
| Shipped roster exceeds what you can invoke | the harness cached an older payload, or the skills directory is not registered | update the plugin; on OpenCode check `opencode debug skill` for each skill's `location` |
|
|
107
|
-
| Skills listed as `leo-<name>` instead of `leo:<name>` | OpenCode, working as designed | invoke them as `leo-<name>`; not a fault |
|
|
108
|
-
| Tier names resolve to models this harness cannot run | mapping and harness disagree | same as row 2 |
|
|
109
|
-
| Machine-local state not writable | the path override points somewhere unwritable | fix or unset it |
|
|
110
|
-
| A skill is genuinely absent from disk | it was never added | see leo:writing-skills |
|
|
111
|
-
|
|
112
|
-
## Doctor never repairs
|
|
113
|
-
|
|
114
|
-
It reports, and it names the fix. It does not reinstall, rewrite configuration,
|
|
115
|
-
or delete state — which is what keeps it safe to run at any tier and at any
|
|
116
|
-
moment.
|
|
117
|
-
|
|
118
|
-
## Works with
|
|
119
|
-
|
|
120
|
-
- leo:writing-skills — for a skill that turned out to be missing because nobody
|
|
121
|
-
wrote it yet.
|
|
122
|
-
- leo:memory — doctor reports whether the store exists and reached each surface.
|
|
123
|
-
- leo:verification — this report is a claim like any other: the script ran this
|
|
124
|
-
turn and its output was read.
|
|
25
|
+
Exit 0 means the `<leos-agent>` block is present and current. Non-zero means it
|
|
26
|
+
is missing, stale, or the file is malformed — quote what it printed and offer
|
|
27
|
+
`/leo-install`. Cursor legitimately reports `skipped`; Hermes skips until
|
|
28
|
+
`~/.hermes/SOUL.md` exists.
|
|
29
|
+
|
|
30
|
+
Then confirm by hand, since `--check` only sees disk, not what got loaded:
|
|
31
|
+
|
|
32
|
+
- Read the harness's global file and verify exactly one `<leos-agent
|
|
33
|
+
version="...">` block, with the version matching `package.json` in the plugin
|
|
34
|
+
root.
|
|
35
|
+
- Confirm the plugin's skills and commands are actually registered in this
|
|
36
|
+
session — `install` and `doctor` should both be listed. If they are not, the
|
|
37
|
+
plugin is on disk but not loaded.
|
|
38
|
+
|
|
39
|
+
| Harness | Global file |
|
|
40
|
+
|---|---|
|
|
41
|
+
| claude | `~/.claude/CLAUDE.md` |
|
|
42
|
+
| codex | `~/.codex/AGENTS.md` (plus `~/.codex/agents/leo-runner.toml` and `leo-executor.toml`) |
|
|
43
|
+
| cursor | none — the always-apply rule carries the payload |
|
|
44
|
+
| hermes | `~/.hermes/SOUL.md` |
|
|
45
|
+
| pi | `~/.pi/agent/AGENTS.md` |
|
|
46
|
+
| opencode | `~/.config/opencode/AGENTS.md` (plus copied `skills/`, `commands/`) |
|
|
47
|
+
|
|
48
|
+
## 2. Global context
|
|
49
|
+
|
|
50
|
+
Inventory everything loaded into *every* session in this harness, not just the
|
|
51
|
+
leos-agent block. Look for what is broken, stale, or contradictory:
|
|
52
|
+
|
|
53
|
+
- The global instruction file outside the block — content that fights the
|
|
54
|
+
payload, notes from an older setup, anything referencing files or flags that
|
|
55
|
+
no longer exist.
|
|
56
|
+
- Memory files, if the harness has them (Claude: `~/.claude/projects/*/memory/`
|
|
57
|
+
and its `MEMORY.md` index). Flag index lines pointing at missing files,
|
|
58
|
+
memories missing frontmatter, duplicates, and facts that name paths or flags
|
|
59
|
+
that no longer exist. Verify before calling one stale.
|
|
60
|
+
- Global settings, agents, skills, commands, and plugins that ship
|
|
61
|
+
always-on instructions. Flag broken JSON, duplicate names, and dangling paths.
|
|
62
|
+
- Total size. Codex concatenates the AGENTS.md chain under a byte cap — over
|
|
63
|
+
~28 KB globally, say so, since repo instructions get crowded out.
|
|
64
|
+
|
|
65
|
+
## 3. Local checkout
|
|
66
|
+
|
|
67
|
+
If the plugin root is a git checkout rather than an installed cache, run:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
python3 <plugin-root>/scripts/check.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Report the failures verbatim. Also note an uncommitted or behind-upstream
|
|
74
|
+
checkout, and a `package.json` version that disagrees with the installed
|
|
75
|
+
block — a same-version reinstall serves the cached build, so a version match
|
|
76
|
+
with different content stays invisible here.
|
|
77
|
+
|
|
78
|
+
## Report
|
|
79
|
+
|
|
80
|
+
Group by section, worst first. One line per finding: what is wrong, where, and
|
|
81
|
+
the fix. End with a one-line verdict. If everything passes, say so plainly and
|
|
82
|
+
do not pad the report.
|