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.
Files changed (44) hide show
  1. monoco/core/agent/adapters.py +24 -1
  2. monoco/core/config.py +63 -1
  3. monoco/core/integrations.py +8 -0
  4. monoco/core/lsp.py +7 -0
  5. monoco/core/output.py +8 -1
  6. monoco/core/setup.py +8 -0
  7. monoco/features/agent/commands.py +73 -2
  8. monoco/features/agent/core.py +48 -0
  9. monoco/features/agent/resources/en/critique.prompty +16 -0
  10. monoco/features/agent/resources/en/develop.prompty +16 -0
  11. monoco/features/agent/resources/en/investigate.prompty +16 -0
  12. monoco/features/agent/resources/en/refine.prompty +14 -0
  13. monoco/features/agent/resources/en/verify.prompty +16 -0
  14. monoco/features/agent/resources/zh/critique.prompty +18 -0
  15. monoco/features/agent/resources/zh/develop.prompty +18 -0
  16. monoco/features/agent/resources/zh/investigate.prompty +18 -0
  17. monoco/features/agent/resources/zh/refine.prompty +16 -0
  18. monoco/features/agent/resources/zh/verify.prompty +18 -0
  19. monoco/features/issue/commands.py +133 -35
  20. monoco/features/issue/core.py +142 -119
  21. monoco/features/issue/domain/__init__.py +0 -0
  22. monoco/features/issue/domain/lifecycle.py +126 -0
  23. monoco/features/issue/domain/models.py +170 -0
  24. monoco/features/issue/domain/parser.py +223 -0
  25. monoco/features/issue/domain/workspace.py +104 -0
  26. monoco/features/issue/engine/__init__.py +22 -0
  27. monoco/features/issue/engine/config.py +189 -0
  28. monoco/features/issue/engine/machine.py +185 -0
  29. monoco/features/issue/engine/models.py +18 -0
  30. monoco/features/issue/linter.py +32 -11
  31. monoco/features/issue/lsp/__init__.py +3 -0
  32. monoco/features/issue/lsp/definition.py +72 -0
  33. monoco/features/issue/models.py +8 -8
  34. monoco/features/issue/validator.py +181 -65
  35. monoco/features/spike/core.py +5 -22
  36. monoco/main.py +0 -15
  37. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
  38. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/RECORD +41 -22
  39. monoco/features/pty/core.py +0 -185
  40. monoco/features/pty/router.py +0 -138
  41. monoco/features/pty/server.py +0 -56
  42. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
  43. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
  44. {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: IssueType = typer.Argument(..., help="Issue type (epic, feature, chore, fix)"),
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[IssueStage] = typer.Option(None, "--stage", help="Issue stage (draft, doing, review)"),
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 = IssueStatus.BACKLOG if is_backlog else IssueStatus.OPEN
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.print({
67
- "issue": issue,
68
- "path": str(rel_path),
69
- "status": "created"
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[IssueStatus] = typer.Option(None, "--status", help="New status"),
81
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="New stage"),
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=IssueStatus.OPEN, stage=IssueStage.DRAFT)
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=IssueStatus.OPEN, stage=IssueStage.DOING)
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, IsolationType.BRANCH, project_root)
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, IsolationType.WORKTREE, project_root)
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=IssueStatus.OPEN, stage=IssueStage.REVIEW)
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[IssueSolution] = typer.Option(None, "--solution", "-s", help="Solution type"),
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
- valid_solutions = [e.value for e in IssueSolution]
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=IssueStatus.CLOSED, solution=solution)
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=IssueStatus.BACKLOG)
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=IssueStatus.OPEN, stage=IssueStage.DRAFT)
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=IssueStatus.CLOSED, solution=IssueSolution.CANCELLED)
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
- IssueType.FEATURE: "green",
431
- IssueType.CHORE: "blue",
432
- IssueType.FIX: "red",
433
- IssueType.EPIC: "magenta"
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[IssueType] = typer.Option(None, "--type", "-t", help="Filter by type"),
462
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Filter by stage"),
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.value != target_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.value if i.stage else "-"
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.value}[/{t_color}]",
539
- f"[{s_color}]{i.status.value}[/{s_color}]",
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 == IssueType.EPIC], key=lambda x: x.id)
609
- stories = [i for i in issues if i.type == IssueType.FEATURE]
610
- tasks = [i for i in issues if i.type in [IssueType.CHORE, IssueType.FIX]]
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 = {IssueStatus.OPEN: "[blue]●[/blue]", IssueStatus.CLOSED: "[green]✔[/green]", IssueStatus.BACKLOG: "[dim]💤[/dim]"}
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]))