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.
- package/LICENSE +21 -0
- package/README.md +18 -11
- package/adapters/cursor/agents/executor.md +2 -2
- package/adapters/cursor/agents/implementer.md +2 -2
- package/adapters/cursor/agents/review-lens.md +22 -0
- package/adapters/cursor/agents/reviewer.md +3 -2
- package/adapters/opencode/agents.json +44 -5
- package/adapters/opencode/plugin.js +435 -47
- package/config/MCP_PINS.md +17 -0
- package/config/models.json +647 -33
- package/hooks/bash-guard.py +51 -9
- package/hooks/session-start.py +27 -0
- package/package.json +19 -8
- package/roles/executor.md +2 -2
- package/roles/implementer.md +2 -2
- package/roles/review-lens.md +20 -0
- package/roles/reviewer.md +3 -2
- package/scripts/doctor.py +520 -0
- package/scripts/ghreview.py +558 -0
- package/scripts/jsonc_bridge.cjs +23 -0
- package/scripts/memory.py +744 -0
- package/scripts/render_adapters.py +253 -121
- package/scripts/resolve_attach_target.py +389 -0
- package/scripts/setup.py +1753 -0
- package/skills/brainstorming/SKILL.md +3 -1
- package/skills/debugging/SKILL.md +4 -2
- package/skills/delegation/SKILL.md +10 -8
- package/skills/doctor/SKILL.md +124 -0
- package/skills/executing-plans/SKILL.md +2 -1
- package/skills/finishing-a-branch/SKILL.md +4 -2
- package/skills/freshness/SKILL.md +131 -0
- package/skills/memory/SKILL.md +154 -0
- package/skills/resolve-ticket/SKILL.md +275 -0
- package/skills/review-pr/SKILL.md +327 -0
- package/skills/setup/SKILL.md +199 -0
- package/skills/setup/agents/openai.yaml +5 -0
- package/skills/test-first/SKILL.md +3 -1
- package/skills/using-leo/SKILL.md +18 -6
- package/skills/using-leo/references/claude-mapping.md +23 -1
- package/skills/using-leo/references/codex-mapping.md +18 -9
- package/skills/using-leo/references/cursor-mapping.md +19 -6
- package/skills/using-leo/references/hermes-mapping.md +18 -7
- package/skills/using-leo/references/opencode-mapping.md +18 -9
- package/skills/verification/SKILL.md +9 -1
- package/skills/visual-verification/SKILL.md +115 -0
- package/skills/watch-review/SKILL.md +128 -0
- package/skills/watch-review/agents/openai.yaml +5 -0
- package/skills/worktrees/SKILL.md +3 -1
- package/skills/writing-plans/SKILL.md +2 -1
- package/skills/writing-skills/SKILL.md +141 -0
- package/vendor/jsonc-parser-3.3.1/LICENSE.md +21 -0
- package/vendor/jsonc-parser-3.3.1/README.md +26 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/edit.js +201 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/format.js +275 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/parser.js +682 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/scanner.js +456 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/impl/string-intern.js +42 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.d.ts +351 -0
- package/vendor/jsonc-parser-3.3.1/lib/umd/main.js +194 -0
- package/vendor/jsonc-parser-3.3.1/package.json +37 -0
- 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()
|