gabriel / muse public
issues.py python
1,196 lines 47.1 KB
Raw
sha256:371209e571fc5fc0010114aaa8f272b15179a2245abd7f62320fa700fbe2a60b feat: add url and base64 id to hub issue/proposal/repo crea… Sonnet 4.6 patch 7 days ago
1 import argparse
2 import base64
3 from ._core import *
4
5 def run_issue_create(args: argparse.Namespace) -> None:
6 """Open a new issue on MuseHub.
7
8 Validates title length and emptiness before making any network calls.
9 Prints the issue URL to stdout in text mode (scriptable); use ``--json``
10 for the full API response merged with the standard envelope.
11
12 Agent quickstart
13 ----------------
14 ::
15
16 muse hub issue create --title "fix: bug" --body "details" --label bug --json
17 muse hub issue create --title "feat: X" --label "phase/1" --json
18 # → {"muse_version": "...", ..., "issueId": "...", "number": 42, ...}
19
20 Exit codes
21 ----------
22 0 Issue created successfully.
23 1 Validation error or not authenticated.
24 2 Not inside a Muse repository.
25 3 API error.
26 """
27 title: str = args.title
28 body: str = _resolve_body(args)
29 labels: list[str] = args.labels
30 symbol_anchors: list[str] = args.symbol_anchors
31 commit_anchors: list[str] = args.commit_anchors
32 agent_id: str = args.agent_id
33 model_id: str = args.model_id
34 assignee: str | None = getattr(args, "assignee", None)
35 json_output: bool = args.json_output
36
37 # ── Local validation first — fail fast before any network I/O ────────────
38 if not title.strip():
39 print("❌ Issue title must not be empty.", file=sys.stderr)
40 raise SystemExit(ExitCode.USER_ERROR)
41 if len(title) > _MAX_ISSUE_TITLE_LEN:
42 print(
43 f"❌ Issue title is too long ({len(title)} chars); "
44 f"maximum is {_MAX_ISSUE_TITLE_LEN}.",
45 file=sys.stderr,
46 )
47 raise SystemExit(ExitCode.USER_ERROR)
48 # Validate assignee handle before any network I/O. allow_empty=False:
49 # passing --assignee with an empty value is an error at creation time.
50 if assignee is not None:
51 _validate_assignee(assignee, allow_empty=False)
52
53 # ── Network calls ─────────────────────────────────────────────────────────
54 elapsed = start_timer()
55 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
56 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
57 repo_id = _resolve_repo_id(hub_url, identity)
58
59 payload: _ProposalPayload = {
60 "title": title,
61 "body": body,
62 "labels": labels,
63 "symbol_anchors": symbol_anchors,
64 "commit_anchors": commit_anchors,
65 "agent_id": agent_id,
66 "model_id": model_id,
67 }
68 data = _hub_api(hub_url, identity, "POST", f"/api/repos/{repo_id}/issues", body=payload)
69
70 _num = data.get("number")
71 try:
72 number = int(_num) if _num is not None else 0
73 except (ValueError, TypeError):
74 number = 0
75
76 # If an assignee was requested, dispatch the assignment now.
77 if assignee is not None:
78 issue_id = data.get("issueId") or str(number)
79 data = _hub_api(
80 hub_url, identity, "POST",
81 f"/api/repos/{repo_id}/issues/{number}/assign",
82 body={"assignee": assignee},
83 )
84
85 parsed = urllib.parse.urlparse(hub_url)
86 parts = parsed.path.strip("/").split("/")
87 if len(parts) >= 2:
88 owner, slug = parts[0], parts[1]
89 server_root = f"{parsed.scheme}://{parsed.netloc}"
90 issue_url = f"{server_root}/{owner}/{slug}/issues/{number}"
91 else:
92 issue_url = str(data.get("issueId", ""))
93
94 raw_issue_id = data.get("issueId", "")
95 if raw_issue_id.startswith("sha256:"):
96 issue_id_b64 = base64.urlsafe_b64encode(bytes.fromhex(raw_issue_id[7:])).rstrip(b"=").decode()
97 else:
98 issue_id_b64 = ""
99
100 if json_output:
101 extras = {"url": data.get("url") or issue_url, "issueIdB64": issue_id_b64}
102 print(json.dumps({**make_envelope(elapsed), **data, **extras}))
103 return
104
105 print(f"✅ Issue #{number} created.", file=sys.stderr)
106 if assignee:
107 print(f" Assigned to {sanitize_display(assignee)}.", file=sys.stderr)
108 print(sanitize_display(issue_url))
109
110 def run_issue_update(args: argparse.Namespace) -> None:
111 """Update an existing issue on MuseHub.
112
113 Updates title, body, anchors, and/or status; omitted fields are left
114 unchanged. All validation runs before any network I/O.
115
116 ``--status closed`` dispatches to the ``/close`` endpoint; ``--status open``
117 dispatches to ``/reopen``. Both may be combined with ``--title`` / ``--body``
118 in a single call — field updates are applied first, then the status change.
119
120 At least one of ``--title``, ``--body``, ``--anchor``, ``--commit-anchor``,
121 ``--status``, or ``--assign`` must be provided.
122
123 JSON output is the final API response merged with the standard envelope.
124 Human-readable text goes to stderr.
125
126 Agent quickstart
127 ----------------
128 ::
129
130 muse hub issue update 42 --status closed --body "done" --json
131 muse hub issue update 42 --title "new title" --json
132 # → {"muse_version": "...", ..., "number": 42, "status": "closed", ...}
133
134 Exit codes
135 ----------
136 0 Issue updated successfully.
137 1 Validation error or not authenticated.
138 2 Not inside a Muse repository.
139 3 API error.
140 """
141 number: int = args.number
142 json_output: bool = args.json_output
143 status: str | None = getattr(args, "status", None)
144 assign: str | None = getattr(args, "assign", None)
145
146 # ── Local validation first — fail fast before any network I/O ────────────
147 if number <= 0:
148 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
149 raise SystemExit(ExitCode.USER_ERROR)
150 # Validate the --assign handle if provided. allow_empty=True because an
151 # empty string is the documented way to unassign an issue.
152 if assign is not None:
153 _validate_assignee(assign, allow_empty=True)
154
155 patch: _ProposalPayload = {}
156 if args.title is not None:
157 if not args.title.strip():
158 print("❌ Issue title must not be empty.", file=sys.stderr)
159 raise SystemExit(ExitCode.USER_ERROR)
160 if len(args.title) > _MAX_ISSUE_TITLE_LEN:
161 print(
162 f"❌ Issue title is too long ({len(args.title)} chars); "
163 f"maximum is {_MAX_ISSUE_TITLE_LEN}.",
164 file=sys.stderr,
165 )
166 raise SystemExit(ExitCode.USER_ERROR)
167 patch["title"] = args.title
168 resolved_body = _resolve_body(args)
169 if args.body is not None or getattr(args, "body_file", None) is not None:
170 patch["body"] = resolved_body
171 if args.symbol_anchors is not None:
172 patch["symbol_anchors"] = args.symbol_anchors
173 if args.commit_anchors is not None:
174 patch["commit_anchors"] = args.commit_anchors
175
176 if not patch and status is None and assign is None:
177 print(
178 "❌ Nothing to update — provide --title, --body, --anchor, "
179 "--commit-anchor, --status, and/or --assign.",
180 file=sys.stderr,
181 )
182 raise SystemExit(ExitCode.USER_ERROR)
183
184 # ── Network calls ─────────────────────────────────────────────────────────
185 elapsed = start_timer()
186 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
187 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
188 repo_id = _resolve_repo_id(hub_url, identity)
189
190 data = {}
191
192 # Apply field updates first (title, body, anchors) if any.
193 if patch:
194 data = _hub_api(
195 hub_url, identity, "PATCH",
196 f"/api/repos/{repo_id}/issues/{number}",
197 body=patch,
198 )
199
200 # Then apply the status transition.
201 if status == "closed":
202 data = _hub_api(
203 hub_url, identity, "POST",
204 f"/api/repos/{repo_id}/issues/{number}/close",
205 )
206 elif status == "open":
207 data = _hub_api(
208 hub_url, identity, "POST",
209 f"/api/repos/{repo_id}/issues/{number}/reopen",
210 )
211
212 # Then apply the assignment.
213 if assign is not None:
214 data = _hub_api(
215 hub_url, identity, "POST",
216 f"/api/repos/{repo_id}/issues/{number}/assign",
217 body={"assignee": assign if assign else None},
218 )
219
220 if json_output:
221 print(json.dumps({**make_envelope(elapsed), **data}))
222 return
223
224 print(f"✅ Issue #{number} updated.", file=sys.stderr)
225
226 def run_issue_read(args: argparse.Namespace) -> None:
227 """Fetch a single issue by its per-repo number.
228
229 Prints a one-line summary to stderr in text mode; use ``--json`` for the
230 full API response merged with the standard envelope.
231
232 Agent quickstart
233 ----------------
234 ::
235
236 muse hub issue read 42 --json | jq '{number,title,state}'
237 # → {"muse_version": "...", ..., "number": 42, "title": "...", "state": "open"}
238
239 Exit codes
240 ----------
241 0 Issue found and printed.
242 1 Not authenticated.
243 2 Not inside a Muse repository.
244 3 API error (includes 404 if the issue does not exist).
245 """
246 number: int = args.number
247 json_output: bool = args.json_output
248
249 if number <= 0:
250 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
251 raise SystemExit(ExitCode.USER_ERROR)
252
253 elapsed = start_timer()
254 from muse.cli.commands.hub import _hub_api, _get_hub_and_optional_identity, _resolve_repo_id # noqa: PLC0415
255 hub_url, identity = _get_hub_and_optional_identity(hub_url_override=_resolve_hub_override(args))
256 repo_id = _resolve_repo_id(hub_url, identity)
257
258 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/issues/{number}")
259
260 if json_output:
261 print(json.dumps({**make_envelope(elapsed), **data}))
262 return
263
264 title = sanitize_display(str(data.get("title", "(no title)")))
265 state = sanitize_display(str(data.get("state", "?")))
266 state_icon = {"open": "🟢", "closed": "⛔"}.get(str(data.get("state", "")), "❓")
267 author = sanitize_display(str(data.get("author", "")))
268 created = sanitize_display(str(data.get("createdAt", ""))[:10])
269 print(f"\n {state_icon} [{state.upper()}] #{number} — {title}", file=sys.stderr)
270 print(f" Author: {author} Created: {created}", file=sys.stderr)
271
272 def run_issue_list(args: argparse.Namespace) -> None:
273 """List issues for the current repo.
274
275 Returns open issues by default; filter with ``--state`` and ``--label``.
276
277 User inputs are sanitized before use:
278 - ``--state`` is constrained by argparse ``choices`` and URL-encoded.
279 - ``--label`` is length-capped locally then percent-encoded in the query
280 string — no raw label value is ever interpolated into the URL directly.
281 - ``--limit`` is clamped to a safe integer range by :func:`clamp_int`.
282
283 JSON output is an object with envelope fields plus ``issues`` (array),
284 ``total`` (int), and ``next_cursor`` (str|null).
285
286 Agent quickstart
287 ----------------
288 ::
289
290 muse hub issue list --json
291 muse hub issue list --state closed --label bug --json
292 muse hub issue list --json | jq '.issues[].number'
293
294 Exit codes
295 ----------
296 0 Success (including empty list).
297 1 Validation error or not authenticated.
298 2 Not inside a Muse repository.
299 3 API error.
300 """
301 state: str = args.state
302 label: str | None = args.label
303 limit: int = clamp_int(args.limit, 1, 10000, "limit")
304 json_output: bool = args.json_output
305
306 # ── Local validation first — fail fast before any network I/O ────────────
307 if label is not None and len(label) > _MAX_ISSUE_LABEL_LEN:
308 print(
309 f"❌ Label is too long ({len(label)} chars); "
310 f"maximum is {_MAX_ISSUE_LABEL_LEN}.",
311 file=sys.stderr,
312 )
313 raise SystemExit(ExitCode.USER_ERROR)
314
315 # ── Network calls ─────────────────────────────────────────────────────────
316 elapsed = start_timer()
317 from muse.cli.commands.hub import _hub_api, _get_hub_and_optional_identity, _resolve_repo_id # noqa: PLC0415
318 hub_url, identity = _get_hub_and_optional_identity(hub_url_override=_resolve_hub_override(args))
319 repo_id = _resolve_repo_id(hub_url, identity)
320
321 # URL-encode state even though argparse already constrains it to a known
322 # set — defense-in-depth against any future code path that bypasses argparse.
323 params = f"?state={urllib.parse.quote(state, safe='')}&per_page={limit}"
324 if label:
325 params += f"&label={urllib.parse.quote(label, safe='')}"
326 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/issues{params}")
327
328 issues_val = data.get("issues", [])
329 issues: list[_HubApiResponse] = (
330 [r for r in issues_val if isinstance(r, dict)]
331 if isinstance(issues_val, list) else []
332 )
333 total: int = int(data.get("total", len(issues)))
334 next_cursor: str | None = data.get("nextCursor") or data.get("next_cursor") # type: ignore[assignment]
335
336 if json_output:
337 print(json.dumps({**make_envelope(elapsed), **{
338 "issues": issues,
339 "total": total,
340 "next_cursor": next_cursor,
341 }}))
342 return
343
344 if not issues:
345 print(f" No issues found (state={sanitize_display(state)}).", file=sys.stderr)
346 return
347
348 print(f"\n Issues — {sanitize_display(hub_url)} ({len(issues)} shown)", file=sys.stderr)
349 print(f" {'─' * 60}", file=sys.stderr)
350 for issue in issues:
351 _state = str(issue.get("state", "?"))
352 state_icon = {"open": "🟢", "closed": "⛔"}.get(_state, "❓")
353 _num = sanitize_display(str(issue.get("number", "?")))
354 _title = sanitize_display(str(issue.get("title", "(no title)")))
355 _author = sanitize_display(str(issue.get("author", "")))
356 print(f" {state_icon} #{_num} {_title} [{_author}]", file=sys.stderr)
357 print("", file=sys.stderr)
358
359 def run_issue_close(args: argparse.Namespace) -> None:
360 """Close an open issue on MuseHub.
361
362 Idempotent — closing an already-closed issue returns the issue unchanged.
363
364 JSON output is the raw API response merged with the standard envelope.
365 Human-readable text goes to stderr.
366
367 Agent quickstart
368 ----------------
369 ::
370
371 muse hub issue close 42 --json
372 # → {"muse_version": "...", ..., "number": 42, "state": "closed", ...}
373
374 Exit codes
375 ----------
376 0 Issue closed successfully.
377 1 Not authenticated.
378 2 Not inside a Muse repository.
379 3 API error (includes 404 if the issue does not exist).
380 """
381 number: int = args.number
382 json_output: bool = args.json_output
383
384 if number <= 0:
385 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
386 raise SystemExit(ExitCode.USER_ERROR)
387
388 elapsed = start_timer()
389 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
390 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
391 repo_id = _resolve_repo_id(hub_url, identity)
392
393 data = _hub_api(
394 hub_url, identity, "POST",
395 f"/api/repos/{repo_id}/issues/{number}/close",
396 )
397
398 if json_output:
399 print(json.dumps({**make_envelope(elapsed), **data}))
400 return
401
402 print(f"✅ Issue #{number} closed.", file=sys.stderr)
403
404 def run_issue_reopen(args: argparse.Namespace) -> None:
405 """Reopen a closed issue on MuseHub.
406
407 Idempotent — reopening an already-open issue returns the issue unchanged.
408
409 JSON output is the raw API response merged with the standard envelope.
410 Human-readable text goes to stderr.
411
412 Agent quickstart
413 ----------------
414 ::
415
416 muse hub issue reopen 42 --json
417 # → {"muse_version": "...", ..., "number": 42, "state": "open", ...}
418
419 Exit codes
420 ----------
421 0 Issue reopened successfully.
422 1 Not authenticated.
423 2 Not inside a Muse repository.
424 3 API error (includes 404 if the issue does not exist).
425 """
426 number: int = args.number
427 json_output: bool = args.json_output
428
429 if number <= 0:
430 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
431 raise SystemExit(ExitCode.USER_ERROR)
432
433 elapsed = start_timer()
434 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
435 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
436 repo_id = _resolve_repo_id(hub_url, identity)
437
438 data = _hub_api(
439 hub_url, identity, "POST",
440 f"/api/repos/{repo_id}/issues/{number}/reopen",
441 )
442
443 if json_output:
444 print(json.dumps({**make_envelope(elapsed), **data}))
445 return
446
447 print(f"✅ Issue #{number} reopened.", file=sys.stderr)
448
449 def run_issue_comment(args: argparse.Namespace) -> None:
450 """Post a Markdown comment on an issue on MuseHub.
451
452 Returns the created comment as a single flat resource merged with the
453 standard envelope.
454
455 Agent quickstart
456 ----------------
457 ::
458
459 muse hub issue comment 42 --body 'Fixed in abc123' --json
460 # → {"muse_version": "...", ..., "commentId": "...", "author": "..."}
461
462 JSON output keys (from hub): ``commentId``, ``issueId``, ``author``,
463 ``body``, ``parentId``, ``isDeleted``, ``createdAt``, ``updatedAt``.
464
465 Exit codes
466 ----------
467 0 Comment posted successfully.
468 1 Validation or auth error.
469 2 Not inside a Muse repository.
470 3 API error (includes 404 if the issue does not exist).
471 """
472 number: int = args.number
473 body: str = _resolve_body(args)
474 json_output: bool = args.json_output
475
476 if number <= 0:
477 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
478 raise SystemExit(ExitCode.USER_ERROR)
479 if not body.strip():
480 print("❌ Comment body must not be empty (use --body or --body-file).", file=sys.stderr)
481 raise SystemExit(ExitCode.USER_ERROR)
482 if len(body) > _MAX_ISSUE_COMMENT_LEN:
483 print(
484 f"❌ Comment body is too long ({len(body)} chars); "
485 f"maximum is {_MAX_ISSUE_COMMENT_LEN}.",
486 file=sys.stderr,
487 )
488 raise SystemExit(ExitCode.USER_ERROR)
489
490 elapsed = start_timer()
491 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
492 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
493 repo_id = _resolve_repo_id(hub_url, identity)
494
495 payload: _ProposalPayload = {"body": body}
496 data = _hub_api(
497 hub_url, identity, "POST",
498 f"/api/repos/{repo_id}/issues/{number}/comments",
499 body=payload,
500 )
501
502 if json_output:
503 print(json.dumps({**make_envelope(elapsed), **data}))
504 return
505
506 comment_id = data.get("commentId", "")
507 print(f"✅ Comment posted on issue #{number} (commentId={comment_id}).", file=sys.stderr)
508
509 def run_issue_comment_delete(args: argparse.Namespace) -> None:
510 """Soft-delete a comment from an issue on MuseHub.
511
512 Deleted comments are hidden from list results but preserved in the audit log.
513 Requires write/admin access or repo ownership.
514
515 JSON output includes envelope fields plus ``deleted`` (bool) and
516 ``comment_id`` (str).
517
518 Agent quickstart
519 ----------------
520 ::
521
522 muse hub issue comment-delete 42 --comment-id <id> --json
523 # → {"muse_version": "...", ..., "deleted": true, "comment_id": "..."}
524
525 Exit codes
526 ----------
527 0 Comment deleted successfully.
528 1 Validation or auth error.
529 2 Not inside a Muse repository.
530 3 API error (includes 404 if the comment does not exist).
531 """
532 number: int = args.number
533 comment_id: str = args.comment_id
534 json_output: bool = args.json_output
535
536 if number <= 0:
537 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
538 raise SystemExit(ExitCode.USER_ERROR)
539 if not comment_id.strip():
540 print("❌ --comment-id must not be empty.", file=sys.stderr)
541 raise SystemExit(ExitCode.USER_ERROR)
542
543 elapsed = start_timer()
544 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
545 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
546 repo_id = _resolve_repo_id(hub_url, identity)
547
548 _hub_api(
549 hub_url, identity, "DELETE",
550 f"/api/repos/{repo_id}/issues/{number}/comments/{comment_id}",
551 )
552
553 if json_output:
554 print(json.dumps({**make_envelope(elapsed), **{"deleted": True, "comment_id": comment_id}}))
555 return
556
557 print(f"✅ Comment {comment_id} deleted from issue #{number}.", file=sys.stderr)
558
559 def run_issue_comment_list(args: argparse.Namespace) -> None:
560 """List non-deleted comments on an issue, oldest first.
561
562 Supports cursor pagination via ``--cursor``.
563
564 JSON output is the raw API response merged with the standard envelope.
565 Human-readable text goes to stdout (not stderr) for easy piping.
566
567 Agent quickstart
568 ----------------
569 ::
570
571 muse hub issue comment-list 42 --json
572 muse hub issue comment-list 42 --limit 50 --json | jq '.comments[].author'
573 muse hub issue comment-list 42 --cursor <token> --json
574
575 JSON output keys (from hub): ``comments`` (list), ``nextCursor`` (str|null),
576 ``total`` (int). Each comment: ``commentId``, ``issueId``, ``author``,
577 ``body``, ``createdAt``, ``updatedAt``.
578
579 Exit codes
580 ----------
581 0 Success.
582 1 Validation or auth error.
583 2 Not inside a Muse repository.
584 3 API error (includes 404 if the issue does not exist).
585 """
586 number: int = args.number
587 limit: int = args.limit
588 cursor: str | None = args.cursor
589 json_output: bool = args.json_output
590
591 if number <= 0:
592 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
593 raise SystemExit(ExitCode.USER_ERROR)
594
595 elapsed = start_timer()
596 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
597 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
598 repo_id = _resolve_repo_id(hub_url, identity)
599
600 qs = f"?limit={limit}"
601 if cursor:
602 qs += f"&cursor={urllib.parse.quote(cursor, safe='')}"
603
604 data = _hub_api(
605 hub_url, identity, "GET",
606 f"/api/repos/{repo_id}/issues/{number}/comments{qs}",
607 )
608
609 if json_output:
610 print(json.dumps({**make_envelope(elapsed), **data}))
611 return
612
613 comments = data.get("comments", [])
614 if not comments:
615 print(f"No comments on issue #{number}.")
616 return
617
618 for c in comments:
619 author = c.get("author", "unknown")
620 created = c.get("createdAt", "")[:10]
621 body = str(c.get("body", "")).split("\n")[0][:80]
622 cid = str(c.get("commentId", ""))
623 print(f" [{created}] {author} ({cid}): {body}")
624
625 next_cursor = data.get("nextCursor")
626 if next_cursor:
627 print(f"\n (more — pass --cursor {next_cursor})")
628
629 def run_issue_assign(args: argparse.Namespace) -> None:
630 """Assign or unassign a collaborator on an issue on MuseHub.
631
632 Pass an empty string for ``--assignee`` to clear the current assignee.
633
634 JSON output is the raw API response merged with the standard envelope.
635 Human-readable text goes to stderr.
636
637 Agent quickstart
638 ----------------
639 ::
640
641 muse hub issue assign 42 --assignee gabriel --json
642 muse hub issue assign 42 --assignee '' --json # unassign
643 # → {"muse_version": "...", ..., "number": 42, "assignee": "gabriel"}
644
645 Exit codes
646 ----------
647 0 Assignee updated successfully.
648 1 Not authenticated.
649 2 Not inside a Muse repository.
650 3 API error (includes 404 if the issue does not exist).
651 """
652 number: int = args.number
653 assignee: str = args.assignee
654 json_output: bool = args.json_output
655
656 if number <= 0:
657 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
658 raise SystemExit(ExitCode.USER_ERROR)
659 # Validate handle before any network I/O. allow_empty=True because an
660 # empty string means "unassign" (documented behaviour of this command).
661 _validate_assignee(assignee, allow_empty=True)
662
663 elapsed = start_timer()
664 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
665 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
666 repo_id = _resolve_repo_id(hub_url, identity)
667
668 payload: dict[str, str | None] = {"assignee": assignee if assignee else None}
669 data = _hub_api(
670 hub_url, identity, "POST",
671 f"/api/repos/{repo_id}/issues/{number}/assign",
672 body=payload,
673 )
674
675 if json_output:
676 print(json.dumps({**make_envelope(elapsed), **data}))
677 return
678
679 if assignee:
680 print(f"✅ Issue #{number} assigned to {sanitize_display(assignee)}.", file=sys.stderr)
681 else:
682 print(f"✅ Issue #{number} unassigned.", file=sys.stderr)
683
684 def run_issue_label(args: argparse.Namespace) -> None:
685 """Manage labels on an issue on MuseHub.
686
687 Use ``--set`` to replace the entire label list, or ``--remove`` to strip
688 a single label without affecting others.
689
690 JSON output is the raw API response merged with the standard envelope.
691 Human-readable text goes to stderr.
692
693 Agent quickstart
694 ----------------
695 ::
696
697 muse hub issue label 42 --set bug enhancement --json
698 muse hub issue label 42 --remove bug --json
699 # → {"muse_version": "...", ..., "labels": [...]}
700
701 Exit codes
702 ----------
703 0 Labels updated successfully.
704 1 Not authenticated.
705 2 Not inside a Muse repository.
706 3 API error (includes 404 if the issue does not exist).
707 """
708 number: int = args.number
709 set_labels: list[str] | None = args.set_labels
710 remove_label: str | None = args.remove_label
711 json_output: bool = args.json_output
712
713 if number <= 0:
714 print(f"❌ Issue number must be a positive integer, got {number}.", file=sys.stderr)
715 raise SystemExit(ExitCode.USER_ERROR)
716
717 elapsed = start_timer()
718 from muse.cli.commands.hub import _hub_api, _get_hub_and_identity, _resolve_repo_id # noqa: PLC0415
719 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
720 repo_id = _resolve_repo_id(hub_url, identity)
721
722 if set_labels is not None:
723 label_body: _ProposalPayload = {"labels": set_labels}
724 data = _hub_api(
725 hub_url, identity, "POST",
726 f"/api/repos/{repo_id}/issues/{number}/labels",
727 body=label_body,
728 )
729 if json_output:
730 print(json.dumps({**make_envelope(elapsed), **data}))
731 return
732 print(f"✅ Issue #{number} labels set to {set_labels}.", file=sys.stderr)
733 else:
734 # remove_label is guaranteed non-None (mutually exclusive group)
735 label_name: str = remove_label or ""
736 data = _hub_api(
737 hub_url, identity, "DELETE",
738 f"/api/repos/{repo_id}/issues/{number}/labels/{urllib.parse.quote(label_name, safe='')}",
739 )
740 if json_output:
741 print(json.dumps({**make_envelope(elapsed), **data}))
742 return
743 print(f"✅ Label '{label_name}' removed from issue #{number}.", file=sys.stderr)
744
745 def register(subs: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
746 """Register issues subcommands."""
747 # ── issue ─────────────────────────────────────────────────────────────────
748 issue_p = subs.add_parser(
749 "issue",
750 help="Manage issues on MuseHub.",
751 formatter_class=argparse.RawDescriptionHelpFormatter,
752 )
753 issue_subs = issue_p.add_subparsers(dest="issue_subcommand", metavar="ISSUE_COMMAND")
754 issue_subs.required = True
755
756 issue_create_p = issue_subs.add_parser(
757 "create",
758 help="Open a new issue.",
759 description=(
760 "Open a new issue on MuseHub.\n\n"
761 f"Title must be non-empty and ≤ {_MAX_ISSUE_TITLE_LEN} characters.\n"
762 "Use --label (repeatable) to apply labels at creation time.\n"
763 "Use --anchor (repeatable) to link to Muse symbols (path/to/file.py::Symbol).\n"
764 "Use --commit-anchor (repeatable) to link to specific Muse commits.\n"
765 "Use --agent-id / --model-id when filing on behalf of an AI agent.\n"
766 "In text mode the issue URL is printed to stdout — capture it with $().\n\n"
767 "Agent quickstart:\n"
768 " muse hub issue create --title 'bug: crash in create_issue' \\\n"
769 " --anchor musehub/services/musehub_issues.py::create_issue \\\n"
770 " --agent-id agentception-worker-42 --model-id claude-sonnet-4-6 --json\n"
771 " URL=$(muse hub issue create --title 'feat: X' --label enhancement)\n\n"
772 "Exit codes: 0 created, 1 validation/auth error, 2 not in repo, 3 API error."
773 ),
774 formatter_class=argparse.RawDescriptionHelpFormatter,
775 )
776 issue_create_p.add_argument(
777 "--hub", dest="hub", default=None, metavar="URL",
778 help="Override the hub URL from config (e.g. http://host.docker.internal:10003/owner/repo).",
779 )
780 issue_create_p.add_argument(
781 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
782 help="Alias for --hub: specify repo as owner/repo (hub base URL taken from config).",
783 )
784 issue_create_p.add_argument("--title", "-t", required=True, help="Issue title.")
785 issue_create_p.add_argument("--body", "-b", default="", help="Issue body (inline text).")
786 issue_create_p.add_argument(
787 "--body-file", default=None, metavar="PATH", dest="body_file",
788 help="Read issue body from PATH (UTF-8). Pass '-' to read from stdin. "
789 "Takes precedence over --body. Use this for large bodies with "
790 "backticks, code blocks, or multi-line content.",
791 )
792 issue_create_p.add_argument(
793 "--label", "-l", dest="labels", action="append", default=[],
794 help="Label name to apply (repeatable).",
795 )
796 issue_create_p.add_argument(
797 "--anchor", "-a", dest="symbol_anchors", action="append", default=[],
798 metavar="FILE::SYMBOL",
799 help=(
800 "Muse symbol address to anchor this issue to (repeatable). "
801 "Format: path/to/file.py::SymbolName. "
802 "Example: --anchor musehub/services/musehub_issues.py::create_issue"
803 ),
804 )
805 issue_create_p.add_argument(
806 "--commit-anchor", dest="commit_anchors", action="append", default=[],
807 metavar="COMMIT_ID",
808 help="Muse commit ID to anchor this issue to (repeatable).",
809 )
810 issue_create_p.add_argument(
811 "--agent-id", dest="agent_id", default="",
812 help="Agent identifier when filing on behalf of an AI agent (e.g. agentception-worker-42).",
813 )
814 issue_create_p.add_argument(
815 "--model-id", dest="model_id", default="",
816 help="Model identifier when filing on behalf of an AI agent (e.g. claude-sonnet-4-6).",
817 )
818 issue_create_p.add_argument(
819 "--assignee", dest="assignee", default=None, metavar="USER",
820 help=(
821 "Assign the issue to USER at creation time. The handle is validated "
822 "before any network I/O: must start with a letter or digit, contain "
823 "only letters/digits/hyphens/underscores, be ASCII-only, and be ≤ "
824 f"{_MAX_HANDLE_LEN} characters. A second POST to the /assign endpoint "
825 "is dispatched immediately after the issue is created."
826 ),
827 )
828 issue_create_p.add_argument(
829 "--json", "-j", action="store_true", dest="json_output",
830 help="Emit JSON with the created issue.",
831 )
832 issue_create_p.set_defaults(func=run_issue_create)
833
834 issue_update_p = issue_subs.add_parser(
835 "update",
836 help="Update an existing issue.",
837 description=(
838 "Update an existing issue on MuseHub.\n\n"
839 "Updates title, body, anchors, and/or status; omitted fields are left unchanged.\n"
840 "At least one of --title, --body, --anchor, --commit-anchor, --status, or --assign must be provided.\n"
841 f"If --title is given it must be non-empty and ≤ {_MAX_ISSUE_TITLE_LEN} characters.\n"
842 "--anchor / --commit-anchor replace the full anchor list (send all desired anchors).\n"
843 "--status closed calls the /close endpoint; --status open calls /reopen.\n\n"
844 "Agent quickstart:\n"
845 " muse hub issue update 42 --status closed\n"
846 " muse hub issue update 42 --status closed --body 'done' --json\n"
847 " muse hub issue update 42 --assign gabriel --status closed\n"
848 " muse hub issue update 42 --body 'updated description' --json\n"
849 " muse hub issue update 42 \\\n"
850 " --anchor musehub/services/musehub_issues.py::create_issue \\\n"
851 " --anchor musehub/db/musehub_models.py::MusehubIssue --json\n\n"
852 "Exit codes: 0 updated, 1 validation/auth error, 2 not in repo, 3 API error."
853 ),
854 formatter_class=argparse.RawDescriptionHelpFormatter,
855 )
856 issue_update_p.add_argument("number", type=int, help="Issue number.")
857 issue_update_p.add_argument(
858 "--hub", dest="hub", default=None, metavar="URL",
859 help="Override the hub URL from config.",
860 )
861 issue_update_p.add_argument(
862 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
863 help="Alias for --hub: specify repo as owner/repo (hub base URL taken from config).",
864 )
865 issue_update_p.add_argument("--title", "-t", default=None, help="New title.")
866 issue_update_p.add_argument("--body", "-b", default=None, help="New body (inline text).")
867 issue_update_p.add_argument(
868 "--body-file", default=None, metavar="PATH", dest="body_file",
869 help="Read new body from PATH. Pass '-' for stdin. Takes precedence over --body.",
870 )
871 issue_update_p.add_argument(
872 "--status", choices=["open", "closed"], default=None,
873 help="Set issue status: 'open' (reopen) or 'closed'. Dispatches to the appropriate endpoint.",
874 )
875 issue_update_p.add_argument(
876 "--anchor", "-a", dest="symbol_anchors", action="append", default=None,
877 metavar="FILE::SYMBOL",
878 help=(
879 "Replacement symbol anchor (repeatable; replaces all existing anchors). "
880 "Format: path/to/file.py::SymbolName."
881 ),
882 )
883 issue_update_p.add_argument(
884 "--commit-anchor", dest="commit_anchors", action="append", default=None,
885 metavar="COMMIT_ID",
886 help="Replacement commit anchor (repeatable; replaces all existing anchors).",
887 )
888 issue_update_p.add_argument(
889 "--assign", dest="assign", default=None, metavar="USER",
890 help=(
891 "Assign the issue to USER. Pass an empty string to unassign. "
892 "Dispatches to the /assign endpoint after any other field updates."
893 ),
894 )
895 issue_update_p.add_argument(
896 "--json", "-j", action="store_true", dest="json_output",
897 help="Emit JSON with the updated issue.",
898 )
899 issue_update_p.set_defaults(func=run_issue_update)
900
901 issue_read_p = issue_subs.add_parser(
902 "read",
903 help="Read a single issue.",
904 description=(
905 "Fetch a single issue by its per-repo number.\n\n"
906 "Agent quickstart:\n"
907 " muse hub issue read 42 --json\n"
908 " muse hub issue read 42 --json | jq '{number,title,state}'\n\n"
909 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
910 ),
911 formatter_class=argparse.RawDescriptionHelpFormatter,
912 )
913 issue_read_p.add_argument("number", type=int, help="Issue number.")
914 issue_read_p.add_argument(
915 "--hub", dest="hub", default=None, metavar="URL",
916 help="Override the hub URL from config.",
917 )
918 issue_read_p.add_argument(
919 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
920 help="Specify repo as owner/repo.",
921 )
922 issue_read_p.add_argument(
923 "--json", "-j", action="store_true", dest="json_output",
924 help="Emit JSON with the issue.",
925 )
926 issue_read_p.set_defaults(func=run_issue_read)
927
928 issue_list_p = issue_subs.add_parser(
929 "list",
930 help="List issues for this repo.",
931 description=(
932 "List issues on MuseHub for the current repo.\n\n"
933 "Agent quickstart:\n"
934 " muse hub issue list --json\n"
935 " muse hub issue list --state closed --json\n"
936 " muse hub issue list --label bug --json\n\n"
937 "Exit codes: 0 success (including empty list), 1 auth error, 2 not in repo, 3 API error."
938 ),
939 formatter_class=argparse.RawDescriptionHelpFormatter,
940 )
941 issue_list_p.add_argument(
942 "--hub", dest="hub", default=None, metavar="URL",
943 help="Override the hub URL from config.",
944 )
945 issue_list_p.add_argument(
946 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
947 help="Specify repo as owner/repo.",
948 )
949 issue_list_p.add_argument(
950 "--state", dest="state", default="open",
951 choices=["open", "closed", "all"],
952 help="Filter by state (default: open).",
953 )
954 issue_list_p.add_argument(
955 "--label", dest="label", default=None, metavar="LABEL",
956 help="Filter by label string.",
957 )
958 issue_list_p.add_argument(
959 "--limit", dest="limit", type=int, default=100, metavar="N",
960 help="Maximum number of issues to return (default: 100).",
961 )
962 issue_list_p.add_argument(
963 "--json", "-j", action="store_true", dest="json_output",
964 help="Emit JSON array of issues.",
965 )
966 issue_list_p.set_defaults(func=run_issue_list)
967
968 issue_close_p = issue_subs.add_parser(
969 "close",
970 help="Close an open issue.",
971 description=(
972 "Close an issue on MuseHub.\n\n"
973 "Agent quickstart:\n"
974 " muse hub issue close 42\n"
975 " muse hub issue close 42 --json\n\n"
976 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
977 ),
978 formatter_class=argparse.RawDescriptionHelpFormatter,
979 )
980 issue_close_p.add_argument("number", type=int, help="Issue number.")
981 issue_close_p.add_argument(
982 "--hub", dest="hub", default=None, metavar="URL",
983 help="Override the hub URL from config.",
984 )
985 issue_close_p.add_argument(
986 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
987 help="Specify repo as owner/repo.",
988 )
989 issue_close_p.add_argument(
990 "--json", "-j", action="store_true", dest="json_output",
991 help="Emit JSON with the updated issue.",
992 )
993 issue_close_p.set_defaults(func=run_issue_close)
994
995 issue_reopen_p = issue_subs.add_parser(
996 "reopen",
997 help="Reopen a closed issue.",
998 description=(
999 "Reopen a closed issue on MuseHub.\n\n"
1000 "Agent quickstart:\n"
1001 " muse hub issue reopen 42\n"
1002 " muse hub issue reopen 42 --json\n\n"
1003 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
1004 ),
1005 formatter_class=argparse.RawDescriptionHelpFormatter,
1006 )
1007 issue_reopen_p.add_argument("number", type=int, help="Issue number.")
1008 issue_reopen_p.add_argument(
1009 "--hub", dest="hub", default=None, metavar="URL",
1010 help="Override the hub URL from config.",
1011 )
1012 issue_reopen_p.add_argument(
1013 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1014 help="Specify repo as owner/repo.",
1015 )
1016 issue_reopen_p.add_argument(
1017 "--json", "-j", action="store_true", dest="json_output",
1018 help="Emit JSON with the updated issue.",
1019 )
1020 issue_reopen_p.set_defaults(func=run_issue_reopen)
1021
1022 issue_comment_p = issue_subs.add_parser(
1023 "comment",
1024 help="Post a comment on an issue.",
1025 description=(
1026 "Post a Markdown comment on an issue on MuseHub.\n\n"
1027 "Agent quickstart:\n"
1028 " muse hub issue comment 42 --body 'Fixed in commit abc123'\n"
1029 " muse hub issue comment 42 --body 'see also #43' --json\n\n"
1030 "Exit codes: 0 success, 1 validation/auth error, 2 not in repo, 3 API error."
1031 ),
1032 formatter_class=argparse.RawDescriptionHelpFormatter,
1033 )
1034 issue_comment_p.add_argument("number", type=int, help="Issue number.")
1035 issue_comment_p.add_argument(
1036 "--hub", dest="hub", default=None, metavar="URL",
1037 help="Override the hub URL from config.",
1038 )
1039 issue_comment_p.add_argument(
1040 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1041 help="Specify repo as owner/repo.",
1042 )
1043 issue_comment_p.add_argument(
1044 "--body", "-b", default=None, dest="body",
1045 help="Comment body (Markdown, inline text).",
1046 )
1047 issue_comment_p.add_argument(
1048 "--body-file", default=None, metavar="PATH", dest="body_file",
1049 help="Read comment body from PATH. Pass '-' for stdin. Takes precedence over --body.",
1050 )
1051 issue_comment_p.add_argument(
1052 "--json", "-j", action="store_true", dest="json_output",
1053 help="Emit JSON with the updated comment list.",
1054 )
1055 issue_comment_p.set_defaults(func=run_issue_comment)
1056
1057 issue_comment_delete_p = issue_subs.add_parser(
1058 "comment-delete",
1059 help="Soft-delete a comment from an issue.",
1060 description=(
1061 "Soft-delete a comment on an issue on MuseHub.\n\n"
1062 "The comment is hidden from list results but preserved in the audit log.\n"
1063 "Requires write/admin access or repo ownership.\n\n"
1064 "Agent quickstart:\n"
1065 " muse hub issue comment-delete 42 --comment-id <id>\n"
1066 " muse hub issue comment-delete 42 --comment-id <id> --json\n\n"
1067 "Exit codes: 0 success, 1 validation/auth error, 2 not in repo, 3 API error."
1068 ),
1069 formatter_class=argparse.RawDescriptionHelpFormatter,
1070 )
1071 issue_comment_delete_p.add_argument("number", type=int, help="Issue number.")
1072 issue_comment_delete_p.add_argument(
1073 "--comment-id", dest="comment_id", required=True, metavar="ID",
1074 help="ID of the comment to delete.",
1075 )
1076 issue_comment_delete_p.add_argument(
1077 "--hub", dest="hub", default=None, metavar="URL",
1078 help="Override the hub URL from config.",
1079 )
1080 issue_comment_delete_p.add_argument(
1081 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1082 help="Specify repo as owner/repo.",
1083 )
1084 issue_comment_delete_p.add_argument(
1085 "--json", "-j", action="store_true", dest="json_output",
1086 help="Emit JSON confirmation on success.",
1087 )
1088 issue_comment_delete_p.set_defaults(func=run_issue_comment_delete)
1089
1090 issue_comment_list_p = issue_subs.add_parser(
1091 "comment-list",
1092 help="List comments on an issue.",
1093 description=(
1094 "List all non-deleted comments on an issue, oldest first.\n\n"
1095 "Agent quickstart:\n"
1096 " muse hub issue comment-list 42\n"
1097 " muse hub issue comment-list 42 --limit 50 --json\n\n"
1098 "Exit codes: 0 success, 1 validation/auth error, 2 not in repo, 3 API error."
1099 ),
1100 formatter_class=argparse.RawDescriptionHelpFormatter,
1101 )
1102 issue_comment_list_p.add_argument("number", type=int, help="Issue number.")
1103 issue_comment_list_p.add_argument(
1104 "--hub", dest="hub", default=None, metavar="URL",
1105 help="Override the hub URL from config.",
1106 )
1107 issue_comment_list_p.add_argument(
1108 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1109 help="Specify repo as owner/repo.",
1110 )
1111 issue_comment_list_p.add_argument(
1112 "--limit", type=int, default=100, metavar="N",
1113 help="Maximum comments to return (default 100).",
1114 )
1115 issue_comment_list_p.add_argument(
1116 "--cursor", default=None, metavar="CURSOR",
1117 help="Pagination cursor from a previous call.",
1118 )
1119 issue_comment_list_p.add_argument(
1120 "--json", "-j", action="store_true", dest="json_output",
1121 help="Emit JSON list of comments.",
1122 )
1123 issue_comment_list_p.set_defaults(func=run_issue_comment_list)
1124
1125 issue_assign_p = issue_subs.add_parser(
1126 "assign",
1127 help="Assign or unassign a collaborator on an issue.",
1128 description=(
1129 "Assign or unassign a collaborator on an issue on MuseHub.\n\n"
1130 "Agent quickstart:\n"
1131 " muse hub issue assign 42 --assignee gabriel\n"
1132 " muse hub issue assign 42 --assignee '' # unassign\n"
1133 " muse hub issue assign 42 --assignee gabriel --json\n\n"
1134 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
1135 ),
1136 formatter_class=argparse.RawDescriptionHelpFormatter,
1137 )
1138 issue_assign_p.add_argument("number", type=int, help="Issue number.")
1139 issue_assign_p.add_argument(
1140 "--assignee", dest="assignee", required=True, metavar="USER",
1141 help="Username to assign, or empty string to unassign.",
1142 )
1143 issue_assign_p.add_argument(
1144 "--hub", dest="hub", default=None, metavar="URL",
1145 help="Override the hub URL from config.",
1146 )
1147 issue_assign_p.add_argument(
1148 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1149 help="Specify repo as owner/repo.",
1150 )
1151 issue_assign_p.add_argument(
1152 "--json", "-j", action="store_true", dest="json_output",
1153 help="Emit JSON with the updated issue.",
1154 )
1155 issue_assign_p.set_defaults(func=run_issue_assign)
1156
1157 issue_label_p = issue_subs.add_parser(
1158 "label",
1159 help="Manage labels on an issue.",
1160 description=(
1161 "Add or remove labels on an issue on MuseHub.\n\n"
1162 "Agent quickstart:\n"
1163 " muse hub issue label 42 --set bug enhancement\n"
1164 " muse hub issue label 42 --remove bug\n"
1165 " muse hub issue label 42 --set bug --json\n\n"
1166 "--set replaces the entire label list. --remove removes a single label.\n"
1167 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
1168 ),
1169 formatter_class=argparse.RawDescriptionHelpFormatter,
1170 )
1171 issue_label_p.add_argument("number", type=int, help="Issue number.")
1172 _issue_label_mutex = issue_label_p.add_mutually_exclusive_group(required=True)
1173 _issue_label_mutex.add_argument(
1174 "--set", dest="set_labels", nargs="+", metavar="LABEL",
1175 help="Replace the entire label list with these labels.",
1176 )
1177 _issue_label_mutex.add_argument(
1178 "--remove", dest="remove_label", metavar="LABEL",
1179 help="Remove a single label from the issue.",
1180 )
1181 issue_label_p.add_argument(
1182 "--hub", dest="hub", default=None, metavar="URL",
1183 help="Override the hub URL from config.",
1184 )
1185 issue_label_p.add_argument(
1186 "--repo", dest="repo", default=None, metavar="OWNER/REPO",
1187 help="Specify repo as owner/repo.",
1188 )
1189 issue_label_p.add_argument(
1190 "--json", "-j", action="store_true", dest="json_output",
1191 help="Emit JSON with the updated issue.",
1192 )
1193 issue_label_p.set_defaults(func=run_issue_label)
1194
1195 issue_p.set_defaults(func=lambda a: issue_p.print_help())
1196
File History 2 commits
sha256:371209e571fc5fc0010114aaa8f272b15179a2245abd7f62320fa700fbe2a60b feat: add url and base64 id to hub issue/proposal/repo crea… Sonnet 4.6 patch 7 days ago
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 7 days ago