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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Issue Criticality System with Immutable Policy Enforcement.
|
|
3
|
+
|
|
4
|
+
This module implements the criticality system that:
|
|
5
|
+
1. Assigns criticality levels (low, medium, high, critical) to issues
|
|
6
|
+
2. Derives policies based on criticality (agent_review, human_review, coverage, etc.)
|
|
7
|
+
3. Enforces immutable policy compliance (can only escalate, never lower)
|
|
8
|
+
4. Supports escalation workflow with approval process
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import List, Optional, Dict
|
|
13
|
+
from pydantic import BaseModel, Field, model_validator, ConfigDict
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CriticalityLevel(str, Enum):
|
|
19
|
+
"""Criticality levels for issues, ordered from lowest to highest."""
|
|
20
|
+
|
|
21
|
+
LOW = "low"
|
|
22
|
+
MEDIUM = "medium"
|
|
23
|
+
HIGH = "high"
|
|
24
|
+
CRITICAL = "critical"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_string(cls, value: str) -> "CriticalityLevel":
|
|
28
|
+
"""Parse criticality level from string (case-insensitive)."""
|
|
29
|
+
mapping = {
|
|
30
|
+
"low": cls.LOW,
|
|
31
|
+
"medium": cls.MEDIUM,
|
|
32
|
+
"high": cls.HIGH,
|
|
33
|
+
"critical": cls.CRITICAL,
|
|
34
|
+
}
|
|
35
|
+
return mapping.get(value.lower(), cls.MEDIUM)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def numeric_value(self) -> int:
|
|
39
|
+
"""Return numeric value for comparison."""
|
|
40
|
+
return {
|
|
41
|
+
CriticalityLevel.LOW: 1,
|
|
42
|
+
CriticalityLevel.MEDIUM: 2,
|
|
43
|
+
CriticalityLevel.HIGH: 3,
|
|
44
|
+
CriticalityLevel.CRITICAL: 4,
|
|
45
|
+
}[self]
|
|
46
|
+
|
|
47
|
+
def __lt__(self, other: "CriticalityLevel") -> bool:
|
|
48
|
+
return self.numeric_value < other.numeric_value
|
|
49
|
+
|
|
50
|
+
def __le__(self, other: "CriticalityLevel") -> bool:
|
|
51
|
+
return self.numeric_value <= other.numeric_value
|
|
52
|
+
|
|
53
|
+
def __gt__(self, other: "CriticalityLevel") -> bool:
|
|
54
|
+
return self.numeric_value > other.numeric_value
|
|
55
|
+
|
|
56
|
+
def __ge__(self, other: "CriticalityLevel") -> bool:
|
|
57
|
+
return self.numeric_value >= other.numeric_value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentReviewLevel(str, Enum):
|
|
61
|
+
"""Agent review intensity levels."""
|
|
62
|
+
|
|
63
|
+
LIGHTWEIGHT = "lightweight"
|
|
64
|
+
STANDARD = "standard"
|
|
65
|
+
STRICT = "strict"
|
|
66
|
+
STRICT_AUDIT = "strict+audit"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class HumanReviewLevel(str, Enum):
|
|
70
|
+
"""Human review requirements."""
|
|
71
|
+
|
|
72
|
+
OPTIONAL = "optional"
|
|
73
|
+
RECOMMENDED = "recommended"
|
|
74
|
+
REQUIRED = "required"
|
|
75
|
+
REQUIRED_RECORD = "required+record"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RollbackAction(str, Enum):
|
|
79
|
+
"""Rollback behavior on failure."""
|
|
80
|
+
|
|
81
|
+
WARN = "warn"
|
|
82
|
+
ROLLBACK = "rollback"
|
|
83
|
+
BLOCK = "block"
|
|
84
|
+
BLOCK_NOTIFY = "block+notify"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Policy(BaseModel):
|
|
88
|
+
"""
|
|
89
|
+
Derived policy based on criticality level.
|
|
90
|
+
Defines quality standards and review requirements.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
agent_review: AgentReviewLevel = Field(
|
|
94
|
+
default=AgentReviewLevel.STANDARD, description="Agent code review intensity"
|
|
95
|
+
)
|
|
96
|
+
human_review: HumanReviewLevel = Field(
|
|
97
|
+
default=HumanReviewLevel.RECOMMENDED, description="Human review requirement"
|
|
98
|
+
)
|
|
99
|
+
min_coverage: int = Field(
|
|
100
|
+
default=70, ge=0, le=100, description="Minimum test coverage percentage"
|
|
101
|
+
)
|
|
102
|
+
rollback_on_failure: RollbackAction = Field(
|
|
103
|
+
default=RollbackAction.ROLLBACK, description="Action on failure"
|
|
104
|
+
)
|
|
105
|
+
require_security_scan: bool = Field(
|
|
106
|
+
default=False, description="Require security vulnerability scan"
|
|
107
|
+
)
|
|
108
|
+
require_performance_check: bool = Field(
|
|
109
|
+
default=False, description="Require performance regression check"
|
|
110
|
+
)
|
|
111
|
+
max_reviewers: int = Field(
|
|
112
|
+
default=1, ge=1, le=5, description="Number of reviewers required"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(frozen=True) # Policies are immutable once derived
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class EscalationStatus(str, Enum):
|
|
119
|
+
"""Status of an escalation request."""
|
|
120
|
+
|
|
121
|
+
PENDING = "pending"
|
|
122
|
+
APPROVED = "approved"
|
|
123
|
+
REJECTED = "rejected"
|
|
124
|
+
CANCELLED = "cancelled"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EscalationRequest(BaseModel):
|
|
128
|
+
"""
|
|
129
|
+
Request to escalate issue criticality.
|
|
130
|
+
Requires approval from authorized personnel.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
id: str = Field(description="Unique escalation request ID")
|
|
134
|
+
issue_id: str = Field(description="Target issue ID")
|
|
135
|
+
from_level: CriticalityLevel = Field(description="Current criticality level")
|
|
136
|
+
to_level: CriticalityLevel = Field(description="Requested criticality level")
|
|
137
|
+
reason: str = Field(description="Business/technical justification")
|
|
138
|
+
requested_by: str = Field(description="User who requested escalation")
|
|
139
|
+
requested_at: datetime = Field(default_factory=datetime.now)
|
|
140
|
+
status: EscalationStatus = Field(default=EscalationStatus.PENDING)
|
|
141
|
+
approved_by: Optional[str] = Field(default=None)
|
|
142
|
+
approved_at: Optional[datetime] = Field(default=None)
|
|
143
|
+
rejection_reason: Optional[str] = Field(default=None)
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="after")
|
|
146
|
+
def validate_escalation_direction(self) -> "EscalationRequest":
|
|
147
|
+
"""Ensure escalation is upward only."""
|
|
148
|
+
if self.to_level <= self.from_level:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Escalation must be to a higher level: "
|
|
151
|
+
f"{self.from_level.value} -> {self.to_level.value}"
|
|
152
|
+
)
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class PolicyResolver:
|
|
157
|
+
"""
|
|
158
|
+
Resolves policies based on criticality level.
|
|
159
|
+
Centralized policy definition to ensure consistency.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# Criticality to Policy mapping (source of truth)
|
|
163
|
+
_POLICY_MAP: Dict[CriticalityLevel, Policy] = {
|
|
164
|
+
CriticalityLevel.LOW: Policy(
|
|
165
|
+
agent_review=AgentReviewLevel.LIGHTWEIGHT,
|
|
166
|
+
human_review=HumanReviewLevel.OPTIONAL,
|
|
167
|
+
min_coverage=0,
|
|
168
|
+
rollback_on_failure=RollbackAction.WARN,
|
|
169
|
+
require_security_scan=False,
|
|
170
|
+
require_performance_check=False,
|
|
171
|
+
max_reviewers=1,
|
|
172
|
+
),
|
|
173
|
+
CriticalityLevel.MEDIUM: Policy(
|
|
174
|
+
agent_review=AgentReviewLevel.STANDARD,
|
|
175
|
+
human_review=HumanReviewLevel.RECOMMENDED,
|
|
176
|
+
min_coverage=70,
|
|
177
|
+
rollback_on_failure=RollbackAction.ROLLBACK,
|
|
178
|
+
require_security_scan=False,
|
|
179
|
+
require_performance_check=False,
|
|
180
|
+
max_reviewers=1,
|
|
181
|
+
),
|
|
182
|
+
CriticalityLevel.HIGH: Policy(
|
|
183
|
+
agent_review=AgentReviewLevel.STRICT,
|
|
184
|
+
human_review=HumanReviewLevel.REQUIRED,
|
|
185
|
+
min_coverage=85,
|
|
186
|
+
rollback_on_failure=RollbackAction.BLOCK,
|
|
187
|
+
require_security_scan=True,
|
|
188
|
+
require_performance_check=False,
|
|
189
|
+
max_reviewers=2,
|
|
190
|
+
),
|
|
191
|
+
CriticalityLevel.CRITICAL: Policy(
|
|
192
|
+
agent_review=AgentReviewLevel.STRICT_AUDIT,
|
|
193
|
+
human_review=HumanReviewLevel.REQUIRED_RECORD,
|
|
194
|
+
min_coverage=90,
|
|
195
|
+
rollback_on_failure=RollbackAction.BLOCK_NOTIFY,
|
|
196
|
+
require_security_scan=True,
|
|
197
|
+
require_performance_check=True,
|
|
198
|
+
max_reviewers=3,
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def resolve(cls, criticality: CriticalityLevel) -> Policy:
|
|
204
|
+
"""Get the policy for a given criticality level."""
|
|
205
|
+
return cls._POLICY_MAP.get(
|
|
206
|
+
criticality, cls._POLICY_MAP[CriticalityLevel.MEDIUM]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def get_all_policies(cls) -> Dict[CriticalityLevel, Policy]:
|
|
211
|
+
"""Get all defined policies (for reporting/documentation)."""
|
|
212
|
+
return cls._POLICY_MAP.copy()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class CriticalityInheritanceService:
|
|
216
|
+
"""
|
|
217
|
+
Handles criticality inheritance rules for child issues.
|
|
218
|
+
Child issues must inherit at least the parent's criticality.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def resolve_child_criticality(
|
|
223
|
+
parent_criticality: Optional[CriticalityLevel],
|
|
224
|
+
proposed_criticality: CriticalityLevel,
|
|
225
|
+
) -> CriticalityLevel:
|
|
226
|
+
"""
|
|
227
|
+
Resolve the effective criticality for a child issue.
|
|
228
|
+
Child must be at least as critical as parent.
|
|
229
|
+
"""
|
|
230
|
+
if parent_criticality is None:
|
|
231
|
+
return proposed_criticality
|
|
232
|
+
|
|
233
|
+
# Child must inherit parent's minimum criticality
|
|
234
|
+
if proposed_criticality < parent_criticality:
|
|
235
|
+
return parent_criticality
|
|
236
|
+
return proposed_criticality
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def can_lower_child_criticality(
|
|
240
|
+
child_criticality: CriticalityLevel, parent_criticality: CriticalityLevel
|
|
241
|
+
) -> bool:
|
|
242
|
+
"""
|
|
243
|
+
Check if a child's criticality can be lowered.
|
|
244
|
+
Can only lower if it won't go below parent's level.
|
|
245
|
+
"""
|
|
246
|
+
# This is a theoretical check - actual lowering is prohibited
|
|
247
|
+
# This method is for validation purposes
|
|
248
|
+
return child_criticality > parent_criticality
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AutoEscalationRule(BaseModel):
|
|
252
|
+
"""Rule for automatically escalating criticality based on conditions."""
|
|
253
|
+
|
|
254
|
+
name: str
|
|
255
|
+
description: str
|
|
256
|
+
path_patterns: List[str] = Field(default_factory=list)
|
|
257
|
+
tag_patterns: List[str] = Field(default_factory=list)
|
|
258
|
+
type_patterns: List[str] = Field(default_factory=list)
|
|
259
|
+
target_level: CriticalityLevel
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class AutoEscalationDetector:
|
|
263
|
+
"""
|
|
264
|
+
Detects when an issue should be auto-escalated based on:
|
|
265
|
+
- File path patterns (e.g., payment/** -> critical)
|
|
266
|
+
- Tags (e.g., "security", "payment")
|
|
267
|
+
- Issue type mappings
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
# Default auto-escalation rules
|
|
271
|
+
DEFAULT_RULES: List[AutoEscalationRule] = [
|
|
272
|
+
AutoEscalationRule(
|
|
273
|
+
name="payment_critical",
|
|
274
|
+
description="Payment-related code requires critical handling",
|
|
275
|
+
path_patterns=["**/payment/**", "**/billing/**", "**/finance/**"],
|
|
276
|
+
tag_patterns=["payment", "billing", "finance"],
|
|
277
|
+
target_level=CriticalityLevel.CRITICAL,
|
|
278
|
+
),
|
|
279
|
+
AutoEscalationRule(
|
|
280
|
+
name="security_high",
|
|
281
|
+
description="Security-related changes require high scrutiny",
|
|
282
|
+
path_patterns=["**/auth/**", "**/security/**", "**/crypto/**"],
|
|
283
|
+
tag_patterns=["security", "auth", "authentication", "authorization"],
|
|
284
|
+
target_level=CriticalityLevel.HIGH,
|
|
285
|
+
),
|
|
286
|
+
AutoEscalationRule(
|
|
287
|
+
name="database_high",
|
|
288
|
+
description="Database schema changes require high scrutiny",
|
|
289
|
+
path_patterns=["**/migrations/**", "**/schema/**"],
|
|
290
|
+
tag_patterns=["database", "migration", "schema"],
|
|
291
|
+
target_level=CriticalityLevel.HIGH,
|
|
292
|
+
),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
def __init__(self, custom_rules: Optional[List[AutoEscalationRule]] = None):
|
|
296
|
+
self.rules = custom_rules or self.DEFAULT_RULES
|
|
297
|
+
|
|
298
|
+
def detect_escalation(
|
|
299
|
+
self,
|
|
300
|
+
current_level: CriticalityLevel,
|
|
301
|
+
file_paths: List[str],
|
|
302
|
+
tags: List[str],
|
|
303
|
+
issue_type: Optional[str] = None,
|
|
304
|
+
) -> Optional[CriticalityLevel]:
|
|
305
|
+
"""
|
|
306
|
+
Detect if issue should be escalated based on rules.
|
|
307
|
+
Returns the highest applicable level or None if no escalation needed.
|
|
308
|
+
"""
|
|
309
|
+
max_level = current_level
|
|
310
|
+
should_escalate = False
|
|
311
|
+
|
|
312
|
+
for rule in self.rules:
|
|
313
|
+
if self._matches_rule(rule, file_paths, tags, issue_type):
|
|
314
|
+
if rule.target_level > max_level:
|
|
315
|
+
max_level = rule.target_level
|
|
316
|
+
should_escalate = True
|
|
317
|
+
|
|
318
|
+
return max_level if should_escalate else None
|
|
319
|
+
|
|
320
|
+
def _matches_rule(
|
|
321
|
+
self,
|
|
322
|
+
rule: AutoEscalationRule,
|
|
323
|
+
file_paths: List[str],
|
|
324
|
+
tags: List[str],
|
|
325
|
+
issue_type: Optional[str],
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""Check if an issue matches an escalation rule."""
|
|
328
|
+
import fnmatch
|
|
329
|
+
|
|
330
|
+
# Check path patterns
|
|
331
|
+
for pattern in rule.path_patterns:
|
|
332
|
+
for path in file_paths:
|
|
333
|
+
if fnmatch.fnmatch(path, pattern) or fnmatch.fnmatch(
|
|
334
|
+
path, f"*/{pattern}"
|
|
335
|
+
):
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
# Check tag patterns
|
|
339
|
+
for pattern in rule.tag_patterns:
|
|
340
|
+
for tag in tags:
|
|
341
|
+
if pattern.lower() in tag.lower():
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
# Check type patterns
|
|
345
|
+
if issue_type and rule.type_patterns:
|
|
346
|
+
if issue_type.lower() in [t.lower() for t in rule.type_patterns]:
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class EscalationApprovalWorkflow:
|
|
353
|
+
"""
|
|
354
|
+
Manages the escalation approval workflow.
|
|
355
|
+
Tracks pending requests and handles approval/rejection.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(self, storage_path: Optional[Path] = None):
|
|
359
|
+
self.storage_path = storage_path
|
|
360
|
+
self._requests: Dict[str, EscalationRequest] = {}
|
|
361
|
+
self._load_requests()
|
|
362
|
+
|
|
363
|
+
def _load_requests(self) -> None:
|
|
364
|
+
"""Load persisted escalation requests."""
|
|
365
|
+
if self.storage_path and self.storage_path.exists():
|
|
366
|
+
import yaml
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
data = yaml.safe_load(self.storage_path.read_text()) or {}
|
|
370
|
+
for req_data in data.get("requests", []):
|
|
371
|
+
req = EscalationRequest(**req_data)
|
|
372
|
+
self._requests[req.id] = req
|
|
373
|
+
except Exception:
|
|
374
|
+
pass # Start fresh if corrupted
|
|
375
|
+
|
|
376
|
+
def _save_requests(self) -> None:
|
|
377
|
+
"""Persist escalation requests."""
|
|
378
|
+
if self.storage_path:
|
|
379
|
+
import yaml
|
|
380
|
+
|
|
381
|
+
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
data = {
|
|
383
|
+
"requests": [
|
|
384
|
+
req.model_dump(mode="json") for req in self._requests.values()
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
self.storage_path.write_text(yaml.dump(data, sort_keys=False))
|
|
388
|
+
|
|
389
|
+
def create_request(
|
|
390
|
+
self,
|
|
391
|
+
issue_id: str,
|
|
392
|
+
from_level: CriticalityLevel,
|
|
393
|
+
to_level: CriticalityLevel,
|
|
394
|
+
reason: str,
|
|
395
|
+
requested_by: str,
|
|
396
|
+
) -> EscalationRequest:
|
|
397
|
+
"""Create a new escalation request."""
|
|
398
|
+
import secrets
|
|
399
|
+
|
|
400
|
+
request_id = f"ESC-{secrets.token_hex(4).upper()}"
|
|
401
|
+
request = EscalationRequest(
|
|
402
|
+
id=request_id,
|
|
403
|
+
issue_id=issue_id,
|
|
404
|
+
from_level=from_level,
|
|
405
|
+
to_level=to_level,
|
|
406
|
+
reason=reason,
|
|
407
|
+
requested_by=requested_by,
|
|
408
|
+
)
|
|
409
|
+
self._requests[request_id] = request
|
|
410
|
+
self._save_requests()
|
|
411
|
+
return request
|
|
412
|
+
|
|
413
|
+
def approve(self, request_id: str, approved_by: str) -> EscalationRequest:
|
|
414
|
+
"""Approve an escalation request."""
|
|
415
|
+
if request_id not in self._requests:
|
|
416
|
+
raise ValueError(f"Escalation request {request_id} not found")
|
|
417
|
+
|
|
418
|
+
request = self._requests[request_id]
|
|
419
|
+
if request.status != EscalationStatus.PENDING:
|
|
420
|
+
raise ValueError(f"Request is already {request.status.value}")
|
|
421
|
+
|
|
422
|
+
request.status = EscalationStatus.APPROVED
|
|
423
|
+
request.approved_by = approved_by
|
|
424
|
+
request.approved_at = datetime.now()
|
|
425
|
+
self._save_requests()
|
|
426
|
+
return request
|
|
427
|
+
|
|
428
|
+
def reject(
|
|
429
|
+
self, request_id: str, rejected_by: str, reason: str
|
|
430
|
+
) -> EscalationRequest:
|
|
431
|
+
"""Reject an escalation request."""
|
|
432
|
+
if request_id not in self._requests:
|
|
433
|
+
raise ValueError(f"Escalation request {request_id} not found")
|
|
434
|
+
|
|
435
|
+
request = self._requests[request_id]
|
|
436
|
+
if request.status != EscalationStatus.PENDING:
|
|
437
|
+
raise ValueError(f"Request is already {request.status.value}")
|
|
438
|
+
|
|
439
|
+
request.status = EscalationStatus.REJECTED
|
|
440
|
+
request.approved_by = rejected_by # Using same field for rejector
|
|
441
|
+
request.approved_at = datetime.now()
|
|
442
|
+
request.rejection_reason = reason
|
|
443
|
+
self._save_requests()
|
|
444
|
+
return request
|
|
445
|
+
|
|
446
|
+
def get_pending_for_issue(self, issue_id: str) -> List[EscalationRequest]:
|
|
447
|
+
"""Get all pending escalation requests for an issue."""
|
|
448
|
+
return [
|
|
449
|
+
req
|
|
450
|
+
for req in self._requests.values()
|
|
451
|
+
if req.issue_id == issue_id and req.status == EscalationStatus.PENDING
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
def get_request(self, request_id: str) -> Optional[EscalationRequest]:
|
|
455
|
+
"""Get a specific escalation request."""
|
|
456
|
+
return self._requests.get(request_id)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class CriticalityTypeMapping:
|
|
460
|
+
"""
|
|
461
|
+
Default criticality mappings based on issue type.
|
|
462
|
+
These are starting points, can be overridden at creation.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
DEFAULT_MAPPINGS: Dict[str, CriticalityLevel] = {
|
|
466
|
+
"epic": CriticalityLevel.HIGH, # Epics are strategic
|
|
467
|
+
"feature": CriticalityLevel.MEDIUM, # Features are value-delivering
|
|
468
|
+
"chore": CriticalityLevel.LOW, # Chores are maintenance
|
|
469
|
+
"fix": CriticalityLevel.HIGH, # Fixes address problems
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@classmethod
|
|
473
|
+
def get_default(cls, issue_type: str) -> CriticalityLevel:
|
|
474
|
+
"""Get default criticality for an issue type."""
|
|
475
|
+
return cls.DEFAULT_MAPPINGS.get(issue_type.lower(), CriticalityLevel.MEDIUM)
|
|
476
|
+
|
|
477
|
+
@classmethod
|
|
478
|
+
def get_all_mappings(cls) -> Dict[str, CriticalityLevel]:
|
|
479
|
+
"""Get all default type mappings."""
|
|
480
|
+
return cls.DEFAULT_MAPPINGS.copy()
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class CriticalityValidator:
|
|
484
|
+
"""
|
|
485
|
+
Validates criticality-related constraints and permissions.
|
|
486
|
+
Enforces the immutable policy: can only escalate, never lower.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def can_modify_criticality(
|
|
491
|
+
current_level: CriticalityLevel,
|
|
492
|
+
proposed_level: CriticalityLevel,
|
|
493
|
+
is_escalation_approved: bool = False,
|
|
494
|
+
) -> tuple[bool, Optional[str]]:
|
|
495
|
+
"""
|
|
496
|
+
Check if criticality modification is allowed.
|
|
497
|
+
Returns (is_allowed, reason_if_denied).
|
|
498
|
+
"""
|
|
499
|
+
# Direct lowering is never allowed
|
|
500
|
+
if proposed_level < current_level:
|
|
501
|
+
return (
|
|
502
|
+
False,
|
|
503
|
+
"Criticality cannot be lowered. Use escalation workflow to increase.",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Same level is always allowed (no-op)
|
|
507
|
+
if proposed_level == current_level:
|
|
508
|
+
return True, None
|
|
509
|
+
|
|
510
|
+
# Escalation requires approval
|
|
511
|
+
if proposed_level > current_level and not is_escalation_approved:
|
|
512
|
+
return (
|
|
513
|
+
False,
|
|
514
|
+
f"Escalation from {current_level.value} to {proposed_level.value} requires approval.",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return True, None
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def validate_policy_compliance(
|
|
521
|
+
criticality: CriticalityLevel,
|
|
522
|
+
actual_coverage: Optional[float],
|
|
523
|
+
has_agent_review: bool,
|
|
524
|
+
has_human_review: bool,
|
|
525
|
+
) -> List[str]:
|
|
526
|
+
"""
|
|
527
|
+
Validate that an issue complies with its criticality policy.
|
|
528
|
+
Returns list of violations.
|
|
529
|
+
"""
|
|
530
|
+
policy = PolicyResolver.resolve(criticality)
|
|
531
|
+
violations = []
|
|
532
|
+
|
|
533
|
+
# Coverage check
|
|
534
|
+
if actual_coverage is not None and actual_coverage < policy.min_coverage:
|
|
535
|
+
violations.append(
|
|
536
|
+
f"Test coverage {actual_coverage:.1f}% below minimum {policy.min_coverage}%"
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Agent review check
|
|
540
|
+
if not has_agent_review:
|
|
541
|
+
violations.append(f"Agent review ({policy.agent_review.value}) required")
|
|
542
|
+
|
|
543
|
+
# Human review check
|
|
544
|
+
if policy.human_review in [
|
|
545
|
+
HumanReviewLevel.REQUIRED,
|
|
546
|
+
HumanReviewLevel.REQUIRED_RECORD,
|
|
547
|
+
]:
|
|
548
|
+
if not has_human_review:
|
|
549
|
+
violations.append(
|
|
550
|
+
f"Human review ({policy.human_review.value}) required"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return violations
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import List, Optional, Any, Dict
|
|
2
|
-
from pydantic import BaseModel, Field, model_validator
|
|
2
|
+
from pydantic import BaseModel, Field, model_validator, field_validator
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from ..models import (
|
|
5
5
|
IssueType,
|
|
@@ -9,6 +9,7 @@ from ..models import (
|
|
|
9
9
|
IssueIsolation,
|
|
10
10
|
current_time,
|
|
11
11
|
)
|
|
12
|
+
from ..criticality import CriticalityLevel
|
|
12
13
|
from monoco.core.lsp import Range
|
|
13
14
|
|
|
14
15
|
|
|
@@ -100,8 +101,21 @@ class IssueFrontmatter(BaseModel):
|
|
|
100
101
|
Contains metadata and validation logic.
|
|
101
102
|
"""
|
|
102
103
|
|
|
103
|
-
id: str
|
|
104
|
+
id: str = Field()
|
|
104
105
|
uid: Optional[str] = None
|
|
106
|
+
|
|
107
|
+
@field_validator("id")
|
|
108
|
+
@classmethod
|
|
109
|
+
def validate_id_format(cls, v: str) -> str:
|
|
110
|
+
import re
|
|
111
|
+
|
|
112
|
+
if not re.match(r"^[A-Z]+-\d{4}$", v):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Invalid Issue ID format: '{v}'. Expected 'TYPE-XXXX' (e.g., FEAT-1234). "
|
|
115
|
+
"For sub-features or sub-tasks, please use the 'parent' field instead of adding suffixes to the ID."
|
|
116
|
+
)
|
|
117
|
+
return v
|
|
118
|
+
|
|
105
119
|
type: IssueType
|
|
106
120
|
status: IssueStatus = IssueStatus.OPEN
|
|
107
121
|
stage: Optional[IssueStage] = None
|
|
@@ -118,6 +132,12 @@ class IssueFrontmatter(BaseModel):
|
|
|
118
132
|
solution: Optional[IssueSolution] = None
|
|
119
133
|
isolation: Optional[IssueIsolation] = None
|
|
120
134
|
|
|
135
|
+
# Criticality System (FEAT-0114)
|
|
136
|
+
criticality: Optional[CriticalityLevel] = Field(
|
|
137
|
+
default=None,
|
|
138
|
+
description="Issue criticality level (low, medium, high, critical)",
|
|
139
|
+
)
|
|
140
|
+
|
|
121
141
|
model_config = {"extra": "allow"}
|
|
122
142
|
|
|
123
143
|
@model_validator(mode="before")
|
|
@@ -129,6 +149,8 @@ class IssueFrontmatter(BaseModel):
|
|
|
129
149
|
v["type"] = v["type"].lower()
|
|
130
150
|
if "status" in v and isinstance(v["status"], str):
|
|
131
151
|
v["status"] = v["status"].lower()
|
|
152
|
+
if "criticality" in v and isinstance(v["criticality"], str):
|
|
153
|
+
v["criticality"] = v["criticality"].lower()
|
|
132
154
|
return v
|
|
133
155
|
|
|
134
156
|
|
|
@@ -198,6 +220,10 @@ class Issue(BaseModel):
|
|
|
198
220
|
if data.get("isolation"):
|
|
199
221
|
ordered_dump["isolation"] = data["isolation"]
|
|
200
222
|
|
|
223
|
+
# 7. Criticality (Optional but recommended)
|
|
224
|
+
if data.get("criticality"):
|
|
225
|
+
ordered_dump["criticality"] = data["criticality"]
|
|
226
|
+
|
|
201
227
|
fm_str = yaml.dump(ordered_dump, sort_keys=False, allow_unicode=True).strip()
|
|
202
228
|
body_str = self.body.to_markdown()
|
|
203
229
|
|