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.
- 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 +106 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +106 -0
- monoco/core/config.py +152 -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 +234 -0
- monoco/core/lsp.py +61 -0
- monoco/core/output.py +13 -2
- 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 +66 -0
- monoco/core/setup.py +88 -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/agent/commands.py +166 -0
- monoco/features/agent/doctor.py +30 -0
- 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 +183 -65
- monoco/features/issue/core.py +172 -77
- monoco/features/issue/linter.py +215 -116
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +23 -19
- monoco/features/issue/monitor.py +94 -0
- monoco/features/issue/resources/en/AGENTS.md +15 -0
- monoco/features/issue/resources/en/SKILL.md +87 -0
- monoco/features/issue/resources/zh/AGENTS.md +15 -0
- monoco/features/issue/resources/zh/SKILL.md +114 -0
- monoco/features/issue/validator.py +269 -0
- monoco/features/pty/core.py +185 -0
- monoco/features/pty/router.py +138 -0
- monoco/features/pty/server.py +56 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +4 -21
- 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 +115 -2
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +10 -3
- monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
- monoco_toolkit-0.1.0.dist-info/RECORD +0 -33
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
monoco/features/issue/core.py
CHANGED
|
@@ -6,7 +6,9 @@ from typing import List, Dict, Optional, Tuple, Any, Set, Set
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from .models import IssueMetadata, IssueType, IssueStatus, IssueSolution, IssueStage, IssueDetail, IsolationType, IssueIsolation, IssueID, current_time, generate_uid
|
|
8
8
|
from monoco.core import git
|
|
9
|
-
from monoco.core.config import get_config
|
|
9
|
+
from monoco.core.config import get_config, MonocoConfig
|
|
10
|
+
from monoco.core.lsp import DiagnosticSeverity
|
|
11
|
+
from .validator import IssueValidator
|
|
10
12
|
|
|
11
13
|
PREFIX_MAP = {
|
|
12
14
|
IssueType.EPIC: "EPIC",
|
|
@@ -17,6 +19,27 @@ PREFIX_MAP = {
|
|
|
17
19
|
|
|
18
20
|
REVERSE_PREFIX_MAP = {v: k for k, v in PREFIX_MAP.items()}
|
|
19
21
|
|
|
22
|
+
def enforce_lifecycle_policy(meta: IssueMetadata) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Apply business rules to ensure IssueMetadata consistency.
|
|
25
|
+
Should be called during Create or Update (but NOT during Read/Lint).
|
|
26
|
+
"""
|
|
27
|
+
if meta.status == IssueStatus.BACKLOG:
|
|
28
|
+
meta.stage = IssueStage.FREEZED
|
|
29
|
+
|
|
30
|
+
elif meta.status == IssueStatus.CLOSED:
|
|
31
|
+
# Enforce stage=done for closed issues
|
|
32
|
+
if meta.stage != IssueStage.DONE:
|
|
33
|
+
meta.stage = IssueStage.DONE
|
|
34
|
+
# Auto-fill closed_at if missing
|
|
35
|
+
if not meta.closed_at:
|
|
36
|
+
meta.closed_at = current_time()
|
|
37
|
+
|
|
38
|
+
elif meta.status == IssueStatus.OPEN:
|
|
39
|
+
# Ensure valid stage for open status
|
|
40
|
+
if meta.stage is None:
|
|
41
|
+
meta.stage = IssueStage.DRAFT
|
|
42
|
+
|
|
20
43
|
def _get_slug(title: str) -> str:
|
|
21
44
|
slug = title.lower()
|
|
22
45
|
# Replace non-word characters (including punctuation, spaces) with hyphens
|
|
@@ -51,7 +74,11 @@ def parse_issue(file_path: Path) -> Optional[IssueMetadata]:
|
|
|
51
74
|
data = yaml.safe_load(match.group(1))
|
|
52
75
|
if not isinstance(data, dict):
|
|
53
76
|
return None
|
|
54
|
-
|
|
77
|
+
|
|
78
|
+
data['path'] = str(file_path.absolute())
|
|
79
|
+
meta = IssueMetadata(**data)
|
|
80
|
+
meta.actions = get_available_actions(meta)
|
|
81
|
+
return meta
|
|
55
82
|
except Exception:
|
|
56
83
|
return None
|
|
57
84
|
|
|
@@ -72,6 +99,8 @@ def parse_issue_detail(file_path: Path) -> Optional[IssueDetail]:
|
|
|
72
99
|
data = yaml.safe_load(yaml_str)
|
|
73
100
|
if not isinstance(data, dict):
|
|
74
101
|
return None
|
|
102
|
+
|
|
103
|
+
data['path'] = str(file_path.absolute())
|
|
75
104
|
return IssueDetail(**data, body=body, raw_content=content)
|
|
76
105
|
except Exception:
|
|
77
106
|
return None
|
|
@@ -139,8 +168,10 @@ def create_issue_file(
|
|
|
139
168
|
tags=tags,
|
|
140
169
|
opened_at=current_time() if status == IssueStatus.OPEN else None
|
|
141
170
|
)
|
|
142
|
-
|
|
143
171
|
|
|
172
|
+
# Enforce lifecycle policies (defaults, auto-corrections)
|
|
173
|
+
enforce_lifecycle_policy(metadata)
|
|
174
|
+
|
|
144
175
|
yaml_header = yaml.dump(metadata.model_dump(exclude_none=True, mode='json'), sort_keys=False, allow_unicode=True)
|
|
145
176
|
slug = _get_slug(title)
|
|
146
177
|
filename = f"{issue_id}-{slug}.md"
|
|
@@ -161,60 +192,56 @@ def create_issue_file(
|
|
|
161
192
|
file_path = target_dir / filename
|
|
162
193
|
file_path.write_text(file_content)
|
|
163
194
|
|
|
195
|
+
# Inject path into returned metadata
|
|
196
|
+
metadata.path = str(file_path.absolute())
|
|
197
|
+
|
|
164
198
|
return metadata, file_path
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
199
|
+
def get_available_actions(meta: IssueMetadata) -> List[Any]:
|
|
200
|
+
from .models import IssueAction
|
|
201
|
+
actions = []
|
|
202
|
+
|
|
203
|
+
if meta.status == IssueStatus.OPEN:
|
|
204
|
+
# Stage-based movements
|
|
205
|
+
if meta.stage == IssueStage.DRAFT:
|
|
206
|
+
actions.append(IssueAction(label="Start", target_status=IssueStatus.OPEN, target_stage=IssueStage.DOING))
|
|
207
|
+
actions.append(IssueAction(label="Freeze", target_status=IssueStatus.BACKLOG))
|
|
208
|
+
elif meta.stage == IssueStage.DOING:
|
|
209
|
+
actions.append(IssueAction(label="Stop", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
210
|
+
actions.append(IssueAction(label="Submit", target_status=IssueStatus.OPEN, target_stage=IssueStage.REVIEW))
|
|
211
|
+
elif meta.stage == IssueStage.REVIEW:
|
|
212
|
+
actions.append(IssueAction(label="Approve", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.IMPLEMENTED))
|
|
213
|
+
actions.append(IssueAction(label="Reject", target_status=IssueStatus.OPEN, target_stage=IssueStage.DOING))
|
|
214
|
+
elif meta.stage == IssueStage.DONE:
|
|
215
|
+
actions.append(IssueAction(label="Reopen", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
216
|
+
actions.append(IssueAction(label="Close", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.IMPLEMENTED))
|
|
217
|
+
|
|
218
|
+
# Generic cancel
|
|
219
|
+
actions.append(IssueAction(label="Cancel", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.CANCELLED))
|
|
181
220
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# If we are transitioning TO Closed, current stage must be Review.
|
|
191
|
-
if current_stage != IssueStage.REVIEW:
|
|
192
|
-
raise ValueError(f"Lifecycle Policy: 'Implemented' issues must be submitted for review first.\nCurrent stage: {current_stage}\nAction: Run `monoco issue submit {issue_id}`.")
|
|
193
|
-
|
|
194
|
-
# Policy: No closing from DOING (General Safety)
|
|
195
|
-
if current_stage == IssueStage.DOING:
|
|
196
|
-
raise ValueError("Cannot close issue in progress (Doing). Please review (`monoco issue submit`) or stop (`monoco issue open`) first.")
|
|
197
|
-
|
|
198
|
-
# Policy: Dependencies must be closed
|
|
199
|
-
if issue_dependencies:
|
|
200
|
-
for dep_id in issue_dependencies:
|
|
201
|
-
dep_path = find_issue_path(issues_root, dep_id)
|
|
202
|
-
if dep_path:
|
|
203
|
-
dep_meta = parse_issue(dep_path)
|
|
204
|
-
if dep_meta and dep_meta.status != IssueStatus.CLOSED:
|
|
205
|
-
raise ValueError(f"Dependency Block: Cannot close {issue_id} because dependency {dep_id} is not closed.")
|
|
221
|
+
elif meta.status == IssueStatus.BACKLOG:
|
|
222
|
+
actions.append(IssueAction(label="Pull", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
223
|
+
actions.append(IssueAction(label="Cancel", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.CANCELLED))
|
|
224
|
+
|
|
225
|
+
elif meta.status == IssueStatus.CLOSED:
|
|
226
|
+
actions.append(IssueAction(label="Reopen", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
227
|
+
|
|
228
|
+
return actions
|
|
206
229
|
|
|
207
230
|
def find_issue_path(issues_root: Path, issue_id: str) -> Optional[Path]:
|
|
208
231
|
parsed = IssueID(issue_id)
|
|
209
232
|
|
|
210
233
|
if not parsed.is_local:
|
|
234
|
+
if not parsed.namespace:
|
|
235
|
+
return None
|
|
236
|
+
|
|
211
237
|
# Resolve Workspace
|
|
212
|
-
#
|
|
213
|
-
# This is a weak assumption but fits current architecture.
|
|
238
|
+
# Traverse up from issues_root to find a config that defines the namespace
|
|
214
239
|
project_root = issues_root.parent
|
|
215
|
-
conf = get_config(str(project_root))
|
|
216
240
|
|
|
241
|
+
# Try current root first
|
|
242
|
+
conf = MonocoConfig.load(str(project_root))
|
|
217
243
|
member_rel_path = conf.project.members.get(parsed.namespace)
|
|
244
|
+
|
|
218
245
|
if not member_rel_path:
|
|
219
246
|
return None
|
|
220
247
|
|
|
@@ -244,7 +271,19 @@ def find_issue_path(issues_root: Path, issue_id: str) -> Optional[Path]:
|
|
|
244
271
|
return f
|
|
245
272
|
return None
|
|
246
273
|
|
|
247
|
-
def update_issue(
|
|
274
|
+
def update_issue(
|
|
275
|
+
issues_root: Path,
|
|
276
|
+
issue_id: str,
|
|
277
|
+
status: Optional[IssueStatus] = None,
|
|
278
|
+
stage: Optional[IssueStage] = None,
|
|
279
|
+
solution: Optional[IssueSolution] = None,
|
|
280
|
+
title: Optional[str] = None,
|
|
281
|
+
parent: Optional[str] = None,
|
|
282
|
+
sprint: Optional[str] = None,
|
|
283
|
+
dependencies: Optional[List[str]] = None,
|
|
284
|
+
related: Optional[List[str]] = None,
|
|
285
|
+
tags: Optional[List[str]] = None
|
|
286
|
+
) -> IssueMetadata:
|
|
248
287
|
path = find_issue_path(issues_root, issue_id)
|
|
249
288
|
if not path:
|
|
250
289
|
raise FileNotFoundError(f"Issue {issue_id} not found.")
|
|
@@ -253,7 +292,7 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
253
292
|
content = path.read_text()
|
|
254
293
|
|
|
255
294
|
# Split Frontmatter and Body
|
|
256
|
-
match = re.search(r"^---(.*?)---\n(.*)
|
|
295
|
+
match = re.search(r"^---(.*?)---\n(.*)\n", content, re.DOTALL | re.MULTILINE)
|
|
257
296
|
if not match:
|
|
258
297
|
# Fallback
|
|
259
298
|
match_simple = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
|
|
@@ -289,9 +328,7 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
289
328
|
raise ValueError(f"Lifecycle Policy: Cannot submit Backlog issue directly. Run `monoco issue pull {issue_id}` first.")
|
|
290
329
|
|
|
291
330
|
if target_status == IssueStatus.CLOSED:
|
|
292
|
-
|
|
293
|
-
raise ValueError(f"Closing an issue requires a solution. Please provide --solution or edit the file metadata.")
|
|
294
|
-
|
|
331
|
+
# Validator will check solution presence
|
|
295
332
|
current_data_stage = data.get('stage')
|
|
296
333
|
|
|
297
334
|
# Policy: IMPLEMENTED requires REVIEW stage
|
|
@@ -304,14 +341,29 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
304
341
|
raise ValueError("Cannot close issue in progress (Doing). Please review (`monoco issue submit`) or stop (`monoco issue open`) first.")
|
|
305
342
|
|
|
306
343
|
# Policy: Dependencies must be closed
|
|
307
|
-
|
|
308
|
-
if
|
|
309
|
-
for dep_id in
|
|
344
|
+
dependencies_to_check = dependencies if dependencies is not None else data.get('dependencies', [])
|
|
345
|
+
if dependencies_to_check:
|
|
346
|
+
for dep_id in dependencies_to_check:
|
|
310
347
|
dep_path = find_issue_path(issues_root, dep_id)
|
|
311
348
|
if dep_path:
|
|
312
349
|
dep_meta = parse_issue(dep_path)
|
|
313
350
|
if dep_meta and dep_meta.status != IssueStatus.CLOSED:
|
|
314
351
|
raise ValueError(f"Dependency Block: Cannot close {issue_id} because dependency {dep_id} is [Status: {dep_meta.status.value}].")
|
|
352
|
+
|
|
353
|
+
# Validate new parent/dependencies/related exist
|
|
354
|
+
if parent is not None and parent != "":
|
|
355
|
+
if not find_issue_path(issues_root, parent):
|
|
356
|
+
raise ValueError(f"Parent issue {parent} not found.")
|
|
357
|
+
|
|
358
|
+
if dependencies is not None:
|
|
359
|
+
for dep_id in dependencies:
|
|
360
|
+
if not find_issue_path(issues_root, dep_id):
|
|
361
|
+
raise ValueError(f"Dependency issue {dep_id} not found.")
|
|
362
|
+
|
|
363
|
+
if related is not None:
|
|
364
|
+
for rel_id in related:
|
|
365
|
+
if not find_issue_path(issues_root, rel_id):
|
|
366
|
+
raise ValueError(f"Related issue {rel_id} not found.")
|
|
315
367
|
|
|
316
368
|
# Update Data
|
|
317
369
|
if status:
|
|
@@ -322,6 +374,27 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
322
374
|
if solution:
|
|
323
375
|
data['solution'] = solution.value
|
|
324
376
|
|
|
377
|
+
if title:
|
|
378
|
+
data['title'] = title
|
|
379
|
+
|
|
380
|
+
if parent is not None:
|
|
381
|
+
if parent == "":
|
|
382
|
+
data.pop('parent', None) # Remove parent field
|
|
383
|
+
else:
|
|
384
|
+
data['parent'] = parent
|
|
385
|
+
|
|
386
|
+
if sprint is not None:
|
|
387
|
+
data['sprint'] = sprint
|
|
388
|
+
|
|
389
|
+
if dependencies is not None:
|
|
390
|
+
data['dependencies'] = dependencies
|
|
391
|
+
|
|
392
|
+
if related is not None:
|
|
393
|
+
data['related'] = related
|
|
394
|
+
|
|
395
|
+
if tags is not None:
|
|
396
|
+
data['tags'] = tags
|
|
397
|
+
|
|
325
398
|
# Lifecycle Hooks
|
|
326
399
|
# 1. Opened At: If transitioning to OPEN
|
|
327
400
|
if target_status == IssueStatus.OPEN and current_status != IssueStatus.OPEN:
|
|
@@ -336,9 +409,24 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
336
409
|
# Touch updated_at
|
|
337
410
|
data['updated_at'] = current_time()
|
|
338
411
|
|
|
339
|
-
# Re-hydrate through Model
|
|
412
|
+
# Re-hydrate through Model
|
|
340
413
|
try:
|
|
341
414
|
updated_meta = IssueMetadata(**data)
|
|
415
|
+
|
|
416
|
+
# Enforce lifecycle policies (defaults, auto-corrections)
|
|
417
|
+
# This ensures that when we update, we also fix invalid states (like Closed but not Done)
|
|
418
|
+
enforce_lifecycle_policy(updated_meta)
|
|
419
|
+
|
|
420
|
+
# Delegate to IssueValidator for static state validation
|
|
421
|
+
# We need to construct the full content to validate body-dependent rules (like checkboxes)
|
|
422
|
+
# Note: 'body' here is the OLD body. We assume update_issue doesn't change body.
|
|
423
|
+
# If body is invalid (unchecked boxes) and we move to DONE, this MUST fail.
|
|
424
|
+
validator = IssueValidator(issues_root)
|
|
425
|
+
diagnostics = validator.validate(updated_meta, body)
|
|
426
|
+
errors = [d for d in diagnostics if d.severity == DiagnosticSeverity.Error]
|
|
427
|
+
if errors:
|
|
428
|
+
raise ValueError(f"Validation Failed: {errors[0].message}")
|
|
429
|
+
|
|
342
430
|
except Exception as e:
|
|
343
431
|
raise ValueError(f"Failed to validate updated metadata: {e}")
|
|
344
432
|
|
|
@@ -376,11 +464,15 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
376
464
|
if path != target_path:
|
|
377
465
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
378
466
|
path.rename(target_path)
|
|
467
|
+
path = target_path # Update local path variable for returned meta
|
|
379
468
|
|
|
380
469
|
# Hook: Recursive Aggregation (FEAT-0003)
|
|
381
470
|
if updated_meta.parent:
|
|
382
471
|
recalculate_parent(issues_root, updated_meta.parent)
|
|
383
|
-
|
|
472
|
+
|
|
473
|
+
# Update returned metadata with final absolute path
|
|
474
|
+
updated_meta.path = str(path.absolute())
|
|
475
|
+
updated_meta.actions = get_available_actions(updated_meta)
|
|
384
476
|
return updated_meta
|
|
385
477
|
|
|
386
478
|
def start_issue_isolation(issues_root: Path, issue_id: str, mode: IsolationType, project_root: Path) -> IssueMetadata:
|
|
@@ -392,6 +484,8 @@ def start_issue_isolation(issues_root: Path, issue_id: str, mode: IsolationType,
|
|
|
392
484
|
raise FileNotFoundError(f"Issue {issue_id} not found.")
|
|
393
485
|
|
|
394
486
|
issue = parse_issue(path)
|
|
487
|
+
if not issue:
|
|
488
|
+
raise ValueError(f"Could not parse metadata for issue {issue_id}")
|
|
395
489
|
|
|
396
490
|
# Idempotency / Conflict Check
|
|
397
491
|
if issue.isolation:
|
|
@@ -467,6 +561,9 @@ def prune_issue_resources(issues_root: Path, issue_id: str, force: bool, project
|
|
|
467
561
|
raise FileNotFoundError(f"Issue {issue_id} not found.")
|
|
468
562
|
|
|
469
563
|
issue = parse_issue(path)
|
|
564
|
+
if not issue:
|
|
565
|
+
raise ValueError(f"Could not parse metadata for issue {issue_id}")
|
|
566
|
+
|
|
470
567
|
deleted_items = []
|
|
471
568
|
|
|
472
569
|
if not issue.isolation:
|
|
@@ -574,15 +671,6 @@ description: Monoco Issue System 的官方技能定义。将 Issue 视为通用
|
|
|
574
671
|
5. **Modification**: `monoco issue start/submit/delete <id>`
|
|
575
672
|
"""
|
|
576
673
|
|
|
577
|
-
PROMPT_CONTENT = """
|
|
578
|
-
### Issue Management
|
|
579
|
-
System for managing tasks using `monoco issue`.
|
|
580
|
-
- **Create**: `monoco issue create <type> -t "Title"` (types: epic, feature, chore, fix)
|
|
581
|
-
- **Status**: `monoco issue open|close|backlog <id>`
|
|
582
|
-
- **Check**: `monoco issue lint` (Must run after manual edits)
|
|
583
|
-
- **Lifecycle**: `monoco issue start|submit|delete <id>`
|
|
584
|
-
- **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`). Do not deviate.
|
|
585
|
-
"""
|
|
586
674
|
|
|
587
675
|
def init(issues_root: Path):
|
|
588
676
|
"""Initialize the Issues directory structure."""
|
|
@@ -603,9 +691,7 @@ def get_resources() -> Dict[str, Any]:
|
|
|
603
691
|
"skills": {
|
|
604
692
|
"issues-management": SKILL_CONTENT
|
|
605
693
|
},
|
|
606
|
-
"prompts": {
|
|
607
|
-
"issues-management": PROMPT_CONTENT
|
|
608
|
-
}
|
|
694
|
+
"prompts": {} # Handled by adapter via resource files
|
|
609
695
|
}
|
|
610
696
|
|
|
611
697
|
|
|
@@ -653,7 +739,7 @@ def get_board_data(issues_root: Path) -> Dict[str, List[IssueMetadata]]:
|
|
|
653
739
|
Get open issues grouped by their stage for Kanban view.
|
|
654
740
|
"""
|
|
655
741
|
board = {
|
|
656
|
-
IssueStage.
|
|
742
|
+
IssueStage.DRAFT.value: [],
|
|
657
743
|
IssueStage.DOING.value: [],
|
|
658
744
|
IssueStage.REVIEW.value: [],
|
|
659
745
|
IssueStage.DONE.value: []
|
|
@@ -769,7 +855,10 @@ def generate_delivery_report(issues_root: Path, issue_id: str, project_root: Pat
|
|
|
769
855
|
commits = git.search_commits_by_message(project_root, f"Ref: {issue_id}")
|
|
770
856
|
|
|
771
857
|
if not commits:
|
|
772
|
-
|
|
858
|
+
meta = parse_issue(path)
|
|
859
|
+
if not meta:
|
|
860
|
+
raise ValueError(f"Could not parse metadata for issue {issue_id}")
|
|
861
|
+
return meta
|
|
773
862
|
|
|
774
863
|
# 2. Aggregate Data
|
|
775
864
|
all_files = set()
|
|
@@ -825,7 +914,10 @@ def generate_delivery_report(issues_root: Path, issue_id: str, project_root: Pat
|
|
|
825
914
|
# We can add it to 'extra' or extend the model later.
|
|
826
915
|
# For now, just persisting the text is enough for FEAT-0002.
|
|
827
916
|
|
|
828
|
-
|
|
917
|
+
meta = parse_issue(path)
|
|
918
|
+
if not meta:
|
|
919
|
+
raise ValueError(f"Could not parse metadata for issue {issue_id}")
|
|
920
|
+
return meta
|
|
829
921
|
|
|
830
922
|
def get_children(issues_root: Path, parent_id: str) -> List[IssueMetadata]:
|
|
831
923
|
"""Find all direct children of an issue."""
|
|
@@ -1033,11 +1125,11 @@ def recalculate_parent(issues_root: Path, parent_id: str):
|
|
|
1033
1125
|
# FEAT-0003 Req: "If first child starts doing, auto-start Parent?"
|
|
1034
1126
|
# If parent is OPEN/TODO and child is DOING/REVIEW/DONE, set parent to DOING?
|
|
1035
1127
|
current_status = data.get("status", "open").lower()
|
|
1036
|
-
current_stage = data.get("stage", "
|
|
1128
|
+
current_stage = data.get("stage", "draft").lower()
|
|
1037
1129
|
|
|
1038
|
-
if current_status == "open" and current_stage == "
|
|
1130
|
+
if current_status == "open" and current_stage == "draft":
|
|
1039
1131
|
# Check if any child is active
|
|
1040
|
-
active_children = [c for c in children if c.status == IssueStatus.OPEN and c.stage != IssueStage.
|
|
1132
|
+
active_children = [c for c in children if c.status == IssueStatus.OPEN and c.stage != IssueStage.DRAFT]
|
|
1041
1133
|
closed_children = [c for c in children if c.status == IssueStatus.CLOSED]
|
|
1042
1134
|
|
|
1043
1135
|
if active_children or closed_children:
|
|
@@ -1143,7 +1235,7 @@ def move_issue(
|
|
|
1143
1235
|
# 5. Update content if ID changed
|
|
1144
1236
|
if new_id != old_id:
|
|
1145
1237
|
# Update frontmatter
|
|
1146
|
-
content = issue.raw_content
|
|
1238
|
+
content = issue.raw_content or ""
|
|
1147
1239
|
match = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
|
|
1148
1240
|
if match:
|
|
1149
1241
|
yaml_str = match.group(1)
|
|
@@ -1159,9 +1251,9 @@ def move_issue(
|
|
|
1159
1251
|
|
|
1160
1252
|
new_content = f"---\n{new_yaml}---{body}"
|
|
1161
1253
|
else:
|
|
1162
|
-
new_content = issue.raw_content
|
|
1254
|
+
new_content = issue.raw_content or ""
|
|
1163
1255
|
else:
|
|
1164
|
-
new_content = issue.raw_content
|
|
1256
|
+
new_content = issue.raw_content or ""
|
|
1165
1257
|
|
|
1166
1258
|
# 6. Write to target
|
|
1167
1259
|
target_path.write_text(new_content)
|
|
@@ -1171,4 +1263,7 @@ def move_issue(
|
|
|
1171
1263
|
|
|
1172
1264
|
# 8. Return updated metadata
|
|
1173
1265
|
final_meta = parse_issue(target_path)
|
|
1266
|
+
if not final_meta:
|
|
1267
|
+
raise ValueError(f"Failed to parse moved issue at {target_path}")
|
|
1268
|
+
|
|
1174
1269
|
return final_meta, target_path
|