monoco-toolkit 0.3.10__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 (100) 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 +31 -4
  6. monoco/core/git.py +23 -0
  7. monoco/core/ingestion/__init__.py +20 -0
  8. monoco/core/ingestion/discovery.py +248 -0
  9. monoco/core/ingestion/watcher.py +343 -0
  10. monoco/core/ingestion/worker.py +436 -0
  11. monoco/core/loader.py +633 -0
  12. monoco/core/registry.py +34 -25
  13. monoco/core/skills.py +119 -80
  14. monoco/daemon/app.py +77 -1
  15. monoco/daemon/commands.py +10 -0
  16. monoco/daemon/mailroom_service.py +196 -0
  17. monoco/daemon/models.py +1 -0
  18. monoco/daemon/scheduler.py +236 -0
  19. monoco/daemon/services.py +185 -0
  20. monoco/daemon/triggers.py +55 -0
  21. monoco/features/agent/adapter.py +17 -7
  22. monoco/features/agent/apoptosis.py +4 -4
  23. monoco/features/agent/manager.py +41 -5
  24. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  25. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  26. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  27. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  28. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  29. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  30. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  31. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  32. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  33. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  34. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  35. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  36. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  37. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  38. monoco/features/agent/session.py +59 -11
  39. monoco/features/artifact/__init__.py +0 -0
  40. monoco/features/artifact/adapter.py +33 -0
  41. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  42. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  43. monoco/features/glossary/adapter.py +18 -7
  44. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  45. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  46. monoco/features/hooks/__init__.py +11 -0
  47. monoco/features/hooks/adapter.py +67 -0
  48. monoco/features/hooks/commands.py +309 -0
  49. monoco/features/hooks/core.py +441 -0
  50. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  51. monoco/features/i18n/adapter.py +18 -5
  52. monoco/features/i18n/core.py +482 -17
  53. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  54. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  55. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  56. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  57. monoco/features/issue/adapter.py +19 -6
  58. monoco/features/issue/commands.py +281 -7
  59. monoco/features/issue/core.py +227 -13
  60. monoco/features/issue/engine/machine.py +114 -4
  61. monoco/features/issue/linter.py +60 -5
  62. monoco/features/issue/models.py +2 -2
  63. monoco/features/issue/resources/en/AGENTS.md +109 -0
  64. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  65. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  66. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  67. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  68. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  69. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  70. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  71. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  72. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  73. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  74. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  75. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  76. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  77. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  78. monoco/features/issue/validator.py +101 -1
  79. monoco/features/memo/adapter.py +21 -8
  80. monoco/features/memo/cli.py +103 -10
  81. monoco/features/memo/core.py +178 -92
  82. monoco/features/memo/models.py +53 -0
  83. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  84. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  85. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  86. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  87. monoco/features/spike/adapter.py +18 -5
  88. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  89. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  90. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  91. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  92. monoco/main.py +38 -1
  93. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/METADATA +7 -1
  94. monoco_toolkit-0.3.11.dist-info/RECORD +181 -0
  95. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  96. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  97. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  98. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/WHEEL +0 -0
  99. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/entry_points.txt +0 -0
  100. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.11.dist-info}/licenses/LICENSE +0 -0
monoco/core/registry.py CHANGED
@@ -1,45 +1,54 @@
1
- from typing import Dict, List
1
+ from typing import Dict, List, Optional
2
2
  from monoco.core.feature import MonocoFeature
3
+ from monoco.core.loader import FeatureLoader, FeatureRegistry as LoaderFeatureRegistry
3
4
 
4
5
 
5
6
  class FeatureRegistry:
6
- _features: Dict[str, MonocoFeature] = {}
7
+ """
8
+ Feature registry that wraps the new unified FeatureLoader.
9
+
10
+ This class provides backward compatibility while delegating to the
11
+ new FeatureLoader for dynamic discovery and lifecycle management.
12
+ """
13
+
14
+ _loader: Optional[FeatureLoader] = None
15
+
16
+ @classmethod
17
+ def _get_loader(cls) -> FeatureLoader:
18
+ """Get or create the default feature loader."""
19
+ if cls._loader is None:
20
+ cls._loader = FeatureLoader()
21
+ # Discover and load all features
22
+ cls._loader.discover()
23
+ cls._loader.load_all()
24
+ return cls._loader
7
25
 
8
26
  @classmethod
9
27
  def register(cls, feature: MonocoFeature):
10
28
  """Register a feature instance."""
11
- cls._features[feature.name] = feature
29
+ loader = cls._get_loader()
30
+ loader.registry.register(feature) # type: ignore
12
31
 
13
32
  @classmethod
14
33
  def get_features(cls) -> List[MonocoFeature]:
15
34
  """Get all registered features."""
16
- return list(cls._features.values())
35
+ loader = cls._get_loader()
36
+ return loader.registry.get_all() # type: ignore
17
37
 
18
38
  @classmethod
19
- def get_feature(cls, name: str) -> MonocoFeature:
39
+ def get_feature(cls, name: str) -> Optional[MonocoFeature]:
20
40
  """Get a specific feature by name."""
21
- return cls._features.get(name)
41
+ loader = cls._get_loader()
42
+ return loader.registry.get(name) # type: ignore
22
43
 
23
44
  @classmethod
24
45
  def load_defaults(cls):
25
46
  """
26
- Load default core features.
27
- TODO: In the future, this could be dynamic via entry points.
28
- """
29
- # Import here to avoid circular dependencies at module level
30
- from monoco.features.issue.adapter import IssueFeature
31
- from monoco.features.spike.adapter import SpikeFeature
32
- from monoco.features.i18n.adapter import I18nFeature
33
- from monoco.features.memo.adapter import MemoFeature
34
-
35
- cls.register(IssueFeature())
36
- cls.register(SpikeFeature())
37
- cls.register(I18nFeature())
38
- cls.register(MemoFeature())
39
-
47
+ Load default core features using the unified FeatureLoader.
40
48
 
41
- from monoco.features.glossary.adapter import GlossaryFeature
42
- cls.register(GlossaryFeature())
43
-
44
- from monoco.features.agent.adapter import AgentFeature
45
- cls.register(AgentFeature())
49
+ This method discovers and loads all features from monoco/features/
50
+ automatically, replacing the manual registration approach.
51
+ """
52
+ loader = cls._get_loader()
53
+ # Features are already discovered and loaded in _get_loader
54
+ # This method is kept for backward compatibility
monoco/core/skills.py CHANGED
@@ -2,17 +2,21 @@
2
2
  Skill Manager for Monoco Toolkit.
3
3
 
4
4
  This module provides centralized management and distribution of Agent Skills
5
- following the agentskills.io standard and the three-level architecture:
6
- - Atom Skills: Atomic capabilities
7
- - Workflow Skills: Orchestration of atoms
8
- - Role Skills: Configuration layer
5
+ following the standardized architecture:
6
+ - Atom Skills: monoco_atom_{name} - Atomic capabilities
7
+ - Workflow Skills: monoco_workflow_{name} - Orchestration of atoms
8
+ - Role Skills: monoco_role_{name} - Configuration layer
9
9
 
10
10
  Key Responsibilities:
11
- 1. Discover skills from resources/{lang}/skills/ directory (legacy)
12
- 2. Discover skills from resources/atoms/, workflows/, roles/ (new three-level)
13
- 3. Validate skill structure and metadata
14
- 4. Distribute skills to target agent framework directories
15
- 5. Support i18n for skill content
11
+ 1. Discover skills from features (monoco/features/{feature}/resources/)
12
+ 2. Validate skill structure and metadata
13
+ 3. Distribute skills to target agent framework directories
14
+ 4. Support i18n for skill content
15
+
16
+ Architecture Principle:
17
+ - Core is framework-only, no skills
18
+ - All skills are defined in Features (value delivery atoms)
19
+ - All skills follow naming convention: monoco_{type}_{name}
16
20
  """
17
21
 
18
22
  import shutil
@@ -165,22 +169,17 @@ class Skill:
165
169
 
166
170
  class SkillManager:
167
171
  """
168
- Central manager for Monoco skills supporting both legacy and three-level architecture.
172
+ Central manager for Monoco skills.
169
173
 
170
- Three-Level Architecture:
171
- - Atom Skills: resources/atoms/*.yaml
172
- - Workflow Skills: resources/workflows/*.yaml
173
- - Role Skills: resources/roles/*.yaml
174
+ Architecture:
175
+ - Atom Skills: resources/{lang}/skills/monoco_atom_*/SKILL.md or resources/atoms/*.yaml
176
+ - Workflow Skills: resources/{lang}/skills/monoco_workflow_*/SKILL.md or resources/workflows/*.yaml
177
+ - Role Skills: resources/{lang}/roles/monoco_role_*.yaml
174
178
 
175
- Legacy Architecture:
176
- - Standard Skills: resources/{lang}/skills/{name}/SKILL.md
177
- - Flow Skills: resources/{lang}/skills/flow_*/SKILL.md
179
+ All skills follow the naming convention: monoco_{type}_{name}
178
180
  """
179
181
 
180
- # Default prefix for flow skills
181
- FLOW_SKILL_PREFIX = "monoco_flow_"
182
-
183
- # Prefix for three-level architecture skills
182
+ # Prefix for standardized skill naming
184
183
  ATOM_PREFIX = "monoco_atom_"
185
184
  WORKFLOW_PREFIX = "monoco_workflow_"
186
185
  ROLE_PREFIX = "monoco_role_"
@@ -189,37 +188,24 @@ class SkillManager:
189
188
  self,
190
189
  root: Path,
191
190
  features: Optional[List] = None,
192
- flow_skill_prefix: str = FLOW_SKILL_PREFIX,
193
191
  ):
194
192
  self.root = root
195
193
  self.features = features or []
196
- self.flow_skill_prefix = flow_skill_prefix
197
194
 
198
- # Legacy skills
195
+ # Skills discovered from resources/{lang}/skills/monoco_*/SKILL.md
199
196
  self.skills: Dict[str, Skill] = {}
200
197
 
201
- # New three-level architecture skills
198
+ # Three-level architecture skills
202
199
  self._skill_loaders: Dict[str, SkillLoader] = {}
203
200
  self._atoms: Dict[str, AtomSkillMetadata] = {}
204
201
  self._workflows: Dict[str, WorkflowSkillMetadata] = {}
205
202
  self._roles: Dict[str, RoleSkillMetadata] = {}
206
203
 
207
- # Load legacy skills
204
+ # Discover skills from features only (core is framework-only, no skills)
208
205
  if self.features:
209
206
  self._discover_skills_from_features()
210
- self._discover_core_skills()
211
-
212
- # Load new three-level skills
213
- self._discover_three_level_skills()
214
-
215
- def _discover_core_skills(self) -> None:
216
- """Discover skills from monoco/core/resources/{lang}/skills/."""
217
- core_resources_dir = self.root / "monoco" / "core" / "resources"
218
-
219
- if not core_resources_dir.exists():
220
- return
207
+ self._discover_three_level_skills()
221
208
 
222
- self._discover_skills_in_resources(core_resources_dir, "monoco_core")
223
209
 
224
210
  def _discover_skills_from_features(self) -> None:
225
211
  """Discover skills from Feature resources."""
@@ -250,7 +236,7 @@ class SkillManager:
250
236
  if not resources_dir.exists():
251
237
  return
252
238
 
253
- skill_names: Set[str] = set()
239
+ skill_folders: Set[Path] = set()
254
240
 
255
241
  for lang_dir in resources_dir.iterdir():
256
242
  if not lang_dir.is_dir() or len(lang_dir.name) != 2:
@@ -262,9 +248,11 @@ class SkillManager:
262
248
 
263
249
  for skill_subdir in skills_dir.iterdir():
264
250
  if skill_subdir.is_dir() and (skill_subdir / "SKILL.md").exists():
265
- skill_names.add(skill_subdir.name)
251
+ # print(f"DEBUG: Found skill folder {skill_subdir.name} in feature {feature_name}")
252
+ skill_folders.add(skill_subdir)
266
253
 
267
- for skill_name in skill_names:
254
+ for skill_dir in skill_folders:
255
+ skill_name = skill_dir.name
268
256
  skill = Skill(
269
257
  root_dir=self.root,
270
258
  skill_name=skill_name,
@@ -277,40 +265,96 @@ class SkillManager:
277
265
  )
278
266
  continue
279
267
 
280
- skill_type = skill.get_type()
281
- if skill_type == "flow":
282
- name = skill_name
283
- if name.startswith("flow_"):
284
- name = name[5:]
285
- skill_key = f"{self.flow_skill_prefix}{name}"
268
+ # Naming Logic: All skills must follow monoco_{type}_{name} convention
269
+ # The skill_key is the folder name (which should match the metadata name)
270
+ if skill_name.startswith("monoco_"):
271
+ skill_key = skill_name
286
272
  else:
287
- skill_key = f"{feature_name}_{skill_name}"
273
+ # Non-compliant skills are skipped (should not happen after standardization)
274
+ console.print(
275
+ f"[yellow]Warning: Skill {skill_name} does not follow monoco_{{type}}_{{name}} naming, skipping[/yellow]"
276
+ )
277
+ continue
288
278
 
289
279
  skill.name = skill_key
290
280
  self.skills[skill_key] = skill
291
281
 
292
282
  def _discover_three_level_skills(self) -> None:
293
- """Discover skills from the new three-level architecture."""
294
- # Discover from agent feature resources
295
- agent_resources_dir = self.root / "monoco" / "features" / "agent" / "resources"
296
-
297
- if not agent_resources_dir.exists():
298
- return
299
-
300
- loader = SkillLoader(agent_resources_dir)
301
- loader.load_all()
302
-
303
- self._skill_loaders["agent"] = loader
283
+ """Discover skills from the new three-level architecture in resources/{atoms,workflows,roles}/."""
284
+ from monoco.core.feature import MonocoFeature
304
285
 
305
- # Merge loaded skills
306
- for name, atom in loader._atoms.items():
307
- self._atoms[f"{self.ATOM_PREFIX}{name}"] = atom
308
-
309
- for name, workflow in loader._workflows.items():
310
- self._workflows[f"{self.WORKFLOW_PREFIX}{name}"] = workflow
311
-
312
- for name, role in loader._roles.items():
313
- self._roles[f"{self.ROLE_PREFIX}{name}"] = role
286
+ for feature in self.features:
287
+ if not isinstance(feature, MonocoFeature):
288
+ continue
289
+
290
+ module_parts = feature.__class__.__module__.split(".")
291
+ if (
292
+ len(module_parts) >= 3
293
+ and module_parts[0] == "monoco"
294
+ and module_parts[1] == "features"
295
+ ):
296
+ feature_name = module_parts[2]
297
+ resources_dir = self.root / "monoco" / "features" / feature_name / "resources"
298
+
299
+ if not resources_dir.exists():
300
+ continue
301
+
302
+ # Discover atoms from resources/atoms/*.yaml
303
+ atoms_dir = resources_dir / "atoms"
304
+ if atoms_dir.exists():
305
+ for atom_file in atoms_dir.glob("*.yaml"):
306
+ try:
307
+ data = yaml.safe_load(atom_file.read_text())
308
+ atom = AtomSkillMetadata(**data)
309
+
310
+ # Ensure name follows monoco_atom_ prefix
311
+ atom_key = atom.name
312
+ if not atom_key.startswith(self.ATOM_PREFIX):
313
+ atom_key = f"{self.ATOM_PREFIX}{atom_key}"
314
+
315
+ self._atoms[atom_key] = atom
316
+ except Exception as e:
317
+ console.print(f"[red]Failed to load atom skill {atom_file}: {e}[/red]")
318
+
319
+ # Discover workflows from resources/workflows/*.yaml
320
+ workflows_dir = resources_dir / "workflows"
321
+ if workflows_dir.exists():
322
+ for workflow_file in workflows_dir.glob("*.yaml"):
323
+ try:
324
+ data = yaml.safe_load(workflow_file.read_text())
325
+ workflow = WorkflowSkillMetadata(**data)
326
+
327
+ # Ensure name follows monoco_workflow_ prefix
328
+ workflow_key = workflow.name
329
+ if not workflow_key.startswith(self.WORKFLOW_PREFIX):
330
+ workflow_key = f"{self.WORKFLOW_PREFIX}{workflow_key}"
331
+
332
+ self._workflows[workflow_key] = workflow
333
+ except Exception as e:
334
+ console.print(f"[red]Failed to load workflow skill {workflow_file}: {e}[/red]")
335
+
336
+ # Discover roles from resources/{lang}/roles/*.yaml
337
+ for lang_dir in resources_dir.iterdir():
338
+ if not lang_dir.is_dir() or len(lang_dir.name) != 2:
339
+ continue
340
+
341
+ roles_dir = lang_dir / "roles"
342
+ if not roles_dir.exists():
343
+ continue
344
+
345
+ for role_file in roles_dir.glob("*.yaml"):
346
+ try:
347
+ data = yaml.safe_load(role_file.read_text())
348
+ role = RoleSkillMetadata(**data)
349
+
350
+ # Ensure name follows monoco_role_ prefix
351
+ role_key = role.name
352
+ if not role_key.startswith(self.ROLE_PREFIX):
353
+ role_key = f"{self.ROLE_PREFIX}{role_key}"
354
+
355
+ self._roles[role_key] = role
356
+ except Exception as e:
357
+ console.print(f"[red]Failed to load role skill {role_file}: {e}[/red]")
314
358
 
315
359
  # ========================================================================
316
360
  # Legacy Skill API (backward compatible)
@@ -688,19 +732,14 @@ class SkillManager:
688
732
  """Get list of available flow skill commands."""
689
733
  commands = []
690
734
 
691
- # Legacy flow skills
692
- for skill in self.get_flow_skills():
693
- role = skill.get_role()
694
- if role:
695
- commands.append(f"/flow:{role}")
696
- else:
697
- name = skill.name
698
- if name.startswith(self.flow_skill_prefix):
699
- role = name[len(self.flow_skill_prefix):]
700
- if role:
701
- commands.append(f"/flow:{role}")
735
+ # Workflow/Flow skills with role attribute from legacy skills
736
+ for skill in self.skills.values():
737
+ if skill.get_type() in ["flow", "workflow"]:
738
+ role = skill.get_role()
739
+ if role:
740
+ commands.append(f"/flow:{role}")
702
741
 
703
- # New role skills
742
+ # Role skills from three-level architecture
704
743
  for role_name in self._roles.keys():
705
744
  short_name = role_name.replace(self.ROLE_PREFIX, "")
706
745
  commands.append(f"/flow:{short_name}")
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"