monoco-toolkit 0.2.5__py3-none-any.whl → 0.2.6__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/core/agent/adapters.py +24 -1
- monoco/core/config.py +63 -1
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/setup.py +8 -0
- monoco/features/agent/commands.py +73 -2
- monoco/features/agent/core.py +48 -0
- monoco/features/agent/resources/en/critique.prompty +16 -0
- monoco/features/agent/resources/en/develop.prompty +16 -0
- monoco/features/agent/resources/en/investigate.prompty +16 -0
- monoco/features/agent/resources/en/refine.prompty +14 -0
- monoco/features/agent/resources/en/verify.prompty +16 -0
- monoco/features/agent/resources/zh/critique.prompty +18 -0
- monoco/features/agent/resources/zh/develop.prompty +18 -0
- monoco/features/agent/resources/zh/investigate.prompty +18 -0
- monoco/features/agent/resources/zh/refine.prompty +16 -0
- monoco/features/agent/resources/zh/verify.prompty +18 -0
- monoco/features/issue/commands.py +133 -35
- monoco/features/issue/core.py +142 -119
- monoco/features/issue/domain/__init__.py +0 -0
- monoco/features/issue/domain/lifecycle.py +126 -0
- monoco/features/issue/domain/models.py +170 -0
- monoco/features/issue/domain/parser.py +223 -0
- monoco/features/issue/domain/workspace.py +104 -0
- monoco/features/issue/engine/__init__.py +22 -0
- monoco/features/issue/engine/config.py +189 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +32 -11
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/models.py +8 -8
- monoco/features/issue/validator.py +181 -65
- monoco/features/spike/core.py +5 -22
- monoco/main.py +0 -15
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/RECORD +41 -22
- monoco/features/pty/core.py +0 -185
- monoco/features/pty/router.py +0 -138
- monoco/features/pty/server.py +0 -56
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,16 +14,18 @@ from . import core
|
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(help="Agent-Native Issue Management.")
|
|
16
16
|
backlog_app = typer.Typer(help="Manage backlog operations.")
|
|
17
|
+
lsp_app = typer.Typer(help="LSP Server commands.")
|
|
17
18
|
app.add_typer(backlog_app, name="backlog")
|
|
19
|
+
app.add_typer(lsp_app, name="lsp")
|
|
18
20
|
console = Console()
|
|
19
21
|
|
|
20
22
|
@app.command("create")
|
|
21
23
|
def create(
|
|
22
|
-
type:
|
|
24
|
+
type: str = typer.Argument(..., help="Issue type (epic, feature, chore, fix, etc.)"),
|
|
23
25
|
title: str = typer.Option(..., "--title", "-t", help="Issue title"),
|
|
24
26
|
parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
|
|
25
27
|
is_backlog: bool = typer.Option(False, "--backlog", help="Create as backlog item"),
|
|
26
|
-
stage: Optional[
|
|
28
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Issue stage"),
|
|
27
29
|
dependencies: List[str] = typer.Option([], "--dependency", "-d", help="Issue dependency ID(s)"),
|
|
28
30
|
related: List[str] = typer.Option([], "--related", "-r", help="Related Issue ID(s)"),
|
|
29
31
|
subdir: Optional[str] = typer.Option(None, "--subdir", "-s", help="Subdirectory for organization (e.g. 'Backend/Auth')"),
|
|
@@ -32,10 +34,11 @@ def create(
|
|
|
32
34
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
33
35
|
json: AgentOutput = False,
|
|
34
36
|
):
|
|
37
|
+
"""Create a new issue."""
|
|
35
38
|
"""Create a new issue."""
|
|
36
39
|
config = get_config()
|
|
37
40
|
issues_root = _resolve_issues_root(config, root)
|
|
38
|
-
status =
|
|
41
|
+
status = "backlog" if is_backlog else "open"
|
|
39
42
|
|
|
40
43
|
if parent:
|
|
41
44
|
parent_path = core.find_issue_path(issues_root, parent)
|
|
@@ -63,11 +66,15 @@ def create(
|
|
|
63
66
|
except ValueError:
|
|
64
67
|
rel_path = path
|
|
65
68
|
|
|
66
|
-
OutputManager.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
if OutputManager.is_agent_mode():
|
|
70
|
+
OutputManager.print({
|
|
71
|
+
"issue": issue,
|
|
72
|
+
"path": str(rel_path),
|
|
73
|
+
"status": "created"
|
|
74
|
+
})
|
|
75
|
+
else:
|
|
76
|
+
console.print(f"[green]✔ Created {issue.id} in status {issue.status}.[/green]")
|
|
77
|
+
console.print(f"Path: {rel_path}")
|
|
71
78
|
|
|
72
79
|
except ValueError as e:
|
|
73
80
|
OutputManager.error(str(e))
|
|
@@ -77,8 +84,8 @@ def create(
|
|
|
77
84
|
def update(
|
|
78
85
|
issue_id: str = typer.Argument(..., help="Issue ID to update"),
|
|
79
86
|
title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
|
|
80
|
-
status: Optional[
|
|
81
|
-
stage: Optional[
|
|
87
|
+
status: Optional[str] = typer.Option(None, "--status", help="New status"),
|
|
88
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
|
|
82
89
|
parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
|
|
83
90
|
sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
|
|
84
91
|
dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
|
|
@@ -125,7 +132,7 @@ def move_open(
|
|
|
125
132
|
issues_root = _resolve_issues_root(config, root)
|
|
126
133
|
try:
|
|
127
134
|
# Pull operation: Force stage to TODO
|
|
128
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
135
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
129
136
|
OutputManager.print({
|
|
130
137
|
"issue": issue,
|
|
131
138
|
"status": "opened"
|
|
@@ -153,13 +160,13 @@ def start(
|
|
|
153
160
|
|
|
154
161
|
try:
|
|
155
162
|
# Implicitly ensure status is Open
|
|
156
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
163
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="doing")
|
|
157
164
|
|
|
158
165
|
isolation_info = None
|
|
159
166
|
|
|
160
167
|
if branch:
|
|
161
168
|
try:
|
|
162
|
-
issue = core.start_issue_isolation(issues_root, issue_id,
|
|
169
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "branch", project_root)
|
|
163
170
|
isolation_info = {"type": "branch", "ref": issue.isolation.ref}
|
|
164
171
|
except Exception as e:
|
|
165
172
|
OutputManager.error(f"Failed to create branch: {e}")
|
|
@@ -167,7 +174,7 @@ def start(
|
|
|
167
174
|
|
|
168
175
|
if worktree:
|
|
169
176
|
try:
|
|
170
|
-
issue = core.start_issue_isolation(issues_root, issue_id,
|
|
177
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "worktree", project_root)
|
|
171
178
|
isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
|
|
172
179
|
except Exception as e:
|
|
173
180
|
OutputManager.error(f"Failed to create worktree: {e}")
|
|
@@ -196,7 +203,7 @@ def submit(
|
|
|
196
203
|
project_root = _resolve_project_root(config)
|
|
197
204
|
try:
|
|
198
205
|
# Implicitly ensure status is Open
|
|
199
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
206
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
|
|
200
207
|
|
|
201
208
|
# Delivery Report Generation
|
|
202
209
|
report_status = "skipped"
|
|
@@ -228,7 +235,7 @@ def submit(
|
|
|
228
235
|
@app.command("close")
|
|
229
236
|
def move_close(
|
|
230
237
|
issue_id: str = typer.Argument(..., help="Issue ID to close"),
|
|
231
|
-
solution: Optional[
|
|
238
|
+
solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type"),
|
|
232
239
|
prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
|
|
233
240
|
force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
|
|
234
241
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
@@ -241,12 +248,15 @@ def move_close(
|
|
|
241
248
|
|
|
242
249
|
# Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
|
|
243
250
|
if solution is None:
|
|
244
|
-
|
|
251
|
+
# Resolve options from engine
|
|
252
|
+
from .engine import get_engine
|
|
253
|
+
engine = get_engine(str(issues_root.parent))
|
|
254
|
+
valid_solutions = engine.issue_config.solutions or []
|
|
245
255
|
OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
|
|
246
256
|
raise typer.Exit(code=1)
|
|
247
257
|
|
|
248
258
|
try:
|
|
249
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
259
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution=solution)
|
|
250
260
|
|
|
251
261
|
pruned_resources = []
|
|
252
262
|
if prune:
|
|
@@ -276,7 +286,7 @@ def push(
|
|
|
276
286
|
config = get_config()
|
|
277
287
|
issues_root = _resolve_issues_root(config, root)
|
|
278
288
|
try:
|
|
279
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
289
|
+
issue = core.update_issue(issues_root, issue_id, status="backlog")
|
|
280
290
|
OutputManager.print({
|
|
281
291
|
"issue": issue,
|
|
282
292
|
"status": "pushed_to_backlog"
|
|
@@ -295,7 +305,7 @@ def pull(
|
|
|
295
305
|
config = get_config()
|
|
296
306
|
issues_root = _resolve_issues_root(config, root)
|
|
297
307
|
try:
|
|
298
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
308
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
299
309
|
OutputManager.print({
|
|
300
310
|
"issue": issue,
|
|
301
311
|
"status": "pulled_from_backlog"
|
|
@@ -314,7 +324,7 @@ def cancel(
|
|
|
314
324
|
config = get_config()
|
|
315
325
|
issues_root = _resolve_issues_root(config, root)
|
|
316
326
|
try:
|
|
317
|
-
issue = core.update_issue(issues_root, issue_id, status=
|
|
327
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution="cancelled")
|
|
318
328
|
OutputManager.print({
|
|
319
329
|
"issue": issue,
|
|
320
330
|
"status": "cancelled"
|
|
@@ -427,10 +437,10 @@ def board(
|
|
|
427
437
|
issue_list = []
|
|
428
438
|
for issue in sorted(issues, key=lambda x: x.updated_at, reverse=True):
|
|
429
439
|
type_color = {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
440
|
+
"feature": "green",
|
|
441
|
+
"chore": "blue",
|
|
442
|
+
"fix": "red",
|
|
443
|
+
"epic": "magenta"
|
|
434
444
|
}.get(issue.type, "white")
|
|
435
445
|
|
|
436
446
|
issue_list.append(
|
|
@@ -458,8 +468,8 @@ def board(
|
|
|
458
468
|
@app.command("list")
|
|
459
469
|
def list_cmd(
|
|
460
470
|
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (open, closed, backlog, all)"),
|
|
461
|
-
type: Optional[
|
|
462
|
-
stage: Optional[
|
|
471
|
+
type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
|
|
472
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Filter by stage"),
|
|
463
473
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
464
474
|
workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
|
|
465
475
|
json: AgentOutput = False,
|
|
@@ -481,7 +491,7 @@ def list_cmd(
|
|
|
481
491
|
for i in issues:
|
|
482
492
|
# Status Filter
|
|
483
493
|
if target_status != "all":
|
|
484
|
-
if i.status
|
|
494
|
+
if i.status != target_status:
|
|
485
495
|
continue
|
|
486
496
|
|
|
487
497
|
# Type Filter
|
|
@@ -530,13 +540,13 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
|
|
|
530
540
|
t_color = type_colors.get(i.type, "white")
|
|
531
541
|
s_color = status_colors.get(i.status, "white")
|
|
532
542
|
|
|
533
|
-
stage_str = i.stage
|
|
543
|
+
stage_str = i.stage if i.stage else "-"
|
|
534
544
|
updated_str = i.updated_at.strftime("%Y-%m-%d %H:%M")
|
|
535
545
|
|
|
536
546
|
table.add_row(
|
|
537
547
|
i.id,
|
|
538
|
-
f"[{t_color}]{i.type
|
|
539
|
-
f"[{s_color}]{i.status
|
|
548
|
+
f"[{t_color}]{i.type}[/{t_color}]",
|
|
549
|
+
f"[{s_color}]{i.status}[/{s_color}]",
|
|
540
550
|
stage_str,
|
|
541
551
|
i.title,
|
|
542
552
|
updated_str
|
|
@@ -605,11 +615,11 @@ def scope(
|
|
|
605
615
|
return
|
|
606
616
|
|
|
607
617
|
tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
|
|
608
|
-
epics = sorted([i for i in issues if i.type ==
|
|
609
|
-
stories = [i for i in issues if i.type ==
|
|
610
|
-
tasks = [i for i in issues if i.type in [
|
|
618
|
+
epics = sorted([i for i in issues if i.type == "epic"], key=lambda x: x.id)
|
|
619
|
+
stories = [i for i in issues if i.type == "feature"]
|
|
620
|
+
tasks = [i for i in issues if i.type in ["chore", "fix"]]
|
|
611
621
|
|
|
612
|
-
status_map = {
|
|
622
|
+
status_map = {"open": "[blue]●[/blue]", "closed": "[green]✔[/green]", "backlog": "[dim]💤[/dim]"}
|
|
613
623
|
|
|
614
624
|
for epic in epics:
|
|
615
625
|
epic_node = tree.add(f"{status_map[epic.status]} [bold]{epic.id}[/bold]: {epic.title}")
|
|
@@ -622,6 +632,57 @@ def scope(
|
|
|
622
632
|
|
|
623
633
|
console.print(Panel(tree, expand=False))
|
|
624
634
|
|
|
635
|
+
@app.command("inspect")
|
|
636
|
+
def inspect(
|
|
637
|
+
target: str = typer.Argument(..., help="Issue ID or File Path"),
|
|
638
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
639
|
+
ast: bool = typer.Option(False, "--ast", help="Output JSON AST structure for debugging"),
|
|
640
|
+
json: AgentOutput = False,
|
|
641
|
+
):
|
|
642
|
+
"""
|
|
643
|
+
Inspect a specific issue and return its metadata (including actions).
|
|
644
|
+
"""
|
|
645
|
+
config = get_config()
|
|
646
|
+
issues_root = _resolve_issues_root(config, root)
|
|
647
|
+
|
|
648
|
+
# Try as Path
|
|
649
|
+
target_path = Path(target)
|
|
650
|
+
if target_path.exists() and target_path.is_file():
|
|
651
|
+
path = target_path
|
|
652
|
+
else:
|
|
653
|
+
# Try as ID
|
|
654
|
+
# Search path logic is needed? Or core.find_issue_path
|
|
655
|
+
path = core.find_issue_path(issues_root, target)
|
|
656
|
+
if not path:
|
|
657
|
+
OutputManager.error(f"Issue or file {target} not found.")
|
|
658
|
+
raise typer.Exit(code=1)
|
|
659
|
+
|
|
660
|
+
# AST Debug Mode
|
|
661
|
+
if ast:
|
|
662
|
+
from .domain.parser import MarkdownParser
|
|
663
|
+
content = path.read_text()
|
|
664
|
+
try:
|
|
665
|
+
domain_issue = MarkdownParser.parse(content, path=str(path))
|
|
666
|
+
print(domain_issue.model_dump_json(indent=2))
|
|
667
|
+
except Exception as e:
|
|
668
|
+
OutputManager.error(f"Failed to parse AST: {e}")
|
|
669
|
+
raise typer.Exit(code=1)
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
# Normal Mode
|
|
673
|
+
meta = core.parse_issue(path)
|
|
674
|
+
|
|
675
|
+
if not meta:
|
|
676
|
+
OutputManager.error(f"Could not parse issue {target}.")
|
|
677
|
+
raise typer.Exit(code=1)
|
|
678
|
+
|
|
679
|
+
# In JSON mode (AgentOutput), we might want to return rich data
|
|
680
|
+
if OutputManager.is_agent_mode():
|
|
681
|
+
OutputManager.print(meta)
|
|
682
|
+
else:
|
|
683
|
+
# For human, print yaml-like or table
|
|
684
|
+
console.print(meta)
|
|
685
|
+
|
|
625
686
|
@app.command("lint")
|
|
626
687
|
def lint(
|
|
627
688
|
recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
|
|
@@ -826,3 +887,40 @@ def commit(
|
|
|
826
887
|
except Exception as e:
|
|
827
888
|
console.print(f"[red]Git Error:[/red] {e}")
|
|
828
889
|
raise typer.Exit(code=1)
|
|
890
|
+
|
|
891
|
+
@lsp_app.command("definition")
|
|
892
|
+
def lsp_definition(
|
|
893
|
+
file: str = typer.Option(..., "--file", "-f", help="Abs path to file"),
|
|
894
|
+
line: int = typer.Option(..., "--line", "-l", help="0-indexed line number"),
|
|
895
|
+
character: int = typer.Option(..., "--char", "-c", help="0-indexed character number"),
|
|
896
|
+
):
|
|
897
|
+
"""
|
|
898
|
+
Handle textDocument/definition request.
|
|
899
|
+
Output: JSON Location | null
|
|
900
|
+
"""
|
|
901
|
+
import json
|
|
902
|
+
from monoco.core.lsp import Position
|
|
903
|
+
from monoco.features.issue.lsp import DefinitionProvider
|
|
904
|
+
|
|
905
|
+
config = get_config()
|
|
906
|
+
# Workspace Root resolution is key here.
|
|
907
|
+
# If we are in a workspace, we want the workspace root, not just issue root.
|
|
908
|
+
# _resolve_project_root returns the closest project root or monoco root.
|
|
909
|
+
workspace_root = _resolve_project_root(config)
|
|
910
|
+
# Search for topmost workspace root to enable cross-project navigation
|
|
911
|
+
current_best = workspace_root
|
|
912
|
+
for parent in [workspace_root] + list(workspace_root.parents):
|
|
913
|
+
if (parent / ".monoco" / "workspace.yaml").exists() or (parent / ".monoco" / "project.yaml").exists():
|
|
914
|
+
current_best = parent
|
|
915
|
+
workspace_root = current_best
|
|
916
|
+
|
|
917
|
+
provider = DefinitionProvider(workspace_root)
|
|
918
|
+
file_path = Path(file)
|
|
919
|
+
|
|
920
|
+
locations = provider.provide_definition(
|
|
921
|
+
file_path,
|
|
922
|
+
Position(line=line, character=character)
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
# helper to serialize
|
|
926
|
+
print(json.dumps([l.model_dump(mode='json') for l in locations]))
|