monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.10__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 (113) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +58 -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/injection.py +63 -29
  11. monoco/core/integrations.py +8 -2
  12. monoco/core/output.py +5 -5
  13. monoco/core/registry.py +9 -1
  14. monoco/core/resource/__init__.py +5 -0
  15. monoco/core/resource/finder.py +98 -0
  16. monoco/core/resource/manager.py +91 -0
  17. monoco/core/resource/models.py +35 -0
  18. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  19. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  20. monoco/core/setup.py +1 -1
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +538 -254
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/features/{scheduler → agent}/__init__.py +5 -3
  26. monoco/features/agent/adapter.py +31 -0
  27. monoco/features/agent/apoptosis.py +44 -0
  28. monoco/features/agent/cli.py +296 -0
  29. monoco/features/agent/config.py +96 -0
  30. monoco/features/agent/defaults.py +12 -0
  31. monoco/features/{scheduler → agent}/engines.py +32 -6
  32. monoco/features/agent/flow_skills.py +281 -0
  33. monoco/features/agent/manager.py +91 -0
  34. monoco/features/{scheduler → agent}/models.py +6 -3
  35. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  36. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  37. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  38. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  39. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  40. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  41. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  42. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  43. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  44. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  45. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  46. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  47. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  48. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  49. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  50. monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
  51. monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
  52. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  53. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  54. monoco/features/{scheduler → agent}/session.py +36 -1
  55. monoco/features/{scheduler → agent}/worker.py +40 -4
  56. monoco/features/glossary/adapter.py +31 -0
  57. monoco/features/glossary/config.py +5 -0
  58. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  59. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  60. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  61. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  62. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  63. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  64. monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
  65. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  66. monoco/features/issue/commands.py +427 -21
  67. monoco/features/issue/core.py +140 -1
  68. monoco/features/issue/criticality.py +553 -0
  69. monoco/features/issue/domain/models.py +28 -2
  70. monoco/features/issue/engine/machine.py +75 -15
  71. monoco/features/issue/git_service.py +185 -0
  72. monoco/features/issue/linter.py +291 -62
  73. monoco/features/issue/models.py +50 -2
  74. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  75. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  76. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  77. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  78. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
  79. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  80. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  81. monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  82. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  83. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
  84. monoco/features/issue/validator.py +185 -65
  85. monoco/features/memo/__init__.py +2 -1
  86. monoco/features/memo/adapter.py +32 -0
  87. monoco/features/memo/cli.py +36 -14
  88. monoco/features/memo/core.py +59 -0
  89. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  90. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  91. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  92. monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
  93. monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
  94. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  95. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  96. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  97. monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
  98. monoco/main.py +2 -3
  99. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  100. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  101. monoco/features/scheduler/cli.py +0 -285
  102. monoco/features/scheduler/config.py +0 -68
  103. monoco/features/scheduler/defaults.py +0 -54
  104. monoco/features/scheduler/manager.py +0 -49
  105. monoco/features/scheduler/reliability.py +0 -106
  106. monoco/features/skills/core.py +0 -102
  107. monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
  108. monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
  109. /monoco/core/{hooks.py → githooks.py} +0 -0
  110. /monoco/features/{skills → glossary}/__init__.py +0 -0
  111. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  112. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  113. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: monoco-i18n
3
3
  description: 文档国际化质量控制。确保多语言文档保持同步。
4
+ type: standard
5
+ version: 1.0.0
4
6
  ---
5
7
 
6
8
  # 文档国际化
@@ -1,6 +1,7 @@
1
1
  import typer
2
2
  from pathlib import Path
3
3
  from typing import Optional, List
4
+ from datetime import datetime
4
5
  from rich.console import Console
5
6
  from rich.tree import Tree
6
7
  from rich.panel import Panel
@@ -10,7 +11,9 @@ import typer
10
11
  from monoco.core.config import get_config
11
12
  from monoco.core.output import OutputManager, AgentOutput
12
13
  from .models import IssueType, IssueStatus, IssueMetadata
14
+ from .criticality import CriticalityLevel
13
15
  from . import core
16
+ from monoco.core import git
14
17
 
15
18
  app = typer.Typer(help="Agent-Native Issue Management.")
16
19
  backlog_app = typer.Typer(help="Manage backlog operations.")
@@ -40,6 +43,7 @@ def create(
40
43
  related: List[str] = typer.Option(
41
44
  [], "--related", "-r", help="Related Issue ID(s)"
42
45
  ),
46
+ force: bool = typer.Option(False, "--force", help="Bypass branch context checks"),
43
47
  subdir: Optional[str] = typer.Option(
44
48
  None,
45
49
  "--subdir",
@@ -49,6 +53,12 @@ def create(
49
53
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
50
54
  tags: List[str] = typer.Option([], "--tag", help="Tags"),
51
55
  domains: List[str] = typer.Option([], "--domain", help="Domains"),
56
+ criticality: Optional[str] = typer.Option(
57
+ None,
58
+ "--criticality",
59
+ "-c",
60
+ help="Criticality level (low, medium, high, critical). Auto-derived from type if not specified.",
61
+ ),
52
62
  root: Optional[str] = typer.Option(
53
63
  None, "--root", help="Override issues root directory"
54
64
  ),
@@ -57,6 +67,13 @@ def create(
57
67
  """Create a new issue."""
58
68
  config = get_config()
59
69
  issues_root = _resolve_issues_root(config, root)
70
+ project_root = _resolve_project_root(config)
71
+
72
+ # Context Check
73
+ _validate_branch_context(
74
+ project_root, allowed=["TRUNK"], force=force, command_name="create"
75
+ )
76
+
60
77
  status = "backlog" if is_backlog else "open"
61
78
 
62
79
  # Sanitize inputs (strip #)
@@ -72,6 +89,18 @@ def create(
72
89
  OutputManager.error(f"Parent issue {parent} not found.")
73
90
  raise typer.Exit(code=1)
74
91
 
92
+ # Parse criticality if provided
93
+ criticality_level = None
94
+ if criticality:
95
+ try:
96
+ criticality_level = CriticalityLevel(criticality.lower())
97
+ except ValueError:
98
+ valid_levels = [e.value for e in CriticalityLevel]
99
+ OutputManager.error(
100
+ f"Invalid criticality: '{criticality}'. Valid: {', '.join(valid_levels)}"
101
+ )
102
+ raise typer.Exit(code=1)
103
+
75
104
  try:
76
105
  issue, path = core.create_issue_file(
77
106
  issues_root,
@@ -86,6 +115,7 @@ def create(
86
115
  subdir=subdir,
87
116
  sprint=sprint,
88
117
  tags=tags,
118
+ criticality=criticality_level,
89
119
  )
90
120
 
91
121
  try:
@@ -104,20 +134,19 @@ def create(
104
134
  console.print(f"Path: {rel_path}")
105
135
 
106
136
  # Prompt for Language
107
- target_langs = config.i18n.target_langs
108
- primary_lang = target_langs[0] if target_langs else "en"
137
+ source_lang = config.i18n.source_lang or "en"
109
138
 
110
- # Simple mapping for display
111
- lang_display = {
112
- "zh": "Chinese (Simplified)",
113
- "en": "English",
114
- "ja": "Japanese",
115
- }.get(primary_lang, primary_lang)
139
+ hint_msgs = {
140
+ "zh": "请使用中文填写 Issue 内容。",
141
+ "en": "Please fill the ticket content in English.",
142
+ }
116
143
 
117
- console.print(
118
- f"\n[bold yellow]Agent Hint:[/bold yellow] Please fill the ticket content in [bold cyan]{lang_display}[/bold cyan]."
144
+ hint_msg = hint_msgs.get(
145
+ source_lang, f"Please fill the ticket content in {source_lang.upper()}."
119
146
  )
120
147
 
148
+ console.print(f"\n[bold yellow]Agent Hint:[/bold yellow] {hint_msg}")
149
+
121
150
  except ValueError as e:
122
151
  OutputManager.error(str(e))
123
152
  raise typer.Exit(code=1)
@@ -179,14 +208,25 @@ def move_open(
179
208
  root: Optional[str] = typer.Option(
180
209
  None, "--root", help="Override issues root directory"
181
210
  ),
211
+ no_commit: bool = typer.Option(
212
+ False, "--no-commit", help="Skip auto-commit of issue file"
213
+ ),
182
214
  json: AgentOutput = False,
183
215
  ):
184
216
  """Move issue to open status and set stage to Draft."""
185
217
  config = get_config()
186
218
  issues_root = _resolve_issues_root(config, root)
219
+ project_root = _resolve_project_root(config)
187
220
  try:
188
221
  # Pull operation: Force stage to TODO
189
- issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
222
+ issue = core.update_issue(
223
+ issues_root,
224
+ issue_id,
225
+ status="open",
226
+ stage="draft",
227
+ no_commit=no_commit,
228
+ project_root=project_root,
229
+ )
190
230
  OutputManager.print({"issue": issue, "status": "opened"})
191
231
  except Exception as e:
192
232
  OutputManager.error(str(e))
@@ -197,10 +237,15 @@ def move_open(
197
237
  def start(
198
238
  issue_id: str = typer.Argument(..., help="Issue ID to start"),
199
239
  branch: bool = typer.Option(
200
- False,
201
- "--branch",
240
+ True,
241
+ "--branch/--no-branch",
202
242
  "-b",
203
- help="[Recommended] Start in a new git branch (feat/<id>-<slug>)",
243
+ help="[Default] Start in a new git branch (feat/<id>-<slug>). Use --no-branch to disable.",
244
+ ),
245
+ direct: bool = typer.Option(
246
+ False,
247
+ "--direct",
248
+ help="Privileged: Work directly on current branch (equivalent to --no-branch).",
204
249
  ),
205
250
  worktree: bool = typer.Option(
206
251
  False,
@@ -211,24 +256,48 @@ def start(
211
256
  root: Optional[str] = typer.Option(
212
257
  None, "--root", help="Override issues root directory"
213
258
  ),
259
+ force: bool = typer.Option(False, "--force", help="Bypass branch context checks"),
260
+ no_commit: bool = typer.Option(
261
+ False, "--no-commit", help="Skip auto-commit of issue file"
262
+ ),
214
263
  json: AgentOutput = False,
215
264
  ):
216
265
  """
217
266
  Start working on an issue (Stage -> Doing).
218
267
 
219
- AGENTS: You SHOULD almost always use --branch to isolate your changes.
268
+ Default behavior is to create a feature branch.
269
+ Use --direct or --no-branch to work on current branch.
220
270
  """
221
271
  config = get_config()
222
272
  issues_root = _resolve_issues_root(config, root)
223
273
  project_root = _resolve_project_root(config)
224
274
 
275
+ # Handle direct flag override
276
+ if direct:
277
+ branch = False
278
+
279
+ # Context Check
280
+ # If creating a new branch (default), we MUST be on trunk to avoid nesting.
281
+ # If direct/no-branch, we don't care.
282
+ if branch:
283
+ _validate_branch_context(
284
+ project_root, allowed=["TRUNK"], force=force, command_name="start"
285
+ )
286
+
225
287
  if branch and worktree:
226
288
  OutputManager.error("Cannot specify both --branch and --worktree.")
227
289
  raise typer.Exit(code=1)
228
290
 
229
291
  try:
230
292
  # Implicitly ensure status is Open
231
- issue = core.update_issue(issues_root, issue_id, status="open", stage="doing")
293
+ issue = core.update_issue(
294
+ issues_root,
295
+ issue_id,
296
+ status="open",
297
+ stage="doing",
298
+ no_commit=no_commit,
299
+ project_root=project_root,
300
+ )
232
301
 
233
302
  isolation_info = None
234
303
 
@@ -256,6 +325,10 @@ def start(
256
325
  OutputManager.error(f"Failed to create worktree: {e}")
257
326
  raise typer.Exit(code=1)
258
327
 
328
+ if not branch and not worktree:
329
+ # Direct mode message
330
+ isolation_info = {"type": "direct", "ref": "current"}
331
+
259
332
  OutputManager.print(
260
333
  {"issue": issue, "status": "started", "isolation": isolation_info}
261
334
  )
@@ -270,15 +343,32 @@ def submit(
270
343
  root: Optional[str] = typer.Option(
271
344
  None, "--root", help="Override issues root directory"
272
345
  ),
346
+ force: bool = typer.Option(False, "--force", help="Bypass branch context checks"),
347
+ no_commit: bool = typer.Option(
348
+ False, "--no-commit", help="Skip auto-commit of issue file"
349
+ ),
273
350
  json: AgentOutput = False,
274
351
  ):
275
352
  """Submit issue for review (Stage -> Review) and generate delivery report."""
276
353
  config = get_config()
277
354
  issues_root = _resolve_issues_root(config, root)
278
355
  project_root = _resolve_project_root(config)
356
+
357
+ # Context Check: Submit should happen on feature branch, not trunk
358
+ _validate_branch_context(
359
+ project_root, forbidden=["TRUNK"], force=force, command_name="submit"
360
+ )
361
+
279
362
  try:
280
363
  # Implicitly ensure status is Open
281
- issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
364
+ issue = core.update_issue(
365
+ issues_root,
366
+ issue_id,
367
+ status="open",
368
+ stage="review",
369
+ no_commit=no_commit,
370
+ project_root=project_root,
371
+ )
282
372
 
283
373
  # Delivery Report Generation
284
374
  report_status = "skipped"
@@ -311,9 +401,17 @@ def move_close(
311
401
  False, "--prune", help="Delete branch/worktree after close"
312
402
  ),
313
403
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
404
+ force_prune: bool = typer.Option(
405
+ False,
406
+ "--force-prune",
407
+ help="Force delete branch/worktree with checking bypassed (includes warning)",
408
+ ),
314
409
  root: Optional[str] = typer.Option(
315
410
  None, "--root", help="Override issues root directory"
316
411
  ),
412
+ no_commit: bool = typer.Option(
413
+ False, "--no-commit", help="Skip auto-commit of issue file"
414
+ ),
317
415
  json: AgentOutput = False,
318
416
  ):
319
417
  """Close issue."""
@@ -333,9 +431,35 @@ def move_close(
333
431
  )
334
432
  raise typer.Exit(code=1)
335
433
 
434
+ # Context Check: Close should happen on trunk (after merge)
435
+ _validate_branch_context(
436
+ project_root,
437
+ allowed=["TRUNK"],
438
+ force=(force or force_prune),
439
+ command_name="close",
440
+ )
441
+
442
+ # Handle force-prune logic
443
+ if force_prune:
444
+ # Use OutputManager to check mode, as `json` arg might not be reliable with Typer Annotated
445
+ if not OutputManager.is_agent_mode() and not force:
446
+ confirm = typer.confirm(
447
+ "⚠️ [Bold Red]Warning:[/Bold Red] You are about to FORCE prune issue resources. Git merge checks will be bypassed.\nAre you sure you want to proceed?",
448
+ default=False,
449
+ )
450
+ if not confirm:
451
+ raise typer.Abort()
452
+ prune = True
453
+ force = True
454
+
336
455
  try:
337
456
  issue = core.update_issue(
338
- issues_root, issue_id, status="closed", solution=solution
457
+ issues_root,
458
+ issue_id,
459
+ status="closed",
460
+ solution=solution,
461
+ no_commit=no_commit,
462
+ project_root=project_root,
339
463
  )
340
464
 
341
465
  pruned_resources = []
@@ -363,13 +487,23 @@ def push(
363
487
  root: Optional[str] = typer.Option(
364
488
  None, "--root", help="Override issues root directory"
365
489
  ),
490
+ no_commit: bool = typer.Option(
491
+ False, "--no-commit", help="Skip auto-commit of issue file"
492
+ ),
366
493
  json: AgentOutput = False,
367
494
  ):
368
495
  """Push issue to backlog."""
369
496
  config = get_config()
370
497
  issues_root = _resolve_issues_root(config, root)
498
+ project_root = _resolve_project_root(config)
371
499
  try:
372
- issue = core.update_issue(issues_root, issue_id, status="backlog")
500
+ issue = core.update_issue(
501
+ issues_root,
502
+ issue_id,
503
+ status="backlog",
504
+ no_commit=no_commit,
505
+ project_root=project_root,
506
+ )
373
507
  OutputManager.print({"issue": issue, "status": "pushed_to_backlog"})
374
508
  except Exception as e:
375
509
  OutputManager.error(str(e))
@@ -382,13 +516,24 @@ def pull(
382
516
  root: Optional[str] = typer.Option(
383
517
  None, "--root", help="Override issues root directory"
384
518
  ),
519
+ no_commit: bool = typer.Option(
520
+ False, "--no-commit", help="Skip auto-commit of issue file"
521
+ ),
385
522
  json: AgentOutput = False,
386
523
  ):
387
524
  """Pull issue from backlog (Open & Draft)."""
388
525
  config = get_config()
389
526
  issues_root = _resolve_issues_root(config, root)
527
+ project_root = _resolve_project_root(config)
390
528
  try:
391
- issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
529
+ issue = core.update_issue(
530
+ issues_root,
531
+ issue_id,
532
+ status="open",
533
+ stage="draft",
534
+ no_commit=no_commit,
535
+ project_root=project_root,
536
+ )
392
537
  OutputManager.print({"issue": issue, "status": "pulled_from_backlog"})
393
538
  except Exception as e:
394
539
  OutputManager.error(str(e))
@@ -401,14 +546,23 @@ def cancel(
401
546
  root: Optional[str] = typer.Option(
402
547
  None, "--root", help="Override issues root directory"
403
548
  ),
549
+ no_commit: bool = typer.Option(
550
+ False, "--no-commit", help="Skip auto-commit of issue file"
551
+ ),
404
552
  json: AgentOutput = False,
405
553
  ):
406
554
  """Cancel issue."""
407
555
  config = get_config()
408
556
  issues_root = _resolve_issues_root(config, root)
557
+ project_root = _resolve_project_root(config)
409
558
  try:
410
559
  issue = core.update_issue(
411
- issues_root, issue_id, status="closed", solution="cancelled"
560
+ issues_root,
561
+ issue_id,
562
+ status="closed",
563
+ solution="cancelled",
564
+ no_commit=no_commit,
565
+ project_root=project_root,
412
566
  )
413
567
  OutputManager.print({"issue": issue, "status": "cancelled"})
414
568
  except Exception as e:
@@ -1154,3 +1308,255 @@ def lsp_definition(
1154
1308
 
1155
1309
  # helper to serialize
1156
1310
  print(json.dumps([l.model_dump(mode="json") for l in locations]))
1311
+
1312
+
1313
+ @app.command("escalate")
1314
+ def escalate(
1315
+ issue_id: str = typer.Argument(..., help="Issue ID to escalate"),
1316
+ to_level: str = typer.Option(
1317
+ ..., "--to", help="Target criticality level (low, medium, high, critical)"
1318
+ ),
1319
+ reason: str = typer.Option(..., "--reason", help="Reason for escalation"),
1320
+ root: Optional[str] = typer.Option(
1321
+ None, "--root", help="Override issues root directory"
1322
+ ),
1323
+ json: AgentOutput = False,
1324
+ ):
1325
+ """
1326
+ Request escalation of issue criticality.
1327
+ Requires approval before taking effect.
1328
+ """
1329
+ from .criticality import (
1330
+ CriticalityLevel,
1331
+ EscalationApprovalWorkflow,
1332
+ CriticalityValidator,
1333
+ )
1334
+ from monoco.core.workspace import find_monoco_root
1335
+
1336
+ config = get_config()
1337
+ issues_root = _resolve_issues_root(config, root)
1338
+
1339
+ # Parse target level
1340
+ try:
1341
+ target_level = CriticalityLevel(to_level.lower())
1342
+ except ValueError:
1343
+ valid_levels = [e.value for e in CriticalityLevel]
1344
+ OutputManager.error(
1345
+ f"Invalid level: '{to_level}'. Valid: {', '.join(valid_levels)}"
1346
+ )
1347
+ raise typer.Exit(code=1)
1348
+
1349
+ # Find issue
1350
+ issue_path = core.find_issue_path(issues_root, issue_id)
1351
+ if not issue_path:
1352
+ OutputManager.error(f"Issue {issue_id} not found.")
1353
+ raise typer.Exit(code=1)
1354
+
1355
+ issue = core.parse_issue(issue_path)
1356
+ if not issue:
1357
+ OutputManager.error(f"Could not parse issue {issue_id}.")
1358
+ raise typer.Exit(code=1)
1359
+
1360
+ current_level = issue.criticality or CriticalityLevel.MEDIUM
1361
+
1362
+ # Validate escalation direction
1363
+ can_modify, error_msg = CriticalityValidator.can_modify_criticality(
1364
+ current_level, target_level, is_escalation_approved=False
1365
+ )
1366
+
1367
+ if not can_modify:
1368
+ OutputManager.error(error_msg or "Escalation not allowed")
1369
+ raise typer.Exit(code=1)
1370
+
1371
+ # Create escalation request
1372
+ project_root = find_monoco_root()
1373
+ storage_path = project_root / ".monoco" / "escalations.yaml"
1374
+ workflow = EscalationApprovalWorkflow(storage_path)
1375
+
1376
+ import getpass
1377
+
1378
+ request = workflow.create_request(
1379
+ issue_id=issue_id,
1380
+ from_level=current_level,
1381
+ to_level=target_level,
1382
+ reason=reason,
1383
+ requested_by=getpass.getuser(),
1384
+ )
1385
+
1386
+ OutputManager.print(
1387
+ {
1388
+ "status": "escalation_requested",
1389
+ "escalation_id": request.id,
1390
+ "issue_id": issue_id,
1391
+ "from": current_level.value,
1392
+ "to": target_level.value,
1393
+ "message": f"Escalation request {request.id} created. Awaiting approval.",
1394
+ }
1395
+ )
1396
+
1397
+
1398
+ @app.command("approve-escalation")
1399
+ def approve_escalation(
1400
+ escalation_id: str = typer.Argument(..., help="Escalation request ID"),
1401
+ root: Optional[str] = typer.Option(
1402
+ None, "--root", help="Override issues root directory"
1403
+ ),
1404
+ json: AgentOutput = False,
1405
+ ):
1406
+ """
1407
+ Approve a pending escalation request.
1408
+ Updates the issue's criticality upon approval.
1409
+ """
1410
+ from .criticality import (
1411
+ EscalationApprovalWorkflow,
1412
+ EscalationStatus,
1413
+ )
1414
+ from monoco.core.workspace import find_monoco_root
1415
+
1416
+ config = get_config()
1417
+ issues_root = _resolve_issues_root(config, root)
1418
+
1419
+ # Load workflow
1420
+ project_root = find_monoco_root()
1421
+ storage_path = project_root / ".monoco" / "escalations.yaml"
1422
+ workflow = EscalationApprovalWorkflow(storage_path)
1423
+
1424
+ request = workflow.get_request(escalation_id)
1425
+ if not request:
1426
+ OutputManager.error(f"Escalation request {escalation_id} not found.")
1427
+ raise typer.Exit(code=1)
1428
+
1429
+ if request.status != EscalationStatus.PENDING:
1430
+ OutputManager.error(f"Request is already {request.status.value}.")
1431
+ raise typer.Exit(code=1)
1432
+
1433
+ # Approve
1434
+ import getpass
1435
+
1436
+ approved = workflow.approve(escalation_id, getpass.getuser())
1437
+
1438
+ # Update issue criticality
1439
+ try:
1440
+ core.update_issue(
1441
+ issues_root,
1442
+ request.issue_id,
1443
+ # Pass criticality through extra fields mechanism or update directly
1444
+ )
1445
+ # We need to update criticality directly
1446
+ issue_path = core.find_issue_path(issues_root, request.issue_id)
1447
+ if issue_path:
1448
+ content = issue_path.read_text()
1449
+ import yaml
1450
+ import re
1451
+
1452
+ match = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
1453
+ if match:
1454
+ yaml_str = match.group(1)
1455
+ data = yaml.safe_load(yaml_str) or {}
1456
+ data["criticality"] = request.to_level.value
1457
+ data["updated_at"] = datetime.now().isoformat()
1458
+
1459
+ new_yaml = yaml.dump(data, sort_keys=False, allow_unicode=True)
1460
+ body = content[match.end() :]
1461
+ new_content = f"---\n{new_yaml}---{body}"
1462
+ issue_path.write_text(new_content)
1463
+
1464
+ OutputManager.print(
1465
+ {
1466
+ "status": "escalation_approved",
1467
+ "escalation_id": escalation_id,
1468
+ "issue_id": request.issue_id,
1469
+ "new_criticality": request.to_level.value,
1470
+ }
1471
+ )
1472
+ except Exception as e:
1473
+ OutputManager.error(f"Failed to update issue: {e}")
1474
+ raise typer.Exit(code=1)
1475
+
1476
+
1477
+ @app.command("show")
1478
+ def show(
1479
+ issue_id: str = typer.Argument(..., help="Issue ID to show"),
1480
+ policy: bool = typer.Option(False, "--policy", help="Show resolved policy"),
1481
+ root: Optional[str] = typer.Option(
1482
+ None, "--root", help="Override issues root directory"
1483
+ ),
1484
+ json: AgentOutput = False,
1485
+ ):
1486
+ """
1487
+ Show issue details, optionally with resolved policy.
1488
+ """
1489
+ config = get_config()
1490
+ issues_root = _resolve_issues_root(config, root)
1491
+
1492
+ issue_path = core.find_issue_path(issues_root, issue_id)
1493
+ if not issue_path:
1494
+ OutputManager.error(f"Issue {issue_id} not found.")
1495
+ raise typer.Exit(code=1)
1496
+
1497
+ issue = core.parse_issue(issue_path)
1498
+ if not issue:
1499
+ OutputManager.error(f"Could not parse issue {issue_id}.")
1500
+ raise typer.Exit(code=1)
1501
+
1502
+ result = {
1503
+ "issue": issue.model_dump(),
1504
+ }
1505
+
1506
+ if policy:
1507
+ resolved_policy = issue.resolved_policy
1508
+ result["policy"] = {
1509
+ "criticality": issue.criticality.value
1510
+ if issue.criticality
1511
+ else "medium (default)",
1512
+ "agent_review": resolved_policy.agent_review.value,
1513
+ "human_review": resolved_policy.human_review.value,
1514
+ "min_coverage": resolved_policy.min_coverage,
1515
+ "rollback_on_failure": resolved_policy.rollback_on_failure.value,
1516
+ "require_security_scan": resolved_policy.require_security_scan,
1517
+ "require_performance_check": resolved_policy.require_performance_check,
1518
+ "max_reviewers": resolved_policy.max_reviewers,
1519
+ }
1520
+
1521
+ OutputManager.print(result)
1522
+
1523
+
1524
+ def _validate_branch_context(
1525
+ project_root: Path,
1526
+ allowed: Optional[List[str]] = None,
1527
+ forbidden: Optional[List[str]] = None,
1528
+ force: bool = False,
1529
+ command_name: str = "Command",
1530
+ ):
1531
+ """
1532
+ Enforce branch context rules.
1533
+ """
1534
+ if force:
1535
+ return
1536
+
1537
+ try:
1538
+ current = git.get_current_branch(project_root)
1539
+ except Exception:
1540
+ # If git fails (not a repo?), skip check or fail?
1541
+ # Let's assume strictness.
1542
+ return
1543
+
1544
+ is_trunk = current in ["main", "master"]
1545
+
1546
+ if allowed:
1547
+ if "TRUNK" in allowed and not is_trunk:
1548
+ # Check if current is strictly in allowed list otherwise
1549
+ if current not in allowed:
1550
+ OutputManager.error(
1551
+ f"❌ {command_name} restricted to 'main' branch. Current: {current}\n"
1552
+ f" Use --force to bypass if necessary."
1553
+ )
1554
+ raise typer.Exit(code=1)
1555
+
1556
+ if forbidden:
1557
+ if "TRUNK" in forbidden and is_trunk:
1558
+ OutputManager.error(
1559
+ f"❌ {command_name} cannot be run on 'main' branch.\n"
1560
+ f" Please checkout your feature branch first."
1561
+ )
1562
+ raise typer.Exit(code=1)