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.
Files changed (58) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +51 -0
  3. monoco/core/hooks/__init__.py +19 -0
  4. monoco/core/hooks/base.py +104 -0
  5. monoco/core/hooks/builtin/__init__.py +11 -0
  6. monoco/core/hooks/builtin/git_cleanup.py +266 -0
  7. monoco/core/hooks/builtin/logging_hook.py +78 -0
  8. monoco/core/hooks/context.py +131 -0
  9. monoco/core/hooks/registry.py +222 -0
  10. monoco/core/integrations.py +6 -0
  11. monoco/core/registry.py +2 -0
  12. monoco/core/setup.py +1 -1
  13. monoco/core/skills.py +226 -42
  14. monoco/features/{scheduler → agent}/__init__.py +4 -2
  15. monoco/features/{scheduler → agent}/cli.py +134 -80
  16. monoco/features/{scheduler → agent}/config.py +17 -3
  17. monoco/features/agent/defaults.py +55 -0
  18. monoco/features/agent/flow_skills.py +281 -0
  19. monoco/features/{scheduler → agent}/manager.py +39 -2
  20. monoco/features/{scheduler → agent}/models.py +6 -3
  21. monoco/features/{scheduler → agent}/reliability.py +1 -1
  22. monoco/features/agent/resources/skills/flow_engineer/SKILL.md +94 -0
  23. monoco/features/agent/resources/skills/flow_manager/SKILL.md +88 -0
  24. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +114 -0
  25. monoco/features/{scheduler → agent}/session.py +36 -1
  26. monoco/features/{scheduler → agent}/worker.py +2 -2
  27. monoco/features/i18n/resources/skills/i18n_scan_workflow/SKILL.md +105 -0
  28. monoco/features/issue/commands.py +427 -21
  29. monoco/features/issue/core.py +100 -0
  30. monoco/features/issue/criticality.py +553 -0
  31. monoco/features/issue/domain/models.py +28 -2
  32. monoco/features/issue/engine/machine.py +70 -13
  33. monoco/features/issue/git_service.py +185 -0
  34. monoco/features/issue/linter.py +291 -62
  35. monoco/features/issue/models.py +49 -2
  36. monoco/features/issue/resources/en/SKILL.md +48 -0
  37. monoco/features/issue/resources/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  38. monoco/features/issue/resources/zh/SKILL.md +50 -0
  39. monoco/features/issue/validator.py +185 -65
  40. monoco/features/memo/__init__.py +2 -1
  41. monoco/features/memo/adapter.py +32 -0
  42. monoco/features/memo/cli.py +36 -14
  43. monoco/features/memo/core.py +59 -0
  44. monoco/features/memo/resources/skills/note_processing_workflow/SKILL.md +140 -0
  45. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  46. monoco/features/memo/resources/zh/SKILL.md +75 -0
  47. monoco/features/spike/resources/skills/research_workflow/SKILL.md +121 -0
  48. monoco/main.py +2 -3
  49. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/METADATA +1 -1
  50. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/RECORD +55 -37
  51. monoco/features/scheduler/defaults.py +0 -54
  52. monoco/features/skills/__init__.py +0 -0
  53. monoco/features/skills/core.py +0 -102
  54. /monoco/core/{hooks.py → githooks.py} +0 -0
  55. /monoco/features/{scheduler → agent}/engines.py +0 -0
  56. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/WHEEL +0 -0
  57. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/entry_points.txt +0 -0
  58. {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
- def enforce_policy(self, meta: IssueMetadata) -> None:
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
- Apply consistency rules to IssueMetadata.
171
+ Validate transition against criticality-based policies.
172
+ Enforces stricter requirements for high/critical issues.
156
173
  """
157
- from ..models import current_time
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
- if meta.status == "backlog":
160
- meta.stage = "freezed"
202
+ violations = []
203
+ policy = PolicyResolver.resolve(meta.criticality)
161
204
 
162
- elif meta.status == "closed":
163
- if meta.stage != "done":
164
- meta.stage = "done"
165
- if not meta.closed_at:
166
- meta.closed_at = current_time()
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
- elif meta.status == "open":
169
- if meta.stage is None:
170
- meta.stage = "draft"
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