monoco-toolkit 0.2.8__py3-none-any.whl → 0.3.1__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 (63) hide show
  1. monoco/cli/project.py +35 -31
  2. monoco/cli/workspace.py +26 -16
  3. monoco/core/agent/__init__.py +0 -2
  4. monoco/core/agent/action.py +44 -20
  5. monoco/core/agent/adapters.py +20 -16
  6. monoco/core/agent/protocol.py +5 -4
  7. monoco/core/agent/state.py +21 -21
  8. monoco/core/config.py +90 -33
  9. monoco/core/execution.py +21 -16
  10. monoco/core/feature.py +8 -5
  11. monoco/core/git.py +61 -30
  12. monoco/core/hooks.py +57 -0
  13. monoco/core/injection.py +47 -44
  14. monoco/core/integrations.py +50 -35
  15. monoco/core/lsp.py +12 -1
  16. monoco/core/output.py +35 -16
  17. monoco/core/registry.py +3 -2
  18. monoco/core/setup.py +190 -124
  19. monoco/core/skills.py +121 -107
  20. monoco/core/state.py +12 -10
  21. monoco/core/sync.py +85 -56
  22. monoco/core/telemetry.py +10 -6
  23. monoco/core/workspace.py +26 -19
  24. monoco/daemon/app.py +123 -79
  25. monoco/daemon/commands.py +14 -13
  26. monoco/daemon/models.py +11 -3
  27. monoco/daemon/reproduce_stats.py +8 -8
  28. monoco/daemon/services.py +32 -33
  29. monoco/daemon/stats.py +59 -40
  30. monoco/features/config/commands.py +38 -25
  31. monoco/features/i18n/adapter.py +4 -5
  32. monoco/features/i18n/commands.py +83 -49
  33. monoco/features/i18n/core.py +94 -54
  34. monoco/features/issue/adapter.py +6 -7
  35. monoco/features/issue/commands.py +468 -272
  36. monoco/features/issue/core.py +419 -312
  37. monoco/features/issue/domain/lifecycle.py +33 -23
  38. monoco/features/issue/domain/models.py +71 -38
  39. monoco/features/issue/domain/parser.py +92 -69
  40. monoco/features/issue/domain/workspace.py +19 -16
  41. monoco/features/issue/engine/__init__.py +3 -3
  42. monoco/features/issue/engine/config.py +18 -25
  43. monoco/features/issue/engine/machine.py +72 -39
  44. monoco/features/issue/engine/models.py +4 -2
  45. monoco/features/issue/linter.py +287 -157
  46. monoco/features/issue/lsp/definition.py +26 -19
  47. monoco/features/issue/migration.py +45 -34
  48. monoco/features/issue/models.py +29 -13
  49. monoco/features/issue/monitor.py +24 -8
  50. monoco/features/issue/resources/en/SKILL.md +6 -2
  51. monoco/features/issue/validator.py +395 -208
  52. monoco/features/skills/__init__.py +0 -1
  53. monoco/features/skills/core.py +24 -18
  54. monoco/features/spike/adapter.py +4 -5
  55. monoco/features/spike/commands.py +51 -38
  56. monoco/features/spike/core.py +24 -16
  57. monoco/main.py +34 -21
  58. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.3.1.dist-info/RECORD +84 -0
  60. monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
  61. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/WHEEL +0 -0
  62. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/entry_points.txt +0 -0
  63. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.1.dist-info}/licenses/LICENSE +0 -0
monoco/core/config.py CHANGED
@@ -1,4 +1,3 @@
1
- import os
2
1
  import yaml
3
2
  from pathlib import Path
4
3
  from typing import Optional, Dict, Any, Callable, Awaitable, List
@@ -11,41 +10,66 @@ from watchdog.observers import Observer
11
10
  from watchdog.events import FileSystemEventHandler
12
11
 
13
12
 
14
-
15
13
  logger = logging.getLogger("monoco.core.config")
16
14
 
15
+
17
16
  class PathsConfig(BaseModel):
18
17
  """Configuration for directory paths."""
18
+
19
19
  root: str = Field(default=".", description="Project root directory")
20
20
  issues: str = Field(default="Issues", description="Directory for issues")
21
- spikes: str = Field(default=".references", description="Directory for spikes/research")
22
- specs: str = Field(default="SPECS", description="Directory for specifications")
21
+ spikes: str = Field(
22
+ default=".references", description="Directory for spikes/research"
23
+ )
24
+
23
25
 
24
26
  class CoreConfig(BaseModel):
25
27
  """Core system configuration."""
28
+
26
29
  log_level: str = Field(default="INFO", description="Logging verbosity")
27
- author: Optional[str] = Field(default=None, description="Default author for new artifacts")
30
+ author: Optional[str] = Field(
31
+ default=None, description="Default author for new artifacts"
32
+ )
33
+
28
34
 
29
35
  class ProjectConfig(BaseModel):
30
36
  """Project identity configuration."""
37
+
31
38
  name: str = Field(default="Monoco Project", description="Project name")
32
39
  key: str = Field(default="MON", description="Project key/prefix for IDs")
33
- spike_repos: Dict[str, str] = Field(default_factory=dict, description="Managed external research repositories (name -> url)")
34
- members: Dict[str, str] = Field(default_factory=dict, description="Workspace member projects (name -> relative_path)")
40
+ spike_repos: Dict[str, str] = Field(
41
+ default_factory=dict,
42
+ description="Managed external research repositories (name -> url)",
43
+ )
44
+ members: Dict[str, str] = Field(
45
+ default_factory=dict,
46
+ description="Workspace member projects (name -> relative_path)",
47
+ )
48
+
35
49
 
36
50
  class I18nConfig(BaseModel):
37
51
  """Configuration for internationalization."""
52
+
38
53
  source_lang: str = Field(default="en", description="Source language code")
39
- target_langs: list[str] = Field(default_factory=lambda: ["zh"], description="Target language codes")
54
+ target_langs: list[str] = Field(
55
+ default_factory=lambda: ["zh"], description="Target language codes"
56
+ )
57
+
40
58
 
41
59
  class UIConfig(BaseModel):
42
60
  """Configuration for UI customizations."""
43
- dictionary: Dict[str, str] = Field(default_factory=dict, description="Custom domain terminology mapping")
61
+
62
+ dictionary: Dict[str, str] = Field(
63
+ default_factory=dict, description="Custom domain terminology mapping"
64
+ )
65
+
44
66
 
45
67
  class TelemetryConfig(BaseModel):
46
68
  """Configuration for Telemetry."""
47
- enabled: Optional[bool] = Field(default=None, description="Whether telemetry is enabled")
48
69
 
70
+ enabled: Optional[bool] = Field(
71
+ default=None, description="Whether telemetry is enabled"
72
+ )
49
73
 
50
74
 
51
75
  class IssueTypeConfig(BaseModel):
@@ -55,6 +79,7 @@ class IssueTypeConfig(BaseModel):
55
79
  folder: str
56
80
  description: Optional[str] = None
57
81
 
82
+
58
83
  class TransitionConfig(BaseModel):
59
84
  name: str
60
85
  label: str
@@ -67,6 +92,7 @@ class TransitionConfig(BaseModel):
67
92
  description: str = ""
68
93
  command_template: Optional[str] = None
69
94
 
95
+
70
96
  class IssueSchemaConfig(BaseModel):
71
97
  types: List[IssueTypeConfig] = Field(default_factory=list)
72
98
  statuses: List[str] = Field(default_factory=list)
@@ -77,49 +103,56 @@ class IssueSchemaConfig(BaseModel):
77
103
  def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
78
104
  if not other:
79
105
  return self
80
-
106
+
81
107
  # Types: merge by name
82
108
  if other.types:
83
109
  type_map = {t.name: t for t in self.types}
84
110
  for ot in other.types:
85
111
  type_map[ot.name] = ot
86
112
  self.types = list(type_map.values())
87
-
113
+
88
114
  # Statuses: replace if provided
89
115
  if other.statuses:
90
116
  self.statuses = other.statuses
91
-
117
+
92
118
  # Stages: replace if provided
93
119
  if other.stages:
94
120
  self.stages = other.stages
95
-
121
+
96
122
  # Solutions: replace if provided
97
123
  if other.solutions:
98
124
  self.solutions = other.solutions
99
-
125
+
100
126
  # Workflows (Transitions): merge by name
101
127
  if other.workflows:
102
128
  wf_map = {w.name: w for w in self.workflows}
103
129
  for ow in other.workflows:
104
130
  wf_map[ow.name] = ow
105
131
  self.workflows = list(wf_map.values())
106
-
132
+
107
133
  return self
108
134
 
135
+
109
136
  class StateMachineConfig(BaseModel):
110
137
  transitions: List[TransitionConfig]
111
138
 
139
+
112
140
  class MonocoConfig(BaseModel):
113
141
  """
114
142
  Main Configuration Schema.
115
143
  Hierarchy: Defaults < User Config (~/.monoco/config.yaml) < Project Config (./.monoco/config.yaml)
116
144
  """
145
+
117
146
  core: CoreConfig = Field(default_factory=CoreConfig)
118
147
  paths: PathsConfig = Field(default_factory=PathsConfig)
119
148
  project: ProjectConfig = Field(default_factory=ProjectConfig)
120
149
  i18n: I18nConfig = Field(default_factory=I18nConfig)
121
150
  ui: UIConfig = Field(default_factory=UIConfig)
122
151
  telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
152
+ hooks: Dict[str, str] = Field(
153
+ default_factory=dict,
154
+ description="Git hooks configuration (hook_name -> command)",
155
+ )
123
156
 
124
157
  issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
125
158
 
@@ -134,10 +167,12 @@ class MonocoConfig(BaseModel):
134
167
  return base
135
168
 
136
169
  @classmethod
137
- def load(cls, project_root: Optional[str] = None, require_project: bool = False) -> "MonocoConfig":
170
+ def load(
171
+ cls, project_root: Optional[str] = None, require_project: bool = False
172
+ ) -> "MonocoConfig":
138
173
  """
139
174
  Load configuration from multiple sources.
140
-
175
+
141
176
  Args:
142
177
  project_root: Explicit root path. If None, uses CWD.
143
178
  require_project: If True, raises error if .monoco directory is missing.
@@ -147,16 +182,18 @@ class MonocoConfig(BaseModel):
147
182
 
148
183
  # 2. Define config paths
149
184
  home_path = Path.home() / ".monoco" / "config.yaml"
150
-
185
+
151
186
  # Determine project path
152
187
  cwd = Path(project_root) if project_root else Path.cwd()
153
188
  # FIX-0009: strict separation of workspace and project config
154
189
  workspace_config_path = cwd / ".monoco" / "workspace.yaml"
155
190
  project_config_path = cwd / ".monoco" / "project.yaml"
156
-
191
+
157
192
  # Strict Workspace Check
158
193
  if require_project and not (cwd / ".monoco").exists():
159
- raise FileNotFoundError(f"Monoco workspace not found in {cwd}. (No .monoco directory)")
194
+ raise FileNotFoundError(
195
+ f"Monoco workspace not found in {cwd}. (No .monoco directory)"
196
+ )
160
197
 
161
198
  # 3. Load User Config
162
199
  if home_path.exists():
@@ -165,12 +202,12 @@ class MonocoConfig(BaseModel):
165
202
  user_config = yaml.safe_load(f)
166
203
  if user_config:
167
204
  cls._deep_merge(config_data, user_config)
168
- except Exception as e:
205
+ except Exception:
169
206
  # We don't want to crash on config load fail, implementing simple warning equivalent
170
207
  pass
171
208
 
172
209
  # 4. Load Project/Workspace Config
173
-
210
+
174
211
  # 4a. Load workspace.yaml (Global Environment)
175
212
  if workspace_config_path.exists():
176
213
  try:
@@ -204,10 +241,14 @@ class MonocoConfig(BaseModel):
204
241
  # 5. Instantiate Model
205
242
  return cls(**config_data)
206
243
 
244
+
207
245
  # Global singleton
208
246
  _settings = None
209
247
 
210
- def get_config(project_root: Optional[str] = None, require_project: bool = False) -> MonocoConfig:
248
+
249
+ def get_config(
250
+ project_root: Optional[str] = None, require_project: bool = False
251
+ ) -> MonocoConfig:
211
252
  global _settings
212
253
  # If explicit root provided, always reload.
213
254
  # If require_project is True, we must reload to ensure validation happens (in case a previous loose load occurred).
@@ -215,11 +256,13 @@ def get_config(project_root: Optional[str] = None, require_project: bool = False
215
256
  _settings = MonocoConfig.load(project_root, require_project=require_project)
216
257
  return _settings
217
258
 
259
+
218
260
  class ConfigScope(str, Enum):
219
261
  GLOBAL = "global"
220
262
  PROJECT = "project"
221
263
  WORKSPACE = "workspace"
222
264
 
265
+
223
266
  def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> Path:
224
267
  """Get the path to the configuration file for a given scope."""
225
268
  if scope == ConfigScope.GLOBAL:
@@ -232,6 +275,7 @@ def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> P
232
275
  cwd = Path(project_root) if project_root else Path.cwd()
233
276
  return cwd / ".monoco" / "project.yaml"
234
277
 
278
+
235
279
  def find_monoco_root(start_path: Optional[Path] = None) -> Path:
236
280
  """
237
281
  Find the Monoco Workspace root.
@@ -239,18 +283,21 @@ def find_monoco_root(start_path: Optional[Path] = None) -> Path:
239
283
  Recursive upward lookup is disabled per FIX-0009.
240
284
  """
241
285
  current = (start_path or Path.cwd()).resolve()
242
-
286
+
243
287
  # Check if we are inside a .monoco folder
244
288
  if current.name == ".monoco":
245
289
  return current.parent
246
-
290
+
247
291
  # Check if current directory has .monoco
248
292
  if (current / ".monoco").exists():
249
293
  return current
250
-
294
+
251
295
  return current
252
296
 
253
- def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> Dict[str, Any]:
297
+
298
+ def load_raw_config(
299
+ scope: ConfigScope, project_root: Optional[str] = None
300
+ ) -> Dict[str, Any]:
254
301
  """Load raw configuration dictionary from a specific scope."""
255
302
  path = get_config_path(scope, project_root)
256
303
  if not path.exists():
@@ -262,15 +309,21 @@ def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> D
262
309
  logger.warning(f"Failed to load config from {path}: {e}")
263
310
  return {}
264
311
 
265
- def save_raw_config(scope: ConfigScope, data: Dict[str, Any], project_root: Optional[str] = None) -> None:
312
+
313
+ def save_raw_config(
314
+ scope: ConfigScope, data: Dict[str, Any], project_root: Optional[str] = None
315
+ ) -> None:
266
316
  """Save raw configuration dictionary to a specific scope."""
267
317
  path = get_config_path(scope, project_root)
268
318
  path.parent.mkdir(parents=True, exist_ok=True)
269
319
  with open(path, "w") as f:
270
320
  yaml.dump(data, f, default_flow_style=False)
271
321
 
322
+
272
323
  class ConfigEventHandler(FileSystemEventHandler):
273
- def __init__(self, loop, on_change: Callable[[], Awaitable[None]], config_path: Path):
324
+ def __init__(
325
+ self, loop, on_change: Callable[[], Awaitable[None]], config_path: Path
326
+ ):
274
327
  self.loop = loop
275
328
  self.on_change = on_change
276
329
  self.config_path = config_path
@@ -279,10 +332,12 @@ class ConfigEventHandler(FileSystemEventHandler):
279
332
  if event.src_path == str(self.config_path):
280
333
  asyncio.run_coroutine_threadsafe(self.on_change(), self.loop)
281
334
 
335
+
282
336
  class ConfigMonitor:
283
337
  """
284
338
  Monitors a configuration file for changes.
285
339
  """
340
+
286
341
  def __init__(self, config_path: Path, on_change: Callable[[], Awaitable[None]]):
287
342
  self.config_path = config_path
288
343
  self.on_change = on_change
@@ -291,13 +346,15 @@ class ConfigMonitor:
291
346
  async def start(self):
292
347
  loop = asyncio.get_running_loop()
293
348
  event_handler = ConfigEventHandler(loop, self.on_change, self.config_path)
294
-
349
+
295
350
  if not self.config_path.exists():
296
351
  # Ensure parent exists at least
297
352
  self.config_path.parent.mkdir(parents=True, exist_ok=True)
298
-
353
+
299
354
  # We watch the parent directory for the specific file
300
- self.observer.schedule(event_handler, str(self.config_path.parent), recursive=False)
355
+ self.observer.schedule(
356
+ event_handler, str(self.config_path.parent), recursive=False
357
+ )
301
358
  self.observer.start()
302
359
  logger.info(f"Config Monitor started for {self.config_path}")
303
360
 
monoco/core/execution.py CHANGED
@@ -1,62 +1,67 @@
1
- import os
2
1
  from pathlib import Path
3
- from typing import List, Dict, Optional
2
+ from typing import List, Optional
4
3
  from pydantic import BaseModel
5
4
 
5
+
6
6
  class ExecutionProfile(BaseModel):
7
7
  name: str
8
- source: str # "Global" or "Project"
8
+ source: str # "Global" or "Project"
9
9
  path: str
10
10
  content: Optional[str] = None
11
11
 
12
- def scan_execution_profiles(project_root: Optional[Path] = None) -> List[ExecutionProfile]:
12
+
13
+ def scan_execution_profiles(
14
+ project_root: Optional[Path] = None,
15
+ ) -> List[ExecutionProfile]:
13
16
  """
14
17
  Scan for execution profiles (SOPs) in global and project scopes.
15
18
  """
16
19
  profiles = []
17
-
20
+
18
21
  # 1. Global Scope
19
22
  global_path = Path.home() / ".monoco" / "execution"
20
23
  if global_path.exists():
21
24
  profiles.extend(_scan_dir(global_path, "Global"))
22
-
25
+
23
26
  # 2. Project Scope
24
27
  if project_root:
25
28
  project_path = project_root / ".monoco" / "execution"
26
29
  if project_path.exists():
27
30
  profiles.extend(_scan_dir(project_path, "Project"))
28
-
31
+
29
32
  return profiles
30
33
 
34
+
31
35
  def _scan_dir(base_path: Path, source: str) -> List[ExecutionProfile]:
32
36
  profiles = []
33
37
  if not base_path.is_dir():
34
38
  return profiles
35
-
39
+
36
40
  for item in base_path.iterdir():
37
41
  if item.is_dir():
38
42
  sop_path = item / "SOP.md"
39
43
  if sop_path.exists():
40
- profiles.append(ExecutionProfile(
41
- name=item.name,
42
- source=source,
43
- path=str(sop_path.absolute())
44
- ))
44
+ profiles.append(
45
+ ExecutionProfile(
46
+ name=item.name, source=source, path=str(sop_path.absolute())
47
+ )
48
+ )
45
49
  return profiles
46
50
 
51
+
47
52
  def get_profile_detail(profile_path: str) -> Optional[ExecutionProfile]:
48
53
  path = Path(profile_path)
49
54
  if not path.exists():
50
55
  return None
51
-
56
+
52
57
  # Determine source (rough heuristic)
53
58
  source = "Project"
54
59
  if str(path).startswith(str(Path.home() / ".monoco")):
55
60
  source = "Global"
56
-
61
+
57
62
  return ExecutionProfile(
58
63
  name=path.parent.name,
59
64
  source=source,
60
65
  path=str(path.absolute()),
61
- content=path.read_text(encoding='utf-8')
66
+ content=path.read_text(encoding="utf-8"),
62
67
  )
monoco/core/feature.py CHANGED
@@ -1,21 +1,24 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from dataclasses import dataclass, field
3
3
  from pathlib import Path
4
- from typing import Dict, List, Optional
4
+ from typing import Dict, List
5
+
5
6
 
6
7
  @dataclass
7
8
  class IntegrationData:
8
9
  """
9
10
  Data collection returned by a feature for integration into the Agent environment.
10
11
  """
12
+
11
13
  # System Prompts to be injected into agent configuration (e.g., .cursorrules)
12
14
  # Key: Section Title (e.g., "Issue Management"), Value: Markdown Content
13
15
  system_prompts: Dict[str, str] = field(default_factory=dict)
14
-
16
+
15
17
  # Paths to skill directories or files to be copied/symlinked
16
18
  # DEPRECATED: Skill distribution is cancelled. Only prompts are synced.
17
19
  skills: List[Path] = field(default_factory=list)
18
20
 
21
+
19
22
  class MonocoFeature(ABC):
20
23
  """
21
24
  Abstract base class for all Monoco features.
@@ -34,7 +37,7 @@ class MonocoFeature(ABC):
34
37
  Lifecycle hook: Physical Structure Initialization.
35
38
  Called during `monoco init`.
36
39
  Responsible for creating necessary directories, files, and config templates.
37
-
40
+
38
41
  Args:
39
42
  root: The root directory of the project.
40
43
  config: The full project configuration dictionary.
@@ -47,11 +50,11 @@ class MonocoFeature(ABC):
47
50
  Lifecycle hook: Agent Environment Integration.
48
51
  Called during `monoco sync`.
49
52
  Responsible for returning data (prompts, skills) needed for the Agent Setup.
50
-
53
+
51
54
  Args:
52
55
  root: The root directory of the project.
53
56
  config: The full project configuration dictionary.
54
-
57
+
55
58
  Returns:
56
59
  IntegrationData object containing prompts and skills.
57
60
  """