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.
- monoco/cli/workspace.py +1 -1
- monoco/core/config.py +58 -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/injection.py +63 -29
- monoco/core/integrations.py +8 -2
- monoco/core/output.py +5 -5
- monoco/core/registry.py +9 -1
- monoco/core/resource/__init__.py +5 -0
- monoco/core/resource/finder.py +98 -0
- monoco/core/resource/manager.py +91 -0
- monoco/core/resource/models.py +35 -0
- monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
- monoco/core/setup.py +1 -1
- monoco/core/skill_framework.py +292 -0
- monoco/core/skills.py +538 -254
- monoco/core/sync.py +73 -1
- monoco/core/workflow_converter.py +420 -0
- monoco/features/{scheduler → agent}/__init__.py +5 -3
- monoco/features/agent/adapter.py +31 -0
- monoco/features/agent/apoptosis.py +44 -0
- monoco/features/agent/cli.py +296 -0
- monoco/features/agent/config.py +96 -0
- monoco/features/agent/defaults.py +12 -0
- monoco/features/{scheduler → agent}/engines.py +32 -6
- monoco/features/agent/flow_skills.py +281 -0
- monoco/features/agent/manager.py +91 -0
- monoco/features/{scheduler → agent}/models.py +6 -3
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
- monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
- monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
- monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
- monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
- monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
- monoco/features/agent/resources/roles/role-manager.yaml +46 -0
- monoco/features/agent/resources/roles/role-planner.yaml +46 -0
- monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
- monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
- monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
- monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
- monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
- monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
- monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
- monoco/features/{scheduler → agent}/session.py +36 -1
- monoco/features/{scheduler → agent}/worker.py +40 -4
- monoco/features/glossary/adapter.py +31 -0
- monoco/features/glossary/config.py +5 -0
- monoco/features/glossary/resources/en/AGENTS.md +29 -0
- monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/glossary/resources/zh/AGENTS.md +29 -0
- monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
- monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
- monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
- monoco/features/issue/commands.py +427 -21
- monoco/features/issue/core.py +140 -1
- monoco/features/issue/criticality.py +553 -0
- monoco/features/issue/domain/models.py +28 -2
- monoco/features/issue/engine/machine.py +75 -15
- monoco/features/issue/git_service.py +185 -0
- monoco/features/issue/linter.py +291 -62
- monoco/features/issue/models.py +50 -2
- monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
- monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
- monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
- monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
- monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
- monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -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/en/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/memo/resources/zh/AGENTS.md +8 -0
- monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
- monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
- monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
- monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
- monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
- monoco/main.py +2 -3
- monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
- monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
- monoco/features/scheduler/cli.py +0 -285
- monoco/features/scheduler/config.py +0 -68
- monoco/features/scheduler/defaults.py +0 -54
- monoco/features/scheduler/manager.py +0 -49
- monoco/features/scheduler/reliability.py +0 -106
- monoco/features/skills/core.py +0 -102
- monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
- monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
- /monoco/core/{hooks.py → githooks.py} +0 -0
- /monoco/features/{skills → glossary}/__init__.py +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
108
|
-
primary_lang = target_langs[0] if target_langs else "en"
|
|
137
|
+
source_lang = config.i18n.source_lang or "en"
|
|
109
138
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
f"
|
|
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(
|
|
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
|
-
|
|
201
|
-
"--branch",
|
|
240
|
+
True,
|
|
241
|
+
"--branch/--no-branch",
|
|
202
242
|
"-b",
|
|
203
|
-
help="[
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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)
|