monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.9__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/cli/workspace.py +1 -1
- monoco/core/config.py +51 -0
- monoco/core/hooks/__init__.py +19 -0
- monoco/core/hooks/base.py +104 -0
- monoco/core/hooks/builtin/__init__.py +11 -0
- monoco/core/hooks/builtin/git_cleanup.py +266 -0
- monoco/core/hooks/builtin/logging_hook.py +78 -0
- monoco/core/hooks/context.py +131 -0
- monoco/core/hooks/registry.py +222 -0
- monoco/core/integrations.py +6 -0
- monoco/core/registry.py +2 -0
- monoco/core/setup.py +1 -1
- monoco/core/skills.py +226 -42
- monoco/features/{scheduler → agent}/__init__.py +4 -2
- monoco/features/{scheduler → agent}/cli.py +134 -80
- monoco/features/{scheduler → agent}/config.py +17 -3
- monoco/features/agent/defaults.py +55 -0
- monoco/features/agent/flow_skills.py +281 -0
- monoco/features/{scheduler → agent}/manager.py +39 -2
- monoco/features/{scheduler → agent}/models.py +6 -3
- monoco/features/{scheduler → agent}/reliability.py +1 -1
- monoco/features/agent/resources/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/skills/flow_manager/SKILL.md +88 -0
- monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/{scheduler → agent}/session.py +36 -1
- monoco/features/{scheduler → agent}/worker.py +2 -2
- monoco/features/i18n/resources/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/issue/commands.py +427 -21
- monoco/features/issue/core.py +100 -0
- monoco/features/issue/criticality.py +553 -0
- monoco/features/issue/domain/models.py +28 -2
- monoco/features/issue/engine/machine.py +70 -13
- monoco/features/issue/git_service.py +185 -0
- monoco/features/issue/linter.py +291 -62
- monoco/features/issue/models.py +49 -2
- monoco/features/issue/resources/en/SKILL.md +48 -0
- monoco/features/issue/resources/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/zh/SKILL.md +50 -0
- monoco/features/issue/validator.py +185 -65
- monoco/features/memo/__init__.py +2 -1
- monoco/features/memo/adapter.py +32 -0
- monoco/features/memo/cli.py +36 -14
- monoco/features/memo/core.py +59 -0
- monoco/features/memo/resources/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/AGENTS.md +8 -0
- monoco/features/memo/resources/zh/SKILL.md +75 -0
- monoco/features/spike/resources/skills/research_workflow/SKILL.md +121 -0
- monoco/main.py +2 -3
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/RECORD +55 -37
- monoco/features/scheduler/defaults.py +0 -54
- monoco/features/skills/__init__.py +0 -0
- monoco/features/skills/core.py +0 -102
- /monoco/core/{hooks.py → githooks.py} +0 -0
- /monoco/features/{scheduler → agent}/engines.py +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
from typing import List, Optional, Dict
|
|
2
2
|
from monoco.core.config import IssueSchemaConfig, TransitionConfig
|
|
3
3
|
from ..models import IssueMetadata
|
|
4
|
+
from ..criticality import (
|
|
5
|
+
CriticalityLevel,
|
|
6
|
+
PolicyResolver,
|
|
7
|
+
HumanReviewLevel,
|
|
8
|
+
)
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
class StateMachine:
|
|
@@ -128,9 +133,11 @@ class StateMachine:
|
|
|
128
133
|
to_status: str,
|
|
129
134
|
to_stage: Optional[str],
|
|
130
135
|
solution: Optional[str] = None,
|
|
136
|
+
meta: Optional[IssueMetadata] = None,
|
|
131
137
|
) -> None:
|
|
132
138
|
"""
|
|
133
139
|
Validate if a transition is allowed. Raises ValueError if not.
|
|
140
|
+
If meta is provided, also validates criticality-based policies.
|
|
134
141
|
"""
|
|
135
142
|
if from_status == to_status and from_stage == to_stage:
|
|
136
143
|
return # No change is always allowed (unless we want to enforce specific updates)
|
|
@@ -150,28 +157,72 @@ class StateMachine:
|
|
|
150
157
|
f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'."
|
|
151
158
|
)
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
# Criticality-based policy checks
|
|
161
|
+
if meta and meta.criticality:
|
|
162
|
+
self._validate_criticality_policy(meta, from_stage, to_stage)
|
|
163
|
+
|
|
164
|
+
def _validate_criticality_policy(
|
|
165
|
+
self,
|
|
166
|
+
meta: IssueMetadata,
|
|
167
|
+
from_stage: Optional[str],
|
|
168
|
+
to_stage: Optional[str],
|
|
169
|
+
) -> None:
|
|
154
170
|
"""
|
|
155
|
-
|
|
171
|
+
Validate transition against criticality-based policies.
|
|
172
|
+
Enforces stricter requirements for high/critical issues.
|
|
156
173
|
"""
|
|
157
|
-
|
|
174
|
+
policy = PolicyResolver.resolve(meta.criticality)
|
|
175
|
+
|
|
176
|
+
# Submit to Review: Enforce agent review for medium+
|
|
177
|
+
if to_stage == "review" and from_stage == "doing":
|
|
178
|
+
if meta.criticality >= CriticalityLevel.MEDIUM:
|
|
179
|
+
# For medium+, agent review is mandatory
|
|
180
|
+
# This is enforced by the policy, but we can't check actual review status here
|
|
181
|
+
# The check is informational - actual enforcement happens in submit command
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Close/Accept: Enforce human review for high/critical
|
|
185
|
+
if to_stage == "done":
|
|
186
|
+
if policy.human_review in [
|
|
187
|
+
HumanReviewLevel.REQUIRED,
|
|
188
|
+
HumanReviewLevel.REQUIRED_RECORD,
|
|
189
|
+
]:
|
|
190
|
+
# For high/critical, human review is mandatory before closing
|
|
191
|
+
# Actual enforcement would check review comments section
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def check_policy_compliance(self, meta: IssueMetadata) -> List[str]:
|
|
195
|
+
"""
|
|
196
|
+
Check if an issue complies with its criticality policy.
|
|
197
|
+
Returns list of policy violations.
|
|
198
|
+
"""
|
|
199
|
+
if not meta.criticality:
|
|
200
|
+
return []
|
|
158
201
|
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
violations = []
|
|
203
|
+
policy = PolicyResolver.resolve(meta.criticality)
|
|
161
204
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
# Stage-based checks
|
|
206
|
+
if meta.stage == "review":
|
|
207
|
+
# In review stage, check coverage requirement
|
|
208
|
+
# Note: Actual coverage check would require external data
|
|
209
|
+
pass
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
211
|
+
if meta.stage == "done" or meta.status == "closed":
|
|
212
|
+
# For high/critical, require review comments
|
|
213
|
+
if policy.human_review in [
|
|
214
|
+
HumanReviewLevel.REQUIRED,
|
|
215
|
+
HumanReviewLevel.REQUIRED_RECORD,
|
|
216
|
+
]:
|
|
217
|
+
# This is a simplified check - full implementation would parse body
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
return violations
|
|
171
221
|
|
|
172
222
|
def enforce_policy(self, meta: IssueMetadata) -> None:
|
|
173
223
|
"""
|
|
174
224
|
Apply consistency rules to IssueMetadata.
|
|
225
|
+
Includes criticality-based defaults.
|
|
175
226
|
"""
|
|
176
227
|
from ..models import current_time
|
|
177
228
|
|
|
@@ -187,3 +238,9 @@ class StateMachine:
|
|
|
187
238
|
elif meta.status == "open":
|
|
188
239
|
if meta.stage is None:
|
|
189
240
|
meta.stage = "draft"
|
|
241
|
+
|
|
242
|
+
# Set default criticality if not set
|
|
243
|
+
if meta.criticality is None:
|
|
244
|
+
from ..criticality import CriticalityTypeMapping
|
|
245
|
+
|
|
246
|
+
meta.criticality = CriticalityTypeMapping.get_default(meta.type.value)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git service for Issue operations.
|
|
3
|
+
|
|
4
|
+
Provides atomic commit functionality for issue file changes,
|
|
5
|
+
ensuring task state transitions are properly tracked in git history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, List, Tuple
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from monoco.core import git
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("monoco.features.issue.git_service")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommitResult:
|
|
20
|
+
"""Result of a commit operation."""
|
|
21
|
+
|
|
22
|
+
success: bool
|
|
23
|
+
commit_hash: Optional[str] = None
|
|
24
|
+
message: Optional[str] = None
|
|
25
|
+
error: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IssueGitService:
|
|
29
|
+
"""
|
|
30
|
+
Service for handling git operations related to Issue files.
|
|
31
|
+
|
|
32
|
+
Responsibilities:
|
|
33
|
+
- Detect if current directory is in a git repository
|
|
34
|
+
- Stage specific issue files
|
|
35
|
+
- Generate atomic commit messages for issue transitions
|
|
36
|
+
- Handle graceful degradation when not in a git repo
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, project_root: Path):
|
|
40
|
+
self.project_root = project_root
|
|
41
|
+
self._is_git_repo: Optional[bool] = None
|
|
42
|
+
|
|
43
|
+
def is_git_repository(self) -> bool:
|
|
44
|
+
"""Check if the project root is inside a git repository."""
|
|
45
|
+
if self._is_git_repo is None:
|
|
46
|
+
self._is_git_repo = git.is_git_repo(self.project_root)
|
|
47
|
+
return self._is_git_repo
|
|
48
|
+
|
|
49
|
+
def commit_issue_change(
|
|
50
|
+
self,
|
|
51
|
+
issue_id: str,
|
|
52
|
+
action: str,
|
|
53
|
+
issue_file_path: Path,
|
|
54
|
+
old_file_path: Optional[Path] = None,
|
|
55
|
+
no_commit: bool = False,
|
|
56
|
+
) -> CommitResult:
|
|
57
|
+
"""
|
|
58
|
+
Atomically commit an issue file change.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
issue_id: The issue ID (e.g., "FEAT-0115")
|
|
62
|
+
action: The action being performed (e.g., "close", "start", "open")
|
|
63
|
+
issue_file_path: Current path to the issue file
|
|
64
|
+
old_file_path: Previous path if the file was moved (e.g., status change)
|
|
65
|
+
no_commit: If True, skip the commit operation
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
CommitResult with success status and commit details
|
|
69
|
+
"""
|
|
70
|
+
if no_commit:
|
|
71
|
+
return CommitResult(success=True, message="Skipped (no-commit flag)")
|
|
72
|
+
|
|
73
|
+
if not self.is_git_repository():
|
|
74
|
+
logger.info("Not in a git repository, skipping auto-commit")
|
|
75
|
+
return CommitResult(success=True, message="Skipped (not a git repo)")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Stage the changes
|
|
79
|
+
self._stage_issue_files(issue_file_path, old_file_path)
|
|
80
|
+
|
|
81
|
+
# Generate and execute commit
|
|
82
|
+
commit_message = self._generate_commit_message(issue_id, action)
|
|
83
|
+
commit_hash = git.git_commit(self.project_root, commit_message)
|
|
84
|
+
|
|
85
|
+
return CommitResult(
|
|
86
|
+
success=True,
|
|
87
|
+
commit_hash=commit_hash,
|
|
88
|
+
message=commit_message,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
error_msg = str(e)
|
|
93
|
+
logger.error(f"Failed to commit issue change: {error_msg}")
|
|
94
|
+
return CommitResult(success=False, error=error_msg)
|
|
95
|
+
|
|
96
|
+
def _stage_issue_files(
|
|
97
|
+
self, current_path: Path, old_path: Optional[Path] = None
|
|
98
|
+
) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Stage issue file changes.
|
|
101
|
+
|
|
102
|
+
If old_path is provided (file was moved), handles the rename properly:
|
|
103
|
+
- Stages deletion of old file
|
|
104
|
+
- Stages addition of new file
|
|
105
|
+
"""
|
|
106
|
+
files_to_stage: List[str] = []
|
|
107
|
+
|
|
108
|
+
# Handle the old file path if file was moved (e.g., open -> closed)
|
|
109
|
+
if old_path and old_path.exists():
|
|
110
|
+
# File was moved, need to stage the deletion
|
|
111
|
+
try:
|
|
112
|
+
rel_old_path = old_path.relative_to(self.project_root)
|
|
113
|
+
files_to_stage.append(str(rel_old_path))
|
|
114
|
+
except ValueError:
|
|
115
|
+
# old_path is not relative to project_root, use absolute
|
|
116
|
+
files_to_stage.append(str(old_path))
|
|
117
|
+
|
|
118
|
+
# Handle the current file path
|
|
119
|
+
if current_path.exists():
|
|
120
|
+
try:
|
|
121
|
+
rel_path = current_path.relative_to(self.project_root)
|
|
122
|
+
files_to_stage.append(str(rel_path))
|
|
123
|
+
except ValueError:
|
|
124
|
+
# current_path is not relative to project_root, use absolute
|
|
125
|
+
files_to_stage.append(str(current_path))
|
|
126
|
+
|
|
127
|
+
if files_to_stage:
|
|
128
|
+
git.git_add(self.project_root, files_to_stage)
|
|
129
|
+
|
|
130
|
+
def _generate_commit_message(self, issue_id: str, action: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Generate a standardized commit message for issue transitions.
|
|
133
|
+
|
|
134
|
+
Format: chore(issue): <action> <issue_id>
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
chore(issue): close FIX-0020
|
|
138
|
+
chore(issue): start FEAT-0115
|
|
139
|
+
chore(issue): open FEAT-0115
|
|
140
|
+
chore(issue): submit FEAT-0115
|
|
141
|
+
"""
|
|
142
|
+
return f"chore(issue): {action} {issue_id}"
|
|
143
|
+
|
|
144
|
+
def get_commit_history(
|
|
145
|
+
self, issue_id: str, max_count: int = 10
|
|
146
|
+
) -> List[Tuple[str, str]]:
|
|
147
|
+
"""
|
|
148
|
+
Get commit history for a specific issue.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of (commit_hash, subject) tuples
|
|
152
|
+
"""
|
|
153
|
+
if not self.is_git_repository():
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
commits = git.search_commits_by_message(self.project_root, issue_id)
|
|
158
|
+
return [(c["hash"], c["subject"]) for c in commits[:max_count]]
|
|
159
|
+
except Exception:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def should_auto_commit(config) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Check if auto-commit should be enabled based on configuration.
|
|
166
|
+
|
|
167
|
+
Checks for:
|
|
168
|
+
1. Explicit disable in config: issue.auto_commit = false
|
|
169
|
+
2. Environment variable: MONOCO_NO_AUTO_COMMIT=1
|
|
170
|
+
"""
|
|
171
|
+
import os
|
|
172
|
+
|
|
173
|
+
# Check environment variable first
|
|
174
|
+
if os.environ.get("MONOCO_NO_AUTO_COMMIT", "0") == "1":
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Check config (if issue config has auto_commit setting)
|
|
178
|
+
try:
|
|
179
|
+
if hasattr(config, "issue") and hasattr(config.issue, "auto_commit"):
|
|
180
|
+
return config.issue.auto_commit
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Default to enabled
|
|
185
|
+
return True
|