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
@@ -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