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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -11
  3. package/adapters/cursor/agents/executor.md +2 -2
  4. package/adapters/cursor/agents/implementer.md +2 -2
  5. package/adapters/cursor/agents/review-lens.md +22 -0
  6. package/adapters/cursor/agents/reviewer.md +3 -2
  7. package/adapters/opencode/agents.json +44 -5
  8. package/adapters/opencode/plugin.js +435 -47
  9. package/config/MCP_PINS.md +17 -0
  10. package/config/models.json +647 -33
  11. package/hooks/bash-guard.py +51 -9
  12. package/hooks/session-start.py +27 -0
  13. package/package.json +19 -8
  14. package/roles/executor.md +2 -2
  15. package/roles/implementer.md +2 -2
  16. package/roles/review-lens.md +20 -0
  17. package/roles/reviewer.md +3 -2
  18. package/scripts/doctor.py +520 -0
  19. package/scripts/ghreview.py +558 -0
  20. package/scripts/jsonc_bridge.cjs +23 -0
  21. package/scripts/memory.py +744 -0
  22. package/scripts/render_adapters.py +253 -121
  23. package/scripts/resolve_attach_target.py +389 -0
  24. package/scripts/setup.py +1753 -0
  25. package/skills/brainstorming/SKILL.md +3 -1
  26. package/skills/debugging/SKILL.md +4 -2
  27. package/skills/delegation/SKILL.md +10 -8
  28. package/skills/doctor/SKILL.md +124 -0
  29. package/skills/executing-plans/SKILL.md +2 -1
  30. package/skills/finishing-a-branch/SKILL.md +4 -2
  31. package/skills/freshness/SKILL.md +131 -0
  32. package/skills/memory/SKILL.md +154 -0
  33. package/skills/resolve-ticket/SKILL.md +275 -0
  34. package/skills/review-pr/SKILL.md +327 -0
  35. package/skills/setup/SKILL.md +199 -0
  36. package/skills/setup/agents/openai.yaml +5 -0
  37. package/skills/test-first/SKILL.md +3 -1
  38. package/skills/using-leo/SKILL.md +18 -6
  39. package/skills/using-leo/references/claude-mapping.md +23 -1
  40. package/skills/using-leo/references/codex-mapping.md +18 -9
  41. package/skills/using-leo/references/cursor-mapping.md +19 -6
  42. package/skills/using-leo/references/hermes-mapping.md +18 -7
  43. package/skills/using-leo/references/opencode-mapping.md +18 -9
  44. package/skills/verification/SKILL.md +9 -1
  45. package/skills/visual-verification/SKILL.md +115 -0
  46. package/skills/watch-review/SKILL.md +128 -0
  47. package/skills/watch-review/agents/openai.yaml +5 -0
  48. package/skills/worktrees/SKILL.md +3 -1
  49. package/skills/writing-plans/SKILL.md +2 -1
  50. package/skills/writing-skills/SKILL.md +141 -0
  51. package/vendor/jsonc-parser-3.3.1/LICENSE.md +21 -0
  52. package/vendor/jsonc-parser-3.3.1/README.md +26 -0
  53. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +201 -0
  54. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +275 -0
  55. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +682 -0
  56. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +456 -0
  57. package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +42 -0
  58. package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +351 -0
  59. package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +194 -0
  60. package/vendor/jsonc-parser-3.3.1/package.json +37 -0
  61. package/workflows/cost-tiered-fix.js +32 -4
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve an identifier (PR number/URL, branch name, or ticket id) to everything
3
+ `/attach-pr` needs: the branch, its PR, and the working directory to attach from.
4
+
5
+ Prints a single JSON object to stdout:
6
+
7
+ {"status": "ok", "branch": ..., "pr_number": ..., "pr_url": ..., "base_ref": ...,
8
+ "pr_state": ..., "pr_title": ..., "workdir": ... | null, "workdir_kind": ...,
9
+ "attach_command": ...}
10
+ {"status": "ambiguous", "message": ..., "candidates": [ {...}, ... ]}
11
+ {"status": "error", "message": ...}
12
+
13
+ Exit code is 0 for "ok" and 1 otherwise, so callers can branch on it directly.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import re
19
+ import shlex
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ from hashlib import sha256
24
+
25
+ PR_NUM_RE = re.compile(r"^#?(\d+)$")
26
+ PR_URL_RE = re.compile(r"^https://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)/pull/([1-9]\d*)$")
27
+ TICKET_RE = re.compile(r"^([A-Za-z][A-Za-z0-9]*)-(\d+)$")
28
+ SAFE_REF_RE = re.compile(r"^(?!-)[A-Za-z0-9][A-Za-z0-9._/-]*$")
29
+
30
+ PR_FIELDS = "number,url,headRefName,baseRefName,state,title"
31
+
32
+
33
+ def run(cmd, cwd=None):
34
+ """Run a command, returning (returncode, stdout, stderr) with output stripped."""
35
+ try:
36
+ p = subprocess.run(
37
+ cmd, cwd=cwd, capture_output=True, text=True, timeout=60, check=False
38
+ )
39
+ except (OSError, subprocess.TimeoutExpired) as exc:
40
+ return 1, "", str(exc)
41
+ return p.returncode, p.stdout.strip(), p.stderr.strip()
42
+
43
+
44
+ def die(message):
45
+ print(json.dumps({"status": "error", "message": message}, indent=2))
46
+ sys.exit(1)
47
+
48
+
49
+ def emit(payload):
50
+ print(json.dumps(payload, indent=2))
51
+ sys.exit(0 if payload.get("status") == "ok" else 1)
52
+
53
+
54
+ def is_safe_pr_url(url):
55
+ """Accept only a canonical public GitHub pull-request URL."""
56
+ return isinstance(url, str) and bool(PR_URL_RE.fullmatch(url))
57
+
58
+
59
+ def is_safe_ref(ref):
60
+ """Keep refs shell-safe *and* ask Git to enforce its ref grammar."""
61
+ if not isinstance(ref, str) or not SAFE_REF_RE.fullmatch(ref):
62
+ return False
63
+ rc, _, _ = run(["git", "check-ref-format", "--branch", ref])
64
+ return rc == 0
65
+
66
+
67
+ def suggested_worktree(root, branch):
68
+ """A readable name with a stable digest prevents slash-to-dash collisions."""
69
+ readable = branch.replace("/", "-")
70
+ digest = sha256(branch.encode("utf-8")).hexdigest()[:10]
71
+ return os.path.join(root, ".claude", "worktrees", f"{readable}-{digest}")
72
+
73
+
74
+ def build_attach_command(workdir, pr_url, base_ref, branch):
75
+ """Build the intentional compound attach command with every value quoted."""
76
+ return (
77
+ 'gh() { echo "$PR_URL"; }; '
78
+ f"cd {shlex.quote(workdir)}; "
79
+ f"PR_URL={shlex.quote(pr_url)} gh pr create --draft "
80
+ f"--base {shlex.quote(base_ref)} --head {shlex.quote(branch)}"
81
+ )
82
+
83
+
84
+ # --- environment checks ----------------------------------------------------
85
+
86
+
87
+ def repo_root():
88
+ rc, out, _ = run(["git", "rev-parse", "--show-toplevel"])
89
+ if rc != 0:
90
+ die("not inside a git repository — cd into the repo before running /attach-pr")
91
+ return out
92
+
93
+
94
+ def require_gh():
95
+ if not shutil.which("gh"):
96
+ die("`gh` is not installed or not on PATH; /attach-pr needs the GitHub CLI")
97
+ rc, _, err = run(["gh", "auth", "status"])
98
+ if rc != 0:
99
+ die(f"`gh` is not authenticated: {err or 'run `gh auth login`'}")
100
+
101
+
102
+ def name_with_owner():
103
+ rc, out, err = run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
104
+ if rc != 0:
105
+ die(f"could not determine the GitHub repo for this directory: {err}")
106
+ return out
107
+
108
+
109
+ # --- git helpers -----------------------------------------------------------
110
+
111
+
112
+ def worktrees():
113
+ """Map branch name -> worktree path, from `git worktree list --porcelain`."""
114
+ rc, out, _ = run(["git", "worktree", "list", "--porcelain"])
115
+ if rc != 0:
116
+ return {}
117
+ result, path = {}, None
118
+ for line in out.splitlines():
119
+ if line.startswith("worktree "):
120
+ path = line[len("worktree ") :]
121
+ elif line.startswith("branch ") and path:
122
+ branch = line[len("branch ") :]
123
+ if branch.startswith("refs/heads/"):
124
+ result[branch[len("refs/heads/") :]] = path
125
+ return result
126
+
127
+
128
+ def branch_exists_local(branch):
129
+ rc, _, _ = run(["git", "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"])
130
+ return rc == 0
131
+
132
+
133
+ def branch_exists_remote(branch):
134
+ rc, out, _ = run(["git", "ls-remote", "--heads", "origin", branch])
135
+ return rc == 0 and bool(out)
136
+
137
+
138
+ def all_known_branches():
139
+ rc, out, _ = run(
140
+ [
141
+ "git",
142
+ "for-each-ref",
143
+ "--format=%(refname:short)",
144
+ "refs/heads",
145
+ "refs/remotes/origin",
146
+ ]
147
+ )
148
+ if rc != 0:
149
+ return []
150
+ names = []
151
+ for ref in out.splitlines():
152
+ name = ref[len("origin/") :] if ref.startswith("origin/") else ref
153
+ if name and name != "HEAD" and name not in names:
154
+ names.append(name)
155
+ return names
156
+
157
+
158
+ # --- PR lookups ------------------------------------------------------------
159
+
160
+
161
+ def pr_by_number(number):
162
+ rc, out, err = run(["gh", "pr", "view", str(number), "--json", PR_FIELDS])
163
+ if rc != 0:
164
+ if "Could not resolve to a PullRequest" in err:
165
+ return None, f"there is no pull request #{number} in this repo"
166
+ return None, err or f"no pull request #{number} in this repo"
167
+ try:
168
+ return json.loads(out), None
169
+ except json.JSONDecodeError as exc:
170
+ return None, f"could not parse `gh pr view` output: {exc}"
171
+
172
+
173
+ def prs_for_branch(branch):
174
+ rc, out, err = run(
175
+ [
176
+ "gh", "pr", "list", "--head", branch, "--state", "all",
177
+ "--json", PR_FIELDS, "--limit", "20",
178
+ ]
179
+ )
180
+ if rc != 0:
181
+ return [], err or f"`gh pr list` failed for branch {branch}"
182
+ try:
183
+ return json.loads(out), None
184
+ except json.JSONDecodeError as exc:
185
+ return [], f"could not parse `gh pr list` output: {exc}"
186
+
187
+
188
+ def prs_by_search(term):
189
+ rc, out, _ = run(
190
+ [
191
+ "gh", "pr", "list", "--search", term, "--state", "all",
192
+ "--json", PR_FIELDS, "--limit", "20",
193
+ ]
194
+ )
195
+ if rc != 0:
196
+ return []
197
+ try:
198
+ return json.loads(out)
199
+ except json.JSONDecodeError:
200
+ return []
201
+
202
+
203
+ def pick_one(prs):
204
+ """Prefer the single OPEN PR; otherwise the highest-numbered one."""
205
+ open_prs = [p for p in prs if p.get("state") == "OPEN"]
206
+ if len(open_prs) == 1:
207
+ return open_prs[0]
208
+ pool = open_prs or prs
209
+ return max(pool, key=lambda p: p.get("number", 0)) if pool else None
210
+
211
+
212
+ def as_candidate(pr):
213
+ return {
214
+ "pr_number": pr.get("number"),
215
+ "url": pr.get("url"),
216
+ "branch": pr.get("headRefName"),
217
+ "base_ref": pr.get("baseRefName"),
218
+ "state": pr.get("state"),
219
+ "title": pr.get("title"),
220
+ }
221
+
222
+
223
+ # --- resolution ------------------------------------------------------------
224
+
225
+
226
+ def resolve(identifier, repo):
227
+ """Return (pr_dict, note) or emit an error/ambiguous payload and exit."""
228
+ ident = identifier.strip()
229
+
230
+ url_match = PR_URL_RE.fullmatch(ident)
231
+ if url_match:
232
+ owner, name, number = url_match.groups()
233
+ if f"{owner}/{name}".lower() != repo.lower():
234
+ die(
235
+ f"that PR URL belongs to {owner}/{name}, but this directory is {repo}. "
236
+ "cd into the right repo, or pass an identifier from this one."
237
+ )
238
+ pr, err = pr_by_number(number)
239
+ if not pr:
240
+ die(err)
241
+ return pr, f"resolved from PR URL #{number}"
242
+
243
+ num_match = PR_NUM_RE.match(ident)
244
+ if num_match:
245
+ number = num_match.group(1)
246
+ pr, err = pr_by_number(number)
247
+ if not pr:
248
+ die(err)
249
+ return pr, f"resolved from PR number #{number}"
250
+
251
+ # A ticket-shaped identifier may also be a literal branch name — colony's bare-ticket
252
+ # branches (`DOCS-5943`) and kebab variants (`docs-6171`) both look like ticket ids.
253
+ # An exact branch match is the more specific reading, so it wins; ticket search is the
254
+ # fallback for ids that name no branch directly.
255
+ if TICKET_RE.match(ident):
256
+ if branch_exists_local(ident) or branch_exists_remote(ident):
257
+ return resolve_branch(ident), f"resolved from branch {ident} (ticket-shaped name)"
258
+ return resolve_ticket(ident), f"resolved from ticket {ident.upper()}"
259
+
260
+ return resolve_branch(ident), f"resolved from branch {ident}"
261
+
262
+
263
+ def resolve_branch(branch):
264
+ local = branch_exists_local(branch)
265
+ remote = branch_exists_remote(branch)
266
+ if not local and not remote:
267
+ die(
268
+ f"branch `{branch}` does not exist locally or on origin. "
269
+ "Check the name (`git branch -a`), or pass a PR number or ticket id instead."
270
+ )
271
+
272
+ prs, err = prs_for_branch(branch)
273
+ if err:
274
+ die(err)
275
+ if not prs:
276
+ where = "locally and on origin" if local and remote else ("locally" if local else "on origin")
277
+ die(
278
+ f"branch `{branch}` exists {where} but has no pull request "
279
+ "(any state). Open a PR for it first — /attach-pr only attaches to an existing PR."
280
+ )
281
+ if len(prs) > 1:
282
+ chosen = pick_one(prs)
283
+ if not (chosen and chosen.get("state") == "OPEN" and
284
+ sum(1 for p in prs if p.get("state") == "OPEN") == 1):
285
+ emit({
286
+ "status": "ambiguous",
287
+ "message": f"branch `{branch}` has {len(prs)} pull requests; ask which one to attach.",
288
+ "candidates": [as_candidate(p) for p in prs],
289
+ })
290
+ return chosen
291
+ return prs[0]
292
+
293
+
294
+ def resolve_ticket(ticket):
295
+ """Ticket-tracker-agnostic: match the id against branch names and PR text."""
296
+ key = ticket.upper()
297
+ found = {}
298
+
299
+ for branch in all_known_branches():
300
+ if key in branch.upper():
301
+ prs, _ = prs_for_branch(branch)
302
+ for pr in prs:
303
+ found[pr["number"]] = pr
304
+
305
+ for pr in prs_by_search(key):
306
+ haystack = f"{pr.get('title', '')} {pr.get('headRefName', '')}".upper()
307
+ if key in haystack:
308
+ found.setdefault(pr["number"], pr)
309
+
310
+ prs = list(found.values())
311
+ if not prs:
312
+ die(
313
+ f"found no branch or pull request referencing `{key}` in this repo. "
314
+ "If the ticket exists but no PR does yet, there is nothing to attach; "
315
+ "otherwise pass the branch name or PR number directly."
316
+ )
317
+ if len(prs) > 1:
318
+ open_prs = [p for p in prs if p.get("state") == "OPEN"]
319
+ if len(open_prs) != 1:
320
+ emit({
321
+ "status": "ambiguous",
322
+ "message": f"`{key}` matches {len(prs)} pull requests; ask which one to attach.",
323
+ "candidates": [as_candidate(p) for p in prs],
324
+ })
325
+ return open_prs[0]
326
+ return prs[0]
327
+
328
+
329
+ # --- working directory -----------------------------------------------------
330
+
331
+
332
+ def resolve_workdir(branch, root):
333
+ """Where to attach from: an existing worktree, the base checkout, or nowhere."""
334
+ wts = worktrees()
335
+ if branch in wts:
336
+ path = wts[branch]
337
+ kind = "worktree" if os.path.realpath(path) != os.path.realpath(root) else "checkout"
338
+ return path, kind
339
+
340
+ rc, current, _ = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
341
+ if rc == 0 and current == branch:
342
+ return root, "checkout"
343
+
344
+ return None, "not_checked_out"
345
+
346
+
347
+ def main():
348
+ if len(sys.argv) != 2 or not sys.argv[1].strip():
349
+ die("usage: resolve_attach_target.py <pr-number|pr-url|branch|TICKET-123>")
350
+
351
+ root = repo_root()
352
+ require_gh()
353
+ repo = name_with_owner()
354
+
355
+ pr, note = resolve(sys.argv[1], repo)
356
+ branch = pr.get("headRefName")
357
+ base_ref = pr.get("baseRefName") or "main"
358
+ pr_url = pr.get("url")
359
+ if not branch:
360
+ die(f"PR #{pr.get('number')} has no head branch recorded; cannot attach")
361
+ if not is_safe_ref(branch) or not is_safe_ref(base_ref):
362
+ die("PR branch or base ref contains unsupported shell-unsafe characters")
363
+ if not is_safe_pr_url(pr_url):
364
+ die("PR URL is not a canonical https://github.com/<owner>/<repo>/pull/<number> URL")
365
+
366
+ workdir, kind = resolve_workdir(branch, root)
367
+
368
+ payload = {
369
+ "status": "ok",
370
+ "note": note,
371
+ "repo": repo,
372
+ "branch": branch,
373
+ "pr_number": pr.get("number"),
374
+ "pr_url": pr_url,
375
+ "base_ref": base_ref,
376
+ "pr_state": pr.get("state"),
377
+ "pr_title": pr.get("title"),
378
+ "workdir": workdir,
379
+ "workdir_kind": kind,
380
+ "repo_root": root,
381
+ "suggested_worktree": suggested_worktree(root, branch),
382
+ }
383
+ if workdir:
384
+ payload["attach_command"] = build_attach_command(workdir, pr_url, base_ref, branch)
385
+ emit(payload)
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()