monoco-toolkit 0.2.4__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 (54) hide show
  1. monoco/cli/project.py +15 -7
  2. monoco/cli/workspace.py +11 -3
  3. monoco/core/agent/adapters.py +24 -1
  4. monoco/core/config.py +81 -3
  5. monoco/core/integrations.py +8 -0
  6. monoco/core/lsp.py +7 -0
  7. monoco/core/output.py +8 -1
  8. monoco/core/resources/en/SKILL.md +1 -1
  9. monoco/core/setup.py +8 -1
  10. monoco/daemon/app.py +18 -12
  11. monoco/features/agent/commands.py +94 -17
  12. monoco/features/agent/core.py +48 -0
  13. monoco/features/agent/resources/en/critique.prompty +16 -0
  14. monoco/features/agent/resources/en/develop.prompty +16 -0
  15. monoco/features/agent/resources/en/investigate.prompty +16 -0
  16. monoco/features/agent/resources/en/refine.prompty +14 -0
  17. monoco/features/agent/resources/en/verify.prompty +16 -0
  18. monoco/features/agent/resources/zh/critique.prompty +18 -0
  19. monoco/features/agent/resources/zh/develop.prompty +18 -0
  20. monoco/features/agent/resources/zh/investigate.prompty +18 -0
  21. monoco/features/agent/resources/zh/refine.prompty +16 -0
  22. monoco/features/agent/resources/zh/verify.prompty +18 -0
  23. monoco/features/config/commands.py +35 -14
  24. monoco/features/i18n/commands.py +89 -10
  25. monoco/features/i18n/core.py +112 -16
  26. monoco/features/issue/commands.py +254 -85
  27. monoco/features/issue/core.py +142 -119
  28. monoco/features/issue/domain/__init__.py +0 -0
  29. monoco/features/issue/domain/lifecycle.py +126 -0
  30. monoco/features/issue/domain/models.py +170 -0
  31. monoco/features/issue/domain/parser.py +223 -0
  32. monoco/features/issue/domain/workspace.py +104 -0
  33. monoco/features/issue/engine/__init__.py +22 -0
  34. monoco/features/issue/engine/config.py +189 -0
  35. monoco/features/issue/engine/machine.py +185 -0
  36. monoco/features/issue/engine/models.py +18 -0
  37. monoco/features/issue/linter.py +32 -11
  38. monoco/features/issue/lsp/__init__.py +3 -0
  39. monoco/features/issue/lsp/definition.py +72 -0
  40. monoco/features/issue/models.py +8 -8
  41. monoco/features/issue/validator.py +204 -65
  42. monoco/features/spike/commands.py +45 -24
  43. monoco/features/spike/core.py +5 -22
  44. monoco/main.py +11 -17
  45. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
  46. monoco_toolkit-0.2.6.dist-info/RECORD +96 -0
  47. monoco/features/issue/executions/refine.md +0 -26
  48. monoco/features/pty/core.py +0 -185
  49. monoco/features/pty/router.py +0 -138
  50. monoco/features/pty/server.py +0 -56
  51. monoco_toolkit-0.2.4.dist-info/RECORD +0 -78
  52. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
  53. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
  54. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
@@ -8,38 +8,42 @@ from rich.table import Table
8
8
  import typer
9
9
 
10
10
  from monoco.core.config import get_config
11
- from monoco.core.output import print_output
11
+ from monoco.core.output import print_output, OutputManager, AgentOutput
12
12
  from .models import IssueType, IssueStatus, IssueSolution, IssueStage, IsolationType, IssueMetadata
13
13
  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')"),
30
32
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
31
33
  tags: List[str] = typer.Option([], "--tag", help="Tags"),
32
34
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
35
+ json: AgentOutput = False,
33
36
  ):
37
+ """Create a new issue."""
34
38
  """Create a new issue."""
35
39
  config = get_config()
36
40
  issues_root = _resolve_issues_root(config, root)
37
- status = IssueStatus.BACKLOG if is_backlog else IssueStatus.OPEN
41
+ status = "backlog" if is_backlog else "open"
38
42
 
39
43
  if parent:
40
44
  parent_path = core.find_issue_path(issues_root, parent)
41
45
  if not parent_path:
42
- console.print(f"[red]✘ Error:[/red] Parent issue {parent} not found.")
46
+ OutputManager.error(f"Parent issue {parent} not found.")
43
47
  raise typer.Exit(code=1)
44
48
 
45
49
  try:
@@ -62,31 +66,40 @@ def create(
62
66
  except ValueError:
63
67
  rel_path = path
64
68
 
65
- console.print(f"[green]✔[/green] Created [bold]{issue.id}[/bold] in status [cyan]{issue.status.value}[/cyan].")
66
- console.print(f"[dim]Path: {rel_path}[/dim]")
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}")
78
+
67
79
  except ValueError as e:
68
- console.print(f"[red]✘ Error:[/red] {str(e)}")
80
+ OutputManager.error(str(e))
69
81
  raise typer.Exit(code=1)
70
82
 
71
83
  @app.command("update")
72
84
  def update(
73
85
  issue_id: str = typer.Argument(..., help="Issue ID to update"),
74
86
  title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
75
- status: Optional[IssueStatus] = typer.Option(None, "--status", help="New status"),
76
- 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"),
77
89
  parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
78
90
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
79
91
  dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
80
92
  related: Optional[List[str]] = typer.Option(None, "--related", "-r", help="Related Issue ID(s)"),
81
93
  tags: Optional[List[str]] = typer.Option(None, "--tag", help="Tags"),
82
94
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
95
+ json: AgentOutput = False,
83
96
  ):
84
97
  """Update an existing issue."""
85
98
  config = get_config()
86
99
  issues_root = _resolve_issues_root(config, root)
87
100
 
88
101
  try:
89
- core.update_issue(
102
+ issue = core.update_issue(
90
103
  issues_root,
91
104
  issue_id,
92
105
  status=status,
@@ -99,25 +112,33 @@ def update(
99
112
  tags=tags
100
113
  )
101
114
 
102
- console.print(f"[green]✔[/green] Updated [bold]{issue_id}[/bold].")
115
+ OutputManager.print({
116
+ "issue": issue,
117
+ "status": "updated"
118
+ })
103
119
  except Exception as e:
104
- console.print(f"[red]✘ Error:[/red] {str(e)}")
120
+ OutputManager.error(str(e))
105
121
  raise typer.Exit(code=1)
106
122
 
123
+
107
124
  @app.command("open")
108
125
  def move_open(
109
126
  issue_id: str = typer.Argument(..., help="Issue ID to open"),
110
127
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
128
+ json: AgentOutput = False,
111
129
  ):
112
130
  """Move issue to open status and set stage to Draft."""
113
131
  config = get_config()
114
132
  issues_root = _resolve_issues_root(config, root)
115
133
  try:
116
134
  # Pull operation: Force stage to TODO
117
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
118
- console.print(f"[green]▶[/green] Issue [bold]{issue_id}[/bold] moved to open/draft.")
135
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
136
+ OutputManager.print({
137
+ "issue": issue,
138
+ "status": "opened"
139
+ })
119
140
  except Exception as e:
120
- console.print(f"[red]✘ Error:[/red] {str(e)}")
141
+ OutputManager.error(str(e))
121
142
  raise typer.Exit(code=1)
122
143
 
123
144
  @app.command("start")
@@ -126,6 +147,7 @@ def start(
126
147
  branch: bool = typer.Option(False, "--branch", "-b", help="Start in a new git branch"),
127
148
  worktree: bool = typer.Option(False, "--worktree", "-w", help="Start in a new git worktree"),
128
149
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
150
+ json: AgentOutput = False,
129
151
  ):
130
152
  """Start working on an issue (Stage -> Doing)."""
131
153
  config = get_config()
@@ -133,32 +155,38 @@ def start(
133
155
  project_root = _resolve_project_root(config)
134
156
 
135
157
  if branch and worktree:
136
- console.print("[red]Error:[/red] Cannot specify both --branch and --worktree.")
158
+ OutputManager.error("Cannot specify both --branch and --worktree.")
137
159
  raise typer.Exit(code=1)
138
160
 
139
161
  try:
140
162
  # Implicitly ensure status is Open
141
- 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")
142
164
 
165
+ isolation_info = None
166
+
143
167
  if branch:
144
168
  try:
145
- meta = core.start_issue_isolation(issues_root, issue_id, IsolationType.BRANCH, project_root)
146
- console.print(f"[green]✔[/green] Switched to branch [bold]{meta.isolation.ref}[/bold]")
169
+ issue = core.start_issue_isolation(issues_root, issue_id, "branch", project_root)
170
+ isolation_info = {"type": "branch", "ref": issue.isolation.ref}
147
171
  except Exception as e:
148
- console.print(f"[red]Error:[/red] Failed to create branch: {e}")
172
+ OutputManager.error(f"Failed to create branch: {e}")
149
173
  raise typer.Exit(code=1)
150
174
 
151
175
  if worktree:
152
176
  try:
153
- meta = core.start_issue_isolation(issues_root, issue_id, IsolationType.WORKTREE, project_root)
154
- console.print(f"[green]✔[/green] Created worktree at [bold]{meta.isolation.path}[/bold]")
177
+ issue = core.start_issue_isolation(issues_root, issue_id, "worktree", project_root)
178
+ isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
155
179
  except Exception as e:
156
- console.print(f"[red]Error:[/red] Failed to create worktree: {e}")
180
+ OutputManager.error(f"Failed to create worktree: {e}")
157
181
  raise typer.Exit(code=1)
158
182
 
159
- console.print(f"[green]🚀[/green] Issue [bold]{issue_id}[/bold] started.")
183
+ OutputManager.print({
184
+ "issue": issue,
185
+ "status": "started",
186
+ "isolation": isolation_info
187
+ })
160
188
  except Exception as e:
161
- console.print(f"[red]✘ Error:[/red] {str(e)}")
189
+ OutputManager.error(str(e))
162
190
  raise typer.Exit(code=1)
163
191
 
164
192
  @app.command("submit")
@@ -167,6 +195,7 @@ def submit(
167
195
  prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after submit"),
168
196
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
169
197
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
198
+ json: AgentOutput = False,
170
199
  ):
171
200
  """Submit issue for review (Stage -> Review) and generate delivery report."""
172
201
  config = get_config()
@@ -174,36 +203,43 @@ def submit(
174
203
  project_root = _resolve_project_root(config)
175
204
  try:
176
205
  # Implicitly ensure status is Open
177
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.REVIEW)
178
- console.print(f"[green]🚀[/green] Issue [bold]{issue_id}[/bold] submitted for review.")
206
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
179
207
 
180
208
  # Delivery Report Generation
209
+ report_status = "skipped"
181
210
  try:
182
211
  core.generate_delivery_report(issues_root, issue_id, project_root)
183
- console.print(f"[dim]✔ Delivery report appended to issue file.[/dim]")
212
+ report_status = "generated"
184
213
  except Exception as e:
185
- console.print(f"[yellow]⚠ Failed to generate delivery report: {e}[/yellow]")
214
+ report_status = f"failed: {e}"
186
215
 
216
+ pruned_resources = []
187
217
  if prune:
188
218
  try:
189
- deleted = core.prune_issue_resources(issues_root, issue_id, force, project_root)
190
- if deleted:
191
- console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
219
+ pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
192
220
  except Exception as e:
193
- console.print(f"[red]Prune Error:[/red] {e}")
221
+ OutputManager.error(f"Prune Error: {e}")
194
222
  raise typer.Exit(code=1)
195
223
 
224
+ OutputManager.print({
225
+ "issue": issue,
226
+ "status": "submitted",
227
+ "report": report_status,
228
+ "pruned": pruned_resources
229
+ })
230
+
196
231
  except Exception as e:
197
- console.print(f"[red]✘ Error:[/red] {str(e)}")
232
+ OutputManager.error(str(e))
198
233
  raise typer.Exit(code=1)
199
234
 
200
235
  @app.command("close")
201
236
  def move_close(
202
237
  issue_id: str = typer.Argument(..., help="Issue ID to close"),
203
- solution: Optional[IssueSolution] = typer.Option(None, "--solution", "-s", help="Solution type"),
238
+ solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type"),
204
239
  prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
205
240
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
206
241
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
242
+ json: AgentOutput = False,
207
243
  ):
208
244
  """Close issue."""
209
245
  config = get_config()
@@ -212,86 +248,108 @@ def move_close(
212
248
 
213
249
  # Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
214
250
  if solution is None:
215
- valid_solutions = [e.value for e in IssueSolution]
216
- console.print(f"[red]✘ Error:[/red] Closing an issue requires a solution.")
217
- console.print(f"Please specify one of: [bold]{', '.join(valid_solutions)}[/bold]")
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 []
255
+ OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
218
256
  raise typer.Exit(code=1)
219
257
 
220
258
  try:
221
- core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=solution)
222
- console.print(f"[dim]✔[/dim] Issue [bold]{issue_id}[/bold] closed.")
259
+ issue = core.update_issue(issues_root, issue_id, status="closed", solution=solution)
223
260
 
261
+ pruned_resources = []
224
262
  if prune:
225
263
  try:
226
- deleted = core.prune_issue_resources(issues_root, issue_id, force, project_root)
227
- if deleted:
228
- console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
264
+ pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
229
265
  except Exception as e:
230
- console.print(f"[red]Prune Error:[/red] {e}")
266
+ OutputManager.error(f"Prune Error: {e}")
231
267
  raise typer.Exit(code=1)
232
268
 
269
+ OutputManager.print({
270
+ "issue": issue,
271
+ "status": "closed",
272
+ "pruned": pruned_resources
273
+ })
274
+
233
275
  except Exception as e:
234
- console.print(f"[red]✘ Error:[/red] {str(e)}")
276
+ OutputManager.error(str(e))
235
277
  raise typer.Exit(code=1)
236
278
 
237
279
  @backlog_app.command("push")
238
280
  def push(
239
281
  issue_id: str = typer.Argument(..., help="Issue ID to push to backlog"),
240
282
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
283
+ json: AgentOutput = False,
241
284
  ):
242
285
  """Push issue to backlog."""
243
286
  config = get_config()
244
287
  issues_root = _resolve_issues_root(config, root)
245
288
  try:
246
- core.update_issue(issues_root, issue_id, status=IssueStatus.BACKLOG)
247
- console.print(f"[blue]💤[/blue] Issue [bold]{issue_id}[/bold] pushed to backlog.")
289
+ issue = core.update_issue(issues_root, issue_id, status="backlog")
290
+ OutputManager.print({
291
+ "issue": issue,
292
+ "status": "pushed_to_backlog"
293
+ })
248
294
  except Exception as e:
249
- console.print(f"[red]✘ Error:[/red] {str(e)}")
295
+ OutputManager.error(str(e))
250
296
  raise typer.Exit(code=1)
251
297
 
252
298
  @backlog_app.command("pull")
253
299
  def pull(
254
300
  issue_id: str = typer.Argument(..., help="Issue ID to pull from backlog"),
255
301
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
302
+ json: AgentOutput = False,
256
303
  ):
257
304
  """Pull issue from backlog (Open & Draft)."""
258
305
  config = get_config()
259
306
  issues_root = _resolve_issues_root(config, root)
260
307
  try:
261
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
262
- console.print(f"[green]🔥[/green] Issue [bold]{issue_id}[/bold] pulled from backlog.")
308
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
309
+ OutputManager.print({
310
+ "issue": issue,
311
+ "status": "pulled_from_backlog"
312
+ })
263
313
  except Exception as e:
264
- console.print(f"[red]✘ Error:[/red] {str(e)}")
314
+ OutputManager.error(str(e))
265
315
  raise typer.Exit(code=1)
266
316
 
267
317
  @app.command("cancel")
268
318
  def cancel(
269
319
  issue_id: str = typer.Argument(..., help="Issue ID to cancel"),
270
320
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
321
+ json: AgentOutput = False,
271
322
  ):
272
323
  """Cancel issue."""
273
324
  config = get_config()
274
325
  issues_root = _resolve_issues_root(config, root)
275
326
  try:
276
- core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=IssueSolution.CANCELLED)
277
- console.print(f"[red]✘[/red] Issue [bold]{issue_id}[/bold] cancelled.")
327
+ issue = core.update_issue(issues_root, issue_id, status="closed", solution="cancelled")
328
+ OutputManager.print({
329
+ "issue": issue,
330
+ "status": "cancelled"
331
+ })
278
332
  except Exception as e:
279
- console.print(f"[red]✘ Error:[/red] {str(e)}")
333
+ OutputManager.error(str(e))
280
334
  raise typer.Exit(code=1)
281
335
 
282
336
  @app.command("delete")
283
337
  def delete(
284
338
  issue_id: str = typer.Argument(..., help="Issue ID to delete"),
285
339
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
340
+ json: AgentOutput = False,
286
341
  ):
287
342
  """Physically remove an issue file."""
288
343
  config = get_config()
289
344
  issues_root = _resolve_issues_root(config, root)
290
345
  try:
291
346
  core.delete_issue_file(issues_root, issue_id)
292
- console.print(f"[red]✔[/red] Issue [bold]{issue_id}[/bold] physically deleted.")
347
+ OutputManager.print({
348
+ "id": issue_id,
349
+ "status": "deleted"
350
+ })
293
351
  except Exception as e:
294
- console.print(f"[red]✘ Error:[/red] {str(e)}")
352
+ OutputManager.error(str(e))
295
353
  raise typer.Exit(code=1)
296
354
 
297
355
  @app.command("move")
@@ -300,6 +358,7 @@ def move(
300
358
  target: str = typer.Option(..., "--to", help="Target project directory (e.g., ../OtherProject)"),
301
359
  renumber: bool = typer.Option(False, "--renumber", help="Automatically renumber on ID conflict"),
302
360
  root: Optional[str] = typer.Option(None, "--root", help="Override source issues root directory"),
361
+ json: AgentOutput = False,
303
362
  ):
304
363
  """Move an issue to another project."""
305
364
  config = get_config()
@@ -314,7 +373,7 @@ def move(
314
373
  elif target_path.name == "Issues" and target_path.exists():
315
374
  target_issues_root = target_path
316
375
  else:
317
- console.print(f"[red]✘ Error:[/red] Target path must be a project root with 'Issues' directory or an 'Issues' directory itself.")
376
+ OutputManager.error("Target path must be a project root with 'Issues' directory or an 'Issues' directory itself.")
318
377
  raise typer.Exit(code=1)
319
378
 
320
379
  try:
@@ -330,32 +389,37 @@ def move(
330
389
  except ValueError:
331
390
  rel_path = new_path
332
391
 
333
- if updated_meta.id != issue_id:
334
- console.print(f"[green]✔[/green] Moved and renumbered: [bold]{issue_id}[/bold] → [bold]{updated_meta.id}[/bold]")
335
- else:
336
- console.print(f"[green]✔[/green] Moved [bold]{issue_id}[/bold] to target project.")
337
-
338
- console.print(f"[dim]New path: {rel_path}[/dim]")
392
+ OutputManager.print({
393
+ "issue": updated_meta,
394
+ "new_path": str(rel_path),
395
+ "status": "moved",
396
+ "renumbered": updated_meta.id != issue_id
397
+ })
339
398
 
340
399
  except FileNotFoundError as e:
341
- console.print(f"[red]✘ Error:[/red] {str(e)}")
400
+ OutputManager.error(str(e))
342
401
  raise typer.Exit(code=1)
343
402
  except ValueError as e:
344
- console.print(f"[red]✘ Conflict:[/red] {str(e)}")
403
+ OutputManager.error(str(e))
345
404
  raise typer.Exit(code=1)
346
405
  except Exception as e:
347
- console.print(f"[red]✘ Error:[/red] {str(e)}")
406
+ OutputManager.error(str(e))
348
407
  raise typer.Exit(code=1)
349
408
 
350
409
  @app.command("board")
351
410
  def board(
352
411
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
412
+ json: AgentOutput = False,
353
413
  ):
354
414
  """Visualize issues in a Kanban board."""
355
415
  config = get_config()
356
416
  issues_root = _resolve_issues_root(config, root)
357
417
 
358
418
  board_data = core.get_board_data(issues_root)
419
+
420
+ if OutputManager.is_agent_mode():
421
+ OutputManager.print(board_data)
422
+ return
359
423
 
360
424
  from rich.columns import Columns
361
425
  from rich.console import RenderableType
@@ -373,10 +437,10 @@ def board(
373
437
  issue_list = []
374
438
  for issue in sorted(issues, key=lambda x: x.updated_at, reverse=True):
375
439
  type_color = {
376
- IssueType.FEATURE: "green",
377
- IssueType.CHORE: "blue",
378
- IssueType.FIX: "red",
379
- IssueType.EPIC: "magenta"
440
+ "feature": "green",
441
+ "chore": "blue",
442
+ "fix": "red",
443
+ "epic": "magenta"
380
444
  }.get(issue.type, "white")
381
445
 
382
446
  issue_list.append(
@@ -404,10 +468,11 @@ def board(
404
468
  @app.command("list")
405
469
  def list_cmd(
406
470
  status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (open, closed, backlog, all)"),
407
- type: Optional[IssueType] = typer.Option(None, "--type", "-t", help="Filter by type"),
408
- 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"),
409
473
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
410
474
  workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
475
+ json: AgentOutput = False,
411
476
  ):
412
477
  """List issues in a table format with filtering."""
413
478
  config = get_config()
@@ -415,7 +480,7 @@ def list_cmd(
415
480
 
416
481
  # Validation
417
482
  if status and status.lower() not in ["open", "closed", "backlog", "all"]:
418
- console.print(f"[red]Invalid status:[/red] {status}. Use open, closed, backlog or all.")
483
+ OutputManager.error(f"Invalid status: {status}. Use open, closed, backlog or all.")
419
484
  raise typer.Exit(code=1)
420
485
 
421
486
  target_status = status.lower() if status else "open"
@@ -426,7 +491,7 @@ def list_cmd(
426
491
  for i in issues:
427
492
  # Status Filter
428
493
  if target_status != "all":
429
- if i.status.value != target_status:
494
+ if i.status != target_status:
430
495
  continue
431
496
 
432
497
  # Type Filter
@@ -439,12 +504,13 @@ def list_cmd(
439
504
 
440
505
  filtered.append(i)
441
506
 
442
- # Sort: Updated Descending
443
- filtered.append(i)
444
-
445
507
  # Sort: Updated Descending
446
508
  filtered.sort(key=lambda x: x.updated_at, reverse=True)
447
509
 
510
+ if OutputManager.is_agent_mode():
511
+ OutputManager.print(filtered)
512
+ return
513
+
448
514
  # Render
449
515
  _render_issues_table(filtered, title=f"Issues ({len(filtered)})")
450
516
 
@@ -474,13 +540,13 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
474
540
  t_color = type_colors.get(i.type, "white")
475
541
  s_color = status_colors.get(i.status, "white")
476
542
 
477
- stage_str = i.stage.value if i.stage else "-"
543
+ stage_str = i.stage if i.stage else "-"
478
544
  updated_str = i.updated_at.strftime("%Y-%m-%d %H:%M")
479
545
 
480
546
  table.add_row(
481
547
  i.id,
482
- f"[{t_color}]{i.type.value}[/{t_color}]",
483
- f"[{s_color}]{i.status.value}[/{s_color}]",
548
+ f"[{t_color}]{i.type}[/{t_color}]",
549
+ f"[{s_color}]{i.status}[/{s_color}]",
484
550
  stage_str,
485
551
  i.title,
486
552
  updated_str
@@ -492,6 +558,7 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
492
558
  def query_cmd(
493
559
  query: str = typer.Argument(..., help="Search query (e.g. '+bug -ui' or 'login')"),
494
560
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
561
+ json: AgentOutput = False,
495
562
  ):
496
563
  """
497
564
  Search issues using advanced syntax.
@@ -512,6 +579,10 @@ def query_cmd(
512
579
  # For now, updated at descending is useful.
513
580
  results.sort(key=lambda x: x.updated_at, reverse=True)
514
581
 
582
+ if OutputManager.is_agent_mode():
583
+ OutputManager.print(results)
584
+ return
585
+
515
586
  _render_issues_table(results, title=f"Search Results for '{query}' ({len(results)})")
516
587
 
517
588
  @app.command("scope")
@@ -521,6 +592,7 @@ def scope(
521
592
  recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
522
593
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
523
594
  workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
595
+ json: AgentOutput = False,
524
596
  ):
525
597
  """Show progress tree."""
526
598
  config = get_config()
@@ -538,12 +610,16 @@ def scope(
538
610
 
539
611
  issues = filtered_issues
540
612
 
613
+ if OutputManager.is_agent_mode():
614
+ OutputManager.print(issues)
615
+ return
616
+
541
617
  tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
542
- epics = sorted([i for i in issues if i.type == IssueType.EPIC], key=lambda x: x.id)
543
- stories = [i for i in issues if i.type == IssueType.FEATURE]
544
- 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"]]
545
621
 
546
- 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]"}
547
623
 
548
624
  for epic in epics:
549
625
  epic_node = tree.add(f"{status_map[epic.status]} [bold]{epic.id}[/bold]: {epic.title}")
@@ -556,6 +632,57 @@ def scope(
556
632
 
557
633
  console.print(Panel(tree, expand=False))
558
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
+
559
686
  @app.command("lint")
560
687
  def lint(
561
688
  recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
@@ -563,11 +690,16 @@ def lint(
563
690
  format: str = typer.Option("table", "--format", "-f", help="Output format (table, json)"),
564
691
  file: Optional[str] = typer.Option(None, "--file", help="Validate a single file instead of scanning the entire workspace"),
565
692
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
693
+ json: AgentOutput = False,
566
694
  ):
567
695
  """Verify the integrity of the Issues directory (declarative check)."""
568
696
  from . import linter
569
697
  config = get_config()
570
698
  issues_root = _resolve_issues_root(config, root)
699
+
700
+ if OutputManager.is_agent_mode():
701
+ format = "json"
702
+
571
703
  linter.run_lint(issues_root, recursive=recursive, fix=fix, format=format, file_path=file)
572
704
 
573
705
  def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
@@ -755,3 +887,40 @@ def commit(
755
887
  except Exception as e:
756
888
  console.print(f"[red]Git Error:[/red] {e}")
757
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]))