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.
- monoco/cli/project.py +15 -7
- monoco/cli/workspace.py +11 -3
- monoco/core/agent/adapters.py +24 -1
- monoco/core/config.py +81 -3
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/resources/en/SKILL.md +1 -1
- monoco/core/setup.py +8 -1
- monoco/daemon/app.py +18 -12
- monoco/features/agent/commands.py +94 -17
- 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/config/commands.py +35 -14
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +112 -16
- monoco/features/issue/commands.py +254 -85
- 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 +204 -65
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +5 -22
- monoco/main.py +11 -17
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
- monoco_toolkit-0.2.6.dist-info/RECORD +96 -0
- monoco/features/issue/executions/refine.md +0 -26
- 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.4.dist-info/RECORD +0 -78
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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')"),
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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[
|
|
76
|
-
stage: Optional[
|
|
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
|
-
|
|
115
|
+
OutputManager.print({
|
|
116
|
+
"issue": issue,
|
|
117
|
+
"status": "updated"
|
|
118
|
+
})
|
|
103
119
|
except Exception as e:
|
|
104
|
-
|
|
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=
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
180
|
+
OutputManager.error(f"Failed to create worktree: {e}")
|
|
157
181
|
raise typer.Exit(code=1)
|
|
158
182
|
|
|
159
|
-
|
|
183
|
+
OutputManager.print({
|
|
184
|
+
"issue": issue,
|
|
185
|
+
"status": "started",
|
|
186
|
+
"isolation": isolation_info
|
|
187
|
+
})
|
|
160
188
|
except Exception as e:
|
|
161
|
-
|
|
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=
|
|
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
|
-
|
|
212
|
+
report_status = "generated"
|
|
184
213
|
except Exception as e:
|
|
185
|
-
|
|
214
|
+
report_status = f"failed: {e}"
|
|
186
215
|
|
|
216
|
+
pruned_resources = []
|
|
187
217
|
if prune:
|
|
188
218
|
try:
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
247
|
-
|
|
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
|
-
|
|
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=
|
|
262
|
-
|
|
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
|
-
|
|
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=
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
+
OutputManager.print({
|
|
348
|
+
"id": issue_id,
|
|
349
|
+
"status": "deleted"
|
|
350
|
+
})
|
|
293
351
|
except Exception as e:
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
400
|
+
OutputManager.error(str(e))
|
|
342
401
|
raise typer.Exit(code=1)
|
|
343
402
|
except ValueError as e:
|
|
344
|
-
|
|
403
|
+
OutputManager.error(str(e))
|
|
345
404
|
raise typer.Exit(code=1)
|
|
346
405
|
except Exception as e:
|
|
347
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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[
|
|
408
|
-
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"),
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
483
|
-
f"[{s_color}]{i.status
|
|
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 ==
|
|
543
|
-
stories = [i for i in issues if i.type ==
|
|
544
|
-
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"]]
|
|
545
621
|
|
|
546
|
-
status_map = {
|
|
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]))
|