monoco-toolkit 0.1.0__py3-none-any.whl → 0.2.5__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 (69) hide show
  1. monoco/cli/__init__.py +0 -0
  2. monoco/cli/project.py +87 -0
  3. monoco/cli/workspace.py +46 -0
  4. monoco/core/agent/__init__.py +5 -0
  5. monoco/core/agent/action.py +144 -0
  6. monoco/core/agent/adapters.py +106 -0
  7. monoco/core/agent/protocol.py +31 -0
  8. monoco/core/agent/state.py +106 -0
  9. monoco/core/config.py +152 -17
  10. monoco/core/execution.py +62 -0
  11. monoco/core/feature.py +58 -0
  12. monoco/core/git.py +51 -2
  13. monoco/core/injection.py +196 -0
  14. monoco/core/integrations.py +234 -0
  15. monoco/core/lsp.py +61 -0
  16. monoco/core/output.py +13 -2
  17. monoco/core/registry.py +36 -0
  18. monoco/core/resources/en/AGENTS.md +8 -0
  19. monoco/core/resources/en/SKILL.md +66 -0
  20. monoco/core/resources/zh/AGENTS.md +8 -0
  21. monoco/core/resources/zh/SKILL.md +66 -0
  22. monoco/core/setup.py +88 -110
  23. monoco/core/skills.py +444 -0
  24. monoco/core/state.py +53 -0
  25. monoco/core/sync.py +224 -0
  26. monoco/core/telemetry.py +4 -1
  27. monoco/core/workspace.py +85 -20
  28. monoco/daemon/app.py +127 -58
  29. monoco/daemon/models.py +4 -0
  30. monoco/daemon/services.py +56 -155
  31. monoco/features/agent/commands.py +166 -0
  32. monoco/features/agent/doctor.py +30 -0
  33. monoco/features/config/commands.py +125 -44
  34. monoco/features/i18n/adapter.py +29 -0
  35. monoco/features/i18n/commands.py +89 -10
  36. monoco/features/i18n/core.py +113 -27
  37. monoco/features/i18n/resources/en/AGENTS.md +8 -0
  38. monoco/features/i18n/resources/en/SKILL.md +94 -0
  39. monoco/features/i18n/resources/zh/AGENTS.md +8 -0
  40. monoco/features/i18n/resources/zh/SKILL.md +94 -0
  41. monoco/features/issue/adapter.py +34 -0
  42. monoco/features/issue/commands.py +183 -65
  43. monoco/features/issue/core.py +172 -77
  44. monoco/features/issue/linter.py +215 -116
  45. monoco/features/issue/migration.py +134 -0
  46. monoco/features/issue/models.py +23 -19
  47. monoco/features/issue/monitor.py +94 -0
  48. monoco/features/issue/resources/en/AGENTS.md +15 -0
  49. monoco/features/issue/resources/en/SKILL.md +87 -0
  50. monoco/features/issue/resources/zh/AGENTS.md +15 -0
  51. monoco/features/issue/resources/zh/SKILL.md +114 -0
  52. monoco/features/issue/validator.py +269 -0
  53. monoco/features/pty/core.py +185 -0
  54. monoco/features/pty/router.py +138 -0
  55. monoco/features/pty/server.py +56 -0
  56. monoco/features/spike/adapter.py +30 -0
  57. monoco/features/spike/commands.py +45 -24
  58. monoco/features/spike/core.py +4 -21
  59. monoco/features/spike/resources/en/AGENTS.md +7 -0
  60. monoco/features/spike/resources/en/SKILL.md +74 -0
  61. monoco/features/spike/resources/zh/AGENTS.md +7 -0
  62. monoco/features/spike/resources/zh/SKILL.md +74 -0
  63. monoco/main.py +115 -2
  64. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +10 -3
  65. monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
  66. monoco_toolkit-0.1.0.dist-info/RECORD +0 -33
  67. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
  68. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
  69. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
@@ -8,7 +8,7 @@ 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
 
@@ -23,13 +23,14 @@ def create(
23
23
  title: str = typer.Option(..., "--title", "-t", help="Issue title"),
24
24
  parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
25
25
  is_backlog: bool = typer.Option(False, "--backlog", help="Create as backlog item"),
26
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Issue stage (todo, doing, review)"),
26
+ stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Issue stage (draft, doing, review)"),
27
27
  dependencies: List[str] = typer.Option([], "--dependency", "-d", help="Issue dependency ID(s)"),
28
28
  related: List[str] = typer.Option([], "--related", "-r", help="Related Issue ID(s)"),
29
29
  subdir: Optional[str] = typer.Option(None, "--subdir", "-s", help="Subdirectory for organization (e.g. 'Backend/Auth')"),
30
30
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
31
31
  tags: List[str] = typer.Option([], "--tag", help="Tags"),
32
32
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
33
+ json: AgentOutput = False,
33
34
  ):
34
35
  """Create a new issue."""
35
36
  config = get_config()
@@ -39,7 +40,7 @@ def create(
39
40
  if parent:
40
41
  parent_path = core.find_issue_path(issues_root, parent)
41
42
  if not parent_path:
42
- console.print(f"[red]✘ Error:[/red] Parent issue {parent} not found.")
43
+ OutputManager.error(f"Parent issue {parent} not found.")
43
44
  raise typer.Exit(code=1)
44
45
 
45
46
  try:
@@ -62,26 +63,75 @@ def create(
62
63
  except ValueError:
63
64
  rel_path = path
64
65
 
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]")
66
+ OutputManager.print({
67
+ "issue": issue,
68
+ "path": str(rel_path),
69
+ "status": "created"
70
+ })
71
+
67
72
  except ValueError as e:
68
- console.print(f"[red]✘ Error:[/red] {str(e)}")
73
+ OutputManager.error(str(e))
69
74
  raise typer.Exit(code=1)
70
75
 
76
+ @app.command("update")
77
+ def update(
78
+ issue_id: str = typer.Argument(..., help="Issue ID to update"),
79
+ 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"),
82
+ parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
83
+ sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
84
+ dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
85
+ related: Optional[List[str]] = typer.Option(None, "--related", "-r", help="Related Issue ID(s)"),
86
+ tags: Optional[List[str]] = typer.Option(None, "--tag", help="Tags"),
87
+ root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
88
+ json: AgentOutput = False,
89
+ ):
90
+ """Update an existing issue."""
91
+ config = get_config()
92
+ issues_root = _resolve_issues_root(config, root)
93
+
94
+ try:
95
+ issue = core.update_issue(
96
+ issues_root,
97
+ issue_id,
98
+ status=status,
99
+ stage=stage,
100
+ title=title,
101
+ parent=parent,
102
+ sprint=sprint,
103
+ dependencies=dependencies,
104
+ related=related,
105
+ tags=tags
106
+ )
107
+
108
+ OutputManager.print({
109
+ "issue": issue,
110
+ "status": "updated"
111
+ })
112
+ except Exception as e:
113
+ OutputManager.error(str(e))
114
+ raise typer.Exit(code=1)
115
+
116
+
71
117
  @app.command("open")
72
118
  def move_open(
73
119
  issue_id: str = typer.Argument(..., help="Issue ID to open"),
74
120
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
121
+ json: AgentOutput = False,
75
122
  ):
76
- """Move issue to open status and set stage to Todo."""
123
+ """Move issue to open status and set stage to Draft."""
77
124
  config = get_config()
78
125
  issues_root = _resolve_issues_root(config, root)
79
126
  try:
80
127
  # Pull operation: Force stage to TODO
81
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.TODO)
82
- console.print(f"[green]▶[/green] Issue [bold]{issue_id}[/bold] moved to open/todo.")
128
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
129
+ OutputManager.print({
130
+ "issue": issue,
131
+ "status": "opened"
132
+ })
83
133
  except Exception as e:
84
- console.print(f"[red]✘ Error:[/red] {str(e)}")
134
+ OutputManager.error(str(e))
85
135
  raise typer.Exit(code=1)
86
136
 
87
137
  @app.command("start")
@@ -90,6 +140,7 @@ def start(
90
140
  branch: bool = typer.Option(False, "--branch", "-b", help="Start in a new git branch"),
91
141
  worktree: bool = typer.Option(False, "--worktree", "-w", help="Start in a new git worktree"),
92
142
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
143
+ json: AgentOutput = False,
93
144
  ):
94
145
  """Start working on an issue (Stage -> Doing)."""
95
146
  config = get_config()
@@ -97,32 +148,38 @@ def start(
97
148
  project_root = _resolve_project_root(config)
98
149
 
99
150
  if branch and worktree:
100
- console.print("[red]Error:[/red] Cannot specify both --branch and --worktree.")
151
+ OutputManager.error("Cannot specify both --branch and --worktree.")
101
152
  raise typer.Exit(code=1)
102
153
 
103
154
  try:
104
155
  # Implicitly ensure status is Open
105
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DOING)
156
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DOING)
106
157
 
158
+ isolation_info = None
159
+
107
160
  if branch:
108
161
  try:
109
- meta = core.start_issue_isolation(issues_root, issue_id, IsolationType.BRANCH, project_root)
110
- console.print(f"[green]✔[/green] Switched to branch [bold]{meta.isolation.ref}[/bold]")
162
+ issue = core.start_issue_isolation(issues_root, issue_id, IsolationType.BRANCH, project_root)
163
+ isolation_info = {"type": "branch", "ref": issue.isolation.ref}
111
164
  except Exception as e:
112
- console.print(f"[red]Error:[/red] Failed to create branch: {e}")
165
+ OutputManager.error(f"Failed to create branch: {e}")
113
166
  raise typer.Exit(code=1)
114
167
 
115
168
  if worktree:
116
169
  try:
117
- meta = core.start_issue_isolation(issues_root, issue_id, IsolationType.WORKTREE, project_root)
118
- console.print(f"[green]✔[/green] Created worktree at [bold]{meta.isolation.path}[/bold]")
170
+ issue = core.start_issue_isolation(issues_root, issue_id, IsolationType.WORKTREE, project_root)
171
+ isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
119
172
  except Exception as e:
120
- console.print(f"[red]Error:[/red] Failed to create worktree: {e}")
173
+ OutputManager.error(f"Failed to create worktree: {e}")
121
174
  raise typer.Exit(code=1)
122
175
 
123
- console.print(f"[green]🚀[/green] Issue [bold]{issue_id}[/bold] started.")
176
+ OutputManager.print({
177
+ "issue": issue,
178
+ "status": "started",
179
+ "isolation": isolation_info
180
+ })
124
181
  except Exception as e:
125
- console.print(f"[red]✘ Error:[/red] {str(e)}")
182
+ OutputManager.error(str(e))
126
183
  raise typer.Exit(code=1)
127
184
 
128
185
  @app.command("submit")
@@ -131,6 +188,7 @@ def submit(
131
188
  prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after submit"),
132
189
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
133
190
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
191
+ json: AgentOutput = False,
134
192
  ):
135
193
  """Submit issue for review (Stage -> Review) and generate delivery report."""
136
194
  config = get_config()
@@ -138,27 +196,33 @@ def submit(
138
196
  project_root = _resolve_project_root(config)
139
197
  try:
140
198
  # Implicitly ensure status is Open
141
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.REVIEW)
142
- console.print(f"[green]🚀[/green] Issue [bold]{issue_id}[/bold] submitted for review.")
199
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.REVIEW)
143
200
 
144
201
  # Delivery Report Generation
202
+ report_status = "skipped"
145
203
  try:
146
204
  core.generate_delivery_report(issues_root, issue_id, project_root)
147
- console.print(f"[dim]✔ Delivery report appended to issue file.[/dim]")
205
+ report_status = "generated"
148
206
  except Exception as e:
149
- console.print(f"[yellow]⚠ Failed to generate delivery report: {e}[/yellow]")
207
+ report_status = f"failed: {e}"
150
208
 
209
+ pruned_resources = []
151
210
  if prune:
152
211
  try:
153
- deleted = core.prune_issue_resources(issues_root, issue_id, force, project_root)
154
- if deleted:
155
- console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
212
+ pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
156
213
  except Exception as e:
157
- console.print(f"[red]Prune Error:[/red] {e}")
214
+ OutputManager.error(f"Prune Error: {e}")
158
215
  raise typer.Exit(code=1)
159
216
 
217
+ OutputManager.print({
218
+ "issue": issue,
219
+ "status": "submitted",
220
+ "report": report_status,
221
+ "pruned": pruned_resources
222
+ })
223
+
160
224
  except Exception as e:
161
- console.print(f"[red]✘ Error:[/red] {str(e)}")
225
+ OutputManager.error(str(e))
162
226
  raise typer.Exit(code=1)
163
227
 
164
228
  @app.command("close")
@@ -168,86 +232,114 @@ def move_close(
168
232
  prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
169
233
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
170
234
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
235
+ json: AgentOutput = False,
171
236
  ):
172
237
  """Close issue."""
173
238
  config = get_config()
174
239
  issues_root = _resolve_issues_root(config, root)
175
240
  project_root = _resolve_project_root(config)
241
+
242
+ # Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
243
+ if solution is None:
244
+ valid_solutions = [e.value for e in IssueSolution]
245
+ OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
246
+ raise typer.Exit(code=1)
247
+
176
248
  try:
177
- core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=solution)
178
- console.print(f"[dim]✔[/dim] Issue [bold]{issue_id}[/bold] closed.")
249
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=solution)
179
250
 
251
+ pruned_resources = []
180
252
  if prune:
181
253
  try:
182
- deleted = core.prune_issue_resources(issues_root, issue_id, force, project_root)
183
- if deleted:
184
- console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
254
+ pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
185
255
  except Exception as e:
186
- console.print(f"[red]Prune Error:[/red] {e}")
256
+ OutputManager.error(f"Prune Error: {e}")
187
257
  raise typer.Exit(code=1)
188
258
 
259
+ OutputManager.print({
260
+ "issue": issue,
261
+ "status": "closed",
262
+ "pruned": pruned_resources
263
+ })
264
+
189
265
  except Exception as e:
190
- console.print(f"[red]✘ Error:[/red] {str(e)}")
266
+ OutputManager.error(str(e))
191
267
  raise typer.Exit(code=1)
192
268
 
193
269
  @backlog_app.command("push")
194
270
  def push(
195
271
  issue_id: str = typer.Argument(..., help="Issue ID to push to backlog"),
196
272
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
273
+ json: AgentOutput = False,
197
274
  ):
198
275
  """Push issue to backlog."""
199
276
  config = get_config()
200
277
  issues_root = _resolve_issues_root(config, root)
201
278
  try:
202
- core.update_issue(issues_root, issue_id, status=IssueStatus.BACKLOG)
203
- console.print(f"[blue]💤[/blue] Issue [bold]{issue_id}[/bold] pushed to backlog.")
279
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.BACKLOG)
280
+ OutputManager.print({
281
+ "issue": issue,
282
+ "status": "pushed_to_backlog"
283
+ })
204
284
  except Exception as e:
205
- console.print(f"[red]✘ Error:[/red] {str(e)}")
285
+ OutputManager.error(str(e))
206
286
  raise typer.Exit(code=1)
207
287
 
208
288
  @backlog_app.command("pull")
209
289
  def pull(
210
290
  issue_id: str = typer.Argument(..., help="Issue ID to pull from backlog"),
211
291
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
292
+ json: AgentOutput = False,
212
293
  ):
213
- """Pull issue from backlog (Open & Todo)."""
294
+ """Pull issue from backlog (Open & Draft)."""
214
295
  config = get_config()
215
296
  issues_root = _resolve_issues_root(config, root)
216
297
  try:
217
- core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.TODO)
218
- console.print(f"[green]🔥[/green] Issue [bold]{issue_id}[/bold] pulled from backlog.")
298
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
299
+ OutputManager.print({
300
+ "issue": issue,
301
+ "status": "pulled_from_backlog"
302
+ })
219
303
  except Exception as e:
220
- console.print(f"[red]✘ Error:[/red] {str(e)}")
304
+ OutputManager.error(str(e))
221
305
  raise typer.Exit(code=1)
222
306
 
223
307
  @app.command("cancel")
224
308
  def cancel(
225
309
  issue_id: str = typer.Argument(..., help="Issue ID to cancel"),
226
310
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
311
+ json: AgentOutput = False,
227
312
  ):
228
313
  """Cancel issue."""
229
314
  config = get_config()
230
315
  issues_root = _resolve_issues_root(config, root)
231
316
  try:
232
- core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=IssueSolution.CANCELLED)
233
- console.print(f"[red]✘[/red] Issue [bold]{issue_id}[/bold] cancelled.")
317
+ issue = core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=IssueSolution.CANCELLED)
318
+ OutputManager.print({
319
+ "issue": issue,
320
+ "status": "cancelled"
321
+ })
234
322
  except Exception as e:
235
- console.print(f"[red]✘ Error:[/red] {str(e)}")
323
+ OutputManager.error(str(e))
236
324
  raise typer.Exit(code=1)
237
325
 
238
326
  @app.command("delete")
239
327
  def delete(
240
328
  issue_id: str = typer.Argument(..., help="Issue ID to delete"),
241
329
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
330
+ json: AgentOutput = False,
242
331
  ):
243
332
  """Physically remove an issue file."""
244
333
  config = get_config()
245
334
  issues_root = _resolve_issues_root(config, root)
246
335
  try:
247
336
  core.delete_issue_file(issues_root, issue_id)
248
- console.print(f"[red]✔[/red] Issue [bold]{issue_id}[/bold] physically deleted.")
337
+ OutputManager.print({
338
+ "id": issue_id,
339
+ "status": "deleted"
340
+ })
249
341
  except Exception as e:
250
- console.print(f"[red]✘ Error:[/red] {str(e)}")
342
+ OutputManager.error(str(e))
251
343
  raise typer.Exit(code=1)
252
344
 
253
345
  @app.command("move")
@@ -256,6 +348,7 @@ def move(
256
348
  target: str = typer.Option(..., "--to", help="Target project directory (e.g., ../OtherProject)"),
257
349
  renumber: bool = typer.Option(False, "--renumber", help="Automatically renumber on ID conflict"),
258
350
  root: Optional[str] = typer.Option(None, "--root", help="Override source issues root directory"),
351
+ json: AgentOutput = False,
259
352
  ):
260
353
  """Move an issue to another project."""
261
354
  config = get_config()
@@ -270,7 +363,7 @@ def move(
270
363
  elif target_path.name == "Issues" and target_path.exists():
271
364
  target_issues_root = target_path
272
365
  else:
273
- console.print(f"[red]✘ Error:[/red] Target path must be a project root with 'Issues' directory or an 'Issues' directory itself.")
366
+ OutputManager.error("Target path must be a project root with 'Issues' directory or an 'Issues' directory itself.")
274
367
  raise typer.Exit(code=1)
275
368
 
276
369
  try:
@@ -286,32 +379,37 @@ def move(
286
379
  except ValueError:
287
380
  rel_path = new_path
288
381
 
289
- if updated_meta.id != issue_id:
290
- console.print(f"[green]✔[/green] Moved and renumbered: [bold]{issue_id}[/bold] → [bold]{updated_meta.id}[/bold]")
291
- else:
292
- console.print(f"[green]✔[/green] Moved [bold]{issue_id}[/bold] to target project.")
293
-
294
- console.print(f"[dim]New path: {rel_path}[/dim]")
382
+ OutputManager.print({
383
+ "issue": updated_meta,
384
+ "new_path": str(rel_path),
385
+ "status": "moved",
386
+ "renumbered": updated_meta.id != issue_id
387
+ })
295
388
 
296
389
  except FileNotFoundError as e:
297
- console.print(f"[red]✘ Error:[/red] {str(e)}")
390
+ OutputManager.error(str(e))
298
391
  raise typer.Exit(code=1)
299
392
  except ValueError as e:
300
- console.print(f"[red]✘ Conflict:[/red] {str(e)}")
393
+ OutputManager.error(str(e))
301
394
  raise typer.Exit(code=1)
302
395
  except Exception as e:
303
- console.print(f"[red]✘ Error:[/red] {str(e)}")
396
+ OutputManager.error(str(e))
304
397
  raise typer.Exit(code=1)
305
398
 
306
399
  @app.command("board")
307
400
  def board(
308
401
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
402
+ json: AgentOutput = False,
309
403
  ):
310
404
  """Visualize issues in a Kanban board."""
311
405
  config = get_config()
312
406
  issues_root = _resolve_issues_root(config, root)
313
407
 
314
408
  board_data = core.get_board_data(issues_root)
409
+
410
+ if OutputManager.is_agent_mode():
411
+ OutputManager.print(board_data)
412
+ return
315
413
 
316
414
  from rich.columns import Columns
317
415
  from rich.console import RenderableType
@@ -319,7 +417,7 @@ def board(
319
417
  columns: List[RenderableType] = []
320
418
 
321
419
  stage_titles = {
322
- "todo": "[bold white]TODO[/bold white]",
420
+ "draft": "[bold white]DRAFT[/bold white]",
323
421
  "doing": "[bold yellow]DOING[/bold yellow]",
324
422
  "review": "[bold cyan]REVIEW[/bold cyan]",
325
423
  "done": "[bold green]DONE[/bold green]"
@@ -364,6 +462,7 @@ def list_cmd(
364
462
  stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Filter by stage"),
365
463
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
366
464
  workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
465
+ json: AgentOutput = False,
367
466
  ):
368
467
  """List issues in a table format with filtering."""
369
468
  config = get_config()
@@ -371,7 +470,7 @@ def list_cmd(
371
470
 
372
471
  # Validation
373
472
  if status and status.lower() not in ["open", "closed", "backlog", "all"]:
374
- console.print(f"[red]Invalid status:[/red] {status}. Use open, closed, backlog or all.")
473
+ OutputManager.error(f"Invalid status: {status}. Use open, closed, backlog or all.")
375
474
  raise typer.Exit(code=1)
376
475
 
377
476
  target_status = status.lower() if status else "open"
@@ -395,12 +494,13 @@ def list_cmd(
395
494
 
396
495
  filtered.append(i)
397
496
 
398
- # Sort: Updated Descending
399
- filtered.append(i)
400
-
401
497
  # Sort: Updated Descending
402
498
  filtered.sort(key=lambda x: x.updated_at, reverse=True)
403
499
 
500
+ if OutputManager.is_agent_mode():
501
+ OutputManager.print(filtered)
502
+ return
503
+
404
504
  # Render
405
505
  _render_issues_table(filtered, title=f"Issues ({len(filtered)})")
406
506
 
@@ -448,6 +548,7 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
448
548
  def query_cmd(
449
549
  query: str = typer.Argument(..., help="Search query (e.g. '+bug -ui' or 'login')"),
450
550
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
551
+ json: AgentOutput = False,
451
552
  ):
452
553
  """
453
554
  Search issues using advanced syntax.
@@ -468,6 +569,10 @@ def query_cmd(
468
569
  # For now, updated at descending is useful.
469
570
  results.sort(key=lambda x: x.updated_at, reverse=True)
470
571
 
572
+ if OutputManager.is_agent_mode():
573
+ OutputManager.print(results)
574
+ return
575
+
471
576
  _render_issues_table(results, title=f"Search Results for '{query}' ({len(results)})")
472
577
 
473
578
  @app.command("scope")
@@ -477,6 +582,7 @@ def scope(
477
582
  recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
478
583
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
479
584
  workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
585
+ json: AgentOutput = False,
480
586
  ):
481
587
  """Show progress tree."""
482
588
  config = get_config()
@@ -494,6 +600,10 @@ def scope(
494
600
 
495
601
  issues = filtered_issues
496
602
 
603
+ if OutputManager.is_agent_mode():
604
+ OutputManager.print(issues)
605
+ return
606
+
497
607
  tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
498
608
  epics = sorted([i for i in issues if i.type == IssueType.EPIC], key=lambda x: x.id)
499
609
  stories = [i for i in issues if i.type == IssueType.FEATURE]
@@ -515,13 +625,21 @@ def scope(
515
625
  @app.command("lint")
516
626
  def lint(
517
627
  recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
628
+ fix: bool = typer.Option(False, "--fix", help="Attempt to automatically fix issues (e.g. missing headings)"),
629
+ format: str = typer.Option("table", "--format", "-f", help="Output format (table, json)"),
630
+ file: Optional[str] = typer.Option(None, "--file", help="Validate a single file instead of scanning the entire workspace"),
518
631
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
632
+ json: AgentOutput = False,
519
633
  ):
520
634
  """Verify the integrity of the Issues directory (declarative check)."""
521
635
  from . import linter
522
636
  config = get_config()
523
637
  issues_root = _resolve_issues_root(config, root)
524
- linter.run_lint(issues_root, recursive=recursive)
638
+
639
+ if OutputManager.is_agent_mode():
640
+ format = "json"
641
+
642
+ linter.run_lint(issues_root, recursive=recursive, fix=fix, format=format, file_path=file)
525
643
 
526
644
  def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
527
645
  """
@@ -547,7 +665,7 @@ def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
547
665
  # We need to detect if we are in a Workspace Root with multiple projects
548
666
  cwd = Path.cwd()
549
667
 
550
- # If CWD is NOT a project root (no monoco.yaml/Issues), scan for subprojects
668
+ # If CWD is NOT a project root (no .monoco/), scan for subprojects
551
669
  if not is_project_root(cwd):
552
670
  subprojects = find_projects(cwd)
553
671
  if len(subprojects) > 1: