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
|
@@ -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
|
-
|
|
460
|
-
|
|
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
|
|
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
|
-
#
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
1099
|
+
# Try to parse ID from branch: FEAT-123-slug format
|
|
1090
1100
|
import re
|
|
1091
1101
|
|
|
1092
|
-
|
|
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)
|