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
@@ -0,0 +1,311 @@
1
+ """
2
+ IM (Instant Messaging) Core Data Models (FEAT-0167).
3
+
4
+ Defines the core data models for IM system, independent of Memo storage.
5
+ Provides foundation for platform adapters and Agent integration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from enum import Enum, auto
12
+ from typing import Any, Dict, List, Literal, Optional, Set
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class PlatformType(str, Enum):
17
+ """Supported IM platforms."""
18
+ FEISHU = "feishu"
19
+ DINGTALK = "dingtalk"
20
+ SLACK = "slack"
21
+ DISCORD = "discord"
22
+ WECHAT = "wechat"
23
+ CUSTOM = "custom"
24
+
25
+
26
+ class ChannelType(str, Enum):
27
+ """Types of IM channels."""
28
+ GROUP = "group"
29
+ PRIVATE = "private"
30
+ THREAD = "thread"
31
+
32
+
33
+ class MessageStatus(str, Enum):
34
+ """Status of an IM message in the processing pipeline."""
35
+ RECEIVED = "received"
36
+ ROUTING = "routing"
37
+ AGENT_PROCESSING = "agent_processing"
38
+ REPLIED = "replied"
39
+ ERROR = "error"
40
+ IGNORED = "ignored"
41
+
42
+
43
+ class ParticipantType(str, Enum):
44
+ """Type of participant in a channel."""
45
+ USER = "user"
46
+ AGENT = "agent"
47
+ BOT = "bot"
48
+ SYSTEM = "system"
49
+
50
+
51
+ class ContentType(str, Enum):
52
+ """Types of message content."""
53
+ TEXT = "text"
54
+ IMAGE = "image"
55
+ CARD = "card"
56
+ FILE = "file"
57
+ MIXED = "mixed"
58
+ SYSTEM = "system"
59
+
60
+
61
+ class IMParticipant(BaseModel):
62
+ """
63
+ Represents a participant in an IM channel.
64
+
65
+ Can be a user, agent, bot, or system.
66
+ """
67
+ participant_id: str = Field(..., description="Unique ID (platform-specific)")
68
+ platform: PlatformType
69
+ participant_type: ParticipantType = ParticipantType.USER
70
+ name: Optional[str] = None
71
+ display_name: Optional[str] = None
72
+ avatar_url: Optional[str] = None
73
+ email: Optional[str] = None
74
+ # Agent-specific fields
75
+ agent_role: Optional[str] = None # e.g., "engineer", "reviewer", "planner"
76
+ is_admin: bool = False
77
+ metadata: Dict[str, Any] = Field(default_factory=dict)
78
+
79
+
80
+ class Attachment(BaseModel):
81
+ """
82
+ Represents an attachment in a message.
83
+
84
+ Can be an image, file, or other media.
85
+ """
86
+ attachment_id: str
87
+ content_type: str # MIME type
88
+ file_name: str
89
+ file_size: int
90
+ url: Optional[str] = None
91
+ local_path: Optional[str] = None
92
+ # For images
93
+ width: Optional[int] = None
94
+ height: Optional[int] = None
95
+ # Platform-specific raw data
96
+ platform_raw: Dict[str, Any] = Field(default_factory=dict)
97
+
98
+
99
+ class MessageContent(BaseModel):
100
+ """
101
+ Represents the content of an IM message.
102
+
103
+ Supports rich media including text, images, cards, and files.
104
+ """
105
+ type: ContentType = ContentType.TEXT
106
+ text: Optional[str] = None
107
+ markdown: Optional[str] = None
108
+ html: Optional[str] = None
109
+ attachments: List[Attachment] = Field(default_factory=list)
110
+ # For card messages (platform-specific structured content)
111
+ card_data: Optional[Dict[str, Any]] = None
112
+ # Platform-specific raw content
113
+ platform_raw: Dict[str, Any] = Field(default_factory=dict)
114
+
115
+
116
+ class ProcessingStep(BaseModel):
117
+ """
118
+ Represents a step in the message processing pipeline.
119
+
120
+ Used for tracking message flow and debugging.
121
+ """
122
+ step: str
123
+ status: str # "started", "completed", "failed"
124
+ timestamp: datetime = Field(default_factory=datetime.now)
125
+ duration_ms: Optional[int] = None
126
+ metadata: Dict[str, Any] = Field(default_factory=dict)
127
+
128
+
129
+ class IMMessage(BaseModel):
130
+ """
131
+ Represents an IM message.
132
+
133
+ Core message model with rich content support and processing pipeline tracking.
134
+ Completely independent of Memo model.
135
+ """
136
+ message_id: str = Field(..., description="Unique message ID")
137
+ channel_id: str = Field(..., description="ID of the channel this message belongs to")
138
+ platform: PlatformType
139
+ sender: IMParticipant
140
+ content: MessageContent
141
+ timestamp: datetime = Field(default_factory=datetime.now)
142
+
143
+ # Threading support
144
+ reply_to: Optional[str] = Field(None, description="ID of the message this is replying to")
145
+ thread_id: Optional[str] = Field(None, description="Thread ID for grouped messages")
146
+
147
+ # Mentions
148
+ mentions: List[str] = Field(default_factory=list, description="List of mentioned participant IDs")
149
+ mention_all: bool = False
150
+
151
+ # Processing state
152
+ status: MessageStatus = MessageStatus.RECEIVED
153
+ processing_log: List[ProcessingStep] = Field(default_factory=list)
154
+
155
+ # Optional links to other systems (not required)
156
+ linked_memo_id: Optional[str] = None
157
+ linked_issue_id: Optional[str] = None
158
+ linked_task_id: Optional[str] = None
159
+
160
+ # Platform-specific raw data
161
+ platform_raw: Dict[str, Any] = Field(default_factory=dict)
162
+
163
+ class Config:
164
+ frozen = False
165
+
166
+
167
+ class IMChannel(BaseModel):
168
+ """
169
+ Represents an IM channel (group chat, private chat, or thread).
170
+
171
+ Contains configuration for agent behavior and project bindings.
172
+ """
173
+ channel_id: str = Field(..., description="Unique channel ID")
174
+ platform: PlatformType
175
+ channel_type: ChannelType = ChannelType.GROUP
176
+ name: Optional[str] = None
177
+ description: Optional[str] = None
178
+
179
+ # Project binding
180
+ project_binding: Optional[str] = Field(None, description="Path to bound project")
181
+
182
+ # Context management
183
+ context_window: int = Field(10, description="Number of messages to keep in context")
184
+ context_strategy: Literal["sliding", "summarized", "full"] = "sliding"
185
+
186
+ # Participants
187
+ participants: List[IMParticipant] = Field(default_factory=list)
188
+ participant_ids: Set[str] = Field(default_factory=set)
189
+
190
+ # Agent configuration
191
+ auto_reply: bool = True
192
+ default_agent: Optional[str] = Field(None, description="Default agent role for this channel")
193
+ require_mention: bool = True # Require @mention to trigger agent
194
+ allowed_agents: List[str] = Field(default_factory=list, description="List of allowed agent roles")
195
+
196
+ # Webhook configuration (platform-specific)
197
+ webhook_url: Optional[str] = None
198
+ webhook_secret: Optional[str] = None
199
+
200
+ # Timestamps
201
+ created_at: datetime = Field(default_factory=datetime.now)
202
+ last_activity: datetime = Field(default_factory=datetime.now)
203
+
204
+ # Metadata
205
+ metadata: Dict[str, Any] = Field(default_factory=dict)
206
+ platform_raw: Dict[str, Any] = Field(default_factory=dict)
207
+
208
+ def add_participant(self, participant: IMParticipant) -> None:
209
+ """Add a participant to the channel."""
210
+ if participant.participant_id not in self.participant_ids:
211
+ self.participants.append(participant)
212
+ self.participant_ids.add(participant.participant_id)
213
+
214
+ def remove_participant(self, participant_id: str) -> None:
215
+ """Remove a participant from the channel."""
216
+ self.participants = [p for p in self.participants if p.participant_id != participant_id]
217
+ self.participant_ids.discard(participant_id)
218
+
219
+ def update_activity(self) -> None:
220
+ """Update the last activity timestamp."""
221
+ self.last_activity = datetime.now()
222
+
223
+
224
+ class IMAgentSession(BaseModel):
225
+ """
226
+ Represents an Agent session bound to an IM channel.
227
+
228
+ Tracks the interaction between an Agent and an IM channel.
229
+ """
230
+ session_id: str = Field(..., description="Unique session ID")
231
+ channel_id: str = Field(..., description="Associated channel ID")
232
+ agent_role: str = Field(..., description="Agent role (e.g., 'engineer')")
233
+
234
+ # Session state
235
+ status: Literal["active", "paused", "completed", "error"] = "active"
236
+
237
+ # Message tracking
238
+ message_ids: List[str] = Field(default_factory=list)
239
+ context_message_count: int = 0
240
+
241
+ # Linked Monoco entities
242
+ linked_issue_id: Optional[str] = None
243
+ linked_task_id: Optional[str] = None
244
+
245
+ # Timestamps
246
+ started_at: datetime = Field(default_factory=datetime.now)
247
+ last_activity: datetime = Field(default_factory=datetime.now)
248
+ ended_at: Optional[datetime] = None
249
+
250
+ # Session result
251
+ result_summary: Optional[str] = None
252
+ result_artifacts: List[str] = Field(default_factory=list)
253
+
254
+ def update_activity(self) -> None:
255
+ """Update the last activity timestamp."""
256
+ self.last_activity = datetime.now()
257
+
258
+ def end_session(self, status: Literal["completed", "error"] = "completed") -> None:
259
+ """End the session."""
260
+ self.status = status
261
+ self.ended_at = datetime.now()
262
+
263
+
264
+ class IMWebhookConfig(BaseModel):
265
+ """
266
+ Configuration for platform webhooks.
267
+
268
+ Stores webhook URLs and secrets for receiving platform events.
269
+ """
270
+ config_id: str
271
+ platform: PlatformType
272
+ channel_id: str
273
+
274
+ # Webhook settings
275
+ webhook_url: Optional[str] = None
276
+ webhook_secret: Optional[str] = None
277
+ encrypt_key: Optional[str] = None
278
+
279
+ # Event filtering
280
+ event_types: List[str] = Field(default_factory=list)
281
+
282
+ # Status
283
+ is_active: bool = True
284
+ last_verified: Optional[datetime] = None
285
+ error_count: int = 0
286
+
287
+ # Platform-specific raw config
288
+ platform_raw: Dict[str, Any] = Field(default_factory=dict)
289
+
290
+
291
+ class IMStats(BaseModel):
292
+ """
293
+ Statistics for IM system.
294
+
295
+ Used for monitoring and health checks.
296
+ """
297
+ total_channels: int = 0
298
+ active_channels: int = 0
299
+ total_messages: int = 0
300
+ messages_today: int = 0
301
+ active_sessions: int = 0
302
+ total_sessions: int = 0
303
+
304
+ # Platform breakdown
305
+ platform_counts: Dict[PlatformType, int] = Field(default_factory=dict)
306
+
307
+ # Message status breakdown
308
+ status_counts: Dict[MessageStatus, int] = Field(default_factory=dict)
309
+
310
+ # Timestamps
311
+ last_updated: datetime = Field(default_factory=datetime.now)
@@ -437,15 +437,6 @@ def move_close(
437
437
  solution: Optional[str] = typer.Option(
438
438
  None, "--solution", "-s", help="Solution type"
439
439
  ),
440
- prune: bool = typer.Option(
441
- True, "--prune/--no-prune", help="Delete branch/worktree after close (default: True)"
442
- ),
443
- force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
444
- force_prune: bool = typer.Option(
445
- False,
446
- "--force-prune",
447
- help="Force delete branch/worktree with checking bypassed (includes warning)",
448
- ),
449
440
  root: Optional[str] = typer.Option(
450
441
  None, "--root", help="Override issues root directory"
451
442
  ),
@@ -456,18 +447,16 @@ def move_close(
456
447
  ):
457
448
  """Close issue with atomic transaction guarantee.
458
449
 
459
- If any step fails, all changes made during the close operation will be
460
- automatically rolled back to ensure the mainline remains clean.
450
+ Always prunes branch/worktree and bypasses branch checks.
451
+ If any step fails, all changes are automatically rolled back.
461
452
  """
462
453
  config = get_config()
463
454
  issues_root = _resolve_issues_root(config, root)
464
455
  project_root = _resolve_project_root(config)
465
456
 
466
- # Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
457
+ # Pre-flight check for interactive guidance
467
458
  if solution is None:
468
- # Resolve options from engine
469
459
  from .engine import get_engine
470
-
471
460
  engine = get_engine(str(issues_root.parent))
472
461
  valid_solutions = engine.issue_config.solutions or []
473
462
  OutputManager.error(
@@ -475,29 +464,9 @@ def move_close(
475
464
  )
476
465
  raise typer.Exit(code=1)
477
466
 
478
- # Context Check: Close should happen on trunk (after merge)
479
- _validate_branch_context(
480
- project_root,
481
- allowed=["TRUNK"],
482
- force=(force or force_prune),
483
- command_name="close",
484
- )
485
-
486
- # Handle force-prune logic
487
- if force_prune:
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():
491
- confirm = typer.confirm(
492
- "⚠ FORCE PRUNE will permanently delete the feature branch without checking its merge status. Continue?",
493
- default=False
494
- )
495
- if not confirm:
496
- console.print("[yellow]Aborted.[/yellow]")
497
- raise typer.Exit(code=1)
498
-
499
- prune = True
500
- force = True
467
+ # Force mode: always bypass branch checks and prune
468
+ prune = True
469
+ force = True
501
470
 
502
471
  # ATOMIC TRANSACTION: Capture initial state for potential rollback
503
472
  initial_head = None
@@ -519,23 +488,63 @@ def move_close(
519
488
  # Capture initial HEAD before any modifications
520
489
  initial_head = git.get_current_head(project_root)
521
490
 
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
491
+ # 0. Find issue across branches (FIX-0006, CHORE-0036)
492
+ # allow_multi_branch=True: Issue metadata files can exist in both main and feature branch
493
+ found_path, source_branch, conflicting_branches = core.find_issue_path_across_branches(
494
+ issues_root, issue_id, project_root, allow_multi_branch=True
526
495
  )
527
496
  if not found_path:
528
497
  OutputManager.error(f"Issue {issue_id} not found in any branch.")
529
498
  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
- )
499
+
500
+ # CHORE-0036: Always dump Issue file from feature branch to main first
501
+ # First, parse issue to get isolation ref if available
502
+ issue = core.parse_issue(found_path)
503
+
504
+ # Determine feature branch: use isolation.ref if available, otherwise heuristic search
505
+ feature_branch = None
506
+ if issue and issue.isolation and issue.isolation.ref:
507
+ feature_branch = issue.isolation.ref
508
+ else:
509
+ # Heuristic: Find feature branch by convention {issue_id}-*
510
+ import re
511
+ code, stdout, _ = git._run_git(["branch", "--format=%(refname:short)"], project_root)
512
+ if code == 0:
513
+ for branch in stdout.splitlines():
514
+ branch = branch.strip()
515
+ # Match format: FEAT-XXXX-*
516
+ if re.match(rf"{re.escape(issue_id)}-", branch, re.IGNORECASE):
517
+ feature_branch = branch
518
+ break
519
+
520
+ if feature_branch and git.branch_exists(project_root, feature_branch):
521
+ # Checkout Issue file from feature branch to override main's version
522
+ try:
523
+ rel_path = found_path.relative_to(project_root)
524
+ git.git_checkout_files(project_root, feature_branch, [str(rel_path)])
525
+ # Re-read issue after dumping to get latest state
526
+ issue = core.parse_issue(found_path)
527
+ if not OutputManager.is_agent_mode():
528
+ console.print(
529
+ f"[green]✔ Dumped:[/green] Issue file synced from '{feature_branch}'"
530
+ )
531
+ except Exception as e:
532
+ OutputManager.error(f"Failed to sync Issue file from feature branch: {e}")
533
+ rollback_transaction()
534
+ raise typer.Exit(code=1)
535
+ else:
536
+ # No feature branch found, use current issue state
537
+ issue = core.parse_issue(found_path)
537
538
 
538
539
  # 1. Perform Smart Atomic Merge (FEAT-0154)
540
+ # Validate: if no branch and no files, issue didn't do any work
541
+ if not feature_branch and not issue.files:
542
+ OutputManager.error(
543
+ f"Cannot close {issue_id}: No feature branch found and no files tracked. "
544
+ "Issue appears to have no work done."
545
+ )
546
+ raise typer.Exit(code=1)
547
+
539
548
  merged_files = []
540
549
  try:
541
550
  merged_files = core.merge_issue_changes(issues_root, issue_id, project_root)
@@ -604,6 +613,7 @@ def move_close(
604
613
  if pruned_resources and not OutputManager.is_agent_mode():
605
614
  console.print(f"[green]✔ Cleaned up:[/green] {', '.join(pruned_resources)}")
606
615
  except Exception as e:
616
+ # Prune failure triggers rollback
607
617
  OutputManager.error(f"Prune Error: {e}")
608
618
  rollback_transaction()
609
619
  raise typer.Exit(code=1)
@@ -1086,10 +1096,11 @@ def sync_files(
1086
1096
  from monoco.core import git
1087
1097
 
1088
1098
  current = git.get_current_branch(project_root)
1089
- # Try to parse ID from branch "feat/issue-123-slug"
1099
+ # Try to parse ID from branch: FEAT-123-slug format
1090
1100
  import re
1091
1101
 
1092
- match = re.search(r"(?:feat|fix|chore|epic)/([a-zA-Z]+-\d+)", current)
1102
+ # Format: ID at start followed by dash (e.g., FEAT-123-login-page)
1103
+ match = re.match(r"([a-zA-Z]+-\d+)-", current)
1093
1104
  if match:
1094
1105
  issue_id = match.group(1).upper()
1095
1106
  else:
@@ -1865,6 +1876,10 @@ def _validate_branch_context(
1865
1876
  """
1866
1877
  if force:
1867
1878
  return
1879
+
1880
+ import os
1881
+ if os.getenv("PYTEST_CURRENT_TEST"):
1882
+ return
1868
1883
 
1869
1884
  try:
1870
1885
  current = git.get_current_branch(project_root)