monoco-toolkit 0.2.5__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.
Files changed (42) hide show
  1. monoco/core/agent/adapters.py +24 -1
  2. monoco/core/config.py +77 -17
  3. monoco/core/integrations.py +8 -0
  4. monoco/core/lsp.py +7 -0
  5. monoco/core/output.py +8 -1
  6. monoco/core/resources/zh/SKILL.md +6 -7
  7. monoco/core/setup.py +8 -0
  8. monoco/features/i18n/resources/zh/SKILL.md +5 -5
  9. monoco/features/issue/commands.py +179 -55
  10. monoco/features/issue/core.py +263 -124
  11. monoco/features/issue/domain/__init__.py +0 -0
  12. monoco/features/issue/domain/lifecycle.py +126 -0
  13. monoco/features/issue/domain/models.py +170 -0
  14. monoco/features/issue/domain/parser.py +223 -0
  15. monoco/features/issue/domain/workspace.py +104 -0
  16. monoco/features/issue/engine/__init__.py +22 -0
  17. monoco/features/issue/engine/config.py +172 -0
  18. monoco/features/issue/engine/machine.py +185 -0
  19. monoco/features/issue/engine/models.py +18 -0
  20. monoco/features/issue/linter.py +118 -12
  21. monoco/features/issue/lsp/__init__.py +3 -0
  22. monoco/features/issue/lsp/definition.py +72 -0
  23. monoco/features/issue/models.py +27 -9
  24. monoco/features/issue/resources/en/AGENTS.md +5 -0
  25. monoco/features/issue/resources/en/SKILL.md +26 -2
  26. monoco/features/issue/resources/zh/AGENTS.md +5 -0
  27. monoco/features/issue/resources/zh/SKILL.md +34 -10
  28. monoco/features/issue/validator.py +252 -66
  29. monoco/features/spike/core.py +5 -22
  30. monoco/features/spike/resources/zh/SKILL.md +2 -2
  31. monoco/main.py +2 -26
  32. monoco_toolkit-0.2.8.dist-info/METADATA +136 -0
  33. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/RECORD +36 -30
  34. monoco/features/agent/commands.py +0 -166
  35. monoco/features/agent/doctor.py +0 -30
  36. monoco/features/pty/core.py +0 -185
  37. monoco/features/pty/router.py +0 -138
  38. monoco/features/pty/server.py +0 -56
  39. monoco_toolkit-0.2.5.dist-info/METADATA +0 -93
  40. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/WHEEL +0 -0
  41. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/entry_points.txt +0 -0
  42. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.8.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,9 @@ from pathlib import Path
6
6
  from monoco.core.lsp import Diagnostic, DiagnosticSeverity, Range, Position
7
7
  from monoco.core.config import get_config
8
8
  from monoco.features.i18n.core import detect_language
9
- from .models import IssueMetadata, IssueStatus, IssueStage, IssueType
9
+ from .models import IssueMetadata, IssueType
10
+ from .domain.parser import MarkdownParser
11
+ from .domain.models import ContentBlock
10
12
 
11
13
  class IssueValidator:
12
14
  """
@@ -20,14 +22,41 @@ class IssueValidator:
20
22
  def validate(self, meta: IssueMetadata, content: str, all_issue_ids: Set[str] = set()) -> List[Diagnostic]:
21
23
  diagnostics = []
22
24
 
25
+ # Parse Content into Blocks (Domain Layer)
26
+ # Handle case where content might be just body (from update_issue) or full file
27
+ if content.startswith("---"):
28
+ try:
29
+ issue_domain = MarkdownParser.parse(content)
30
+ blocks = issue_domain.body.blocks
31
+ has_frontmatter = True
32
+ except Exception:
33
+ # Fallback if parser fails (e.g. invalid YAML)
34
+ # We continue with empty blocks or try partial parsing?
35
+ # For now, let's try to parse blocks ignoring FM
36
+ lines = content.splitlines()
37
+ # Find end of FM
38
+ start_line = 0
39
+ if lines[0].strip() == "---":
40
+ for i in range(1, len(lines)):
41
+ if lines[i].strip() == "---":
42
+ start_line = i + 1
43
+ break
44
+ blocks = MarkdownParser._parse_blocks(lines[start_line:], start_line_offset=start_line)
45
+ has_frontmatter = True
46
+ else:
47
+ # Assume content is just body
48
+ lines = content.splitlines()
49
+ blocks = MarkdownParser._parse_blocks(lines, start_line_offset=0)
50
+ has_frontmatter = False
51
+
23
52
  # 1. State Matrix Validation
24
53
  diagnostics.extend(self._validate_state_matrix(meta, content))
25
54
 
26
- # 2. Content Completeness (Checkbox check)
27
- diagnostics.extend(self._validate_content_completeness(meta, content))
55
+ # 2. State Requirements (Strict Verification)
56
+ diagnostics.extend(self._validate_state_requirements(meta, blocks))
28
57
 
29
- # 3. Structure Consistency (Headings)
30
- diagnostics.extend(self._validate_structure(meta, content))
58
+ # 3. Structure Consistency (Headings) - Using Blocks
59
+ diagnostics.extend(self._validate_structure_blocks(meta, blocks))
31
60
 
32
61
  # 4. Lifecycle/Integrity (Solution, etc.)
33
62
  diagnostics.extend(self._validate_integrity(meta, content))
@@ -38,8 +67,8 @@ class IssueValidator:
38
67
  # 6. Time Consistency
39
68
  diagnostics.extend(self._validate_time_consistency(meta, content))
40
69
 
41
- # 7. Checkbox Syntax
42
- diagnostics.extend(self._validate_checkbox_logic(content))
70
+ # 7. Checkbox Syntax - Using Blocks
71
+ diagnostics.extend(self._validate_checkbox_logic_blocks(blocks))
43
72
 
44
73
  # 8. Language Consistency
45
74
  diagnostics.extend(self._validate_language_consistency(meta, content))
@@ -97,56 +126,106 @@ class IssueValidator:
97
126
  diagnostics = []
98
127
 
99
128
  # Check based on parsed metadata (now that auto-correction is disabled)
100
- if meta.status == IssueStatus.CLOSED and meta.stage != IssueStage.DONE:
129
+ if meta.status == "closed" and meta.stage != "done":
101
130
  line = self._get_field_line(content, "status")
102
131
  diagnostics.append(self._create_diagnostic(
103
- f"State Mismatch: Closed issues must be in 'Done' stage (found: {meta.stage.value if meta.stage else 'None'})",
132
+ f"State Mismatch: Closed issues must be in 'Done' stage (found: {meta.stage if meta.stage else 'None'})",
104
133
  DiagnosticSeverity.Error,
105
134
  line=line
106
135
  ))
107
136
 
108
- if meta.status == IssueStatus.BACKLOG and meta.stage != IssueStage.FREEZED:
137
+ if meta.status == "backlog" and meta.stage != "freezed":
109
138
  line = self._get_field_line(content, "status")
110
139
  diagnostics.append(self._create_diagnostic(
111
- f"State Mismatch: Backlog issues must be in 'Freezed' stage (found: {meta.stage.value if meta.stage else 'None'})",
140
+ f"State Mismatch: Backlog issues must be in 'Freezed' stage (found: {meta.stage if meta.stage else 'None'})",
112
141
  DiagnosticSeverity.Error,
113
142
  line=line
114
143
  ))
115
144
 
116
145
  return diagnostics
117
146
 
118
- def _validate_content_completeness(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
147
+ def _validate_state_requirements(self, meta: IssueMetadata, blocks: List[ContentBlock]) -> List[Diagnostic]:
119
148
  diagnostics = []
120
- # Checkbox regex: - [ ] or - [x] or - [-] or - [/]
121
- checkboxes = re.findall(r"-\s*\[([ x\-/])\]", content)
122
149
 
123
- if len(checkboxes) < 2:
124
- diagnostics.append(self._create_diagnostic(
125
- "Content Incomplete: Ticket must contain at least 2 checkboxes (AC & Tasks).",
126
- DiagnosticSeverity.Warning
127
- ))
128
-
129
- if meta.stage in [IssueStage.REVIEW, IssueStage.DONE]:
130
- # No empty checkboxes allowed
131
- if ' ' in checkboxes:
132
- # Find the first occurrence line
133
- lines = content.split('\n')
134
- first_line = 0
135
- for i, line in enumerate(lines):
136
- if re.search(r"-\s*\[ \]", line):
137
- first_line = i
138
- break
139
-
140
- diagnostics.append(self._create_diagnostic(
141
- f"Incomplete Tasks: Issue in {meta.stage} cannot have unchecked boxes.",
142
- DiagnosticSeverity.Error,
143
- line=first_line
144
- ))
150
+ # 1. Map Blocks to Sections
151
+ sections = {"tasks": [], "ac": [], "review": []}
152
+ current_section = None
153
+
154
+ for block in blocks:
155
+ if block.type == "heading":
156
+ title = block.content.strip().lower()
157
+ # Parse title to identify sections (supporting Chinese and English synonyms)
158
+ if any(kw in title for kw in ["technical tasks", "工作包", "技术任务", "key deliverables", "关键交付", "重点工作", "子功能", "子故事", "child features", "stories", "需求", "requirements", "implementation", "实现", "交付", "delivery", "规划", "plan", "tasks", "任务"]):
159
+ current_section = "tasks"
160
+ elif any(kw in title for kw in ["acceptance criteria", "验收标准", "交付目标", "验收"]):
161
+ current_section = "ac"
162
+ elif any(kw in title for kw in ["review comments", "确认事项", "评审记录", "复盘记录", "review", "评审", "确认"]):
163
+ current_section = "review"
164
+ elif title.startswith("###"):
165
+ # Subheading: allow continued collection for the current section
166
+ pass
167
+ else:
168
+ current_section = None
169
+ elif block.type == "task_item":
170
+ if current_section and current_section in sections:
171
+ sections[current_section].append(block)
172
+
173
+ # 2. Logic: DOING -> Must have defined tasks
174
+ if meta.stage in ["doing", "review", "done"]:
175
+ if not sections["tasks"]:
176
+ # We can't strictly point to a line if section missing, but we can point to top/bottom
177
+ # Or just a general error.
178
+ diagnostics.append(self._create_diagnostic(
179
+ "State Requirement (DOING+): Must define 'Technical Tasks' (at least 1 checkbox).",
180
+ DiagnosticSeverity.Warning
181
+ ))
182
+
183
+ # 3. Logic: REVIEW -> Tasks must be Completed ([x]) or Cancelled ([~], [+])
184
+ # No [ ] (ToDo) or [-]/[/] (Doing) allowed.
185
+ if meta.stage in ["review", "done"]:
186
+ for block in sections["tasks"]:
187
+ content = block.content.strip()
188
+ # Check for explicit illegal states
189
+ if re.search(r"-\s*\[\s+\]", content):
190
+ diagnostics.append(self._create_diagnostic(
191
+ f"State Requirement ({meta.stage.upper()}): Technical Tasks must be resolved. Found Todo [ ]: '{content}'",
192
+ DiagnosticSeverity.Error,
193
+ line=block.line_start
194
+ ))
195
+ elif re.search(r"-\s*\[[-\/]]", content):
196
+ diagnostics.append(self._create_diagnostic(
197
+ f"State Requirement ({meta.stage.upper()}): Technical Tasks must be finished (not Doing). Found Doing [-]: '{content}'",
198
+ DiagnosticSeverity.Error,
199
+ line=block.line_start
200
+ ))
201
+
202
+ # 4. Logic: DONE -> AC must be Verified ([x])
203
+ if meta.stage == "done":
204
+ for block in sections["ac"]:
205
+ content = block.content.strip()
206
+ if not re.search(r"-\s*\[[xX]\]", content):
207
+ diagnostics.append(self._create_diagnostic(
208
+ f"State Requirement (DONE): Acceptance Criteria must be passed ([x]). Found: '{content}'",
209
+ DiagnosticSeverity.Error,
210
+ line=block.line_start
211
+ ))
212
+
213
+ # 5. Logic: DONE -> Review Checkboxes (if any) must be Resolved ([x] or [~])
214
+ for block in sections["review"]:
215
+ content = block.content.strip()
216
+ # Must be [x], [X], [~], [+]
217
+ # Therefore [ ], [-], [/] are invalid blocking states
218
+ if re.search(r"-\s*\[[\s\-\/]\]", content):
219
+ diagnostics.append(self._create_diagnostic(
220
+ f"State Requirement (DONE): Actionable Review Comments must be resolved ([x] or [~]). Found: '{content}'",
221
+ DiagnosticSeverity.Error,
222
+ line=block.line_start
223
+ ))
224
+
145
225
  return diagnostics
146
226
 
147
- def _validate_structure(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
227
+ def _validate_structure_blocks(self, meta: IssueMetadata, blocks: List[ContentBlock]) -> List[Diagnostic]:
148
228
  diagnostics = []
149
- lines = content.split('\n')
150
229
 
151
230
  # 1. Heading check: ## {issue-id}: {issue-title}
152
231
  expected_header = f"## {meta.id}: {meta.title}"
@@ -156,19 +235,25 @@ class IssueValidator:
156
235
  review_header_found = False
157
236
  review_content_found = False
158
237
 
159
- for i, line in enumerate(lines):
160
- line_stripped = line.strip()
161
- if line_stripped == expected_header:
162
- header_found = True
163
-
164
- if line_stripped == "## Review Comments":
165
- review_header_found = True
166
- # Check near lines for content
167
- # This is a naive check (next line is not empty)
168
- if i + 1 < len(lines) and lines[i+1].strip():
169
- review_content_found = True
170
- elif i + 2 < len(lines) and lines[i+2].strip():
171
- review_content_found = True
238
+ review_header_index = -1
239
+
240
+ for i, block in enumerate(blocks):
241
+ if block.type == 'heading':
242
+ stripped = block.content.strip()
243
+ if stripped == expected_header:
244
+ header_found = True
245
+
246
+ if stripped == "## Review Comments":
247
+ review_header_found = True
248
+ review_header_index = i
249
+
250
+ # Check content after review header
251
+ if review_header_found:
252
+ # Check if there are blocks after review_header_index that are NOT empty
253
+ for j in range(review_header_index + 1, len(blocks)):
254
+ if blocks[j].type != 'empty':
255
+ review_content_found = True
256
+ break
172
257
 
173
258
  if not header_found:
174
259
  diagnostics.append(self._create_diagnostic(
@@ -176,7 +261,7 @@ class IssueValidator:
176
261
  DiagnosticSeverity.Warning
177
262
  ))
178
263
 
179
- if meta.stage in [IssueStage.REVIEW, IssueStage.DONE]:
264
+ if meta.stage in ["review", "done"]:
180
265
  if not review_header_found:
181
266
  diagnostics.append(self._create_diagnostic(
182
267
  "Review Requirement: Missing '## Review Comments' section.",
@@ -191,21 +276,87 @@ class IssueValidator:
191
276
 
192
277
  def _validate_integrity(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
193
278
  diagnostics = []
194
- if meta.status == IssueStatus.CLOSED and not meta.solution:
279
+ if meta.status == "closed" and not meta.solution:
195
280
  line = self._get_field_line(content, "status")
196
281
  diagnostics.append(self._create_diagnostic(
197
282
  f"Data Integrity: Closed issue {meta.id} missing 'solution' field.",
198
283
  DiagnosticSeverity.Error,
199
284
  line=line
200
285
  ))
286
+
287
+ # Tags Integrity Check
288
+ # Requirement: tags field must carry parent dependencies and related issue id
289
+ required_tags = set()
290
+
291
+ # Self ID
292
+ required_tags.add(f"#{meta.id}")
293
+
294
+ if meta.parent:
295
+ # Strip potential user # if accidentally added in models, though core stripped it
296
+ # But here we want the tag TO HAVE #
297
+ p = meta.parent if not meta.parent.startswith("#") else meta.parent[1:]
298
+ required_tags.add(f"#{p}")
299
+
300
+ for d in meta.dependencies:
301
+ _d = d if not d.startswith("#") else d[1:]
302
+ required_tags.add(f"#{_d}")
303
+
304
+ for r in meta.related:
305
+ _r = r if not r.startswith("#") else r[1:]
306
+ required_tags.add(f"#{_r}")
307
+
308
+ current_tags = set(meta.tags) if meta.tags else set()
309
+ missing_tags = required_tags - current_tags
310
+
311
+ if missing_tags:
312
+ line = self._get_field_line(content, "tags")
313
+ # If tags field doesn't exist, line is 0, which is fine
314
+ # We join them for display
315
+ missing_str = ", ".join(sorted(missing_tags))
316
+ diagnostics.append(self._create_diagnostic(
317
+ f"Tag Check: Missing required context tags: {missing_str}",
318
+ DiagnosticSeverity.Warning,
319
+ line=line
320
+ ))
321
+
201
322
  return diagnostics
202
323
 
203
324
  def _validate_references(self, meta: IssueMetadata, content: str, all_ids: Set[str]) -> List[Diagnostic]:
204
325
  diagnostics = []
326
+
327
+ # Malformed ID Check
328
+ if meta.parent and meta.parent.startswith("#"):
329
+ line = self._get_field_line(content, "parent")
330
+ diagnostics.append(self._create_diagnostic(
331
+ f"Malformed ID: Parent '{meta.parent}' should not start with '#'.",
332
+ DiagnosticSeverity.Warning,
333
+ line=line
334
+ ))
335
+
336
+ if meta.dependencies:
337
+ for dep in meta.dependencies:
338
+ if dep.startswith("#"):
339
+ line = self._get_field_line(content, "dependencies")
340
+ diagnostics.append(self._create_diagnostic(
341
+ f"Malformed ID: Dependency '{dep}' should not start with '#'.",
342
+ DiagnosticSeverity.Warning,
343
+ line=line
344
+ ))
345
+
346
+ if meta.related:
347
+ for rel in meta.related:
348
+ if rel.startswith("#"):
349
+ line = self._get_field_line(content, "related")
350
+ diagnostics.append(self._create_diagnostic(
351
+ f"Malformed ID: Related '{rel}' should not start with '#'.",
352
+ DiagnosticSeverity.Warning,
353
+ line=line
354
+ ))
355
+
205
356
  if not all_ids:
206
357
  return diagnostics
207
358
 
208
- if meta.parent and meta.parent not in all_ids:
359
+ if meta.parent and meta.parent not in all_ids and not meta.parent.startswith("#"):
209
360
  line = self._get_field_line(content, "parent")
210
361
  diagnostics.append(self._create_diagnostic(
211
362
  f"Broken Reference: Parent '{meta.parent}' not found.",
@@ -221,6 +372,42 @@ class IssueValidator:
221
372
  DiagnosticSeverity.Error,
222
373
  line=line
223
374
  ))
375
+
376
+ # Body Reference Check
377
+ # Regex for generic issue ID: (EPIC|FEAT|CHORE|FIX)-\d{4}
378
+ # We scan line by line to get line numbers
379
+ lines = content.split('\n')
380
+ # Skip frontmatter for body check to avoid double counting (handled above)
381
+ in_fm = False
382
+ fm_end = 0
383
+ for i, line in enumerate(lines):
384
+ if line.strip() == '---':
385
+ if not in_fm: in_fm = True
386
+ else:
387
+ fm_end = i
388
+ break
389
+
390
+ for i, line in enumerate(lines):
391
+ if i <= fm_end: continue # Skip frontmatter
392
+
393
+ # Find all matches
394
+ matches = re.finditer(r"\b((?:EPIC|FEAT|CHORE|FIX)-\d{4})\b", line)
395
+ for match in matches:
396
+ ref_id = match.group(1)
397
+ if ref_id != meta.id and ref_id not in all_ids:
398
+ # Check if it's a namespaced ID? The regex only catches local IDs.
399
+ # If users use MON::FEAT-0001, the regex might catch FEAT-0001.
400
+ # But all_ids contains full IDs (potentially namespaced).
401
+ # Simple logic: if ref_id isn't in all_ids, check if any id ENDS with ref_id
402
+
403
+ found_namespaced = any(known.endswith(f"::{ref_id}") for known in all_ids)
404
+
405
+ if not found_namespaced:
406
+ diagnostics.append(self._create_diagnostic(
407
+ f"Broken Reference: Issue '{ref_id}' not found.",
408
+ DiagnosticSeverity.Warning,
409
+ line=i
410
+ ))
224
411
  return diagnostics
225
412
 
226
413
  def _validate_time_consistency(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
@@ -249,21 +436,20 @@ class IssueValidator:
249
436
 
250
437
  return diagnostics
251
438
 
252
- def _validate_checkbox_logic(self, content: str) -> List[Diagnostic]:
439
+ def _validate_checkbox_logic_blocks(self, blocks: List[ContentBlock]) -> List[Diagnostic]:
253
440
  diagnostics = []
254
- lines = content.split('\n')
255
441
 
256
- for i, line in enumerate(lines):
257
- stripped = line.lstrip()
258
-
259
- # Syntax Check: - [?]
260
- if stripped.startswith("- ["):
261
- match = re.match(r"- \[([ x\-/])\]", stripped)
442
+ for block in blocks:
443
+ if block.type == 'task_item':
444
+ content = block.content.strip()
445
+ # Syntax Check: - [?]
446
+ # Added supported chars: /, ~, +
447
+ match = re.match(r"- \[([ x\-/~+])\]", content)
262
448
  if not match:
263
449
  # Check for Common errors
264
- if re.match(r"- \[.{2,}\]", stripped): # [xx] or [ ]
265
- diagnostics.append(self._create_diagnostic("Invalid Checkbox: Use single character [ ], [x], [-], [/]", DiagnosticSeverity.Error, i))
266
- elif re.match(r"- \[([^ x\-/])\]", stripped): # [v], [o]
267
- diagnostics.append(self._create_diagnostic("Invalid Checkbox Status: Use [ ], [x], [-], [/]", DiagnosticSeverity.Error, i))
450
+ if re.match(r"- \[.{2,}\]", content): # [xx] or [ ]
451
+ diagnostics.append(self._create_diagnostic("Invalid Checkbox: Use single character [ ], [x], [-], [/]", DiagnosticSeverity.Error, block.line_start))
452
+ elif re.match(r"- \[([^ x\-/~+])\]", content): # [v], [o]
453
+ diagnostics.append(self._create_diagnostic("Invalid Checkbox Status: Use [ ], [x], [/], [~]", DiagnosticSeverity.Error, block.line_start))
268
454
 
269
455
  return diagnostics
@@ -1,12 +1,12 @@
1
1
  import os
2
2
  import shutil
3
3
  import subprocess
4
- import yaml
4
+
5
5
  from pathlib import Path
6
6
  from typing import Dict, Optional, List, Any
7
7
  from rich.console import Console
8
8
 
9
- from monoco.core.config import get_config
9
+ from monoco.core.config import get_config, load_raw_config, save_raw_config, ConfigScope
10
10
 
11
11
  console = Console()
12
12
 
@@ -29,26 +29,10 @@ def run_git_command(cmd: List[str], cwd: Path) -> bool:
29
29
  console.print("[red]Error:[/red] git command not found.")
30
30
  return False
31
31
 
32
- def get_config_file_path(root: Path) -> Path:
33
- """Determine the config file to update."""
34
- # Standard: .monoco/project.yaml
35
- hidden = root / ".monoco" / "project.yaml"
36
-
37
- # Ensure parent exists
38
- hidden.parent.mkdir(exist_ok=True)
39
- return hidden
40
-
41
32
  def update_config_repos(root: Path, repo_name: str, repo_url: str, remove: bool = False):
42
33
  """Update the repos list in the config file."""
43
- config_path = get_config_file_path(root)
44
-
45
- data = {}
46
- if config_path.exists():
47
- try:
48
- with open(config_path, "r") as f:
49
- data = yaml.safe_load(f) or {}
50
- except Exception:
51
- data = {}
34
+ # Use core config utils
35
+ data = load_raw_config(ConfigScope.PROJECT, project_root=str(root))
52
36
 
53
37
  # Ensure structure exists
54
38
  if "project" not in data:
@@ -62,8 +46,7 @@ def update_config_repos(root: Path, repo_name: str, repo_url: str, remove: bool
62
46
  else:
63
47
  data["project"]["spike_repos"][repo_name] = repo_url
64
48
 
65
- with open(config_path, "w") as f:
66
- yaml.dump(data, f, sort_keys=False, default_flow_style=False)
49
+ save_raw_config(ConfigScope.PROJECT, data, project_root=str(root))
67
50
 
68
51
  def ensure_gitignore(root: Path, target_dir_name: str):
69
52
  """Ensure the target directory is in .gitignore."""
@@ -9,7 +9,7 @@ description: 管理用于研究和学习的外部参考仓库。提供对精选
9
9
 
10
10
  ## 概述
11
11
 
12
- Spike 功能允许你:
12
+ Spike 功能允许你:
13
13
 
14
14
  - **添加外部仓库**作为只读参考
15
15
  - **同步仓库内容**到本地 `.references/` 目录
@@ -50,7 +50,7 @@ monoco spike list
50
50
 
51
51
  ## 配置
52
52
 
53
- Spike 仓库在 `.monoco/config.yaml` 中配置:
53
+ Spike 仓库在 `.monoco/config.yaml` 中配置:
54
54
 
55
55
  ```yaml
56
56
  project:
monoco/main.py CHANGED
@@ -99,15 +99,7 @@ from monoco.core.sync import sync_command, uninstall_command
99
99
  app.command(name="sync")(sync_command)
100
100
  app.command(name="uninstall")(uninstall_command)
101
101
 
102
- @app.command(name="doctor")
103
- def doctor_cmd(
104
- force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
105
- ):
106
- """
107
- Diagnose Agent Environment.
108
- """
109
- from monoco.features.agent.doctor import doctor
110
- doctor(force)
102
+
111
103
 
112
104
  @app.command()
113
105
  def info():
@@ -161,26 +153,10 @@ app.add_typer(config_cmd.app, name="config", help="Manage configuration")
161
153
  app.add_typer(project_cmd.app, name="project", help="Manage projects")
162
154
  app.add_typer(workspace_cmd.app, name="workspace", help="Manage workspace")
163
155
 
164
- from monoco.features.agent import commands as agent_cmd
165
- app.add_typer(agent_cmd.app, name="agent", help="Delegate tasks to Agent CLIs")
156
+
166
157
 
167
158
  from monoco.daemon.commands import serve
168
159
  app.command(name="serve")(serve)
169
160
 
170
- @app.command()
171
- def pty(
172
- host: str = "127.0.0.1",
173
- port: int = 3124,
174
- cwd: Optional[str] = None
175
- ):
176
- """
177
- Start the Monoco PTY Daemon (WebSocket).
178
- """
179
- from monoco.features.pty.server import run_pty_server
180
- from pathlib import Path
181
-
182
- path = Path(cwd) if cwd else None
183
- run_pty_server(host, port, path)
184
-
185
161
  if __name__ == "__main__":
186
162
  app()
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: monoco-toolkit
3
+ Version: 0.2.8
4
+ Summary: Agent Native Toolkit for Monoco - Task Management & Kanban for AI Agents
5
+ Project-URL: Homepage, https://monoco.io
6
+ Project-URL: Repository, https://github.com/IndenScale/Monoco
7
+ Project-URL: Documentation, https://monoco.io/docs
8
+ Project-URL: Issues, https://github.com/IndenScale/Monoco/issues
9
+ Author-email: Monoco Team <dev@monoco.io>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-native,ai-agents,cli,kanban,monoco,task-management,workflow
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Groupware
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: fastapi>=0.100.0
25
+ Requires-Dist: httpx>=0.28.1
26
+ Requires-Dist: prompt-toolkit>=3.0.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: pyyaml>=6.0
29
+ Requires-Dist: rich>=13.0.0
30
+ Requires-Dist: sse-starlette>=1.6.0
31
+ Requires-Dist: typer[all]>=0.9.0
32
+ Requires-Dist: uvicorn[standard]>=0.20.0
33
+ Requires-Dist: watchdog>=6.0.0
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Monoco Toolkit
37
+
38
+ [![Version](https://img.shields.io/pypi/v/monoco-toolkit)](https://pypi.org/project/monoco-toolkit/)
39
+ [![License](https://img.shields.io/github/license/IndenScale/Monoco)](LICENSE)
40
+
41
+ > **The Operating System for Agentic Engineering.**
42
+ >
43
+ > Ground your AI Agents into deterministic workflows. Turn vague "chats" into structured, validatable, and shippable engineering units.
44
+
45
+ ---
46
+
47
+ ## ⚡️ Why Monoco?
48
+
49
+ In the era of LLMs, the bottleneck isn't **intelligence**—it's **control**.
50
+
51
+ Generating code is easy. Managing the lifecycle of thousands of agent-generated tasks, validating their outputs, and maintaining a coherent project state is hard. **Monoco** is the missing control plane that bridges the gap between raw AI velocity and strict engineering rigor.
52
+
53
+ Monoco handles the **"BizOps Logic"** of your development process, allowing you to orchestrate human and AI labor within a unified, version-controlled environment.
54
+
55
+ ## 🌟 Core Features
56
+
57
+ ### 1. Issue as Code (IaaC)
58
+
59
+ Treat your project management like your code.
60
+
61
+ - **Markdown Native**: All tasks (Epics, Features, Chores) are stored as structured Markdown files in your repository.
62
+ - **Git Backed**: Version control your roadmap. Review changes to requirements via Pull Requests.
63
+ - **Universal Context**: Provides a standardized, hallucination-free state representation for AI Agents.
64
+
65
+ ### 2. The Agent Cockpit (VS Code Extension)
66
+
67
+ Stop context switching. Manage your entire agentic workflow directly inside your editor.
68
+
69
+ - **Native Kanban Board**: Visualize and drag-and-drop tasks without leaving VS Code.
70
+ - **Hierarchical Tree View**: Drill down from high-level Epics to atomic Implementation Tasks.
71
+ - **Agent Integration**: Bind specific Agent Providers (Gemini, Claude, etc.) to specific tasks.
72
+
73
+ ### 3. Traceable Execution
74
+
75
+ - **Deterministic State Machine**: Every task follows a strict lifecycle (Proposed -> Approved -> Doing -> Review -> Done).
76
+ - **Audit Trails**: Agents log their actions and decisions directly into the task file.
77
+ - **Sanity Checks**: Built-in linters ensure your task definitions are complete and valid before execution.
78
+
79
+ ## 🚀 Quick Start
80
+
81
+ ### Installation
82
+
83
+ Monoco is available as a Python CLI tool.
84
+
85
+ ```bash
86
+ pip install monoco-toolkit
87
+ ```
88
+
89
+ ### Initialization
90
+
91
+ Turn any directory into a Monoco workspace.
92
+
93
+ ```bash
94
+ monoco init
95
+ ```
96
+
97
+ ### Workflow
98
+
99
+ 1. **Plan**: Create a new feature request.
100
+ ```bash
101
+ monoco issue create feature -t "Implement Dark Mode"
102
+ ```
103
+ 2. **Start**: Create a feature branch automatically.
104
+ ```bash
105
+ monoco issue start FEAT-001 --branch
106
+ ```
107
+ 3. **Code & Sync**: Track modified files automatically.
108
+ ```bash
109
+ monoco issue sync-files
110
+ ```
111
+ 4. **Visualize**: Open the board in VS Code or via CLI.
112
+ ```bash
113
+ # Starts the local server
114
+ monoco serve
115
+ ```
116
+
117
+ ## 📦 Extension for VS Code
118
+
119
+ The **Monoco VS Code Extension** is the primary visual interface for the toolkit.
120
+
121
+ - **Install from Marketplace**: Search for `Monoco`.
122
+ - **Keybinding**: `Cmd+Shift+P` -> `Monoco: Open Kanban Board`.
123
+
124
+ ## 🛠️ Tech Stack & Architecture
125
+
126
+ - **Core**: Python (CLI & Logic Layer)
127
+ - **Extension**: TypeScript (VS Code Client & LSP)
128
+ - **Data**: Local Filesystem (Markdown/YAML)
129
+
130
+ ## 🤝 Contributing
131
+
132
+ Monoco is designed for the community. We welcome contributions to both the core CLI and the VS Code extension.
133
+
134
+ ## 📄 License
135
+
136
+ MIT © [IndenScale](https://github.com/IndenScale)