monoco-toolkit 0.1.1__py3-none-any.whl → 0.2.8__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/__init__.py +0 -0
- monoco/cli/project.py +87 -0
- monoco/cli/workspace.py +46 -0
- monoco/core/agent/__init__.py +5 -0
- monoco/core/agent/action.py +144 -0
- monoco/core/agent/adapters.py +129 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +106 -0
- monoco/core/config.py +212 -17
- monoco/core/execution.py +62 -0
- monoco/core/feature.py +58 -0
- monoco/core/git.py +51 -2
- monoco/core/injection.py +196 -0
- monoco/core/integrations.py +242 -0
- monoco/core/lsp.py +68 -0
- monoco/core/output.py +21 -3
- monoco/core/registry.py +36 -0
- monoco/core/resources/en/AGENTS.md +8 -0
- monoco/core/resources/en/SKILL.md +66 -0
- monoco/core/resources/zh/AGENTS.md +8 -0
- monoco/core/resources/zh/SKILL.md +65 -0
- monoco/core/setup.py +96 -110
- monoco/core/skills.py +444 -0
- monoco/core/state.py +53 -0
- monoco/core/sync.py +224 -0
- monoco/core/telemetry.py +4 -1
- monoco/core/workspace.py +85 -20
- monoco/daemon/app.py +127 -58
- monoco/daemon/models.py +4 -0
- monoco/daemon/services.py +56 -155
- monoco/features/config/commands.py +125 -44
- monoco/features/i18n/adapter.py +29 -0
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +113 -27
- monoco/features/i18n/resources/en/AGENTS.md +8 -0
- monoco/features/i18n/resources/en/SKILL.md +94 -0
- monoco/features/i18n/resources/zh/AGENTS.md +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +94 -0
- monoco/features/issue/adapter.py +34 -0
- monoco/features/issue/commands.py +343 -101
- monoco/features/issue/core.py +384 -150
- 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 +172 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +325 -120
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +46 -24
- monoco/features/issue/monitor.py +94 -0
- monoco/features/issue/resources/en/AGENTS.md +20 -0
- monoco/features/issue/resources/en/SKILL.md +111 -0
- monoco/features/issue/resources/zh/AGENTS.md +20 -0
- monoco/features/issue/resources/zh/SKILL.md +138 -0
- monoco/features/issue/validator.py +455 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +6 -40
- monoco/features/spike/resources/en/AGENTS.md +7 -0
- monoco/features/spike/resources/en/SKILL.md +74 -0
- monoco/features/spike/resources/zh/AGENTS.md +7 -0
- monoco/features/spike/resources/zh/SKILL.md +74 -0
- monoco/main.py +91 -2
- monoco_toolkit-0.2.8.dist-info/METADATA +136 -0
- monoco_toolkit-0.2.8.dist-info/RECORD +83 -0
- monoco_toolkit-0.1.1.dist-info/METADATA +0 -93
- monoco_toolkit-0.1.1.dist-info/RECORD +0 -33
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,38 +8,49 @@ 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"
|
|
42
|
+
|
|
43
|
+
# Sanitize inputs (strip #)
|
|
44
|
+
if parent and parent.startswith("#"):
|
|
45
|
+
parent = parent[1:]
|
|
38
46
|
|
|
47
|
+
dependencies = [d[1:] if d.startswith("#") else d for d in dependencies]
|
|
48
|
+
related = [r[1:] if r.startswith("#") else r for r in related]
|
|
49
|
+
|
|
39
50
|
if parent:
|
|
40
51
|
parent_path = core.find_issue_path(issues_root, parent)
|
|
41
52
|
if not parent_path:
|
|
42
|
-
|
|
53
|
+
OutputManager.error(f"Parent issue {parent} not found.")
|
|
43
54
|
raise typer.Exit(code=1)
|
|
44
55
|
|
|
45
56
|
try:
|
|
@@ -62,26 +73,79 @@ def create(
|
|
|
62
73
|
except ValueError:
|
|
63
74
|
rel_path = path
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
if OutputManager.is_agent_mode():
|
|
77
|
+
OutputManager.print({
|
|
78
|
+
"issue": issue,
|
|
79
|
+
"path": str(rel_path),
|
|
80
|
+
"status": "created"
|
|
81
|
+
})
|
|
82
|
+
else:
|
|
83
|
+
console.print(f"[green]✔ Created {issue.id} in status {issue.status}.[/green]")
|
|
84
|
+
console.print(f"Path: {rel_path}")
|
|
85
|
+
|
|
67
86
|
except ValueError as e:
|
|
68
|
-
|
|
87
|
+
OutputManager.error(str(e))
|
|
69
88
|
raise typer.Exit(code=1)
|
|
70
89
|
|
|
90
|
+
@app.command("update")
|
|
91
|
+
def update(
|
|
92
|
+
issue_id: str = typer.Argument(..., help="Issue ID to update"),
|
|
93
|
+
title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
|
|
94
|
+
status: Optional[str] = typer.Option(None, "--status", help="New status"),
|
|
95
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
|
|
96
|
+
parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
|
|
97
|
+
sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
|
|
98
|
+
dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
|
|
99
|
+
related: Optional[List[str]] = typer.Option(None, "--related", "-r", help="Related Issue ID(s)"),
|
|
100
|
+
tags: Optional[List[str]] = typer.Option(None, "--tag", help="Tags"),
|
|
101
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
102
|
+
json: AgentOutput = False,
|
|
103
|
+
):
|
|
104
|
+
"""Update an existing issue."""
|
|
105
|
+
config = get_config()
|
|
106
|
+
issues_root = _resolve_issues_root(config, root)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
issue = core.update_issue(
|
|
110
|
+
issues_root,
|
|
111
|
+
issue_id,
|
|
112
|
+
status=status,
|
|
113
|
+
stage=stage,
|
|
114
|
+
title=title,
|
|
115
|
+
parent=parent,
|
|
116
|
+
sprint=sprint,
|
|
117
|
+
dependencies=dependencies,
|
|
118
|
+
related=related,
|
|
119
|
+
tags=tags
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
OutputManager.print({
|
|
123
|
+
"issue": issue,
|
|
124
|
+
"status": "updated"
|
|
125
|
+
})
|
|
126
|
+
except Exception as e:
|
|
127
|
+
OutputManager.error(str(e))
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
|
|
71
131
|
@app.command("open")
|
|
72
132
|
def move_open(
|
|
73
133
|
issue_id: str = typer.Argument(..., help="Issue ID to open"),
|
|
74
134
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
135
|
+
json: AgentOutput = False,
|
|
75
136
|
):
|
|
76
|
-
"""Move issue to open status and set stage to
|
|
137
|
+
"""Move issue to open status and set stage to Draft."""
|
|
77
138
|
config = get_config()
|
|
78
139
|
issues_root = _resolve_issues_root(config, root)
|
|
79
140
|
try:
|
|
80
141
|
# Pull operation: Force stage to TODO
|
|
81
|
-
core.update_issue(issues_root, issue_id, status=
|
|
82
|
-
|
|
142
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
143
|
+
OutputManager.print({
|
|
144
|
+
"issue": issue,
|
|
145
|
+
"status": "opened"
|
|
146
|
+
})
|
|
83
147
|
except Exception as e:
|
|
84
|
-
|
|
148
|
+
OutputManager.error(str(e))
|
|
85
149
|
raise typer.Exit(code=1)
|
|
86
150
|
|
|
87
151
|
@app.command("start")
|
|
@@ -90,6 +154,7 @@ def start(
|
|
|
90
154
|
branch: bool = typer.Option(False, "--branch", "-b", help="Start in a new git branch"),
|
|
91
155
|
worktree: bool = typer.Option(False, "--worktree", "-w", help="Start in a new git worktree"),
|
|
92
156
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
157
|
+
json: AgentOutput = False,
|
|
93
158
|
):
|
|
94
159
|
"""Start working on an issue (Stage -> Doing)."""
|
|
95
160
|
config = get_config()
|
|
@@ -97,32 +162,38 @@ def start(
|
|
|
97
162
|
project_root = _resolve_project_root(config)
|
|
98
163
|
|
|
99
164
|
if branch and worktree:
|
|
100
|
-
|
|
165
|
+
OutputManager.error("Cannot specify both --branch and --worktree.")
|
|
101
166
|
raise typer.Exit(code=1)
|
|
102
167
|
|
|
103
168
|
try:
|
|
104
169
|
# Implicitly ensure status is Open
|
|
105
|
-
core.update_issue(issues_root, issue_id, status=
|
|
170
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="doing")
|
|
106
171
|
|
|
172
|
+
isolation_info = None
|
|
173
|
+
|
|
107
174
|
if branch:
|
|
108
175
|
try:
|
|
109
|
-
|
|
110
|
-
|
|
176
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "branch", project_root)
|
|
177
|
+
isolation_info = {"type": "branch", "ref": issue.isolation.ref}
|
|
111
178
|
except Exception as e:
|
|
112
|
-
|
|
179
|
+
OutputManager.error(f"Failed to create branch: {e}")
|
|
113
180
|
raise typer.Exit(code=1)
|
|
114
181
|
|
|
115
182
|
if worktree:
|
|
116
183
|
try:
|
|
117
|
-
|
|
118
|
-
|
|
184
|
+
issue = core.start_issue_isolation(issues_root, issue_id, "worktree", project_root)
|
|
185
|
+
isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
|
|
119
186
|
except Exception as e:
|
|
120
|
-
|
|
187
|
+
OutputManager.error(f"Failed to create worktree: {e}")
|
|
121
188
|
raise typer.Exit(code=1)
|
|
122
189
|
|
|
123
|
-
|
|
190
|
+
OutputManager.print({
|
|
191
|
+
"issue": issue,
|
|
192
|
+
"status": "started",
|
|
193
|
+
"isolation": isolation_info
|
|
194
|
+
})
|
|
124
195
|
except Exception as e:
|
|
125
|
-
|
|
196
|
+
OutputManager.error(str(e))
|
|
126
197
|
raise typer.Exit(code=1)
|
|
127
198
|
|
|
128
199
|
@app.command("submit")
|
|
@@ -131,6 +202,7 @@ def submit(
|
|
|
131
202
|
prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after submit"),
|
|
132
203
|
force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
|
|
133
204
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
205
|
+
json: AgentOutput = False,
|
|
134
206
|
):
|
|
135
207
|
"""Submit issue for review (Stage -> Review) and generate delivery report."""
|
|
136
208
|
config = get_config()
|
|
@@ -138,116 +210,153 @@ def submit(
|
|
|
138
210
|
project_root = _resolve_project_root(config)
|
|
139
211
|
try:
|
|
140
212
|
# Implicitly ensure status is Open
|
|
141
|
-
core.update_issue(issues_root, issue_id, status=
|
|
142
|
-
console.print(f"[green]🚀[/green] Issue [bold]{issue_id}[/bold] submitted for review.")
|
|
213
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
|
|
143
214
|
|
|
144
215
|
# Delivery Report Generation
|
|
216
|
+
report_status = "skipped"
|
|
145
217
|
try:
|
|
146
218
|
core.generate_delivery_report(issues_root, issue_id, project_root)
|
|
147
|
-
|
|
219
|
+
report_status = "generated"
|
|
148
220
|
except Exception as e:
|
|
149
|
-
|
|
221
|
+
report_status = f"failed: {e}"
|
|
150
222
|
|
|
223
|
+
pruned_resources = []
|
|
151
224
|
if prune:
|
|
152
225
|
try:
|
|
153
|
-
|
|
154
|
-
if deleted:
|
|
155
|
-
console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
|
|
226
|
+
pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
|
|
156
227
|
except Exception as e:
|
|
157
|
-
|
|
228
|
+
OutputManager.error(f"Prune Error: {e}")
|
|
158
229
|
raise typer.Exit(code=1)
|
|
159
230
|
|
|
231
|
+
OutputManager.print({
|
|
232
|
+
"issue": issue,
|
|
233
|
+
"status": "submitted",
|
|
234
|
+
"report": report_status,
|
|
235
|
+
"pruned": pruned_resources
|
|
236
|
+
})
|
|
237
|
+
|
|
160
238
|
except Exception as e:
|
|
161
|
-
|
|
239
|
+
OutputManager.error(str(e))
|
|
162
240
|
raise typer.Exit(code=1)
|
|
163
241
|
|
|
164
242
|
@app.command("close")
|
|
165
243
|
def move_close(
|
|
166
244
|
issue_id: str = typer.Argument(..., help="Issue ID to close"),
|
|
167
|
-
solution: Optional[
|
|
245
|
+
solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type"),
|
|
168
246
|
prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
|
|
169
247
|
force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
|
|
170
248
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
249
|
+
json: AgentOutput = False,
|
|
171
250
|
):
|
|
172
251
|
"""Close issue."""
|
|
173
252
|
config = get_config()
|
|
174
253
|
issues_root = _resolve_issues_root(config, root)
|
|
175
254
|
project_root = _resolve_project_root(config)
|
|
255
|
+
|
|
256
|
+
# Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
|
|
257
|
+
if solution is None:
|
|
258
|
+
# Resolve options from engine
|
|
259
|
+
from .engine import get_engine
|
|
260
|
+
engine = get_engine(str(issues_root.parent))
|
|
261
|
+
valid_solutions = engine.issue_config.solutions or []
|
|
262
|
+
OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
|
|
263
|
+
raise typer.Exit(code=1)
|
|
264
|
+
|
|
176
265
|
try:
|
|
177
|
-
core.update_issue(issues_root, issue_id, status=
|
|
178
|
-
console.print(f"[dim]✔[/dim] Issue [bold]{issue_id}[/bold] closed.")
|
|
266
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution=solution)
|
|
179
267
|
|
|
268
|
+
pruned_resources = []
|
|
180
269
|
if prune:
|
|
181
270
|
try:
|
|
182
|
-
|
|
183
|
-
if deleted:
|
|
184
|
-
console.print(f"[dim]✔ Pruned resources: {', '.join(deleted)}[/dim]")
|
|
271
|
+
pruned_resources = core.prune_issue_resources(issues_root, issue_id, force, project_root)
|
|
185
272
|
except Exception as e:
|
|
186
|
-
|
|
273
|
+
OutputManager.error(f"Prune Error: {e}")
|
|
187
274
|
raise typer.Exit(code=1)
|
|
188
275
|
|
|
276
|
+
OutputManager.print({
|
|
277
|
+
"issue": issue,
|
|
278
|
+
"status": "closed",
|
|
279
|
+
"pruned": pruned_resources
|
|
280
|
+
})
|
|
281
|
+
|
|
189
282
|
except Exception as e:
|
|
190
|
-
|
|
283
|
+
OutputManager.error(str(e))
|
|
191
284
|
raise typer.Exit(code=1)
|
|
192
285
|
|
|
193
286
|
@backlog_app.command("push")
|
|
194
287
|
def push(
|
|
195
288
|
issue_id: str = typer.Argument(..., help="Issue ID to push to backlog"),
|
|
196
289
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
290
|
+
json: AgentOutput = False,
|
|
197
291
|
):
|
|
198
292
|
"""Push issue to backlog."""
|
|
199
293
|
config = get_config()
|
|
200
294
|
issues_root = _resolve_issues_root(config, root)
|
|
201
295
|
try:
|
|
202
|
-
core.update_issue(issues_root, issue_id, status=
|
|
203
|
-
|
|
296
|
+
issue = core.update_issue(issues_root, issue_id, status="backlog")
|
|
297
|
+
OutputManager.print({
|
|
298
|
+
"issue": issue,
|
|
299
|
+
"status": "pushed_to_backlog"
|
|
300
|
+
})
|
|
204
301
|
except Exception as e:
|
|
205
|
-
|
|
302
|
+
OutputManager.error(str(e))
|
|
206
303
|
raise typer.Exit(code=1)
|
|
207
304
|
|
|
208
305
|
@backlog_app.command("pull")
|
|
209
306
|
def pull(
|
|
210
307
|
issue_id: str = typer.Argument(..., help="Issue ID to pull from backlog"),
|
|
211
308
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
309
|
+
json: AgentOutput = False,
|
|
212
310
|
):
|
|
213
|
-
"""Pull issue from backlog (Open &
|
|
311
|
+
"""Pull issue from backlog (Open & Draft)."""
|
|
214
312
|
config = get_config()
|
|
215
313
|
issues_root = _resolve_issues_root(config, root)
|
|
216
314
|
try:
|
|
217
|
-
core.update_issue(issues_root, issue_id, status=
|
|
218
|
-
|
|
315
|
+
issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
|
|
316
|
+
OutputManager.print({
|
|
317
|
+
"issue": issue,
|
|
318
|
+
"status": "pulled_from_backlog"
|
|
319
|
+
})
|
|
219
320
|
except Exception as e:
|
|
220
|
-
|
|
321
|
+
OutputManager.error(str(e))
|
|
221
322
|
raise typer.Exit(code=1)
|
|
222
323
|
|
|
223
324
|
@app.command("cancel")
|
|
224
325
|
def cancel(
|
|
225
326
|
issue_id: str = typer.Argument(..., help="Issue ID to cancel"),
|
|
226
327
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
328
|
+
json: AgentOutput = False,
|
|
227
329
|
):
|
|
228
330
|
"""Cancel issue."""
|
|
229
331
|
config = get_config()
|
|
230
332
|
issues_root = _resolve_issues_root(config, root)
|
|
231
333
|
try:
|
|
232
|
-
core.update_issue(issues_root, issue_id, status=
|
|
233
|
-
|
|
334
|
+
issue = core.update_issue(issues_root, issue_id, status="closed", solution="cancelled")
|
|
335
|
+
OutputManager.print({
|
|
336
|
+
"issue": issue,
|
|
337
|
+
"status": "cancelled"
|
|
338
|
+
})
|
|
234
339
|
except Exception as e:
|
|
235
|
-
|
|
340
|
+
OutputManager.error(str(e))
|
|
236
341
|
raise typer.Exit(code=1)
|
|
237
342
|
|
|
238
343
|
@app.command("delete")
|
|
239
344
|
def delete(
|
|
240
345
|
issue_id: str = typer.Argument(..., help="Issue ID to delete"),
|
|
241
346
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
347
|
+
json: AgentOutput = False,
|
|
242
348
|
):
|
|
243
349
|
"""Physically remove an issue file."""
|
|
244
350
|
config = get_config()
|
|
245
351
|
issues_root = _resolve_issues_root(config, root)
|
|
246
352
|
try:
|
|
247
353
|
core.delete_issue_file(issues_root, issue_id)
|
|
248
|
-
|
|
354
|
+
OutputManager.print({
|
|
355
|
+
"id": issue_id,
|
|
356
|
+
"status": "deleted"
|
|
357
|
+
})
|
|
249
358
|
except Exception as e:
|
|
250
|
-
|
|
359
|
+
OutputManager.error(str(e))
|
|
251
360
|
raise typer.Exit(code=1)
|
|
252
361
|
|
|
253
362
|
@app.command("move")
|
|
@@ -256,6 +365,7 @@ def move(
|
|
|
256
365
|
target: str = typer.Option(..., "--to", help="Target project directory (e.g., ../OtherProject)"),
|
|
257
366
|
renumber: bool = typer.Option(False, "--renumber", help="Automatically renumber on ID conflict"),
|
|
258
367
|
root: Optional[str] = typer.Option(None, "--root", help="Override source issues root directory"),
|
|
368
|
+
json: AgentOutput = False,
|
|
259
369
|
):
|
|
260
370
|
"""Move an issue to another project."""
|
|
261
371
|
config = get_config()
|
|
@@ -270,7 +380,7 @@ def move(
|
|
|
270
380
|
elif target_path.name == "Issues" and target_path.exists():
|
|
271
381
|
target_issues_root = target_path
|
|
272
382
|
else:
|
|
273
|
-
|
|
383
|
+
OutputManager.error("Target path must be a project root with 'Issues' directory or an 'Issues' directory itself.")
|
|
274
384
|
raise typer.Exit(code=1)
|
|
275
385
|
|
|
276
386
|
try:
|
|
@@ -286,32 +396,37 @@ def move(
|
|
|
286
396
|
except ValueError:
|
|
287
397
|
rel_path = new_path
|
|
288
398
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
399
|
+
OutputManager.print({
|
|
400
|
+
"issue": updated_meta,
|
|
401
|
+
"new_path": str(rel_path),
|
|
402
|
+
"status": "moved",
|
|
403
|
+
"renumbered": updated_meta.id != issue_id
|
|
404
|
+
})
|
|
295
405
|
|
|
296
406
|
except FileNotFoundError as e:
|
|
297
|
-
|
|
407
|
+
OutputManager.error(str(e))
|
|
298
408
|
raise typer.Exit(code=1)
|
|
299
409
|
except ValueError as e:
|
|
300
|
-
|
|
410
|
+
OutputManager.error(str(e))
|
|
301
411
|
raise typer.Exit(code=1)
|
|
302
412
|
except Exception as e:
|
|
303
|
-
|
|
413
|
+
OutputManager.error(str(e))
|
|
304
414
|
raise typer.Exit(code=1)
|
|
305
415
|
|
|
306
416
|
@app.command("board")
|
|
307
417
|
def board(
|
|
308
418
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
419
|
+
json: AgentOutput = False,
|
|
309
420
|
):
|
|
310
421
|
"""Visualize issues in a Kanban board."""
|
|
311
422
|
config = get_config()
|
|
312
423
|
issues_root = _resolve_issues_root(config, root)
|
|
313
424
|
|
|
314
425
|
board_data = core.get_board_data(issues_root)
|
|
426
|
+
|
|
427
|
+
if OutputManager.is_agent_mode():
|
|
428
|
+
OutputManager.print(board_data)
|
|
429
|
+
return
|
|
315
430
|
|
|
316
431
|
from rich.columns import Columns
|
|
317
432
|
from rich.console import RenderableType
|
|
@@ -319,7 +434,7 @@ def board(
|
|
|
319
434
|
columns: List[RenderableType] = []
|
|
320
435
|
|
|
321
436
|
stage_titles = {
|
|
322
|
-
"
|
|
437
|
+
"draft": "[bold white]DRAFT[/bold white]",
|
|
323
438
|
"doing": "[bold yellow]DOING[/bold yellow]",
|
|
324
439
|
"review": "[bold cyan]REVIEW[/bold cyan]",
|
|
325
440
|
"done": "[bold green]DONE[/bold green]"
|
|
@@ -329,10 +444,10 @@ def board(
|
|
|
329
444
|
issue_list = []
|
|
330
445
|
for issue in sorted(issues, key=lambda x: x.updated_at, reverse=True):
|
|
331
446
|
type_color = {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
447
|
+
"feature": "green",
|
|
448
|
+
"chore": "blue",
|
|
449
|
+
"fix": "red",
|
|
450
|
+
"epic": "magenta"
|
|
336
451
|
}.get(issue.type, "white")
|
|
337
452
|
|
|
338
453
|
issue_list.append(
|
|
@@ -360,10 +475,11 @@ def board(
|
|
|
360
475
|
@app.command("list")
|
|
361
476
|
def list_cmd(
|
|
362
477
|
status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (open, closed, backlog, all)"),
|
|
363
|
-
type: Optional[
|
|
364
|
-
stage: Optional[
|
|
478
|
+
type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
|
|
479
|
+
stage: Optional[str] = typer.Option(None, "--stage", help="Filter by stage"),
|
|
365
480
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
366
481
|
workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
|
|
482
|
+
json: AgentOutput = False,
|
|
367
483
|
):
|
|
368
484
|
"""List issues in a table format with filtering."""
|
|
369
485
|
config = get_config()
|
|
@@ -371,7 +487,7 @@ def list_cmd(
|
|
|
371
487
|
|
|
372
488
|
# Validation
|
|
373
489
|
if status and status.lower() not in ["open", "closed", "backlog", "all"]:
|
|
374
|
-
|
|
490
|
+
OutputManager.error(f"Invalid status: {status}. Use open, closed, backlog or all.")
|
|
375
491
|
raise typer.Exit(code=1)
|
|
376
492
|
|
|
377
493
|
target_status = status.lower() if status else "open"
|
|
@@ -382,7 +498,7 @@ def list_cmd(
|
|
|
382
498
|
for i in issues:
|
|
383
499
|
# Status Filter
|
|
384
500
|
if target_status != "all":
|
|
385
|
-
if i.status
|
|
501
|
+
if i.status != target_status:
|
|
386
502
|
continue
|
|
387
503
|
|
|
388
504
|
# Type Filter
|
|
@@ -395,12 +511,13 @@ def list_cmd(
|
|
|
395
511
|
|
|
396
512
|
filtered.append(i)
|
|
397
513
|
|
|
398
|
-
# Sort: Updated Descending
|
|
399
|
-
filtered.append(i)
|
|
400
|
-
|
|
401
514
|
# Sort: Updated Descending
|
|
402
515
|
filtered.sort(key=lambda x: x.updated_at, reverse=True)
|
|
403
516
|
|
|
517
|
+
if OutputManager.is_agent_mode():
|
|
518
|
+
OutputManager.print(filtered)
|
|
519
|
+
return
|
|
520
|
+
|
|
404
521
|
# Render
|
|
405
522
|
_render_issues_table(filtered, title=f"Issues ({len(filtered)})")
|
|
406
523
|
|
|
@@ -430,13 +547,13 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
|
|
|
430
547
|
t_color = type_colors.get(i.type, "white")
|
|
431
548
|
s_color = status_colors.get(i.status, "white")
|
|
432
549
|
|
|
433
|
-
stage_str = i.stage
|
|
550
|
+
stage_str = i.stage if i.stage else "-"
|
|
434
551
|
updated_str = i.updated_at.strftime("%Y-%m-%d %H:%M")
|
|
435
552
|
|
|
436
553
|
table.add_row(
|
|
437
554
|
i.id,
|
|
438
|
-
f"[{t_color}]{i.type
|
|
439
|
-
f"[{s_color}]{i.status
|
|
555
|
+
f"[{t_color}]{i.type}[/{t_color}]",
|
|
556
|
+
f"[{s_color}]{i.status}[/{s_color}]",
|
|
440
557
|
stage_str,
|
|
441
558
|
i.title,
|
|
442
559
|
updated_str
|
|
@@ -448,6 +565,7 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
|
|
|
448
565
|
def query_cmd(
|
|
449
566
|
query: str = typer.Argument(..., help="Search query (e.g. '+bug -ui' or 'login')"),
|
|
450
567
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
568
|
+
json: AgentOutput = False,
|
|
451
569
|
):
|
|
452
570
|
"""
|
|
453
571
|
Search issues using advanced syntax.
|
|
@@ -468,6 +586,10 @@ def query_cmd(
|
|
|
468
586
|
# For now, updated at descending is useful.
|
|
469
587
|
results.sort(key=lambda x: x.updated_at, reverse=True)
|
|
470
588
|
|
|
589
|
+
if OutputManager.is_agent_mode():
|
|
590
|
+
OutputManager.print(results)
|
|
591
|
+
return
|
|
592
|
+
|
|
471
593
|
_render_issues_table(results, title=f"Search Results for '{query}' ({len(results)})")
|
|
472
594
|
|
|
473
595
|
@app.command("scope")
|
|
@@ -477,6 +599,7 @@ def scope(
|
|
|
477
599
|
recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
|
|
478
600
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
479
601
|
workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
|
|
602
|
+
json: AgentOutput = False,
|
|
480
603
|
):
|
|
481
604
|
"""Show progress tree."""
|
|
482
605
|
config = get_config()
|
|
@@ -494,12 +617,16 @@ def scope(
|
|
|
494
617
|
|
|
495
618
|
issues = filtered_issues
|
|
496
619
|
|
|
620
|
+
if OutputManager.is_agent_mode():
|
|
621
|
+
OutputManager.print(issues)
|
|
622
|
+
return
|
|
623
|
+
|
|
497
624
|
tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
|
|
498
|
-
epics = sorted([i for i in issues if i.type ==
|
|
499
|
-
stories = [i for i in issues if i.type ==
|
|
500
|
-
tasks = [i for i in issues if i.type in [
|
|
625
|
+
epics = sorted([i for i in issues if i.type == "epic"], key=lambda x: x.id)
|
|
626
|
+
stories = [i for i in issues if i.type == "feature"]
|
|
627
|
+
tasks = [i for i in issues if i.type in ["chore", "fix"]]
|
|
501
628
|
|
|
502
|
-
status_map = {
|
|
629
|
+
status_map = {"open": "[blue]●[/blue]", "closed": "[green]✔[/green]", "backlog": "[dim]💤[/dim]"}
|
|
503
630
|
|
|
504
631
|
for epic in epics:
|
|
505
632
|
epic_node = tree.add(f"{status_map[epic.status]} [bold]{epic.id}[/bold]: {epic.title}")
|
|
@@ -512,16 +639,112 @@ def scope(
|
|
|
512
639
|
|
|
513
640
|
console.print(Panel(tree, expand=False))
|
|
514
641
|
|
|
642
|
+
@app.command("sync-files")
|
|
643
|
+
def sync_files(
|
|
644
|
+
issue_id: Optional[str] = typer.Argument(None, help="Issue ID to sync (default: current context)"),
|
|
645
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
646
|
+
json: AgentOutput = False,
|
|
647
|
+
):
|
|
648
|
+
"""
|
|
649
|
+
Sync issue 'files' field with git changed files.
|
|
650
|
+
"""
|
|
651
|
+
config = get_config()
|
|
652
|
+
issues_root = _resolve_issues_root(config, root)
|
|
653
|
+
project_root = _resolve_project_root(config)
|
|
654
|
+
|
|
655
|
+
if not issue_id:
|
|
656
|
+
# Infer from branch
|
|
657
|
+
from monoco.core import git
|
|
658
|
+
current = git.get_current_branch(project_root)
|
|
659
|
+
# Try to parse ID from branch "feat/issue-123-slug"
|
|
660
|
+
import re
|
|
661
|
+
match = re.search(r"(?:feat|fix|chore|epic)/([a-zA-Z]+-\d+)", current)
|
|
662
|
+
if match:
|
|
663
|
+
issue_id = match.group(1).upper()
|
|
664
|
+
else:
|
|
665
|
+
OutputManager.error("Cannot infer Issue ID from current branch. Please specify Issue ID.")
|
|
666
|
+
raise typer.Exit(code=1)
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
changed = core.sync_issue_files(issues_root, issue_id, project_root)
|
|
670
|
+
OutputManager.print({
|
|
671
|
+
"id": issue_id,
|
|
672
|
+
"status": "synced",
|
|
673
|
+
"files": changed
|
|
674
|
+
})
|
|
675
|
+
except Exception as e:
|
|
676
|
+
OutputManager.error(str(e))
|
|
677
|
+
raise typer.Exit(code=1)
|
|
678
|
+
|
|
679
|
+
@app.command("inspect")
|
|
680
|
+
def inspect(
|
|
681
|
+
target: str = typer.Argument(..., help="Issue ID or File Path"),
|
|
682
|
+
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
683
|
+
ast: bool = typer.Option(False, "--ast", help="Output JSON AST structure for debugging"),
|
|
684
|
+
json: AgentOutput = False,
|
|
685
|
+
):
|
|
686
|
+
"""
|
|
687
|
+
Inspect a specific issue and return its metadata (including actions).
|
|
688
|
+
"""
|
|
689
|
+
config = get_config()
|
|
690
|
+
issues_root = _resolve_issues_root(config, root)
|
|
691
|
+
|
|
692
|
+
# Try as Path
|
|
693
|
+
target_path = Path(target)
|
|
694
|
+
if target_path.exists() and target_path.is_file():
|
|
695
|
+
path = target_path
|
|
696
|
+
else:
|
|
697
|
+
# Try as ID
|
|
698
|
+
# Search path logic is needed? Or core.find_issue_path
|
|
699
|
+
path = core.find_issue_path(issues_root, target)
|
|
700
|
+
if not path:
|
|
701
|
+
OutputManager.error(f"Issue or file {target} not found.")
|
|
702
|
+
raise typer.Exit(code=1)
|
|
703
|
+
|
|
704
|
+
# AST Debug Mode
|
|
705
|
+
if ast:
|
|
706
|
+
from .domain.parser import MarkdownParser
|
|
707
|
+
content = path.read_text()
|
|
708
|
+
try:
|
|
709
|
+
domain_issue = MarkdownParser.parse(content, path=str(path))
|
|
710
|
+
print(domain_issue.model_dump_json(indent=2))
|
|
711
|
+
except Exception as e:
|
|
712
|
+
OutputManager.error(f"Failed to parse AST: {e}")
|
|
713
|
+
raise typer.Exit(code=1)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
# Normal Mode
|
|
717
|
+
meta = core.parse_issue(path)
|
|
718
|
+
|
|
719
|
+
if not meta:
|
|
720
|
+
OutputManager.error(f"Could not parse issue {target}.")
|
|
721
|
+
raise typer.Exit(code=1)
|
|
722
|
+
|
|
723
|
+
# In JSON mode (AgentOutput), we might want to return rich data
|
|
724
|
+
if OutputManager.is_agent_mode():
|
|
725
|
+
OutputManager.print(meta)
|
|
726
|
+
else:
|
|
727
|
+
# For human, print yaml-like or table
|
|
728
|
+
console.print(meta)
|
|
729
|
+
|
|
515
730
|
@app.command("lint")
|
|
516
731
|
def lint(
|
|
517
732
|
recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
|
|
733
|
+
fix: bool = typer.Option(False, "--fix", help="Attempt to automatically fix issues (e.g. missing headings)"),
|
|
734
|
+
format: str = typer.Option("table", "--format", "-f", help="Output format (table, json)"),
|
|
735
|
+
file: Optional[str] = typer.Option(None, "--file", help="Validate a single file instead of scanning the entire workspace"),
|
|
518
736
|
root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
|
|
737
|
+
json: AgentOutput = False,
|
|
519
738
|
):
|
|
520
739
|
"""Verify the integrity of the Issues directory (declarative check)."""
|
|
521
740
|
from . import linter
|
|
522
741
|
config = get_config()
|
|
523
742
|
issues_root = _resolve_issues_root(config, root)
|
|
524
|
-
|
|
743
|
+
|
|
744
|
+
if OutputManager.is_agent_mode():
|
|
745
|
+
format = "json"
|
|
746
|
+
|
|
747
|
+
linter.run_lint(issues_root, recursive=recursive, fix=fix, format=format, file_path=file)
|
|
525
748
|
|
|
526
749
|
def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
|
|
527
750
|
"""
|
|
@@ -544,27 +767,9 @@ def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
|
|
|
544
767
|
return path
|
|
545
768
|
|
|
546
769
|
# 2. Handle Default / Contextual Execution (No --root)
|
|
547
|
-
#
|
|
770
|
+
# Strict Workspace Check: If not in a project root, we rely on the config root.
|
|
771
|
+
# (The global app callback already enforces presence of .monoco for most commands)
|
|
548
772
|
cwd = Path.cwd()
|
|
549
|
-
|
|
550
|
-
# If CWD is NOT a project root (no monoco.yaml/Issues), scan for subprojects
|
|
551
|
-
if not is_project_root(cwd):
|
|
552
|
-
subprojects = find_projects(cwd)
|
|
553
|
-
if len(subprojects) > 1:
|
|
554
|
-
console.print(f"[yellow]Workspace detected with {len(subprojects)} projects:[/yellow]")
|
|
555
|
-
for p in subprojects:
|
|
556
|
-
console.print(f" - [bold]{p.name}[/bold]")
|
|
557
|
-
console.print("\n[yellow]Please specify a project using --root <PATH>.[/yellow]")
|
|
558
|
-
# We don't exit here strictly, but usually this means we can't find 'Issues' in CWD anyway
|
|
559
|
-
# so the config fallbacks below will likely fail or point to non-existent CWD/Issues.
|
|
560
|
-
# But let's fail fast to be helpful.
|
|
561
|
-
raise typer.Exit(code=1)
|
|
562
|
-
elif len(subprojects) == 1:
|
|
563
|
-
# Auto-select the only child project?
|
|
564
|
-
# It's safer to require explicit intent, but let's try to be helpful if it's obvious.
|
|
565
|
-
# However, standard behavior is usually "operate on current dir".
|
|
566
|
-
# Let's stick to standard config resolution, but maybe warn.
|
|
567
|
-
pass
|
|
568
773
|
|
|
569
774
|
# 3. Config Fallback
|
|
570
775
|
config_issues_path = Path(config.paths.issues)
|
|
@@ -708,3 +913,40 @@ def commit(
|
|
|
708
913
|
except Exception as e:
|
|
709
914
|
console.print(f"[red]Git Error:[/red] {e}")
|
|
710
915
|
raise typer.Exit(code=1)
|
|
916
|
+
|
|
917
|
+
@lsp_app.command("definition")
|
|
918
|
+
def lsp_definition(
|
|
919
|
+
file: str = typer.Option(..., "--file", "-f", help="Abs path to file"),
|
|
920
|
+
line: int = typer.Option(..., "--line", "-l", help="0-indexed line number"),
|
|
921
|
+
character: int = typer.Option(..., "--char", "-c", help="0-indexed character number"),
|
|
922
|
+
):
|
|
923
|
+
"""
|
|
924
|
+
Handle textDocument/definition request.
|
|
925
|
+
Output: JSON Location | null
|
|
926
|
+
"""
|
|
927
|
+
import json
|
|
928
|
+
from monoco.core.lsp import Position
|
|
929
|
+
from monoco.features.issue.lsp import DefinitionProvider
|
|
930
|
+
|
|
931
|
+
config = get_config()
|
|
932
|
+
# Workspace Root resolution is key here.
|
|
933
|
+
# If we are in a workspace, we want the workspace root, not just issue root.
|
|
934
|
+
# _resolve_project_root returns the closest project root or monoco root.
|
|
935
|
+
workspace_root = _resolve_project_root(config)
|
|
936
|
+
# Search for topmost workspace root to enable cross-project navigation
|
|
937
|
+
current_best = workspace_root
|
|
938
|
+
for parent in [workspace_root] + list(workspace_root.parents):
|
|
939
|
+
if (parent / ".monoco" / "workspace.yaml").exists() or (parent / ".monoco" / "project.yaml").exists():
|
|
940
|
+
current_best = parent
|
|
941
|
+
workspace_root = current_best
|
|
942
|
+
|
|
943
|
+
provider = DefinitionProvider(workspace_root)
|
|
944
|
+
file_path = Path(file)
|
|
945
|
+
|
|
946
|
+
locations = provider.provide_definition(
|
|
947
|
+
file_path,
|
|
948
|
+
Position(line=line, character=character)
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
# helper to serialize
|
|
952
|
+
print(json.dumps([l.model_dump(mode='json') for l in locations]))
|