monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__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 (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -46,6 +46,32 @@ def get_issue_dir(issue_type: str, issues_root: Path) -> Path:
46
46
  return issues_root / folder
47
47
 
48
48
 
49
+ def _extract_issue_id_from_branch(branch: str) -> Optional[str]:
50
+ """
51
+ Extract issue ID from a branch name.
52
+
53
+ Supported patterns:
54
+ - <ID>-slug (e.g., FEAT-0123-login-page)
55
+ - <ID> (e.g., FEAT-0123)
56
+
57
+ Args:
58
+ branch: Branch name to parse
59
+
60
+ Returns:
61
+ The extracted issue ID (uppercase) or None if no match
62
+ """
63
+ # Pattern: <prefix>-<number> at start of branch name
64
+ # Examples:
65
+ # FEAT-0123-login-page -> FEAT-0123
66
+ # FEAT-0123 -> FEAT-0123
67
+ # FIX-0001-critical -> FIX-0001
68
+ pattern = r'^([a-z]+-\d+)'
69
+ match = re.match(pattern, branch, re.IGNORECASE)
70
+ if match:
71
+ return match.group(1).upper()
72
+ return None
73
+
74
+
49
75
  def _parse_isolation_ref(ref: str) -> str:
50
76
  """
51
77
  Parse isolation ref and strip any 'branch:' or 'worktree:' prefix.
@@ -83,6 +109,55 @@ def _get_slug(title: str) -> str:
83
109
  return slug
84
110
 
85
111
 
112
+ def _unquote_git_path(path: str) -> str:
113
+ """
114
+ Decode Git C-quoting format paths to native strings.
115
+
116
+ Git uses C-quoting for paths containing non-ASCII characters or special characters:
117
+ - Wraps the path in double quotes: "path"
118
+ - Escapes bytes as octal: \351\207\215 represents UTF-8 bytes
119
+
120
+ Args:
121
+ path: Path string, potentially in Git C-quoting format
122
+
123
+ Returns:
124
+ Decoded native path string
125
+
126
+ Examples:
127
+ >>> _unquote_git_path('"path-\\351\\207\\215.md"')
128
+ 'path-重.md'
129
+ >>> _unquote_git_path('"path\\ with\\ space.md"')
130
+ 'path with space.md'
131
+ >>> _unquote_git_path('normal/path.md')
132
+ 'normal/path.md'
133
+ """
134
+ path = path.strip()
135
+
136
+ # Check if the path is wrapped in double quotes (C-quoting format)
137
+ if not (path.startswith('"') and path.endswith('"')):
138
+ return path
139
+
140
+ # Remove outer quotes
141
+ path = path[1:-1]
142
+
143
+ # Collect all bytes (octal escapes -> bytes, ASCII chars -> their byte values)
144
+ bytes_list = []
145
+ i = 0
146
+ while i < len(path):
147
+ if path[i] == '\\' and i + 3 < len(path) and path[i+1:i+4].isdigit():
148
+ # This is an octal escape sequence (e.g., \351)
149
+ byte_val = int(path[i+1:i+4], 8)
150
+ bytes_list.append(byte_val)
151
+ i += 4
152
+ else:
153
+ # Regular ASCII character
154
+ bytes_list.append(ord(path[i]))
155
+ i += 1
156
+
157
+ # Decode the entire byte sequence as UTF-8
158
+ return bytes(bytes_list).decode('utf-8')
159
+
160
+
86
161
  def parse_issue(file_path: Path, raise_error: bool = False) -> Optional[IssueMetadata]:
87
162
  if not file_path.suffix == ".md":
88
163
  return None
@@ -494,14 +569,15 @@ def find_issue_path_across_branches(
494
569
  issues_root: Path,
495
570
  issue_id: str,
496
571
  project_root: Optional[Path] = None,
497
- include_archived: bool = True
498
- ) -> Tuple[Optional[Path], Optional[str]]:
572
+ include_archived: bool = True,
573
+ allow_multi_branch: bool = False
574
+ ) -> Tuple[Optional[Path], Optional[str], Optional[List[str]]]:
499
575
  """
500
576
  Find issue path across all local git branches.
501
577
 
502
578
  Implements the "Golden Path" logic:
503
579
  - If issue found in exactly one branch -> return it silently
504
- - If issue found in multiple branches -> raise error (conflict)
580
+ - If issue found in multiple branches -> handle based on allow_multi_branch flag
505
581
  - If issue not found in any branch -> return None
506
582
 
507
583
  Args:
@@ -509,12 +585,14 @@ def find_issue_path_across_branches(
509
585
  issue_id: Issue ID to find
510
586
  project_root: Project root (defaults to issues_root.parent)
511
587
  include_archived: Whether to search in archived directory
588
+ allow_multi_branch: If True, return all conflicting branches instead of raising error
512
589
 
513
590
  Returns:
514
- Tuple of (file_path, branch_name) or (None, None) if not found
591
+ Tuple of (file_path, branch_name, conflicting_branches) or (None, None, None) if not found
592
+ conflicting_branches is None if no conflict, or a list of branch names if conflict detected
515
593
 
516
594
  Raises:
517
- RuntimeError: If issue found in multiple branches (conflict)
595
+ RuntimeError: If issue found in multiple branches and allow_multi_branch is False (conflict)
518
596
  """
519
597
  # First, try to find in current working tree
520
598
  local_path = find_issue_path(issues_root, issue_id, include_archived)
@@ -525,54 +603,16 @@ def find_issue_path_across_branches(
525
603
 
526
604
  # If not a git repo, just return local result
527
605
  if not git.is_git_repo(project_root):
528
- return (local_path, None) if local_path else (None, None)
606
+ return (local_path, None, None) if local_path else (None, None, None)
529
607
 
530
608
  # Get current branch
531
609
  current_branch = git.get_current_branch(project_root)
532
610
 
533
- # Search in all branches to detect conflicts (FIX-0006)
534
- # If found locally, we still check other branches to detect potential duplicate files (conflict)
611
+ # Issue files existing in multiple branches is NORMAL - not a conflict
612
+ # Just return the local path without any conflict checking
535
613
  if local_path:
536
- # Get relative path for git checking
537
- try:
538
- rel_path = local_path.relative_to(project_root)
539
- except ValueError:
540
- # If path is outside project root, we can't search other branches anyway
541
- return local_path, current_branch
542
-
543
- conflicting_branches = _find_branches_with_file(project_root, str(rel_path), current_branch)
544
-
545
- # SPECIAL CASE (FIX-0006): During 'issue close' or similar operations,
546
- # it is expected that the file exists in the feature branch (isolation branch)
547
- # and now in the target branch (e.g. main). This is NOT a conflict.
548
- if conflicting_branches:
549
- # Try to parse metadata to see if these "conflicts" are actually the isolation branch
550
- try:
551
- meta = parse_issue(local_path)
552
- if meta and meta.isolation:
553
- iso_type = getattr(meta.isolation, "type", None)
554
- iso_ref = getattr(meta.isolation, "ref", None)
555
-
556
- source_branch = None
557
- if iso_type == "branch":
558
- source_branch = iso_ref
559
- elif iso_ref and iso_ref.startswith("branch:"):
560
- source_branch = iso_ref[7:]
561
-
562
- if source_branch:
563
- # Filter out the source branch from conflicts
564
- conflicting_branches = [b for b in conflicting_branches if b != source_branch]
565
- except Exception:
566
- # If parsing fails, stick with raw conflicts to be safe
567
- pass
568
-
569
- if conflicting_branches:
570
- raise RuntimeError(
571
- f"Issue {issue_id} found in multiple branches: {current_branch}, {', '.join(conflicting_branches)}. "
572
- f"Please resolve the conflict by merging branches or deleting duplicate issue files."
573
- )
574
- return local_path, current_branch
575
-
614
+ return local_path, current_branch, None
615
+
576
616
  # Not found locally, search in all branches
577
617
  return _search_issue_in_branches(issues_root, issue_id, project_root, include_archived)
578
618
 
@@ -615,33 +655,34 @@ def _search_issue_in_branches(
615
655
  issue_id: str,
616
656
  project_root: Path,
617
657
  include_archived: bool = True
618
- ) -> Tuple[Optional[Path], Optional[str]]:
658
+ ) -> Tuple[Optional[Path], Optional[str], Optional[List[str]]]:
619
659
  """
620
660
  Search for an issue file across all branches.
621
661
 
622
662
  Returns:
623
- Tuple of (file_path, branch_name) or (None, None)
663
+ Tuple of (file_path, branch_name, conflicting_branches) or (None, None, None)
664
+ conflicting_branches is None if no conflict, or a list of branch names if conflict detected
624
665
 
625
666
  Raises:
626
- RuntimeError: If issue found in multiple branches
667
+ RuntimeError: If issue found in multiple branches and allow_multi_branch is False
627
668
  """
628
669
  parsed = IssueID(issue_id)
629
670
 
630
671
  if not parsed.is_local:
631
672
  # For workspace issues, just use standard find
632
673
  path = find_issue_path(issues_root, issue_id, include_archived)
633
- return (path, None) if path else (None, None)
674
+ return (path, None, None) if path else (None, None, None)
634
675
 
635
676
  # Get issue type from prefix
636
677
  try:
637
678
  prefix = parsed.local_id.split("-")[0].upper()
638
679
  except IndexError:
639
- return None, None
680
+ return None, None, None
640
681
 
641
682
  reverse_prefix_map = get_reverse_prefix_map(issues_root)
642
683
  issue_type = reverse_prefix_map.get(prefix)
643
684
  if not issue_type:
644
- return None, None
685
+ return None, None, None
645
686
 
646
687
  # Build possible paths to search
647
688
  base_dir = get_issue_dir(issue_type, issues_root)
@@ -657,7 +698,7 @@ def _search_issue_in_branches(
657
698
  # Get all local branches
658
699
  code, stdout, _ = git._run_git(["branch", "--format=%(refname:short)"], project_root)
659
700
  if code != 0:
660
- return None, None
701
+ return None, None, None
661
702
 
662
703
  all_branches = [b.strip() for b in stdout.splitlines() if b.strip()]
663
704
 
@@ -687,15 +728,14 @@ def _search_issue_in_branches(
687
728
  found_in_branches.append((branch, line))
688
729
 
689
730
  if not found_in_branches:
690
- return None, None
731
+ return None, None, None
691
732
 
692
733
  if len(found_in_branches) > 1:
693
- # Found in multiple branches - conflict
734
+ # Found in multiple branches - return all branches for caller to handle
694
735
  branches = [b for b, _ in found_in_branches]
695
- raise RuntimeError(
696
- f"Issue {issue_id} found in multiple branches: {', '.join(branches)}. "
697
- f"Please resolve the conflict by merging branches or deleting duplicate issue files."
698
- )
736
+ branch, file_path = found_in_branches[0]
737
+ full_path = project_root / file_path
738
+ return full_path, branch, branches
699
739
 
700
740
  # Golden path: exactly one match
701
741
  branch, file_path = found_in_branches[0]
@@ -704,11 +744,11 @@ def _search_issue_in_branches(
704
744
  try:
705
745
  git.git_checkout_files(project_root, branch, [file_path])
706
746
  full_path = project_root / file_path
707
- return full_path, branch
747
+ return full_path, branch, None
708
748
  except Exception:
709
749
  # If checkout fails, return the path anyway - caller can handle
710
750
  full_path = project_root / file_path
711
- return full_path, branch
751
+ return full_path, branch, None
712
752
 
713
753
 
714
754
  def update_issue(
@@ -1068,7 +1108,7 @@ def start_issue_isolation(
1068
1108
  )
1069
1109
 
1070
1110
  slug = _get_slug(issue.title)
1071
- branch_name = f"feat/{issue_id.lower()}-{slug}"
1111
+ branch_name = f"{issue_id}-{slug}"
1072
1112
 
1073
1113
  isolation_meta = None
1074
1114
 
@@ -1206,9 +1246,41 @@ def delete_issue_file(issues_root: Path, issue_id: str):
1206
1246
  path.unlink()
1207
1247
 
1208
1248
 
1249
+ def _has_uncommitted_changes(project_root: Path) -> Tuple[bool, List[str]]:
1250
+ """
1251
+ Check if there are uncommitted changes in the working directory.
1252
+
1253
+ Returns:
1254
+ Tuple of (has_changes, list_of_changed_files)
1255
+ """
1256
+ # Check for staged changes
1257
+ cmd_staged = ["diff", "--cached", "--name-only"]
1258
+ code, stdout, _ = git._run_git(cmd_staged, project_root)
1259
+ staged_files = [f.strip() for f in stdout.splitlines() if f.strip()] if code == 0 else []
1260
+
1261
+ # Check for unstaged changes
1262
+ cmd_unstaged = ["diff", "--name-only"]
1263
+ code, stdout, _ = git._run_git(cmd_unstaged, project_root)
1264
+ unstaged_files = [f.strip() for f in stdout.splitlines() if f.strip()] if code == 0 else []
1265
+
1266
+ # Check for untracked files
1267
+ cmd_untracked = ["ls-files", "--others", "--exclude-standard"]
1268
+ code, stdout, _ = git._run_git(cmd_untracked, project_root)
1269
+ untracked_files = [f.strip() for f in stdout.splitlines() if f.strip()] if code == 0 else []
1270
+
1271
+ all_changes = list(set(staged_files + unstaged_files + untracked_files))
1272
+ all_changes.sort()
1273
+
1274
+ return len(all_changes) > 0, all_changes
1275
+
1276
+
1209
1277
  def sync_issue_files(issues_root: Path, issue_id: str, project_root: Path) -> List[str]:
1210
1278
  """
1211
1279
  Sync 'files' field in issue metadata with actual changed files in git.
1280
+
1281
+ Pre-condition: All changes must be committed. If there are uncommitted changes,
1282
+ this function will raise an error with instructions on how to proceed.
1283
+
1212
1284
  Strategies:
1213
1285
  1. Isolation Ref: If issue has isolation (branch/worktree), use that ref.
1214
1286
  2. Convention: If no isolation, look for branch `*/<id>-*`.
@@ -1224,6 +1296,20 @@ def sync_issue_files(issues_root: Path, issue_id: str, project_root: Path) -> Li
1224
1296
  if not issue:
1225
1297
  raise ValueError(f"Could not parse issue {issue_id}")
1226
1298
 
1299
+ # CHORE-0036: Check for uncommitted changes
1300
+ has_uncommitted, uncommitted_files = _has_uncommitted_changes(project_root)
1301
+ if has_uncommitted:
1302
+ files_list = "\n - ".join([""] + uncommitted_files)
1303
+ raise RuntimeError(
1304
+ f"Uncommitted changes detected in working directory:{files_list}\n\n"
1305
+ f"You must explicitly handle these files before syncing:\n"
1306
+ f" 1. git add <files> && git commit -m '...' (to commit)\n"
1307
+ f" 2. git checkout -- <files> (to discard)\n"
1308
+ f" 3. echo '<pattern>' >> .gitignore (to ignore)\n"
1309
+ f" 4. git rm <files> (to remove)\n\n"
1310
+ f"After handling, run: monoco issue sync-files {issue_id}"
1311
+ )
1312
+
1227
1313
  # Determine Target Branch
1228
1314
  target_ref = None
1229
1315
 
@@ -1232,15 +1318,15 @@ def sync_issue_files(issues_root: Path, issue_id: str, project_root: Path) -> Li
1232
1318
  if target_ref == "current":
1233
1319
  target_ref = git.get_current_branch(project_root)
1234
1320
  else:
1235
- # Heuristic Search
1236
- # 1. Is current branch related?
1321
+ # Heuristic Search - STRICT ID MATCHING ONLY
1322
+ # 1. Is current branch's extracted ID exactly matching the issue ID?
1237
1323
  current = git.get_current_branch(project_root)
1238
- if issue_id.lower() in current.lower():
1324
+ branch_issue_id = _extract_issue_id_from_branch(current)
1325
+ if branch_issue_id and branch_issue_id.upper() == issue_id.upper():
1239
1326
  target_ref = current
1240
1327
  else:
1241
- # 2. Search for branch
1242
- # Limitation: core.git doesn't list all branches yet.
1243
- # We skip this for now to avoid complexity, relying on isolation or current context.
1328
+ # 2. Search for branch - not implemented yet
1329
+ # To avoid ambiguity, we only match current branch if it exactly matches
1244
1330
  pass
1245
1331
 
1246
1332
  if not target_ref:
@@ -1270,6 +1356,14 @@ def sync_issue_files(issues_root: Path, issue_id: str, project_root: Path) -> Li
1270
1356
 
1271
1357
  changed_files = [f.strip() for f in stdout.splitlines() if f.strip()]
1272
1358
 
1359
+ # Decode Git C-quoting format paths to native paths
1360
+ changed_files = [_unquote_git_path(f) for f in changed_files]
1361
+
1362
+ # FEAT-0172: Exclude issue file itself from files list
1363
+ # Issue file is handled separately by update_issue (directory move + status update)
1364
+ relative_issue_path = str(path.relative_to(project_root))
1365
+ changed_files = [f for f in changed_files if f != relative_issue_path]
1366
+
1273
1367
  # Sort for consistency
1274
1368
  changed_files.sort()
1275
1369
 
@@ -1308,28 +1402,21 @@ def merge_issue_changes(
1308
1402
  if issue.isolation and issue.isolation.ref:
1309
1403
  source_ref = _parse_isolation_ref(issue.isolation.ref)
1310
1404
  else:
1311
- # Heuristic: Search for branch by convention
1312
- # We can't use 'current' here safely if we are on main,
1313
- # but let's assume we might be calling this from elsewhere?
1314
- # Actually, for 'close', we are likely on main.
1315
- # So we search for a branch named 'feat/{id}-*' or similar?
1316
- pass
1405
+ # Heuristic: Search for branch by convention {id}-*
1406
+ import re
1407
+ code, stdout, _ = git._run_git(["branch", "--format=%(refname:short)"], project_root)
1408
+ if code == 0:
1409
+ for branch in stdout.splitlines():
1410
+ branch = branch.strip()
1411
+ if re.match(rf"{re.escape(issue_id)}-", branch, re.IGNORECASE):
1412
+ source_ref = branch
1413
+ break
1317
1414
 
1318
- # If local metadata doesn't have isolation ref, we might be stuck.
1319
- # But let's assume valid workflow.
1320
1415
  if not source_ref:
1321
- # Try to find a branch starting with feat/{id} or {id}
1322
- # This is a bit weak, needs better implementation in 'git' or 'issue' module
1323
- # For now, if we can't find it, we error.
1324
- pass
1325
-
1326
- if not source_ref or not git.branch_exists(project_root, source_ref):
1327
- # Fallback: maybe we are currently ON the feature branch?
1328
- # If so, source_ref should be current. But we expect to call this from MAIN.
1329
- pass
1330
-
1331
- if not source_ref:
1332
- raise RuntimeError(f"Could not determine source branch for Issue {issue_id}. Ensure isolation ref is set.")
1416
+ # No branch found - files should already be in current branch
1417
+ # This is valid for direct workflow (no feature branch created)
1418
+ # Nothing to merge, files are already in place
1419
+ return issue.files
1333
1420
 
1334
1421
  if not git.branch_exists(project_root, source_ref):
1335
1422
  raise RuntimeError(f"Source branch {source_ref} does not exist.")
@@ -1360,10 +1447,20 @@ def merge_issue_changes(
1360
1447
  if not issue.files:
1361
1448
  return []
1362
1449
 
1363
- # 1. Conflict Check
1364
- # A conflict occurs if a file in 'files' has changed on HEAD (main)
1365
- # since the common ancestor of HEAD and source_ref.
1450
+ # Compatibility: Decode any legacy escaped paths in files field
1451
+ # This handles files stored before the _unquote_git_path fix was applied
1452
+ decoded_files = [_unquote_git_path(f) for f in issue.files]
1366
1453
 
1454
+ if not decoded_files:
1455
+ return []
1456
+
1457
+ # Identify the issue file path
1458
+ relative_issue_path = str(path.relative_to(project_root))
1459
+
1460
+ # Separate files: issue file vs code files
1461
+ code_files = [f for f in decoded_files if f != relative_issue_path]
1462
+
1463
+ # 1. Conflict Check (only for code files, issue file skips conflict check)
1367
1464
  current_head = git.get_current_branch(project_root)
1368
1465
  try:
1369
1466
  base = git.get_merge_base(project_root, current_head, source_ref)
@@ -1371,7 +1468,7 @@ def merge_issue_changes(
1371
1468
  raise RuntimeError(f"Failed to determine merge base: {e}")
1372
1469
 
1373
1470
  conflicts = []
1374
- for f in issue.files:
1471
+ for f in code_files:
1375
1472
  # Has main changed this file?
1376
1473
  if git.has_diff(project_root, base, current_head, [f]):
1377
1474
  # Has feature also changed this file?
@@ -1386,12 +1483,15 @@ def merge_issue_changes(
1386
1483
  )
1387
1484
 
1388
1485
  # 2. Perform Atomic Merge (Selective Checkout)
1389
- try:
1390
- git.git_checkout_files(project_root, source_ref, issue.files)
1391
- except Exception as e:
1392
- raise RuntimeError(f"Selective checkout failed: {e}")
1486
+ # FEAT-0172: Only merge code files, issue file is handled separately
1487
+ files_to_merge = code_files
1488
+ if files_to_merge:
1489
+ try:
1490
+ git.git_checkout_files(project_root, source_ref, files_to_merge)
1491
+ except Exception as e:
1492
+ raise RuntimeError(f"Selective checkout failed: {e}")
1393
1493
 
1394
- return issue.files
1494
+ return files_to_merge
1395
1495
 
1396
1496
 
1397
1497
  # Resources
@@ -26,22 +26,3 @@ def list_domains():
26
26
  )
27
27
 
28
28
  console.print(table)
29
-
30
-
31
- @app.command("check")
32
- def check_domain(domain: str = typer.Argument(..., help="Domain name to check")):
33
- """Check if a domain is valid and resolve it."""
34
- service = DomainService()
35
-
36
- if service.is_canonical(domain):
37
- console.print(f"[green]✔ '{domain}' is a canonical domain.[/green]")
38
- elif service.is_alias(domain):
39
- canonical = service.get_canonical(domain)
40
- console.print(f"[yellow]➜ '{domain}' is an alias for '{canonical}'.[/yellow]")
41
- else:
42
- if service.config.strict:
43
- console.print(f"[red]✘ '{domain}' is NOT a valid domain.[/red]")
44
- else:
45
- console.print(
46
- f"[yellow]⚠ '{domain}' is undefined (Strict Mode: OFF).[/yellow]"
47
- )
@@ -1,131 +1,26 @@
1
- # Issue Management (Agent Guidance)
2
-
3
- ## Issue Management
1
+ # Issue Management
4
2
 
5
3
  System for managing tasks using `monoco issue`.
6
4
 
7
- - **Create**: `monoco issue create <type> -t "Title"` (types: epic, feature, chore, fix)
5
+ - **Create**: `monoco issue create <type> -t "Title"`
8
6
  - **Status**: `monoco issue open|close|backlog <id>`
9
- - **Check**: `monoco issue lint` (Must run after manual edits)
7
+ - **Check**: `monoco issue lint`
10
8
  - **Lifecycle**: `monoco issue start|submit|delete <id>`
11
- - **Sync Context**: `monoco issue sync-files [id]` (Update file tracking)
12
- - **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`). Do not deviate.
13
- - **Rules**:
14
- 1. **Issue First**: You MUST create an Issue (`monoco issue create`) before starting any work (research, design, or drafting).
15
- 2. **Heading**: Must have `## {ID}: {Title}` (matches metadata).
16
- 3. **Checkboxes**: Min 2 using `- [ ]`, `- [x]`, `- [-]`, `- [/]`.
17
- 4. **Review**: `## Review Comments` section required for Review/Done stages.
18
- 5. **Environment Policies**:
19
- - Must use `monoco issue start --branch`.
20
- - 🛑 **NO** direct coding on `main`/`master` (Linter will fail).
21
- - **Prune Timing**: ONLY prune environment (branch/worktree) during `monoco issue close --prune`. NEVER prune at `submit` stage.
22
- - Must update `files` field after coding (via `sync-files` or manual).
23
-
24
- ## Git Merge Strategy
25
-
26
- ### Core Principles
27
-
28
- To ensure safe merging of Feature branches into the mainline and prevent "stale state pollution", the following merge strategy must be followed:
29
-
30
- #### 1. No Manual Merge
31
-
32
- - **🛑 STRICTLY FORBIDDEN**: Agents must NOT manually execute `git merge` to merge Feature branches
33
- - **🛑 STRICTLY FORBIDDEN**: Using `git pull origin main` followed by direct commits
34
- - **✅ ONLY AUTHORITATIVE PATH**: Must use `monoco issue close` for closing the loop
35
-
36
- #### 2. Safe Merge Flow
37
-
38
- The correct Issue closing workflow is as follows:
39
-
40
- ```bash
41
- # 1. Ensure you're on main/master branch and code is merged
42
- $ git checkout main
43
- $ git pull origin main
44
-
45
- # 2. Confirm Feature branch changes are merged to mainline
46
- # (via PR/MR or other code review process)
47
-
48
- # 3. Use monoco issue close to close Issue (prune by default)
49
- $ monoco issue close FEAT-XXXX --solution implemented
50
-
51
- # 4. To keep branch, use --no-prune
52
- $ monoco issue close FEAT-XXXX --solution implemented --no-prune
53
- ```
54
-
55
- #### 3. Conflict Resolution Principles
9
+ - **Sync Context**: `monoco issue sync-files [id]`
10
+ - **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`)
56
11
 
57
- When Feature branch conflicts with mainline:
12
+ ## Standard Workflow
58
13
 
59
- 1. **Auto-merge Stop**: If `touched files` (Issue `files` field) conflict with mainline, automation tools **MUST IMMEDIATELY STOP** merging and throw a clear error.
14
+ 1. **Create Issue**: `monoco issue create feature -t "Title"`
15
+ 2. **Start Branch**: `monoco issue start FEAT-XXX --branch`
16
+ 3. **Implement**: Normal coding and testing.
17
+ 4. **Sync Files**: `monoco issue sync-files` (Update `files` field).
18
+ 5. **Submit**: `monoco issue submit FEAT-XXX`.
19
+ 6. **Close & Merge**: `monoco issue close FEAT-XXX --solution implemented` (The only way to merge).
60
20
 
61
- 2. **Manual Cherry-Pick Mode**:
62
- - Error message will instruct Agent to switch to manual Cherry-Pick mode
63
- - **Core Principle**: Only pick valid changes belonging to this Feature, STRICTLY FORBIDDEN from overwriting updates to unrelated Issues on mainline
64
- - Use `git cherry-pick <commit>` to apply valid commits one by one
65
-
66
- 3. **Fallback Strategy**:
67
- ```bash
68
- # 1. Create temporary branch for conflict resolution
69
- $ git checkout main
70
- $ git checkout -b temp/FEAT-XXXX-resolve
71
-
72
- # 2. Cherry-pick valid commits one by one
73
- $ git cherry-pick <commit-hash-1>
74
- $ git cherry-pick <commit-hash-2>
75
-
76
- # 3. If conflicts occur, only keep changes from this Feature
77
- # Discard any modifications that would overwrite other Issue updates on mainline
78
-
79
- # 4. Merge temporary branch when done
80
- $ git checkout main
81
- $ git merge temp/FEAT-XXXX-resolve
82
-
83
- # 5. Close Issue
84
- $ monoco issue close FEAT-XXXX --solution implemented
85
- ```
86
-
87
- #### 4. Smart Atomic Merge Based on files Field
88
-
89
- The Issue's `files` field records the Actual Impact Scope of the Feature branch:
90
-
91
- - **Generation**: `monoco issue sync-files` uses `git diff --name-only base...target` logic
92
- - **Purpose**: Serves as a merge whitelist, only merging files in the list, filtering out implicit overwrites caused by "stale baseline"
93
- - **Limitation**: Cannot defend against explicit accidental modifications (e.g., inadvertently formatting other Issue files)
94
-
95
- **Future Enhancement**: Implement selective merge logic based on `files` list:
96
- ```bash
97
- # Selective merge (planned)
98
- $ git checkout main
99
- $ git checkout feature/FEAT-XXXX -- <files...>
100
- ```
101
-
102
- #### 5. Cleanup Strategy
103
-
104
- - **Default Cleanup**: `monoco issue close` executes `--prune` by default, deleting Feature branch/worktree
105
- - **Keep Branch**: To preserve branch, explicitly use `--no-prune`
106
- - **Force Cleanup**: Use `--force` to force delete unmerged branches (use with caution)
107
-
108
- ```bash
109
- # Default branch cleanup
110
- $ monoco issue close FEAT-XXXX --solution implemented
111
- # ✔ Cleaned up: branch:feat/feat-XXXX-xxx
112
-
113
- # Keep branch
114
- $ monoco issue close FEAT-XXXX --solution implemented --no-prune
115
-
116
- # Force cleanup (caution)
117
- $ monoco issue close FEAT-XXXX --solution implemented --force
118
- ```
119
-
120
- ### Summary
121
-
122
- | Operation | Command | Description |
123
- |-----------|---------|-------------|
124
- | Create Issue | `monoco issue create feature -t "Title"` | Create Issue before development |
125
- | Start Development | `monoco issue start FEAT-XXXX --branch` | Create Feature branch |
126
- | Sync Files | `monoco issue sync-files` | Update files field |
127
- | Submit Review | `monoco issue submit FEAT-XXXX` | Enter Review stage |
128
- | Close Issue | `monoco issue close FEAT-XXXX --solution implemented` | Only merge path |
129
- | Keep Branch | `monoco issue close ... --no-prune` | Close without deleting branch |
21
+ ## Git Merge Strategy
130
22
 
131
- > ⚠️ **WARNING**: Any manual merge operation bypassing `monoco issue close` may cause mainline state pollution and violate workflow compliance requirements.
23
+ - **NO Manual Merge**: Strictly forbidden from using `git merge` or `git pull` into main.
24
+ - **Atomic Merge**: `monoco issue close` merges only the files listed in the `files` field.
25
+ - **Conflicts**: If conflicts occur, follow the instructions provided by the `close` command (usually manual cherry-pick).
26
+ - **Cleanup**: `monoco issue close` prunes the branch/worktree by default. Use `--no-prune` to keep it.