gabriel / musehub public
issues.py python
734 lines 27.7 KB
Raw
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 6 days ago
1 """Write executors for issue operations: create, update, close, reopen, assign, set/remove labels, comment."""
2
3 import logging
4
5 from musehub.types.json_types import JSONObject, JSONValue
6 from musehub.db.database import AsyncSessionLocal
7 from musehub.services import musehub_issues, musehub_repository
8 from musehub.services.musehub_mcp_executor import MusehubToolResult, _check_db_available
9
10 logger = logging.getLogger(__name__)
11
12
13 def _issue_data(issue: "IssueResponse") -> JSONObject:
14 """Serialise an IssueResponse to a ``JSONObject``."""
15 from musehub.models.musehub import IssueResponse # local import for forward ref
16 return {
17 "issue_id": issue.issue_id,
18 "number": issue.number,
19 "title": issue.title,
20 "body": issue.body,
21 "state": issue.state,
22 "labels": list(issue.labels),
23 "author": issue.author,
24 "assignee": issue.assignee,
25 "symbol_anchors": list(issue.symbol_anchors),
26 "commit_anchors": list(issue.commit_anchors),
27 "agent_id": issue.agent_id,
28 "model_id": issue.model_id,
29 "created_at": issue.created_at.isoformat() if issue.created_at else None,
30 "updated_at": issue.updated_at.isoformat() if issue.updated_at else None,
31 }
32
33
34 async def execute_create_issue(
35 *,
36 repo_id: str,
37 title: str,
38 body: str = "",
39 labels: list[str] | None = None,
40 symbol_anchors: list[str] | None = None,
41 commit_anchors: list[str] | None = None,
42 agent_id: str = "",
43 model_id: str = "",
44 actor: str = "",
45 ) -> MusehubToolResult:
46 """Open a new issue in a MuseHub repository.
47
48 The caller must be authenticated and have write access: either the repo
49 owner, or an accepted write/admin collaborator. For private repos this
50 restricts issue creation to authorised parties only.
51
52 Args:
53 repo_id: sha256 genesis ID of the target repository.
54 title: Issue title.
55 body: Optional markdown description.
56 labels: Optional list of label strings to apply on creation.
57 symbol_anchors: Muse symbol addresses this issue is anchored to
58 (e.g. ``["musehub/services/musehub_issues.py::create_issue"]``).
59 commit_anchors: Muse commit IDs this issue is anchored to.
60 agent_id: Agent identifier when filed by an AI agent (e.g. ``"agentception-worker-42"``).
61 model_id: Model identifier when filed by an AI agent (e.g. ``"claude-sonnet-4-6"``).
62 actor: Authenticated user ID (MSign handle).
63
64 Returns:
65 ``MusehubToolResult`` with ``data.issue_id`` and ``data.number`` on success.
66 """
67 if (err := _check_db_available()) is not None:
68 return err
69
70 try:
71 async with AsyncSessionLocal() as session:
72 repo = await musehub_repository.get_repo(session, repo_id)
73 if repo is None:
74 return MusehubToolResult(
75 ok=False,
76 error_code="repo_not_found",
77 error_message=f"Repository '{repo_id}' not found.",
78 hint="Call musehub_search_repos() to find available repositories, or musehub_set_context(owner, slug) to focus on a repo.",
79 )
80 if (err := await _require_public_or_write_access(
81 session, repo_id, actor, repo.owner, repo.visibility
82 )) is not None:
83 return err
84
85 actor_identity_id = await musehub_repository.get_identity_id_for_handle(session, actor)
86 issue = await musehub_issues.create_issue(
87 session,
88 repo_id=repo_id,
89 title=title,
90 body=body,
91 labels=labels or [],
92 author=actor,
93 author_identity_id=actor_identity_id,
94 symbol_anchors=symbol_anchors or None,
95 commit_anchors=commit_anchors or None,
96 agent_id=agent_id,
97 model_id=model_id,
98 )
99 await session.commit()
100 logger.info("MCP create_issue #%d in %s: %s", issue.number, repo_id, title)
101 return MusehubToolResult(ok=True, data=_issue_data(issue))
102 except Exception as exc:
103 logger.exception("MCP create_issue failed: %s", exc)
104 return MusehubToolResult(
105 ok=False,
106 error_code="invalid_args",
107 error_message=str(exc),
108 )
109
110
111 async def execute_update_issue(
112 *,
113 repo_id: str,
114 issue_number: int,
115 title: str | None = None,
116 body: str | None = None,
117 labels: list[str] | None = None,
118 symbol_anchors: list[str] | None = None,
119 commit_anchors: list[str] | None = None,
120 state: str | None = None,
121 assignee: str | None = None,
122 actor: str = "",
123 ) -> MusehubToolResult:
124 """Update an existing issue's title, body, labels, anchors, state, or assignee.
125
126 Only supplied (non-None) fields are modified. Closing/reopening an issue
127 requires passing ``state="closed"`` or ``state="open"`` respectively. The
128 caller must be authenticated and have write access to the repository.
129
130 Args:
131 repo_id: sha256 genesis ID of the repository.
132 issue_number: Per-repo issue number.
133 title: New title (optional).
134 body: New markdown body (optional).
135 labels: Replacement label list (optional; replaces existing labels).
136 symbol_anchors: Replacement symbol anchor list (optional; replaces existing anchors).
137 commit_anchors: Replacement commit anchor list (optional; replaces existing anchors).
138 state: ``"open"`` or ``"closed"`` (optional).
139 assignee: Username to assign, or empty string to unassign (optional).
140 actor: Authenticated user ID (MSign handle).
141
142 Returns:
143 ``MusehubToolResult`` with updated issue data on success.
144 """
145 if (err := _check_db_available()) is not None:
146 return err
147
148 try:
149 async with AsyncSessionLocal() as session:
150 repo = await musehub_repository.get_repo(session, repo_id)
151 if repo is None:
152 return MusehubToolResult(
153 ok=False,
154 error_code="repo_not_found",
155 error_message=f"Repository '{repo_id}' not found.",
156 hint="Call musehub_search_repos() to find available repositories, or musehub_set_context(owner, slug) to focus on a repo.",
157 )
158 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
159 return err
160
161 if title is not None or body is not None or labels is not None or symbol_anchors is not None or commit_anchors is not None:
162 issue = await musehub_issues.update_issue(
163 session,
164 repo_id,
165 issue_number,
166 title=title,
167 body=body,
168 labels=labels,
169 symbol_anchors=symbol_anchors,
170 commit_anchors=commit_anchors,
171 )
172 if issue is None:
173 return MusehubToolResult(
174 ok=False,
175 error_code="issue_not_found",
176 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
177 hint="Call musehub_list_issues() to see open issues.",
178 )
179
180 if state == "closed":
181 issue = await musehub_issues.close_issue(session, repo_id, issue_number)
182 elif state == "open":
183 issue = await musehub_issues.reopen_issue(session, repo_id, issue_number)
184
185 if assignee is not None:
186 issue = await musehub_issues.assign_issue(
187 session,
188 repo_id,
189 issue_number,
190 assignee=assignee or None,
191 )
192
193 # Fetch final state if not already set.
194 if title is None and body is None and labels is None and symbol_anchors is None and commit_anchors is None and state is None and assignee is None:
195 issue = await musehub_issues.get_issue(session, repo_id, issue_number)
196
197 await session.commit()
198
199 if issue is None:
200 return MusehubToolResult(
201 ok=False,
202 error_code="issue_not_found",
203 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
204 hint="Call musehub_list_issues() to see open issues.",
205 )
206 return MusehubToolResult(ok=True, data=_issue_data(issue))
207 except Exception as exc:
208 logger.exception("MCP update_issue failed: %s", exc)
209 return MusehubToolResult(
210 ok=False,
211 error_code="invalid_args",
212 error_message=str(exc),
213 )
214
215
216 async def execute_create_issue_comment(
217 *,
218 repo_id: str,
219 issue_number: int,
220 body: str,
221 actor: str = "",
222 ) -> MusehubToolResult:
223 """Add a comment to an existing issue.
224
225 The caller must be authenticated and have write access to the repository.
226 An empty ``actor`` is rejected immediately with a forbidden error.
227
228 Args:
229 repo_id: sha256 genesis ID of the repository.
230 issue_number: Per-repo issue number.
231 body: Markdown comment body.
232 actor: Authenticated user ID (MSign handle).
233
234 Returns:
235 ``MusehubToolResult`` with ``data.comment_id`` on success.
236 """
237 if (err := _check_db_available()) is not None:
238 return err
239
240 try:
241 async with AsyncSessionLocal() as session:
242 repo = await musehub_repository.get_repo(session, repo_id)
243 if repo is None:
244 return MusehubToolResult(
245 ok=False,
246 error_code="repo_not_found",
247 error_message=f"Repository '{repo_id}' not found.",
248 )
249 if (err := await _require_public_or_write_access(
250 session, repo_id, actor, repo.owner, repo.visibility
251 )) is not None:
252 return err
253
254 issue = await musehub_issues.get_issue(session, repo_id, issue_number)
255 if issue is None:
256 return MusehubToolResult(
257 ok=False,
258 error_code="issue_not_found",
259 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
260 hint="Call musehub_list_issues() to see open issues.",
261 )
262
263 actor_identity_id = await musehub_repository.get_identity_id_for_handle(session, actor)
264 comment = await musehub_issues.create_comment(
265 session,
266 issue_id=issue.issue_id,
267 repo_id=repo_id,
268 author=actor,
269 author_identity_id=actor_identity_id,
270 body=body,
271 )
272 await session.commit()
273 data: JSONObject = {
274 "comment_id": comment.comment_id,
275 "issue_number": issue_number,
276 "author": comment.author,
277 "body": comment.body,
278 "created_at": comment.created_at.isoformat() if comment.created_at else None,
279 }
280 logger.info("MCP create_issue_comment on #%d in %s", issue_number, repo_id)
281 return MusehubToolResult(ok=True, data=data)
282 except Exception as exc:
283 logger.exception("MCP create_issue_comment failed: %s", exc)
284 return MusehubToolResult(
285 ok=False,
286 error_code="invalid_args",
287 error_message=str(exc),
288 )
289
290
291 async def _require_write_access(
292 session: "AsyncSession",
293 repo_id: str,
294 actor: str,
295 repo_owner: str,
296 ) -> "MusehubToolResult | None":
297 """Return a ``forbidden`` error result if *actor* lacks write access, else None.
298
299 Mirrors the ownership + collaborator check performed by the REST route layer
300 (``_guard_repo_owner``). An empty *actor* is treated as unauthenticated and
301 always returns a ``forbidden`` error.
302
303 Use this for state-changing operations (close, reopen, assign, delete) that
304 require explicit repo membership regardless of visibility.
305
306 Args:
307 session: Active async DB session.
308 repo_id: sha256 genesis ID of the repository.
309 actor: Identity handle of the MCP caller.
310 repo_owner: Owner handle of the repository.
311
312 Returns:
313 ``None`` when access is granted, or a ``MusehubToolResult`` with
314 ``error_code="forbidden"`` when access is denied.
315 """
316 if not actor:
317 return MusehubToolResult(
318 ok=False,
319 error_code="forbidden",
320 error_message="Authentication required.",
321 hint="Provide a valid MSign Authorization header.",
322 )
323 has_access = await musehub_repository.check_write_access(
324 session, repo_id, actor, repo_owner
325 )
326 if not has_access:
327 return MusehubToolResult(
328 ok=False,
329 error_code="forbidden",
330 error_message="Only the repo owner or a write/admin collaborator may perform this action.",
331 hint="Use musehub_read_context() to confirm the repo owner.",
332 )
333 return None
334
335
336 async def _require_public_or_write_access(
337 session: "AsyncSession",
338 repo_id: str,
339 actor: str,
340 repo_owner: str,
341 repo_visibility: str,
342 ) -> "MusehubToolResult | None":
343 """Return a ``forbidden`` error result when *actor* cannot write, else None.
344
345 Mirrors the REST ``_guard_write_access`` helper:
346
347 - Public repos allow any authenticated (non-empty) *actor* to create issues,
348 comments, and proposals.
349 - Private repos require the actor to be the owner or an accepted write/admin
350 collaborator — matching ``_guard_repo_owner``.
351
352 An empty *actor* is always rejected regardless of visibility.
353
354 Args:
355 session: Active async DB session.
356 repo_id: sha256 genesis ID of the repository.
357 actor: Identity handle of the MCP caller.
358 repo_owner: Owner handle of the repository.
359 repo_visibility: ``"public"`` or ``"private"``.
360
361 Returns:
362 ``None`` when access is granted, or a ``MusehubToolResult`` with
363 ``error_code="forbidden"`` when access is denied.
364 """
365 if not actor:
366 return MusehubToolResult(
367 ok=False,
368 error_code="forbidden",
369 error_message="Authentication required.",
370 hint="Provide a valid MSign Authorization header.",
371 )
372 if repo_visibility == "public":
373 return None
374 has_access = await musehub_repository.check_write_access(
375 session, repo_id, actor, repo_owner
376 )
377 if not has_access:
378 return MusehubToolResult(
379 ok=False,
380 error_code="forbidden",
381 error_message="Only the repo owner or a write/admin collaborator may write to a private repository.",
382 hint="Use musehub_read_context() to confirm the repo owner.",
383 )
384 return None
385
386
387 async def execute_close_issue(
388 *,
389 repo_id: str,
390 issue_number: int,
391 actor: str = "",
392 ) -> MusehubToolResult:
393 """Close an open issue.
394
395 Idempotent — closing an already-closed issue returns the issue unchanged.
396
397 Args:
398 repo_id: sha256 genesis ID of the repository.
399 issue_number: Per-repo issue number.
400 actor: Authenticated user ID (MSign handle).
401
402 Returns:
403 ``MusehubToolResult`` with updated issue data on success.
404 """
405 if (err := _check_db_available()) is not None:
406 return err
407
408 try:
409 async with AsyncSessionLocal() as session:
410 repo = await musehub_repository.get_repo(session, repo_id)
411 if repo is None:
412 return MusehubToolResult(
413 ok=False,
414 error_code="repo_not_found",
415 error_message=f"Repository '{repo_id}' not found.",
416 hint="Call musehub_search_repos() to find available repositories.",
417 )
418 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
419 return err
420 issue = await musehub_issues.close_issue(session, repo_id, issue_number)
421 if issue is None:
422 return MusehubToolResult(
423 ok=False,
424 error_code="issue_not_found",
425 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
426 hint="Call musehub_list_issues() to see open issues.",
427 )
428 await session.commit()
429 logger.info("MCP close_issue #%d in %s by %s", issue_number, repo_id, actor)
430 return MusehubToolResult(ok=True, data=_issue_data(issue))
431 except Exception as exc:
432 logger.exception("MCP close_issue failed: %s", exc)
433 return MusehubToolResult(
434 ok=False,
435 error_code="invalid_args",
436 error_message=str(exc),
437 )
438
439
440 async def execute_reopen_issue(
441 *,
442 repo_id: str,
443 issue_number: int,
444 actor: str = "",
445 ) -> MusehubToolResult:
446 """Reopen a closed issue.
447
448 Idempotent — reopening an already-open issue returns the issue unchanged.
449
450 Args:
451 repo_id: sha256 genesis ID of the repository.
452 issue_number: Per-repo issue number.
453 actor: Authenticated user ID (MSign handle).
454
455 Returns:
456 ``MusehubToolResult`` with updated issue data on success.
457 """
458 if (err := _check_db_available()) is not None:
459 return err
460
461 try:
462 async with AsyncSessionLocal() as session:
463 repo = await musehub_repository.get_repo(session, repo_id)
464 if repo is None:
465 return MusehubToolResult(
466 ok=False,
467 error_code="repo_not_found",
468 error_message=f"Repository '{repo_id}' not found.",
469 hint="Call musehub_search_repos() to find available repositories.",
470 )
471 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
472 return err
473 issue = await musehub_issues.reopen_issue(session, repo_id, issue_number)
474 if issue is None:
475 return MusehubToolResult(
476 ok=False,
477 error_code="issue_not_found",
478 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
479 hint="Call musehub_list_issues() to see open issues.",
480 )
481 await session.commit()
482 logger.info("MCP reopen_issue #%d in %s by %s", issue_number, repo_id, actor)
483 return MusehubToolResult(ok=True, data=_issue_data(issue))
484 except Exception as exc:
485 logger.exception("MCP reopen_issue failed: %s", exc)
486 return MusehubToolResult(
487 ok=False,
488 error_code="invalid_args",
489 error_message=str(exc),
490 )
491
492
493 async def execute_assign_issue(
494 *,
495 repo_id: str,
496 issue_number: int,
497 assignee: str,
498 actor: str = "",
499 ) -> MusehubToolResult:
500 """Assign or unassign a collaborator on an issue.
501
502 Pass an empty string for ``assignee`` to clear the current assignee.
503
504 Args:
505 repo_id: sha256 genesis ID of the repository.
506 issue_number: Per-repo issue number.
507 assignee: Username to assign, or empty string to unassign.
508 actor: Authenticated user ID (MSign handle).
509
510 Returns:
511 ``MusehubToolResult`` with updated issue data on success.
512 """
513 if (err := _check_db_available()) is not None:
514 return err
515
516 try:
517 async with AsyncSessionLocal() as session:
518 repo = await musehub_repository.get_repo(session, repo_id)
519 if repo is None:
520 return MusehubToolResult(
521 ok=False,
522 error_code="repo_not_found",
523 error_message=f"Repository '{repo_id}' not found.",
524 hint="Call musehub_search_repos() to find available repositories.",
525 )
526 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
527 return err
528 issue = await musehub_issues.assign_issue(
529 session,
530 repo_id,
531 issue_number,
532 assignee=assignee or None,
533 )
534 if issue is None:
535 return MusehubToolResult(
536 ok=False,
537 error_code="issue_not_found",
538 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
539 hint="Call musehub_list_issues() to see open issues.",
540 )
541 await session.commit()
542 logger.info("MCP assign_issue #%d in %s to %r by %s", issue_number, repo_id, assignee, actor)
543 return MusehubToolResult(ok=True, data=_issue_data(issue))
544 except Exception as exc:
545 logger.exception("MCP assign_issue failed: %s", exc)
546 return MusehubToolResult(
547 ok=False,
548 error_code="invalid_args",
549 error_message=str(exc),
550 )
551
552
553 async def execute_update_issue_labels(
554 *,
555 repo_id: str,
556 issue_number: int,
557 labels: list[str],
558 actor: str = "",
559 ) -> MusehubToolResult:
560 """Bulk-replace the label list on an issue.
561
562 The replacement is total — the new list replaces all existing labels.
563 Pass an empty list to remove all labels.
564
565 Args:
566 repo_id: sha256 genesis ID of the repository.
567 issue_number: Per-repo issue number.
568 labels: Replacement label list (replaces all existing labels).
569 actor: Authenticated user ID (MSign handle).
570
571 Returns:
572 ``MusehubToolResult`` with updated issue data on success.
573 """
574 if (err := _check_db_available()) is not None:
575 return err
576
577 try:
578 async with AsyncSessionLocal() as session:
579 repo = await musehub_repository.get_repo(session, repo_id)
580 if repo is None:
581 return MusehubToolResult(
582 ok=False,
583 error_code="repo_not_found",
584 error_message=f"Repository '{repo_id}' not found.",
585 hint="Call musehub_search_repos() to find available repositories.",
586 )
587 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
588 return err
589 issue = await musehub_issues.assign_labels(
590 session,
591 repo_id,
592 issue_number,
593 labels=labels,
594 )
595 if issue is None:
596 return MusehubToolResult(
597 ok=False,
598 error_code="issue_not_found",
599 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
600 hint="Call musehub_list_issues() to see open issues.",
601 )
602 await session.commit()
603 logger.info("MCP set_issue_labels #%d in %s to %r by %s", issue_number, repo_id, labels, actor)
604 return MusehubToolResult(ok=True, data=_issue_data(issue))
605 except Exception as exc:
606 logger.exception("MCP set_issue_labels failed: %s", exc)
607 return MusehubToolResult(
608 ok=False,
609 error_code="invalid_args",
610 error_message=str(exc),
611 )
612
613
614 async def execute_delete_issue_comment(
615 *,
616 repo_id: str,
617 issue_number: int,
618 comment_id: str,
619 actor: str = "",
620 ) -> MusehubToolResult:
621 """Soft-delete a comment from an issue.
622
623 Deleted comments are hidden from list results but preserved in the audit
624 log. The caller must be the repo owner or a write/admin collaborator.
625
626 Args:
627 repo_id: sha256 genesis ID of the repository.
628 issue_number: Per-repo issue number.
629 comment_id: sha256 genesis ID of the comment to delete.
630 actor: Authenticated user ID (MSign handle).
631
632 Returns:
633 ``MusehubToolResult`` with ``data.deleted=true`` on success.
634 """
635 if (err := _check_db_available()) is not None:
636 return err
637
638 try:
639 async with AsyncSessionLocal() as session:
640 repo = await musehub_repository.get_repo(session, repo_id)
641 if repo is None:
642 return MusehubToolResult(
643 ok=False,
644 error_code="repo_not_found",
645 error_message=f"Repository '{repo_id}' not found.",
646 hint="Call musehub_search_repos() to find available repositories.",
647 )
648 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
649 return err
650 issue = await musehub_issues.get_issue(session, repo_id, issue_number)
651 if issue is None:
652 return MusehubToolResult(
653 ok=False,
654 error_code="issue_not_found",
655 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
656 hint="Call musehub_list_issues() to see open issues.",
657 )
658 deleted = await musehub_issues.delete_comment(session, comment_id, issue.issue_id)
659 if not deleted:
660 return MusehubToolResult(
661 ok=False,
662 error_code="comment_not_found",
663 error_message=f"Comment '{comment_id}' not found on issue #{issue_number}.",
664 )
665 await session.commit()
666 logger.info("MCP delete_issue_comment %s on #%d in %s by %s", comment_id, issue_number, repo_id, actor)
667 return MusehubToolResult(ok=True, data={"deleted": True, "comment_id": comment_id})
668 except Exception as exc:
669 logger.exception("MCP delete_issue_comment failed: %s", exc)
670 return MusehubToolResult(
671 ok=False,
672 error_code="invalid_args",
673 error_message=str(exc),
674 )
675
676
677 async def execute_remove_issue_label(
678 *,
679 repo_id: str,
680 issue_number: int,
681 label: str,
682 actor: str = "",
683 ) -> MusehubToolResult:
684 """Remove a single label from an issue.
685
686 Idempotent — silently no-ops when the label is not present on the issue.
687
688 Args:
689 repo_id: sha256 genesis ID of the repository.
690 issue_number: Per-repo issue number.
691 label: Label name to remove.
692 actor: Authenticated user ID (MSign handle).
693
694 Returns:
695 ``MusehubToolResult`` with updated issue data on success.
696 """
697 if (err := _check_db_available()) is not None:
698 return err
699
700 try:
701 async with AsyncSessionLocal() as session:
702 repo = await musehub_repository.get_repo(session, repo_id)
703 if repo is None:
704 return MusehubToolResult(
705 ok=False,
706 error_code="repo_not_found",
707 error_message=f"Repository '{repo_id}' not found.",
708 hint="Call musehub_search_repos() to find available repositories.",
709 )
710 if (err := await _require_write_access(session, repo_id, actor, repo.owner)) is not None:
711 return err
712 issue = await musehub_issues.remove_label(
713 session,
714 repo_id,
715 issue_number,
716 label=label,
717 )
718 if issue is None:
719 return MusehubToolResult(
720 ok=False,
721 error_code="issue_not_found",
722 error_message=f"Issue #{issue_number} not found in repo '{repo_id}'.",
723 hint="Call musehub_list_issues() to see open issues.",
724 )
725 await session.commit()
726 logger.info("MCP remove_issue_label #%d in %s label=%r by %s", issue_number, repo_id, label, actor)
727 return MusehubToolResult(ok=True, data=_issue_data(issue))
728 except Exception as exc:
729 logger.exception("MCP remove_issue_label failed: %s", exc)
730 return MusehubToolResult(
731 ok=False,
732 error_code="invalid_args",
733 error_message=str(exc),
734 )
File History 1 commit
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 6 days ago