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.
- monoco/core/automation/__init__.py +0 -11
- monoco/core/automation/handlers.py +108 -26
- monoco/core/config.py +28 -10
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +1 -39
- monoco/core/router/action.py +3 -142
- monoco/core/scheduler/events.py +28 -2
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +6 -0
- monoco/core/watcher/base.py +18 -1
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/memo.py +40 -48
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/scheduler.py +1 -16
- monoco/daemon/services.py +15 -0
- monoco/features/agent/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +65 -50
- monoco/features/issue/core.py +199 -99
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/automation/config.py +0 -338
- monoco/core/execution.py +0 -67
- monoco/core/executor/__init__.py +0 -38
- monoco/core/executor/agent_action.py +0 -254
- monoco/core/executor/git_action.py +0 -303
- monoco/core/executor/im_action.py +0 -309
- monoco/core/executor/pytest_action.py +0 -218
- monoco/core/router/router.py +0 -392
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
monoco/features/issue/core.py
CHANGED
|
@@ -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
|
-
|
|
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 ->
|
|
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
|
-
#
|
|
534
|
-
#
|
|
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
|
-
|
|
537
|
-
|
|
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 -
|
|
734
|
+
# Found in multiple branches - return all branches for caller to handle
|
|
694
735
|
branches = [b for b, _ in found_in_branches]
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
-
#
|
|
1364
|
-
#
|
|
1365
|
-
|
|
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
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
|
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
|
|
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"`
|
|
5
|
+
- **Create**: `monoco issue create <type> -t "Title"`
|
|
8
6
|
- **Status**: `monoco issue open|close|backlog <id>`
|
|
9
|
-
- **Check**: `monoco issue lint`
|
|
7
|
+
- **Check**: `monoco issue lint`
|
|
10
8
|
- **Lifecycle**: `monoco issue start|submit|delete <id>`
|
|
11
|
-
- **Sync Context**: `monoco issue sync-files [id]`
|
|
12
|
-
- **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`)
|
|
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
|
-
|
|
12
|
+
## Standard Workflow
|
|
58
13
|
|
|
59
|
-
1. **
|
|
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
|
-
|
|
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
|
-
|
|
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.
|