monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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 (130) 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/automation/__init__.py +51 -0
  6. monoco/core/automation/config.py +338 -0
  7. monoco/core/automation/field_watcher.py +296 -0
  8. monoco/core/automation/handlers.py +723 -0
  9. monoco/core/config.py +31 -4
  10. monoco/core/executor/__init__.py +38 -0
  11. monoco/core/executor/agent_action.py +254 -0
  12. monoco/core/executor/git_action.py +303 -0
  13. monoco/core/executor/im_action.py +309 -0
  14. monoco/core/executor/pytest_action.py +218 -0
  15. monoco/core/git.py +38 -0
  16. monoco/core/hooks/context.py +74 -13
  17. monoco/core/ingestion/__init__.py +20 -0
  18. monoco/core/ingestion/discovery.py +248 -0
  19. monoco/core/ingestion/watcher.py +343 -0
  20. monoco/core/ingestion/worker.py +436 -0
  21. monoco/core/loader.py +633 -0
  22. monoco/core/registry.py +34 -25
  23. monoco/core/router/__init__.py +55 -0
  24. monoco/core/router/action.py +341 -0
  25. monoco/core/router/router.py +392 -0
  26. monoco/core/scheduler/__init__.py +63 -0
  27. monoco/core/scheduler/base.py +152 -0
  28. monoco/core/scheduler/engines.py +175 -0
  29. monoco/core/scheduler/events.py +171 -0
  30. monoco/core/scheduler/local.py +377 -0
  31. monoco/core/skills.py +119 -80
  32. monoco/core/watcher/__init__.py +57 -0
  33. monoco/core/watcher/base.py +365 -0
  34. monoco/core/watcher/dropzone.py +152 -0
  35. monoco/core/watcher/issue.py +303 -0
  36. monoco/core/watcher/memo.py +200 -0
  37. monoco/core/watcher/task.py +238 -0
  38. monoco/daemon/app.py +77 -1
  39. monoco/daemon/commands.py +10 -0
  40. monoco/daemon/events.py +34 -0
  41. monoco/daemon/mailroom_service.py +196 -0
  42. monoco/daemon/models.py +1 -0
  43. monoco/daemon/scheduler.py +207 -0
  44. monoco/daemon/services.py +27 -58
  45. monoco/daemon/triggers.py +55 -0
  46. monoco/features/agent/__init__.py +25 -7
  47. monoco/features/agent/adapter.py +17 -7
  48. monoco/features/agent/cli.py +91 -57
  49. monoco/features/agent/engines.py +31 -170
  50. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  51. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  52. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  53. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  54. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  55. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  56. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  57. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  58. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  59. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  61. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  62. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  63. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  64. monoco/features/agent/worker.py +1 -1
  65. monoco/features/artifact/__init__.py +0 -0
  66. monoco/features/artifact/adapter.py +33 -0
  67. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  68. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  69. monoco/features/glossary/adapter.py +18 -7
  70. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  71. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  72. monoco/features/hooks/__init__.py +11 -0
  73. monoco/features/hooks/adapter.py +67 -0
  74. monoco/features/hooks/commands.py +309 -0
  75. monoco/features/hooks/core.py +441 -0
  76. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  77. monoco/features/i18n/adapter.py +18 -5
  78. monoco/features/i18n/core.py +482 -17
  79. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  80. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  81. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  82. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  83. monoco/features/issue/adapter.py +19 -6
  84. monoco/features/issue/commands.py +352 -20
  85. monoco/features/issue/core.py +475 -16
  86. monoco/features/issue/engine/machine.py +114 -4
  87. monoco/features/issue/linter.py +60 -5
  88. monoco/features/issue/models.py +2 -2
  89. monoco/features/issue/resources/en/AGENTS.md +109 -0
  90. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  91. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  92. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  93. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  94. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  95. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  96. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  97. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  98. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  99. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  100. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  101. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  102. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  103. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  104. monoco/features/issue/validator.py +101 -1
  105. monoco/features/memo/adapter.py +21 -8
  106. monoco/features/memo/cli.py +103 -10
  107. monoco/features/memo/core.py +178 -92
  108. monoco/features/memo/models.py +53 -0
  109. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  110. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  111. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  112. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  113. monoco/features/spike/adapter.py +18 -5
  114. monoco/features/spike/commands.py +5 -3
  115. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  116. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  117. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  118. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  119. monoco/main.py +38 -1
  120. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
  121. monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
  122. monoco/features/agent/apoptosis.py +0 -44
  123. monoco/features/agent/manager.py +0 -91
  124. monoco/features/agent/session.py +0 -121
  125. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  126. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  127. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  128. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  129. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  130. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import typer
2
2
  from pathlib import Path
3
3
  from typing import Optional, List
4
4
  from datetime import datetime
5
+ import logging
5
6
  from rich.console import Console
6
7
  from rich.tree import Tree
7
8
  from rich.panel import Panel
@@ -24,6 +25,7 @@ from . import domain_commands
24
25
 
25
26
  app.add_typer(domain_commands.app, name="domain")
26
27
  console = Console()
28
+ logger = logging.getLogger(__name__)
27
29
 
28
30
 
29
31
  @app.command("create")
@@ -43,6 +45,9 @@ def create(
43
45
  related: List[str] = typer.Option(
44
46
  [], "--related", "-r", help="Related Issue ID(s)"
45
47
  ),
48
+ from_memo: List[str] = typer.Option(
49
+ [], "--from-memo", "-m", help="Memo ID(s) to link to this issue"
50
+ ),
46
51
  force: bool = typer.Option(False, "--force", help="Bypass branch context checks"),
47
52
  subdir: Optional[str] = typer.Option(
48
53
  None,
@@ -118,20 +123,46 @@ def create(
118
123
  criticality=criticality_level,
119
124
  )
120
125
 
126
+ # Link memos to the newly created issue
127
+ linked_memos = []
128
+ missing_memos = []
129
+ if from_memo:
130
+ from monoco.features.memo.core import load_memos, update_memo
131
+
132
+ existing_memos = {m.uid: m for m in load_memos(issues_root)}
133
+
134
+ for memo_id in from_memo:
135
+ if memo_id in existing_memos:
136
+ # Only update if not already linked to this issue (idempotency)
137
+ memo = existing_memos[memo_id]
138
+ if memo.ref != issue.id:
139
+ update_memo(issues_root, memo_id, {"status": "tracked", "ref": issue.id})
140
+ linked_memos.append(memo_id)
141
+ else:
142
+ missing_memos.append(memo_id)
143
+
121
144
  try:
122
145
  rel_path = path.relative_to(Path.cwd())
123
146
  except ValueError:
124
147
  rel_path = path
125
148
 
126
149
  if OutputManager.is_agent_mode():
127
- OutputManager.print(
128
- {"issue": issue, "path": str(rel_path), "status": "created"}
129
- )
150
+ result = {"issue": issue, "path": str(rel_path), "status": "created"}
151
+ if linked_memos:
152
+ result["linked_memos"] = linked_memos
153
+ if missing_memos:
154
+ result["missing_memos"] = missing_memos
155
+ OutputManager.print(result)
130
156
  else:
131
157
  console.print(
132
158
  f"[green]✔ Created {issue.id} in status {issue.status}.[/green]"
133
159
  )
134
160
  console.print(f"Path: {rel_path}")
161
+
162
+ if linked_memos:
163
+ console.print(f"[green]✔ Linked {len(linked_memos)} memo(s): {', '.join(linked_memos)}[/green]")
164
+ if missing_memos:
165
+ console.print(f"[yellow]⚠ Memo(s) not found: {', '.join(missing_memos)}[/yellow]")
135
166
 
136
167
  # Prompt for Language
137
168
  source_lang = config.i18n.source_lang or "en"
@@ -162,6 +193,7 @@ def update(
162
193
  title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
163
194
  status: Optional[str] = typer.Option(None, "--status", help="New status"),
164
195
  stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
196
+ solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type (implemented, cancelled, wontfix, duplicate)"),
165
197
  parent: Optional[str] = typer.Option(
166
198
  None, "--parent", "-p", help="Parent Issue ID"
167
199
  ),
@@ -188,6 +220,7 @@ def update(
188
220
  issue_id,
189
221
  status=status,
190
222
  stage=stage,
223
+ solution=solution,
191
224
  title=title,
192
225
  parent=parent,
193
226
  sprint=sprint,
@@ -360,6 +393,13 @@ def submit(
360
393
  )
361
394
 
362
395
  try:
396
+ # FEAT-0163: Automatically sync files before submission to ensure manifest completeness
397
+ try:
398
+ core.sync_issue_files(issues_root, issue_id, project_root)
399
+ except Exception as se:
400
+ # Just log warning, don't fail submit if sync fails (defensive)
401
+ logger.warning(f"Auto-sync failed during submit for {issue_id}: {se}")
402
+
363
403
  # Implicitly ensure status is Open
364
404
  issue = core.update_issue(
365
405
  issues_root,
@@ -398,7 +438,7 @@ def move_close(
398
438
  None, "--solution", "-s", help="Solution type"
399
439
  ),
400
440
  prune: bool = typer.Option(
401
- False, "--prune", help="Delete branch/worktree after close"
441
+ True, "--prune/--no-prune", help="Delete branch/worktree after close (default: True)"
402
442
  ),
403
443
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
404
444
  force_prune: bool = typer.Option(
@@ -414,7 +454,11 @@ def move_close(
414
454
  ),
415
455
  json: AgentOutput = False,
416
456
  ):
417
- """Close issue."""
457
+ """Close issue with atomic transaction guarantee.
458
+
459
+ If any step fails, all changes made during the close operation will be
460
+ automatically rolled back to ensure the mainline remains clean.
461
+ """
418
462
  config = get_config()
419
463
  issues_root = _resolve_issues_root(config, root)
420
464
  project_root = _resolve_project_root(config)
@@ -441,43 +485,141 @@ def move_close(
441
485
 
442
486
  # Handle force-prune logic
443
487
  if force_prune:
444
- # Use OutputManager to check mode, as `json` arg might not be reliable with Typer Annotated
445
- if not OutputManager.is_agent_mode() and not force:
488
+ # FEAT-0125: force-prune requires interactive confirmation to avoid accidental data loss
489
+ # unless in agent mode (which is handled by typer.confirm's default)
490
+ if not OutputManager.is_agent_mode():
446
491
  confirm = typer.confirm(
447
- "⚠️ [Bold Red]Warning:[/Bold Red] You are about to FORCE prune issue resources. Git merge checks will be bypassed.\nAre you sure you want to proceed?",
448
- default=False,
492
+ " FORCE PRUNE will permanently delete the feature branch without checking its merge status. Continue?",
493
+ default=False
449
494
  )
450
495
  if not confirm:
451
- raise typer.Abort()
496
+ console.print("[yellow]Aborted.[/yellow]")
497
+ raise typer.Exit(code=1)
498
+
452
499
  prune = True
453
500
  force = True
454
501
 
502
+ # ATOMIC TRANSACTION: Capture initial state for potential rollback
503
+ initial_head = None
504
+ transaction_commits = []
505
+
506
+ def rollback_transaction():
507
+ """Rollback all changes made during the transaction."""
508
+ if initial_head and transaction_commits:
509
+ try:
510
+ git.git_reset_hard(project_root, initial_head)
511
+ if not OutputManager.is_agent_mode():
512
+ console.print(f"[yellow]↩ Rolled back to {initial_head[:7]}[/yellow]")
513
+ except Exception as rollback_error:
514
+ if not OutputManager.is_agent_mode():
515
+ console.print(f"[red]⚠ Rollback failed: {rollback_error}[/red]")
516
+ console.print(f"[red] Manual recovery may be required. Run: git reset --hard {initial_head[:7]}[/red]")
517
+
455
518
  try:
456
- issue = core.update_issue(
457
- issues_root,
458
- issue_id,
459
- status="closed",
460
- solution=solution,
461
- no_commit=no_commit,
462
- project_root=project_root,
519
+ # Capture initial HEAD before any modifications
520
+ initial_head = git.get_current_head(project_root)
521
+
522
+ # 0. Find issue across branches (FIX-0006)
523
+ # This will raise RuntimeError if issue found in multiple branches
524
+ found_path, source_branch = core.find_issue_path_across_branches(
525
+ issues_root, issue_id, project_root
463
526
  )
527
+ if not found_path:
528
+ OutputManager.error(f"Issue {issue_id} not found in any branch.")
529
+ raise typer.Exit(code=1)
530
+
531
+ # If issue was found in a different branch, notify user
532
+ if source_branch and source_branch != git.get_current_branch(project_root):
533
+ if not OutputManager.is_agent_mode():
534
+ console.print(
535
+ f"[green]✔ Found {issue_id} in branch '{source_branch}', synced to working tree.[/green]"
536
+ )
537
+
538
+ # 1. Perform Smart Atomic Merge (FEAT-0154)
539
+ merged_files = []
540
+ try:
541
+ merged_files = core.merge_issue_changes(issues_root, issue_id, project_root)
542
+ if merged_files:
543
+ if not OutputManager.is_agent_mode():
544
+ console.print(
545
+ f"[green]✔ Smart Merge:[/green] Synced {len(merged_files)} files from feature branch."
546
+ )
547
+
548
+ # Auto-commit merged files if not no_commit
549
+ if not no_commit:
550
+ commit_msg = f"feat: atomic merge changes from {issue_id}"
551
+ try:
552
+ commit_hash = git.git_commit(project_root, commit_msg)
553
+ transaction_commits.append(commit_hash)
554
+ if not OutputManager.is_agent_mode():
555
+ console.print(f"[green]✔ Committed merged changes.[/green]")
556
+ except Exception as e:
557
+ # If commit fails (e.g. nothing to commit?), just warn
558
+ if not OutputManager.is_agent_mode():
559
+ console.print(f"[yellow]⚠ Commit skipped: {e}[/yellow]")
560
+
561
+ except Exception as e:
562
+ OutputManager.error(f"Merge Error: {e}")
563
+ rollback_transaction()
564
+ raise typer.Exit(code=1)
464
565
 
566
+ # 2. Update issue status to closed
567
+ try:
568
+ issue = core.update_issue(
569
+ issues_root,
570
+ issue_id,
571
+ status="closed",
572
+ solution=solution,
573
+ no_commit=no_commit,
574
+ project_root=project_root,
575
+ )
576
+ # Track the auto-commit from update_issue if it occurred
577
+ if hasattr(issue, 'commit_result') and issue.commit_result:
578
+ transaction_commits.append(issue.commit_result)
579
+ except Exception as e:
580
+ OutputManager.error(f"Update Error: {e}")
581
+ rollback_transaction()
582
+ raise typer.Exit(code=1)
583
+
584
+ # 3. Prune issue resources (branch/worktree)
465
585
  pruned_resources = []
466
586
  if prune:
587
+ # Get isolation info for confirmation prompt
588
+ isolation_info = None
589
+ if issue.isolation:
590
+ isolation_type = issue.isolation.type if issue.isolation.type else None
591
+ isolation_ref = issue.isolation.ref
592
+ isolation_info = (isolation_type, isolation_ref)
593
+
594
+ # Auto-prune without confirmation (FEAT-0082 Update)
595
+ if not OutputManager.is_agent_mode() and isolation_info:
596
+ iso_type, iso_ref = isolation_info
597
+ if iso_ref:
598
+ console.print(f"[dim]Cleaning up {iso_type}: {iso_ref}...[/dim]")
599
+
467
600
  try:
468
601
  pruned_resources = core.prune_issue_resources(
469
602
  issues_root, issue_id, force, project_root
470
603
  )
604
+ if pruned_resources and not OutputManager.is_agent_mode():
605
+ console.print(f"[green]✔ Cleaned up:[/green] {', '.join(pruned_resources)}")
471
606
  except Exception as e:
472
607
  OutputManager.error(f"Prune Error: {e}")
608
+ rollback_transaction()
473
609
  raise typer.Exit(code=1)
474
610
 
611
+ # Success: Clear transaction state as all operations completed
475
612
  OutputManager.print(
476
613
  {"issue": issue, "status": "closed", "pruned": pruned_resources}
477
614
  )
478
615
 
616
+ except typer.Abort:
617
+ # User cancelled, rollback already handled
618
+ raise
479
619
  except Exception as e:
620
+ # Catch-all for unexpected errors
480
621
  OutputManager.error(str(e))
622
+ rollback_transaction()
481
623
  raise typer.Exit(code=1)
482
624
 
483
625
 
@@ -727,6 +869,9 @@ def list_cmd(
727
869
  workspace: bool = typer.Option(
728
870
  False, "--workspace", "-w", help="Include issues from workspace members"
729
871
  ),
872
+ all: bool = typer.Option(
873
+ False, "--all", "-a", help="Include archived issues in the list"
874
+ ),
730
875
  json: AgentOutput = False,
731
876
  ):
732
877
  """List issues in a table format with filtering."""
@@ -734,15 +879,15 @@ def list_cmd(
734
879
  issues_root = _resolve_issues_root(config, root)
735
880
 
736
881
  # Validation
737
- if status and status.lower() not in ["open", "closed", "backlog", "all"]:
882
+ if status and status.lower() not in ["open", "closed", "backlog", "archived", "all"]:
738
883
  OutputManager.error(
739
- f"Invalid status: {status}. Use open, closed, backlog or all."
884
+ f"Invalid status: {status}. Use open, closed, backlog, archived or all."
740
885
  )
741
886
  raise typer.Exit(code=1)
742
887
 
743
888
  target_status = status.lower() if status else "open"
744
889
 
745
- issues = core.list_issues(issues_root, recursive_workspace=workspace)
890
+ issues = core.list_issues(issues_root, recursive_workspace=workspace, include_archived=all)
746
891
  filtered = []
747
892
 
748
893
  for i in issues:
@@ -1521,6 +1666,193 @@ def show(
1521
1666
  OutputManager.print(result)
1522
1667
 
1523
1668
 
1669
+ @app.command("check-critical")
1670
+ def check_critical(
1671
+ fail_on_warning: bool = typer.Option(
1672
+ False,
1673
+ "--fail-on-warning",
1674
+ help="Exit with error code if high priority issues are found",
1675
+ ),
1676
+ root: Optional[str] = typer.Option(
1677
+ None, "--root", help="Override issues root directory"
1678
+ ),
1679
+ json: AgentOutput = False,
1680
+ ):
1681
+ """
1682
+ Check for incomplete critical/high priority issues.
1683
+
1684
+ Returns:
1685
+ 0: No critical/high issues found
1686
+ 1: High priority issues found (warning)
1687
+ 2: Critical issues found (blocking)
1688
+ """
1689
+ from .criticality import CriticalityLevel
1690
+
1691
+ config = get_config()
1692
+ issues_root = _resolve_issues_root(config, root)
1693
+
1694
+ issues = core.list_issues(issues_root)
1695
+
1696
+ critical_issues = []
1697
+ high_issues = []
1698
+
1699
+ for issue in issues:
1700
+ if issue.status == "closed":
1701
+ continue
1702
+
1703
+ criticality = issue.criticality
1704
+ if not criticality:
1705
+ continue
1706
+
1707
+ if criticality == CriticalityLevel.CRITICAL:
1708
+ critical_issues.append(issue)
1709
+ elif criticality == CriticalityLevel.HIGH:
1710
+ high_issues.append(issue)
1711
+
1712
+ result = {
1713
+ "critical_count": len(critical_issues),
1714
+ "high_count": len(high_issues),
1715
+ "critical_issues": [
1716
+ {"id": i.id, "title": i.title, "stage": i.stage}
1717
+ for i in critical_issues
1718
+ ],
1719
+ "high_issues": [
1720
+ {"id": i.id, "title": i.title, "stage": i.stage}
1721
+ for i in high_issues
1722
+ ],
1723
+ }
1724
+
1725
+ if OutputManager.is_agent_mode() or json:
1726
+ OutputManager.print(result)
1727
+ else:
1728
+ if critical_issues:
1729
+ console.print("[red]❌ Critical issues (blocking):[/red]")
1730
+ for issue in critical_issues:
1731
+ console.print(f" [red]• {issue.id}:[/red] {issue.title} ({issue.stage})")
1732
+
1733
+ if high_issues:
1734
+ console.print("[yellow]⚠️ High priority issues (warning):[/yellow]")
1735
+ for issue in high_issues:
1736
+ console.print(f" [yellow]• {issue.id}:[/yellow] {issue.title} ({issue.stage})")
1737
+
1738
+ if not critical_issues and not high_issues:
1739
+ console.print("[green]✓ No incomplete critical or high priority issues.[/green]")
1740
+
1741
+ # Determine exit code
1742
+ if critical_issues:
1743
+ raise typer.Exit(code=2)
1744
+ elif high_issues and fail_on_warning:
1745
+ raise typer.Exit(code=1)
1746
+ elif high_issues:
1747
+ # Warning only, don't fail
1748
+ pass
1749
+
1750
+ raise typer.Exit(code=0)
1751
+
1752
+
1753
+ @app.command("sync-isolation")
1754
+ def sync_isolation(
1755
+ issue_id: str = typer.Argument(..., help="Issue ID to sync isolation for"),
1756
+ branch: Optional[str] = typer.Option(
1757
+ None, "--branch", help="Current branch name (auto-detected if not provided)"
1758
+ ),
1759
+ root: Optional[str] = typer.Option(
1760
+ None, "--root", help="Override issues root directory"
1761
+ ),
1762
+ json: AgentOutput = False,
1763
+ ):
1764
+ """
1765
+ Sync issue isolation configuration with current branch.
1766
+
1767
+ Updates the isolation.ref field to match the current branch context.
1768
+ This is typically called by the post-checkout hook.
1769
+ """
1770
+ import subprocess
1771
+ from .core import update_issue_field
1772
+
1773
+ config = get_config()
1774
+ issues_root = _resolve_issues_root(config, root)
1775
+ project_root = Path(config.paths.root).resolve()
1776
+
1777
+ # Auto-detect branch if not provided
1778
+ if not branch:
1779
+ try:
1780
+ result = subprocess.run(
1781
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
1782
+ cwd=project_root,
1783
+ capture_output=True,
1784
+ text=True,
1785
+ check=True,
1786
+ )
1787
+ branch = result.stdout.strip()
1788
+ except Exception:
1789
+ OutputManager.error("Could not detect current branch. Use --branch.")
1790
+ raise typer.Exit(code=1)
1791
+
1792
+ # Find the issue
1793
+ issue_path = core.find_issue_path(issues_root, issue_id)
1794
+ if not issue_path:
1795
+ OutputManager.error(f"Issue {issue_id} not found.")
1796
+ raise typer.Exit(code=1)
1797
+
1798
+ # Parse current issue
1799
+ issue = core.parse_issue(issue_path)
1800
+ if not issue:
1801
+ OutputManager.error(f"Could not parse issue {issue_id}.")
1802
+ raise typer.Exit(code=1)
1803
+
1804
+ # Check if isolation needs updating
1805
+ current_isolation = issue.isolation
1806
+ if current_isolation:
1807
+ current_ref = current_isolation.ref or ""
1808
+ current_isolation_dict = {
1809
+ "type": current_isolation.type,
1810
+ "ref": current_isolation.ref,
1811
+ }
1812
+ if current_isolation.path:
1813
+ current_isolation_dict["path"] = current_isolation.path
1814
+ if current_isolation.created_at:
1815
+ current_isolation_dict["created_at"] = current_isolation.created_at.isoformat()
1816
+ else:
1817
+ current_ref = ""
1818
+ current_isolation_dict = {}
1819
+
1820
+ # Expected ref format: branch:branch-name
1821
+ expected_ref = f"branch:{branch}"
1822
+
1823
+ if current_ref == expected_ref:
1824
+ if OutputManager.is_agent_mode() or json:
1825
+ OutputManager.print({"updated": False, "message": "Isolation already up to date"})
1826
+ else:
1827
+ console.print(f"[dim]Isolation already up to date for {issue_id}[/dim]")
1828
+ raise typer.Exit(code=0)
1829
+
1830
+ # Update the isolation field
1831
+ new_isolation = {**current_isolation_dict, "ref": expected_ref, "type": "branch"}
1832
+
1833
+ try:
1834
+ update_issue_field(
1835
+ issue_path,
1836
+ "isolation",
1837
+ new_isolation,
1838
+ )
1839
+
1840
+ result = {
1841
+ "updated": True,
1842
+ "issue_id": issue_id,
1843
+ "isolation": new_isolation,
1844
+ }
1845
+
1846
+ if OutputManager.is_agent_mode() or json:
1847
+ OutputManager.print(result)
1848
+ else:
1849
+ console.print(f"[green]✓ Updated isolation for {issue_id}:[/green] {expected_ref}")
1850
+
1851
+ except Exception as e:
1852
+ OutputManager.error(f"Failed to update isolation: {e}")
1853
+ raise typer.Exit(code=1)
1854
+
1855
+
1524
1856
  def _validate_branch_context(
1525
1857
  project_root: Path,
1526
1858
  allowed: Optional[List[str]] = None,