monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.11__py3-none-any.whl

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 (132) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/config.py +38 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/hooks/builtin/git_cleanup.py +1 -1
  8. monoco/core/ingestion/__init__.py +20 -0
  9. monoco/core/ingestion/discovery.py +248 -0
  10. monoco/core/ingestion/watcher.py +343 -0
  11. monoco/core/ingestion/worker.py +436 -0
  12. monoco/core/injection.py +63 -29
  13. monoco/core/integrations.py +2 -2
  14. monoco/core/loader.py +633 -0
  15. monoco/core/output.py +5 -5
  16. monoco/core/registry.py +34 -19
  17. monoco/core/resource/__init__.py +5 -0
  18. monoco/core/resource/finder.py +98 -0
  19. monoco/core/resource/manager.py +91 -0
  20. monoco/core/resource/models.py +35 -0
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +524 -385
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/daemon/app.py +77 -1
  26. monoco/daemon/commands.py +10 -0
  27. monoco/daemon/mailroom_service.py +196 -0
  28. monoco/daemon/models.py +1 -0
  29. monoco/daemon/scheduler.py +236 -0
  30. monoco/daemon/services.py +185 -0
  31. monoco/daemon/triggers.py +55 -0
  32. monoco/features/agent/__init__.py +2 -2
  33. monoco/features/agent/adapter.py +41 -0
  34. monoco/features/agent/apoptosis.py +44 -0
  35. monoco/features/agent/cli.py +101 -144
  36. monoco/features/agent/config.py +35 -21
  37. monoco/features/agent/defaults.py +6 -49
  38. monoco/features/agent/engines.py +32 -6
  39. monoco/features/agent/manager.py +47 -6
  40. monoco/features/agent/models.py +2 -2
  41. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  42. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  43. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  44. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  45. monoco/{core/resources/en → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +3 -1
  46. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +94 -0
  47. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +93 -0
  48. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +85 -0
  49. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +114 -0
  50. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  51. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  52. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  53. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +49 -0
  54. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +46 -0
  55. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +46 -0
  56. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +47 -0
  57. monoco/{core/resources/zh → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +3 -1
  58. monoco/features/agent/resources/{skills/flow_engineer → zh/skills/monoco_workflow_agent_engineer}/SKILL.md +2 -2
  59. monoco/features/agent/resources/{skills/flow_manager → zh/skills/monoco_workflow_agent_manager}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +259 -0
  61. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +137 -0
  62. monoco/features/agent/session.py +59 -11
  63. monoco/features/agent/worker.py +38 -2
  64. monoco/features/artifact/__init__.py +0 -0
  65. monoco/features/artifact/adapter.py +33 -0
  66. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  67. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  68. monoco/features/glossary/__init__.py +0 -0
  69. monoco/features/glossary/adapter.py +42 -0
  70. monoco/features/glossary/config.py +5 -0
  71. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  72. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +35 -0
  73. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  74. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +35 -0
  75. monoco/features/hooks/__init__.py +11 -0
  76. monoco/features/hooks/adapter.py +67 -0
  77. monoco/features/hooks/commands.py +309 -0
  78. monoco/features/hooks/core.py +441 -0
  79. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  80. monoco/features/i18n/adapter.py +18 -5
  81. monoco/features/i18n/core.py +482 -17
  82. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  83. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +105 -0
  84. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  85. monoco/features/i18n/resources/{skills/i18n_scan_workflow → zh/skills/monoco_workflow_i18n_scan}/SKILL.md +2 -2
  86. monoco/features/issue/adapter.py +19 -6
  87. monoco/features/issue/commands.py +281 -7
  88. monoco/features/issue/core.py +272 -19
  89. monoco/features/issue/engine/machine.py +118 -5
  90. monoco/features/issue/linter.py +60 -5
  91. monoco/features/issue/models.py +3 -2
  92. monoco/features/issue/resources/en/AGENTS.md +109 -0
  93. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_atom_issue/SKILL.md} +3 -1
  94. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  95. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  96. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +159 -0
  97. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  98. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  99. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  100. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  101. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  102. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_atom_issue_lifecycle/SKILL.md} +3 -1
  103. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  104. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  105. monoco/features/issue/resources/{skills/issue_lifecycle_workflow → zh/skills/monoco_workflow_issue_management}/SKILL.md +2 -2
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  107. monoco/features/issue/validator.py +101 -1
  108. monoco/features/memo/adapter.py +21 -8
  109. monoco/features/memo/cli.py +103 -10
  110. monoco/features/memo/core.py +178 -92
  111. monoco/features/memo/models.py +53 -0
  112. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +77 -0
  113. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +140 -0
  114. monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_atom_memo/SKILL.md} +3 -1
  115. monoco/features/memo/resources/{skills/note_processing_workflow → zh/skills/monoco_workflow_note_processing}/SKILL.md +2 -2
  116. monoco/features/spike/adapter.py +18 -5
  117. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  118. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +121 -0
  119. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  120. monoco/features/spike/resources/{skills/research_workflow → zh/skills/monoco_workflow_research}/SKILL.md +2 -2
  121. monoco/main.py +38 -1
  122. monoco_toolkit-0.3.11.dist-info/METADATA +130 -0
  123. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  124. monoco/features/agent/reliability.py +0 -106
  125. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
  126. monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
  127. monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
  128. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  129. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  130. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,32 @@
1
1
  from pathlib import Path
2
2
  from typing import Dict
3
- from monoco.core.feature import MonocoFeature, IntegrationData
3
+ from monoco.core.loader import FeatureModule, FeatureMetadata
4
+ from monoco.core.feature import IntegrationData
4
5
  from monoco.features.issue import core
5
6
 
6
7
 
7
- class IssueFeature(MonocoFeature):
8
- @property
9
- def name(self) -> str:
10
- return "issue"
8
+ class IssueFeature(FeatureModule):
9
+ """Issue management feature module with unified lifecycle support."""
11
10
 
12
- def initialize(self, root: Path, config: Dict) -> None:
11
+ @property
12
+ def metadata(self) -> FeatureMetadata:
13
+ return FeatureMetadata(
14
+ name="issue",
15
+ version="1.0.0",
16
+ description="Issue management system for Monoco",
17
+ dependencies=["core"],
18
+ priority=10, # High priority - load early
19
+ )
20
+
21
+ def _on_mount(self, context: "FeatureContext") -> None: # type: ignore
22
+ """Initialize issue feature with workspace context."""
23
+ root = context.root
24
+ config = context.config
13
25
  issues_path = root / config.get("paths", {}).get("issues", "Issues")
14
26
  core.init(issues_path)
15
27
 
16
28
  def integrate(self, root: Path, config: Dict) -> IntegrationData:
29
+ """Provide integration data for agent environment."""
17
30
  # Determine language from config, default to 'en'
18
31
  lang = config.get("i18n", {}).get("source_lang", "en")
19
32
 
@@ -43,6 +43,9 @@ def create(
43
43
  related: List[str] = typer.Option(
44
44
  [], "--related", "-r", help="Related Issue ID(s)"
45
45
  ),
46
+ from_memo: List[str] = typer.Option(
47
+ [], "--from-memo", "-m", help="Memo ID(s) to link to this issue"
48
+ ),
46
49
  force: bool = typer.Option(False, "--force", help="Bypass branch context checks"),
47
50
  subdir: Optional[str] = typer.Option(
48
51
  None,
@@ -118,20 +121,46 @@ def create(
118
121
  criticality=criticality_level,
119
122
  )
120
123
 
124
+ # Link memos to the newly created issue
125
+ linked_memos = []
126
+ missing_memos = []
127
+ if from_memo:
128
+ from monoco.features.memo.core import load_memos, update_memo
129
+
130
+ existing_memos = {m.uid: m for m in load_memos(issues_root)}
131
+
132
+ for memo_id in from_memo:
133
+ if memo_id in existing_memos:
134
+ # Only update if not already linked to this issue (idempotency)
135
+ memo = existing_memos[memo_id]
136
+ if memo.ref != issue.id:
137
+ update_memo(issues_root, memo_id, {"status": "tracked", "ref": issue.id})
138
+ linked_memos.append(memo_id)
139
+ else:
140
+ missing_memos.append(memo_id)
141
+
121
142
  try:
122
143
  rel_path = path.relative_to(Path.cwd())
123
144
  except ValueError:
124
145
  rel_path = path
125
146
 
126
147
  if OutputManager.is_agent_mode():
127
- OutputManager.print(
128
- {"issue": issue, "path": str(rel_path), "status": "created"}
129
- )
148
+ result = {"issue": issue, "path": str(rel_path), "status": "created"}
149
+ if linked_memos:
150
+ result["linked_memos"] = linked_memos
151
+ if missing_memos:
152
+ result["missing_memos"] = missing_memos
153
+ OutputManager.print(result)
130
154
  else:
131
155
  console.print(
132
156
  f"[green]✔ Created {issue.id} in status {issue.status}.[/green]"
133
157
  )
134
158
  console.print(f"Path: {rel_path}")
159
+
160
+ if linked_memos:
161
+ console.print(f"[green]✔ Linked {len(linked_memos)} memo(s): {', '.join(linked_memos)}[/green]")
162
+ if missing_memos:
163
+ console.print(f"[yellow]⚠ Memo(s) not found: {', '.join(missing_memos)}[/yellow]")
135
164
 
136
165
  # Prompt for Language
137
166
  source_lang = config.i18n.source_lang or "en"
@@ -162,6 +191,7 @@ def update(
162
191
  title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
163
192
  status: Optional[str] = typer.Option(None, "--status", help="New status"),
164
193
  stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
194
+ solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type (implemented, cancelled, wontfix, duplicate)"),
165
195
  parent: Optional[str] = typer.Option(
166
196
  None, "--parent", "-p", help="Parent Issue ID"
167
197
  ),
@@ -188,6 +218,7 @@ def update(
188
218
  issue_id,
189
219
  status=status,
190
220
  stage=stage,
221
+ solution=solution,
191
222
  title=title,
192
223
  parent=parent,
193
224
  sprint=sprint,
@@ -398,7 +429,7 @@ def move_close(
398
429
  None, "--solution", "-s", help="Solution type"
399
430
  ),
400
431
  prune: bool = typer.Option(
401
- False, "--prune", help="Delete branch/worktree after close"
432
+ True, "--prune/--no-prune", help="Delete branch/worktree after close (default: True)"
402
433
  ),
403
434
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
404
435
  force_prune: bool = typer.Option(
@@ -453,6 +484,32 @@ def move_close(
453
484
  force = True
454
485
 
455
486
  try:
487
+ # 0. Perform Smart Atomic Merge (FEAT-0154)
488
+ merged_files = []
489
+ try:
490
+ merged_files = core.merge_issue_changes(issues_root, issue_id, project_root)
491
+ if merged_files:
492
+ if not OutputManager.is_agent_mode():
493
+ console.print(
494
+ f"[green]✔ Smart Merge:[/green] Synced {len(merged_files)} files from feature branch."
495
+ )
496
+
497
+ # Auto-commit merged files if not no_commit
498
+ if not no_commit:
499
+ commit_msg = f"feat: atomic merge changes from {issue_id}"
500
+ try:
501
+ git.git_commit(project_root, commit_msg)
502
+ if not OutputManager.is_agent_mode():
503
+ console.print(f"[green]✔ Committed merged changes.[/green]")
504
+ except Exception as e:
505
+ # If commit fails (e.g. nothing to commit?), just warn
506
+ if not OutputManager.is_agent_mode():
507
+ console.print(f"[yellow]⚠ Commit skipped: {e}[/yellow]")
508
+
509
+ except Exception as e:
510
+ OutputManager.error(f"Merge Error: {e}")
511
+ raise typer.Exit(code=1)
512
+
456
513
  issue = core.update_issue(
457
514
  issues_root,
458
515
  issue_id,
@@ -464,10 +521,37 @@ def move_close(
464
521
 
465
522
  pruned_resources = []
466
523
  if prune:
524
+ # Get isolation info for confirmation prompt
525
+ isolation_info = None
526
+ if issue.isolation:
527
+ isolation_type = issue.isolation.type.value if issue.isolation.type else None
528
+ isolation_ref = issue.isolation.ref
529
+ isolation_info = (isolation_type, isolation_ref)
530
+
531
+ # Interactive confirmation before pruning (non-agent mode only)
532
+ if not OutputManager.is_agent_mode() and isolation_info and not force:
533
+ iso_type, iso_ref = isolation_info
534
+ if iso_ref:
535
+ console.print(f"\n[bold yellow]⚠️ Resource Cleanup Confirmation[/bold yellow]")
536
+ console.print(f"Issue [cyan]{issue_id}[/cyan] will be closed with the following action:")
537
+ console.print(f" • Delete {iso_type}: [bold]{iso_ref}[/bold]")
538
+ console.print(f"\n[dim]This operation will permanently remove the {iso_type}. "
539
+ f"Ensure all changes have been merged to main.[/dim]")
540
+ confirm = typer.confirm(
541
+ f"\nProceed with closing {issue_id} and deleting {iso_type}?",
542
+ default=True,
543
+ )
544
+ if not confirm:
545
+ console.print(f"[yellow]Close operation cancelled.[/yellow]")
546
+ console.print(f"[dim]Tip: Use --no-prune to close without deleting {iso_type}.[/dim]")
547
+ raise typer.Abort()
548
+
467
549
  try:
468
550
  pruned_resources = core.prune_issue_resources(
469
551
  issues_root, issue_id, force, project_root
470
552
  )
553
+ if pruned_resources and not OutputManager.is_agent_mode():
554
+ console.print(f"[green]✔ Cleaned up:[/green] {', '.join(pruned_resources)}")
471
555
  except Exception as e:
472
556
  OutputManager.error(f"Prune Error: {e}")
473
557
  raise typer.Exit(code=1)
@@ -727,6 +811,9 @@ def list_cmd(
727
811
  workspace: bool = typer.Option(
728
812
  False, "--workspace", "-w", help="Include issues from workspace members"
729
813
  ),
814
+ all: bool = typer.Option(
815
+ False, "--all", "-a", help="Include archived issues in the list"
816
+ ),
730
817
  json: AgentOutput = False,
731
818
  ):
732
819
  """List issues in a table format with filtering."""
@@ -734,15 +821,15 @@ def list_cmd(
734
821
  issues_root = _resolve_issues_root(config, root)
735
822
 
736
823
  # Validation
737
- if status and status.lower() not in ["open", "closed", "backlog", "all"]:
824
+ if status and status.lower() not in ["open", "closed", "backlog", "archived", "all"]:
738
825
  OutputManager.error(
739
- f"Invalid status: {status}. Use open, closed, backlog or all."
826
+ f"Invalid status: {status}. Use open, closed, backlog, archived or all."
740
827
  )
741
828
  raise typer.Exit(code=1)
742
829
 
743
830
  target_status = status.lower() if status else "open"
744
831
 
745
- issues = core.list_issues(issues_root, recursive_workspace=workspace)
832
+ issues = core.list_issues(issues_root, recursive_workspace=workspace, include_archived=all)
746
833
  filtered = []
747
834
 
748
835
  for i in issues:
@@ -1521,6 +1608,193 @@ def show(
1521
1608
  OutputManager.print(result)
1522
1609
 
1523
1610
 
1611
+ @app.command("check-critical")
1612
+ def check_critical(
1613
+ fail_on_warning: bool = typer.Option(
1614
+ False,
1615
+ "--fail-on-warning",
1616
+ help="Exit with error code if high priority issues are found",
1617
+ ),
1618
+ root: Optional[str] = typer.Option(
1619
+ None, "--root", help="Override issues root directory"
1620
+ ),
1621
+ json: AgentOutput = False,
1622
+ ):
1623
+ """
1624
+ Check for incomplete critical/high priority issues.
1625
+
1626
+ Returns:
1627
+ 0: No critical/high issues found
1628
+ 1: High priority issues found (warning)
1629
+ 2: Critical issues found (blocking)
1630
+ """
1631
+ from .criticality import CriticalityLevel
1632
+
1633
+ config = get_config()
1634
+ issues_root = _resolve_issues_root(config, root)
1635
+
1636
+ issues = core.list_issues(issues_root)
1637
+
1638
+ critical_issues = []
1639
+ high_issues = []
1640
+
1641
+ for issue in issues:
1642
+ if issue.status == "closed":
1643
+ continue
1644
+
1645
+ criticality = issue.criticality
1646
+ if not criticality:
1647
+ continue
1648
+
1649
+ if criticality == CriticalityLevel.CRITICAL:
1650
+ critical_issues.append(issue)
1651
+ elif criticality == CriticalityLevel.HIGH:
1652
+ high_issues.append(issue)
1653
+
1654
+ result = {
1655
+ "critical_count": len(critical_issues),
1656
+ "high_count": len(high_issues),
1657
+ "critical_issues": [
1658
+ {"id": i.id, "title": i.title, "stage": i.stage}
1659
+ for i in critical_issues
1660
+ ],
1661
+ "high_issues": [
1662
+ {"id": i.id, "title": i.title, "stage": i.stage}
1663
+ for i in high_issues
1664
+ ],
1665
+ }
1666
+
1667
+ if OutputManager.is_agent_mode() or json:
1668
+ OutputManager.print(result)
1669
+ else:
1670
+ if critical_issues:
1671
+ console.print("[red]❌ Critical issues (blocking):[/red]")
1672
+ for issue in critical_issues:
1673
+ console.print(f" [red]• {issue.id}:[/red] {issue.title} ({issue.stage})")
1674
+
1675
+ if high_issues:
1676
+ console.print("[yellow]⚠️ High priority issues (warning):[/yellow]")
1677
+ for issue in high_issues:
1678
+ console.print(f" [yellow]• {issue.id}:[/yellow] {issue.title} ({issue.stage})")
1679
+
1680
+ if not critical_issues and not high_issues:
1681
+ console.print("[green]✓ No incomplete critical or high priority issues.[/green]")
1682
+
1683
+ # Determine exit code
1684
+ if critical_issues:
1685
+ raise typer.Exit(code=2)
1686
+ elif high_issues and fail_on_warning:
1687
+ raise typer.Exit(code=1)
1688
+ elif high_issues:
1689
+ # Warning only, don't fail
1690
+ pass
1691
+
1692
+ raise typer.Exit(code=0)
1693
+
1694
+
1695
+ @app.command("sync-isolation")
1696
+ def sync_isolation(
1697
+ issue_id: str = typer.Argument(..., help="Issue ID to sync isolation for"),
1698
+ branch: Optional[str] = typer.Option(
1699
+ None, "--branch", help="Current branch name (auto-detected if not provided)"
1700
+ ),
1701
+ root: Optional[str] = typer.Option(
1702
+ None, "--root", help="Override issues root directory"
1703
+ ),
1704
+ json: AgentOutput = False,
1705
+ ):
1706
+ """
1707
+ Sync issue isolation configuration with current branch.
1708
+
1709
+ Updates the isolation.ref field to match the current branch context.
1710
+ This is typically called by the post-checkout hook.
1711
+ """
1712
+ import subprocess
1713
+ from .core import update_issue_field
1714
+
1715
+ config = get_config()
1716
+ issues_root = _resolve_issues_root(config, root)
1717
+ project_root = Path(config.paths.root).resolve()
1718
+
1719
+ # Auto-detect branch if not provided
1720
+ if not branch:
1721
+ try:
1722
+ result = subprocess.run(
1723
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
1724
+ cwd=project_root,
1725
+ capture_output=True,
1726
+ text=True,
1727
+ check=True,
1728
+ )
1729
+ branch = result.stdout.strip()
1730
+ except Exception:
1731
+ OutputManager.error("Could not detect current branch. Use --branch.")
1732
+ raise typer.Exit(code=1)
1733
+
1734
+ # Find the issue
1735
+ issue_path = core.find_issue_path(issues_root, issue_id)
1736
+ if not issue_path:
1737
+ OutputManager.error(f"Issue {issue_id} not found.")
1738
+ raise typer.Exit(code=1)
1739
+
1740
+ # Parse current issue
1741
+ issue = core.parse_issue(issue_path)
1742
+ if not issue:
1743
+ OutputManager.error(f"Could not parse issue {issue_id}.")
1744
+ raise typer.Exit(code=1)
1745
+
1746
+ # Check if isolation needs updating
1747
+ current_isolation = issue.isolation
1748
+ if current_isolation:
1749
+ current_ref = current_isolation.ref or ""
1750
+ current_isolation_dict = {
1751
+ "type": current_isolation.type,
1752
+ "ref": current_isolation.ref,
1753
+ }
1754
+ if current_isolation.path:
1755
+ current_isolation_dict["path"] = current_isolation.path
1756
+ if current_isolation.created_at:
1757
+ current_isolation_dict["created_at"] = current_isolation.created_at.isoformat()
1758
+ else:
1759
+ current_ref = ""
1760
+ current_isolation_dict = {}
1761
+
1762
+ # Expected ref format: branch:branch-name
1763
+ expected_ref = f"branch:{branch}"
1764
+
1765
+ if current_ref == expected_ref:
1766
+ if OutputManager.is_agent_mode() or json:
1767
+ OutputManager.print({"updated": False, "message": "Isolation already up to date"})
1768
+ else:
1769
+ console.print(f"[dim]Isolation already up to date for {issue_id}[/dim]")
1770
+ raise typer.Exit(code=0)
1771
+
1772
+ # Update the isolation field
1773
+ new_isolation = {**current_isolation_dict, "ref": expected_ref, "type": "branch"}
1774
+
1775
+ try:
1776
+ update_issue_field(
1777
+ issue_path,
1778
+ "isolation",
1779
+ new_isolation,
1780
+ )
1781
+
1782
+ result = {
1783
+ "updated": True,
1784
+ "issue_id": issue_id,
1785
+ "isolation": new_isolation,
1786
+ }
1787
+
1788
+ if OutputManager.is_agent_mode() or json:
1789
+ OutputManager.print(result)
1790
+ else:
1791
+ console.print(f"[green]✓ Updated isolation for {issue_id}:[/green] {expected_ref}")
1792
+
1793
+ except Exception as e:
1794
+ OutputManager.error(f"Failed to update isolation: {e}")
1795
+ raise typer.Exit(code=1)
1796
+
1797
+
1524
1798
  def _validate_branch_context(
1525
1799
  project_root: Path,
1526
1800
  allowed: Optional[List[str]] = None,