monoco-toolkit 0.3.9__py3-none-any.whl → 0.3.11__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 (132) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/config.py +38 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/hooks/builtin/git_cleanup.py +1 -1
  8. monoco/core/ingestion/__init__.py +20 -0
  9. monoco/core/ingestion/discovery.py +248 -0
  10. monoco/core/ingestion/watcher.py +343 -0
  11. monoco/core/ingestion/worker.py +436 -0
  12. monoco/core/injection.py +63 -29
  13. monoco/core/integrations.py +2 -2
  14. monoco/core/loader.py +633 -0
  15. monoco/core/output.py +5 -5
  16. monoco/core/registry.py +34 -19
  17. monoco/core/resource/__init__.py +5 -0
  18. monoco/core/resource/finder.py +98 -0
  19. monoco/core/resource/manager.py +91 -0
  20. monoco/core/resource/models.py +35 -0
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +524 -385
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/daemon/app.py +77 -1
  26. monoco/daemon/commands.py +10 -0
  27. monoco/daemon/mailroom_service.py +196 -0
  28. monoco/daemon/models.py +1 -0
  29. monoco/daemon/scheduler.py +236 -0
  30. monoco/daemon/services.py +185 -0
  31. monoco/daemon/triggers.py +55 -0
  32. monoco/features/agent/__init__.py +2 -2
  33. monoco/features/agent/adapter.py +41 -0
  34. monoco/features/agent/apoptosis.py +44 -0
  35. monoco/features/agent/cli.py +101 -144
  36. monoco/features/agent/config.py +35 -21
  37. monoco/features/agent/defaults.py +6 -49
  38. monoco/features/agent/engines.py +32 -6
  39. monoco/features/agent/manager.py +47 -6
  40. monoco/features/agent/models.py +2 -2
  41. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  42. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  43. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  44. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  45. monoco/{core/resources/en → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +3 -1
  46. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +94 -0
  47. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +93 -0
  48. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +85 -0
  49. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +114 -0
  50. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  51. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  52. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  53. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +49 -0
  54. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +46 -0
  55. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +46 -0
  56. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +47 -0
  57. monoco/{core/resources/zh → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +3 -1
  58. monoco/features/agent/resources/{skills/flow_engineer → zh/skills/monoco_workflow_agent_engineer}/SKILL.md +2 -2
  59. monoco/features/agent/resources/{skills/flow_manager → zh/skills/monoco_workflow_agent_manager}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +259 -0
  61. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +137 -0
  62. monoco/features/agent/session.py +59 -11
  63. monoco/features/agent/worker.py +38 -2
  64. monoco/features/artifact/__init__.py +0 -0
  65. monoco/features/artifact/adapter.py +33 -0
  66. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  67. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  68. monoco/features/glossary/__init__.py +0 -0
  69. monoco/features/glossary/adapter.py +42 -0
  70. monoco/features/glossary/config.py +5 -0
  71. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  72. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +35 -0
  73. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  74. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +35 -0
  75. monoco/features/hooks/__init__.py +11 -0
  76. monoco/features/hooks/adapter.py +67 -0
  77. monoco/features/hooks/commands.py +309 -0
  78. monoco/features/hooks/core.py +441 -0
  79. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  80. monoco/features/i18n/adapter.py +18 -5
  81. monoco/features/i18n/core.py +482 -17
  82. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  83. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +105 -0
  84. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_atom_i18n/SKILL.md} +3 -1
  85. monoco/features/i18n/resources/{skills/i18n_scan_workflow → zh/skills/monoco_workflow_i18n_scan}/SKILL.md +2 -2
  86. monoco/features/issue/adapter.py +19 -6
  87. monoco/features/issue/commands.py +281 -7
  88. monoco/features/issue/core.py +272 -19
  89. monoco/features/issue/engine/machine.py +118 -5
  90. monoco/features/issue/linter.py +60 -5
  91. monoco/features/issue/models.py +3 -2
  92. monoco/features/issue/resources/en/AGENTS.md +109 -0
  93. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_atom_issue/SKILL.md} +3 -1
  94. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  95. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  96. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +159 -0
  97. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  98. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  99. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  100. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  101. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  102. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_atom_issue_lifecycle/SKILL.md} +3 -1
  103. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +167 -0
  104. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +224 -0
  105. monoco/features/issue/resources/{skills/issue_lifecycle_workflow → zh/skills/monoco_workflow_issue_management}/SKILL.md +2 -2
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +203 -0
  107. monoco/features/issue/validator.py +101 -1
  108. monoco/features/memo/adapter.py +21 -8
  109. monoco/features/memo/cli.py +103 -10
  110. monoco/features/memo/core.py +178 -92
  111. monoco/features/memo/models.py +53 -0
  112. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +77 -0
  113. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +140 -0
  114. monoco/features/memo/resources/zh/{SKILL.md → skills/monoco_atom_memo/SKILL.md} +3 -1
  115. monoco/features/memo/resources/{skills/note_processing_workflow → zh/skills/monoco_workflow_note_processing}/SKILL.md +2 -2
  116. monoco/features/spike/adapter.py +18 -5
  117. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  118. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +121 -0
  119. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_atom_spike/SKILL.md} +3 -1
  120. monoco/features/spike/resources/{skills/research_workflow → zh/skills/monoco_workflow_research}/SKILL.md +2 -2
  121. monoco/main.py +38 -1
  122. monoco_toolkit-0.3.11.dist-info/METADATA +130 -0
  123. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  124. monoco/features/agent/reliability.py +0 -106
  125. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +0 -114
  126. monoco_toolkit-0.3.9.dist-info/METADATA +0 -127
  127. monoco_toolkit-0.3.9.dist-info/RECORD +0 -115
  128. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  129. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  130. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  131. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  132. {monoco_toolkit-0.3.9.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
monoco/core/sync.py CHANGED
@@ -46,6 +46,12 @@ def sync_command(
46
46
  help="Specific file to update (default: auto-detect from config or standard files)",
47
47
  ),
48
48
  check: bool = typer.Option(False, "--check", help="Dry run check mode"),
49
+ workflows: bool = typer.Option(
50
+ False,
51
+ "--workflows",
52
+ "-w",
53
+ help="Also distribute Flow Skills as Antigravity Workflows to .agent/workflows/",
54
+ ),
49
55
  ):
50
56
  """
51
57
  Synchronize Agent Environment (System Prompts & Skills).
@@ -84,7 +90,39 @@ def sync_command(
84
90
  f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]"
85
91
  )
86
92
 
87
- # 3. Distribute Skills
93
+
94
+
95
+ # 3. Distribute Roles
96
+ console.print("[bold blue]Distributing agent roles...[/bold blue]")
97
+
98
+ # Source: Builtin Resource Dir
99
+ # monoco/core/sync.py -> monoco/core -> monoco -> features/agent/resources/roles
100
+ resource_dir = Path(__file__).parent.parent / "features" / "agent" / "resources" / "roles"
101
+
102
+ # Target: .monoco/roles
103
+ target_roles_dir = root / ".monoco" / "roles"
104
+ # Only create if we have sources
105
+ if resource_dir.exists():
106
+ target_roles_dir.mkdir(parents=True, exist_ok=True)
107
+ import shutil
108
+
109
+ count = 0
110
+ for yaml_file in resource_dir.glob("*.yaml"):
111
+ target_file = target_roles_dir / yaml_file.name
112
+ try:
113
+ # Copy only if different or new? For now, nice and simple overwrite.
114
+ shutil.copy2(yaml_file, target_file)
115
+ console.print(f"[dim] ✓ Synced role {yaml_file.name}[/dim]")
116
+ count += 1
117
+ except Exception as e:
118
+ console.print(f"[red] Failed to sync role {yaml_file.name}: {e}[/red]")
119
+
120
+ if count > 0:
121
+ console.print(f"[green] ✓ Updated {count} roles in .monoco/roles/[/green]")
122
+ else:
123
+ console.print("[yellow] No builtin roles found to sync.[/yellow]")
124
+
125
+ # 4. Distribute Skills
88
126
  console.print("[bold blue]Distributing skills to agent frameworks...[/bold blue]")
89
127
 
90
128
  # Determine language from config
@@ -124,6 +162,26 @@ def sync_command(
124
162
  "[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]"
125
163
  )
126
164
 
165
+ # 5. Distribute Workflows (if --workflows flag is set)
166
+ if workflows:
167
+ console.print("[bold blue]Distributing Flow Skills as Workflows...[/bold blue]")
168
+
169
+ try:
170
+ workflow_results = skill_manager.distribute_workflows(force=False, lang=skill_lang)
171
+ success_count = sum(1 for v in workflow_results.values() if v)
172
+ if workflow_results:
173
+ console.print(
174
+ f"[green] ✓ Distributed {success_count}/{len(workflow_results)} workflows to .agent/workflows/[/green]"
175
+ )
176
+ else:
177
+ console.print(
178
+ "[yellow] No Flow Skills found to convert[/yellow]"
179
+ )
180
+ except Exception as e:
181
+ console.print(
182
+ f"[red] Failed to distribute workflows: {e}[/red]"
183
+ )
184
+
127
185
  # 4. Determine Targets
128
186
  targets = _get_targets(root, config, target)
129
187
 
@@ -238,3 +296,17 @@ def uninstall_command(
238
296
  console.print(
239
297
  "[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]"
240
298
  )
299
+
300
+ # 3. Clean up Workflows
301
+ console.print("[bold blue]Cleaning up distributed workflows...[/bold blue]")
302
+
303
+ try:
304
+ removed_count = skill_manager.cleanup_workflows()
305
+ if removed_count > 0:
306
+ console.print(
307
+ f"[green] ✓ Removed {removed_count} workflows from .agent/workflows/[/green]"
308
+ )
309
+ except Exception as e:
310
+ console.print(
311
+ f"[red] Failed to clean workflows: {e}[/red]"
312
+ )
@@ -0,0 +1,420 @@
1
+ """
2
+ Flow Skill to Antigravity Workflow Converter.
3
+
4
+ This module converts Monoco Flow Skills to Antigravity Workflow format.
5
+
6
+ Conversion Rules:
7
+ 1. Frontmatter: Only keep 'description', discard other fields (name, type, role, version, author)
8
+ 2. Filename: monoco_flow_engineer/SKILL.md -> flow-engineer.md
9
+ 3. Content: Remove Mermaid state diagrams, convert to simple step lists
10
+ 4. Output: .agent/workflows/ directory
11
+ """
12
+
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Optional, Tuple
16
+ import yaml
17
+ from rich.console import Console
18
+
19
+ console = Console()
20
+
21
+
22
+ class FlowSkillConverter:
23
+ """Converts Flow Skill files to Antigravity Workflow format."""
24
+
25
+ # Source directories to search for Flow Skills
26
+ SOURCE_PATTERNS = [
27
+ "monoco/features/agent/resources/{lang}/skills/flow_*/SKILL.md",
28
+ "monoco/features/*/resources/{lang}/skills/flow_*/SKILL.md",
29
+ ]
30
+
31
+ def __init__(self, root_dir: Path):
32
+ """
33
+ Initialize converter with project root directory.
34
+
35
+ Args:
36
+ root_dir: Project root directory
37
+ """
38
+ self.root_dir = root_dir
39
+
40
+ def _is_flow_skill(self, skill_file: Path) -> bool:
41
+ """
42
+ Check if a SKILL.md file is a Flow Skill by reading its frontmatter.
43
+
44
+ Args:
45
+ skill_file: Path to SKILL.md file
46
+
47
+ Returns:
48
+ True if the skill is a Flow Skill (type == "flow")
49
+ """
50
+ try:
51
+ content = skill_file.read_text(encoding="utf-8")
52
+ if not content.startswith("---"):
53
+ return False
54
+
55
+ parts = content.split("---", 2)
56
+ if len(parts) < 3:
57
+ return False
58
+
59
+ frontmatter = yaml.safe_load(parts[1].strip()) or {}
60
+ return frontmatter.get("type") == "flow"
61
+ except Exception:
62
+ return False
63
+
64
+ def discover_flow_skills(self, lang: str = "zh") -> list[Path]:
65
+ """
66
+ Discover all Flow Skill files in the source directories.
67
+
68
+ Flow Skills are discovered from monoco/features/*/resources/ directories,
69
+ not from the distributed .claude/skills/ directory.
70
+
71
+ Args:
72
+ lang: Language code (default: "zh")
73
+
74
+ Returns:
75
+ List of paths to Flow Skill SKILL.md files
76
+ """
77
+ flow_skills = []
78
+ seen_names = set()
79
+
80
+ # Search in source directories
81
+ features_dir = self.root_dir / "monoco" / "features"
82
+ if features_dir.exists():
83
+ for feature_dir in features_dir.iterdir():
84
+ if not feature_dir.is_dir():
85
+ continue
86
+
87
+ resources_dir = feature_dir / "resources" / lang / "skills"
88
+ if not resources_dir.exists():
89
+ continue
90
+
91
+ for skill_dir in resources_dir.iterdir():
92
+ if not skill_dir.is_dir():
93
+ continue
94
+
95
+ skill_file = skill_dir / "SKILL.md"
96
+ if skill_file.exists() and skill_dir.name not in seen_names:
97
+ # Check if it's a flow skill by reading frontmatter
98
+ if self._is_flow_skill(skill_file):
99
+ flow_skills.append(skill_file)
100
+ seen_names.add(skill_dir.name)
101
+
102
+ return sorted(flow_skills)
103
+
104
+ def convert_skill(self, skill_file: Path) -> Tuple[str, str]:
105
+ """
106
+ Convert a Flow Skill file to Antigravity Workflow format.
107
+
108
+ Args:
109
+ skill_file: Path to the Flow Skill SKILL.md file
110
+
111
+ Returns:
112
+ Tuple of (workflow_filename, workflow_content)
113
+ """
114
+ content = skill_file.read_text(encoding="utf-8")
115
+
116
+ # Parse frontmatter
117
+ frontmatter, body = self._extract_frontmatter(content)
118
+
119
+ # Convert frontmatter (keep only description)
120
+ new_frontmatter = self._convert_frontmatter(frontmatter)
121
+
122
+ # Convert body content
123
+ new_body = self._convert_body(body)
124
+
125
+ # Generate workflow filename
126
+ workflow_filename = self._generate_filename(skill_file)
127
+
128
+ # Combine
129
+ if new_frontmatter:
130
+ workflow_content = f"---\n{new_frontmatter}---\n\n{new_body}"
131
+ else:
132
+ workflow_content = new_body
133
+
134
+ return workflow_filename, workflow_content
135
+
136
+ def _extract_frontmatter(self, content: str) -> Tuple[dict, str]:
137
+ """
138
+ Extract YAML frontmatter from markdown content.
139
+
140
+ Args:
141
+ content: Full markdown content
142
+
143
+ Returns:
144
+ Tuple of (frontmatter_dict, body_content)
145
+ """
146
+ if not content.startswith("---"):
147
+ return {}, content
148
+
149
+ parts = content.split("---", 2)
150
+ if len(parts) < 3:
151
+ return {}, content
152
+
153
+ try:
154
+ frontmatter = yaml.safe_load(parts[1].strip()) or {}
155
+ except yaml.YAMLError:
156
+ frontmatter = {}
157
+
158
+ body = parts[2].strip()
159
+ return frontmatter, body
160
+
161
+ def _convert_frontmatter(self, frontmatter: dict) -> str:
162
+ """
163
+ Convert frontmatter to Antigravity Workflow format.
164
+
165
+ Only keeps 'description' field.
166
+
167
+ Args:
168
+ frontmatter: Original frontmatter dictionary
169
+
170
+ Returns:
171
+ New frontmatter as YAML string
172
+ """
173
+ description = frontmatter.get("description", "")
174
+
175
+ if not description:
176
+ return ""
177
+
178
+ # Create minimal frontmatter with only description
179
+ new_frontmatter = {"description": description}
180
+
181
+ return yaml.dump(new_frontmatter, allow_unicode=True, sort_keys=False)
182
+
183
+ def _convert_body(self, body: str) -> str:
184
+ """
185
+ Convert body content to Antigravity Workflow format.
186
+
187
+ - Remove Mermaid state diagrams
188
+ - Keep step sections but simplify
189
+ - Remove complex formatting
190
+
191
+ Args:
192
+ body: Original body content
193
+
194
+ Returns:
195
+ Converted body content
196
+ """
197
+ lines = body.split("\n")
198
+ result_lines = []
199
+ in_mermaid = False
200
+ skip_section = False
201
+
202
+ for line in lines:
203
+ # Detect Mermaid code block start
204
+ if line.strip().startswith("```mermaid"):
205
+ in_mermaid = True
206
+ continue
207
+
208
+ # Detect Mermaid code block end
209
+ if in_mermaid and line.strip() == "```":
210
+ in_mermaid = False
211
+ continue
212
+
213
+ # Skip lines inside Mermaid block
214
+ if in_mermaid:
215
+ continue
216
+
217
+ # Skip "工作流状态机" section header
218
+ if "工作流状态机" in line or "Workflow State Machine" in line:
219
+ skip_section = True
220
+ continue
221
+
222
+ # Detect new section (level 2 header)
223
+ if line.strip().startswith("## ") and skip_section:
224
+ skip_section = False
225
+
226
+ if skip_section:
227
+ continue
228
+
229
+ # Keep the line
230
+ result_lines.append(line)
231
+
232
+ # Clean up excessive blank lines
233
+ cleaned_lines = self._cleanup_blank_lines(result_lines)
234
+
235
+ return "\n".join(cleaned_lines)
236
+
237
+ def _cleanup_blank_lines(self, lines: list[str]) -> list[str]:
238
+ """
239
+ Clean up excessive blank lines while preserving structure.
240
+
241
+ Args:
242
+ lines: List of content lines
243
+
244
+ Returns:
245
+ Cleaned list of lines
246
+ """
247
+ result = []
248
+ prev_blank = False
249
+
250
+ for line in lines:
251
+ is_blank = not line.strip()
252
+
253
+ # Skip consecutive blank lines
254
+ if is_blank and prev_blank:
255
+ continue
256
+
257
+ result.append(line)
258
+ prev_blank = is_blank
259
+
260
+ # Remove trailing blank lines
261
+ while result and not result[-1].strip():
262
+ result.pop()
263
+
264
+ return result
265
+
266
+ def _generate_filename(self, skill_file: Path) -> str:
267
+ """
268
+ Generate workflow filename from skill file path.
269
+
270
+ Conversion: flow_engineer/SKILL.md -> flow-engineer.md
271
+
272
+ Args:
273
+ skill_file: Path to the Flow Skill SKILL.md file
274
+
275
+ Returns:
276
+ Workflow filename
277
+ """
278
+ # Get parent directory name (e.g., "flow_engineer")
279
+ skill_dir_name = skill_file.parent.name
280
+
281
+ # Remove "flow_" prefix
282
+ if skill_dir_name.startswith("flow_"):
283
+ role_name = skill_dir_name[len("flow_"):]
284
+ else:
285
+ role_name = skill_dir_name
286
+
287
+ # Convert to workflow filename
288
+ workflow_filename = f"flow-{role_name}.md"
289
+
290
+ return workflow_filename
291
+
292
+
293
+ class WorkflowDistributor:
294
+ """Distributes converted workflows to target directory."""
295
+
296
+ def __init__(self, root_dir: Path):
297
+ """
298
+ Initialize distributor.
299
+
300
+ Args:
301
+ root_dir: Project root directory
302
+ """
303
+ self.root_dir = root_dir
304
+ self.converter = FlowSkillConverter(root_dir)
305
+
306
+ def distribute(self, force: bool = False, lang: str = "zh") -> dict[str, bool]:
307
+ """
308
+ Convert and distribute all Flow Skills to .agent/workflows/.
309
+
310
+ Args:
311
+ force: Overwrite existing files even if unchanged
312
+ lang: Language code for Flow Skills (default: "zh")
313
+
314
+ Returns:
315
+ Dictionary mapping workflow filenames to success status
316
+ """
317
+ results = {}
318
+
319
+ # Discover flow skills
320
+ flow_skills = self.converter.discover_flow_skills(lang=lang)
321
+
322
+ if not flow_skills:
323
+ console.print("[yellow]No Flow Skills found to convert[/yellow]")
324
+ return results
325
+
326
+ # Target directory
327
+ workflows_dir = self.root_dir / ".agent" / "workflows"
328
+ workflows_dir.mkdir(parents=True, exist_ok=True)
329
+
330
+ console.print(f"[dim]Found {len(flow_skills)} Flow Skills to convert[/dim]")
331
+
332
+ for skill_file in flow_skills:
333
+ try:
334
+ workflow_filename, workflow_content = self.converter.convert_skill(skill_file)
335
+ target_file = workflows_dir / workflow_filename
336
+
337
+ # Check if update is needed
338
+ if target_file.exists() and not force:
339
+ existing_content = target_file.read_text(encoding="utf-8")
340
+ if existing_content == workflow_content:
341
+ console.print(f"[dim] = {workflow_filename} is up to date[/dim]")
342
+ results[workflow_filename] = True
343
+ continue
344
+
345
+ # Write workflow file
346
+ target_file.write_text(workflow_content, encoding="utf-8")
347
+ console.print(f"[green] ✓ Created {workflow_filename}[/green]")
348
+ results[workflow_filename] = True
349
+
350
+ except Exception as e:
351
+ console.print(f"[red] ✗ Failed to convert {skill_file.name}: {e}[/red]")
352
+ results[skill_file.name] = False
353
+
354
+ return results
355
+
356
+ def cleanup(self, lang: str = "zh") -> int:
357
+ """
358
+ Remove all distributed workflows from .agent/workflows/.
359
+
360
+ Args:
361
+ lang: Language code for Flow Skills (default: "zh")
362
+
363
+ Returns:
364
+ Number of files removed
365
+ """
366
+ workflows_dir = self.root_dir / ".agent" / "workflows"
367
+
368
+ if not workflows_dir.exists():
369
+ return 0
370
+
371
+ removed_count = 0
372
+
373
+ # Discover flow skills to know which files to remove
374
+ flow_skills = self.converter.discover_flow_skills(lang=lang)
375
+ workflow_filenames = set()
376
+
377
+ for skill_file in flow_skills:
378
+ workflow_filename = self.converter._generate_filename(skill_file)
379
+ workflow_filenames.add(workflow_filename)
380
+
381
+ # Remove workflow files
382
+ for workflow_file in workflows_dir.glob("flow-*.md"):
383
+ if workflow_file.name in workflow_filenames:
384
+ workflow_file.unlink()
385
+ console.print(f"[green] ✓ Removed {workflow_file.name}[/green]")
386
+ removed_count += 1
387
+
388
+ # Remove empty directory
389
+ if workflows_dir.exists() and not any(workflows_dir.iterdir()):
390
+ workflows_dir.rmdir()
391
+ console.print(f"[dim] Removed empty directory: {workflows_dir}[/dim]")
392
+
393
+ if removed_count == 0:
394
+ console.print(f"[dim]No workflows to remove from {workflows_dir}[/dim]")
395
+
396
+ return removed_count
397
+
398
+
399
+ def convert_flow_skill_to_workflow(skill_content: str) -> str:
400
+ """
401
+ Convert Flow Skill content to Antigravity Workflow format.
402
+
403
+ This is a standalone utility function for direct content conversion.
404
+
405
+ Args:
406
+ skill_content: Original Flow Skill markdown content
407
+
408
+ Returns:
409
+ Converted Workflow markdown content
410
+ """
411
+ converter = FlowSkillConverter(Path("."))
412
+
413
+ frontmatter, body = converter._extract_frontmatter(skill_content)
414
+ new_frontmatter = converter._convert_frontmatter(frontmatter)
415
+ new_body = converter._convert_body(body)
416
+
417
+ if new_frontmatter:
418
+ return f"---\n{new_frontmatter}---\n\n{new_body}"
419
+ else:
420
+ return new_body
monoco/daemon/app.py CHANGED
@@ -9,6 +9,8 @@ from typing import Optional, List, Dict
9
9
  from monoco.daemon.services import Broadcaster, ProjectManager
10
10
  from monoco.core.git import GitMonitor
11
11
  from monoco.core.config import get_config, ConfigMonitor, ConfigScope, get_config_path
12
+ from monoco.daemon.scheduler import SchedulerService
13
+ from monoco.daemon.mailroom_service import MailroomService
12
14
 
13
15
  # Configure logging
14
16
  logging.basicConfig(level=logging.INFO)
@@ -34,6 +36,8 @@ broadcaster = Broadcaster()
34
36
  git_monitor: GitMonitor | None = None
35
37
  config_monitors: List[ConfigMonitor] = []
36
38
  project_manager: ProjectManager | None = None
39
+ scheduler_service: SchedulerService | None = None
40
+ mailroom_service: MailroomService | None = None
37
41
 
38
42
 
39
43
  @asynccontextmanager
@@ -41,7 +45,7 @@ async def lifespan(app: FastAPI):
41
45
  # Startup
42
46
  logger.info("Starting Monoco Daemon services...")
43
47
 
44
- global project_manager, git_monitor, config_monitors
48
+ global project_manager, git_monitor, config_monitors, scheduler_service, mailroom_service
45
49
  # Use MONOCO_SERVER_ROOT if set, otherwise CWD
46
50
  env_root = os.getenv("MONOCO_SERVER_ROOT")
47
51
  workspace_root = Path(env_root) if env_root else Path.cwd()
@@ -72,6 +76,17 @@ async def lifespan(app: FastAPI):
72
76
  ]
73
77
 
74
78
  await project_manager.start_all()
79
+ # Start Scheduler
80
+ scheduler_service = SchedulerService(project_manager)
81
+ await scheduler_service.start()
82
+
83
+ # Start Mailroom Service
84
+ mailroom_service = MailroomService(
85
+ workspace_root=workspace_root,
86
+ broadcaster=broadcaster,
87
+ )
88
+ await mailroom_service.start()
89
+
75
90
  git_task = asyncio.create_task(git_monitor.start())
76
91
  config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
77
92
 
@@ -84,6 +99,10 @@ async def lifespan(app: FastAPI):
84
99
  m.stop()
85
100
  if project_manager:
86
101
  project_manager.stop_all()
102
+ if scheduler_service:
103
+ scheduler_service.stop()
104
+ if mailroom_service:
105
+ await mailroom_service.stop()
87
106
 
88
107
  await git_task
89
108
  await asyncio.gather(*config_tasks)
@@ -301,6 +320,21 @@ async def create_issue_endpoint(payload: CreateIssueRequest):
301
320
  related=payload.related,
302
321
  subdir=payload.subdir,
303
322
  )
323
+
324
+ # Link memos to the newly created issue
325
+ if payload.from_memos:
326
+ from monoco.features.memo.core import load_memos, update_memo
327
+
328
+ existing_memos = {m.uid: m for m in load_memos(project.issues_root)}
329
+
330
+ for memo_id in payload.from_memos:
331
+ if memo_id in existing_memos:
332
+ # Only update if not already linked to this issue (idempotency)
333
+ memo = existing_memos[memo_id]
334
+ if memo.ref != issue.id:
335
+ update_memo(project.issues_root, memo_id, {"status": "tracked", "ref": issue.id})
336
+ # Non-blocking: ignore missing memos (just log warning)
337
+
304
338
  return issue
305
339
  except Exception as e:
306
340
  raise HTTPException(status_code=400, detail=str(e))
@@ -489,3 +523,45 @@ async def update_workspace_state(state: WorkspaceState):
489
523
  raise HTTPException(
490
524
  status_code=500, detail=f"Failed to persist state: {str(e)}"
491
525
  )
526
+
527
+
528
+ # --- Mailroom API Endpoints ---
529
+
530
+
531
+ @app.get("/api/v1/mailroom/status")
532
+ async def get_mailroom_status():
533
+ """
534
+ Get Mailroom service status, capabilities, and statistics.
535
+ """
536
+ if not mailroom_service:
537
+ raise HTTPException(status_code=503, detail="Mailroom service not initialized")
538
+
539
+ return mailroom_service.get_status()
540
+
541
+
542
+ @app.post("/api/v1/mailroom/discover")
543
+ async def trigger_mailroom_discovery():
544
+ """
545
+ Trigger environment discovery for conversion tools.
546
+ """
547
+ if not mailroom_service:
548
+ raise HTTPException(status_code=503, detail="Mailroom service not initialized")
549
+
550
+ discovery = mailroom_service.get_discovery()
551
+ tools = discovery.discover(force=True)
552
+
553
+ total_tools = sum(len(t) for t in tools.values())
554
+ capabilities = discovery.get_capabilities_summary()
555
+
556
+ return {
557
+ "discovered": total_tools,
558
+ "capabilities": capabilities,
559
+ "tools": [
560
+ {
561
+ "name": tool.name,
562
+ "type": tool.tool_type.value,
563
+ "version": tool.version,
564
+ }
565
+ for tool in discovery.get_all_tools()
566
+ ],
567
+ }
monoco/daemon/commands.py CHANGED
@@ -15,6 +15,9 @@ def serve(
15
15
  False, "--reload", "-r", help="Enable auto-reload for dev"
16
16
  ),
17
17
  root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
18
+ max_agents: Optional[int] = typer.Option(
19
+ None, "--max-agents", help="Override global maximum concurrent agents (default: 3)"
20
+ ),
18
21
  ):
19
22
  """
20
23
  Start the Monoco Daemon server.
@@ -26,6 +29,13 @@ def serve(
26
29
  print_output(
27
30
  f"Workspace Root: {os.environ['MONOCO_SERVER_ROOT']}", title="Monoco Serve"
28
31
  )
32
+
33
+ # Set max agents override if provided
34
+ if max_agents is not None:
35
+ os.environ["MONOCO_MAX_AGENTS"] = str(max_agents)
36
+ print_output(
37
+ f"Max Agents: {max_agents}", title="Monoco Serve"
38
+ )
29
39
 
30
40
  print_output(
31
41
  f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"