monoco-toolkit 0.2.7__py3-none-any.whl → 0.3.0__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 (66) 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 +500 -260
  36. monoco/features/issue/core.py +504 -293
  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 +326 -111
  46. monoco/features/issue/lsp/definition.py +26 -19
  47. monoco/features/issue/migration.py +45 -34
  48. monoco/features/issue/models.py +30 -13
  49. monoco/features/issue/monitor.py +24 -8
  50. monoco/features/issue/resources/en/AGENTS.md +5 -0
  51. monoco/features/issue/resources/en/SKILL.md +30 -2
  52. monoco/features/issue/resources/zh/AGENTS.md +5 -0
  53. monoco/features/issue/resources/zh/SKILL.md +26 -1
  54. monoco/features/issue/validator.py +417 -172
  55. monoco/features/skills/__init__.py +0 -1
  56. monoco/features/skills/core.py +24 -18
  57. monoco/features/spike/adapter.py +4 -5
  58. monoco/features/spike/commands.py +51 -38
  59. monoco/features/spike/core.py +24 -16
  60. monoco/main.py +34 -21
  61. {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +10 -3
  62. monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
  63. monoco_toolkit-0.2.7.dist-info/RECORD +0 -83
  64. {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
  65. {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
  66. {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,27 +1,28 @@
1
1
  import re
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Set
3
+ from typing import Dict, Optional
4
4
  from pydantic import BaseModel
5
- from .parser import MarkdownParser
6
- from .models import Issue
7
- from monoco.core.config import get_config, MonocoConfig
8
- from monoco.core.lsp import Location, Range, Position
5
+ from monoco.core.config import get_config
6
+
9
7
 
10
8
  class IssueLocation(BaseModel):
11
9
  project_id: str
12
10
  file_path: str
13
11
  issue_id: str
14
12
 
13
+
15
14
  class WorkspaceSymbolIndex:
16
15
  """
17
16
  Maintains a global index of all issues in the Monoco Workspace.
18
17
  Allows resolving Issue IDs (local or namespaced) to file locations.
19
18
  """
20
-
19
+
21
20
  def __init__(self, root_path: Path):
22
21
  self.root_path = root_path
23
22
  self.index: Dict[str, IssueLocation] = {} # Map<FullID, Location>
24
- self.local_map: Dict[str, str] = {} # Map<LocalID, FullID> for current context project
23
+ self.local_map: Dict[
24
+ str, str
25
+ ] = {} # Map<LocalID, FullID> for current context project
25
26
  self._is_indexed = False
26
27
 
27
28
  def build_index(self, recursive: bool = True):
@@ -29,15 +30,15 @@ class WorkspaceSymbolIndex:
29
30
  Scans the workspace and subprojects to build the index.
30
31
  """
31
32
  self.index.clear()
32
-
33
+
33
34
  # 1. Index local project
34
35
  project_name = "local"
35
36
  conf = get_config(str(self.root_path))
36
37
  if conf and conf.project and conf.project.name:
37
38
  project_name = conf.project.name.lower()
38
-
39
+
39
40
  self._index_project(self.root_path, project_name)
40
-
41
+
41
42
  # 2. Index workspace members
42
43
  if recursive:
43
44
  try:
@@ -47,7 +48,7 @@ class WorkspaceSymbolIndex:
47
48
  self._index_project(member_root, member_name.lower())
48
49
  except Exception:
49
50
  pass
50
-
51
+
51
52
  self._is_indexed = True
52
53
 
53
54
  def _index_project(self, project_root: Path, project_name: str):
@@ -69,12 +70,14 @@ class WorkspaceSymbolIndex:
69
70
  loc = IssueLocation(
70
71
  project_id=project_name,
71
72
  file_path=str(f.absolute()),
72
- issue_id=issue_id
73
+ issue_id=issue_id,
73
74
  )
74
75
  self.index[full_id] = loc
75
- self.index[issue_id] = loc # Alias for local lookup
76
+ self.index[issue_id] = loc # Alias for local lookup
76
77
 
77
- def resolve(self, issue_id: str, context_project: Optional[str] = None) -> Optional[IssueLocation]:
78
+ def resolve(
79
+ self, issue_id: str, context_project: Optional[str] = None
80
+ ) -> Optional[IssueLocation]:
78
81
  """
79
82
  Resolves an issue ID to its location.
80
83
  Supports 'Project::ID' and 'ID'.
@@ -94,11 +97,11 @@ class WorkspaceSymbolIndex:
94
97
  # 1. Try exact match
95
98
  if issue_id in self.index:
96
99
  return self.index[issue_id]
97
-
100
+
98
101
  # 2. Try contextual resolution if it's a local ID
99
102
  if "::" not in issue_id and context_project:
100
103
  full_id = f"{context_project}::{issue_id}"
101
104
  if full_id in self.index:
102
105
  return self.index[full_id]
103
-
106
+
104
107
  return None
@@ -1,18 +1,18 @@
1
1
  from typing import Optional
2
- from .models import Transition
3
2
 
4
3
  from .machine import StateMachine
5
4
  from .config import DEFAULT_ISSUE_CONFIG
6
5
  from monoco.core.config import get_config
7
6
 
7
+
8
8
  def get_engine(project_root: Optional[str] = None) -> StateMachine:
9
9
  # 1. Load Core Config (merges workspace & project yamls)
10
10
  core_config = get_config(project_root)
11
-
11
+
12
12
  # 2. Start with Defaults
13
13
  # Use model_copy to avoid mutating the global default instance
14
14
  final_config = DEFAULT_ISSUE_CONFIG.model_copy(deep=True)
15
-
15
+
16
16
  # 3. Merge User Overrides
17
17
  if core_config.issue:
18
18
  # core_config.issue is already an IssueSchemaConfig (parse/validated by Pydantic)
@@ -1,9 +1,11 @@
1
- from monoco.core.config import IssueSchemaConfig, IssueTypeConfig, TransitionConfig, StateMachineConfig
1
+ from monoco.core.config import IssueSchemaConfig, IssueTypeConfig, TransitionConfig
2
2
 
3
3
  DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
4
4
  types=[
5
5
  IssueTypeConfig(name="epic", label="Epic", prefix="EPIC", folder="Epics"),
6
- IssueTypeConfig(name="feature", label="Feature", prefix="FEAT", folder="Features"),
6
+ IssueTypeConfig(
7
+ name="feature", label="Feature", prefix="FEAT", folder="Features"
8
+ ),
7
9
  IssueTypeConfig(name="chore", label="Chore", prefix="CHORE", folder="Chores"),
8
10
  IssueTypeConfig(name="fix", label="Fix", prefix="FIX", folder="Fixes"),
9
11
  ],
@@ -12,8 +14,6 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
12
14
  solutions=["implemented", "cancelled", "wontfix", "duplicate"],
13
15
  workflows=[
14
16
  # --- UNIVERSAL AGENT ACTIONS ---
15
-
16
-
17
17
  # --- OPEN -> OPEN Transitions (Stage changes) ---
18
18
  TransitionConfig(
19
19
  name="start",
@@ -24,11 +24,8 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
24
24
  to_status="open",
25
25
  to_stage="doing",
26
26
  command_template="monoco issue start {id}",
27
- description="Start working on the issue"
27
+ description="Start working on the issue",
28
28
  ),
29
-
30
-
31
-
32
29
  TransitionConfig(
33
30
  name="stop",
34
31
  label="Stop",
@@ -38,7 +35,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
38
35
  to_status="open",
39
36
  to_stage="draft",
40
37
  command_template="monoco issue stop {id}",
41
- description="Stop working and return to draft"
38
+ description="Stop working and return to draft",
42
39
  ),
43
40
  TransitionConfig(
44
41
  name="submit",
@@ -49,7 +46,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
49
46
  to_status="open",
50
47
  to_stage="review",
51
48
  command_template="monoco issue submit {id}",
52
- description="Submit for review"
49
+ description="Submit for review",
53
50
  ),
54
51
  TransitionConfig(
55
52
  name="reject",
@@ -60,9 +57,8 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
60
57
  to_status="open",
61
58
  to_stage="doing",
62
59
  command_template="monoco issue update {id} --stage doing",
63
- description="Reject review and return to doing"
60
+ description="Reject review and return to doing",
64
61
  ),
65
-
66
62
  # --- OPEN -> CLOSED Transitions ---
67
63
  TransitionConfig(
68
64
  name="accept",
@@ -74,7 +70,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
74
70
  to_stage="done",
75
71
  required_solution="implemented",
76
72
  command_template="monoco issue close {id} --solution implemented",
77
- description="Accept and close issue"
73
+ description="Accept and close issue",
78
74
  ),
79
75
  TransitionConfig(
80
76
  name="close_done",
@@ -86,7 +82,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
86
82
  to_stage="done",
87
83
  required_solution="implemented",
88
84
  command_template="monoco issue close {id} --solution implemented",
89
- description="Close completed issue"
85
+ description="Close completed issue",
90
86
  ),
91
87
  TransitionConfig(
92
88
  name="cancel",
@@ -98,7 +94,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
98
94
  to_stage="done",
99
95
  required_solution="cancelled",
100
96
  command_template="monoco issue cancel {id}",
101
- description="Cancel the issue"
97
+ description="Cancel the issue",
102
98
  ),
103
99
  TransitionConfig(
104
100
  name="wontfix",
@@ -109,9 +105,8 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
109
105
  to_stage="done",
110
106
  required_solution="wontfix",
111
107
  command_template="monoco issue close {id} --solution wontfix",
112
- description="Mark as won't fix"
108
+ description="Mark as won't fix",
113
109
  ),
114
-
115
110
  # --- BACKLOG Transitions ---
116
111
  TransitionConfig(
117
112
  name="push",
@@ -121,9 +116,8 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
121
116
  to_status="backlog",
122
117
  to_stage="freezed",
123
118
  command_template="monoco issue backlog push {id}",
124
- description="Move issue to backlog"
119
+ description="Move issue to backlog",
125
120
  ),
126
-
127
121
  TransitionConfig(
128
122
  name="pull",
129
123
  label="Pull",
@@ -132,7 +126,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
132
126
  to_status="open",
133
127
  to_stage="draft",
134
128
  command_template="monoco issue backlog pull {id}",
135
- description="Restore issue from backlog"
129
+ description="Restore issue from backlog",
136
130
  ),
137
131
  TransitionConfig(
138
132
  name="cancel_backlog",
@@ -143,9 +137,8 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
143
137
  to_stage="done",
144
138
  required_solution="cancelled",
145
139
  command_template="monoco issue cancel {id}",
146
- description="Cancel backlog issue"
140
+ description="Cancel backlog issue",
147
141
  ),
148
-
149
142
  # --- CLOSED Transitions ---
150
143
  TransitionConfig(
151
144
  name="reopen",
@@ -155,7 +148,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
155
148
  to_status="open",
156
149
  to_stage="draft",
157
150
  command_template="monoco issue open {id}",
158
- description="Reopen a closed issue"
151
+ description="Reopen a closed issue",
159
152
  ),
160
153
  TransitionConfig(
161
154
  name="reopen_from_done",
@@ -166,7 +159,7 @@ DEFAULT_ISSUE_CONFIG = IssueSchemaConfig(
166
159
  to_status="open",
167
160
  to_stage="draft",
168
161
  command_template="monoco issue open {id}",
169
- description="Reopen a done issue"
162
+ description="Reopen a done issue",
170
163
  ),
171
- ]
164
+ ],
172
165
  )
@@ -1,6 +1,7 @@
1
1
  from typing import List, Optional, Dict
2
2
  from monoco.core.config import IssueSchemaConfig, TransitionConfig
3
- from ..models import IssueStatus, IssueStage, IssueMetadata, IssueSolution, IssueType
3
+ from ..models import IssueMetadata
4
+
4
5
 
5
6
  class StateMachine:
6
7
  def __init__(self, config: IssueSchemaConfig):
@@ -30,15 +31,20 @@ class StateMachine:
30
31
  return []
31
32
  return [t.name for t in self.issue_config.types]
32
33
 
33
- def can_transition(self, current_status: str, current_stage: Optional[str],
34
- target_status: str, target_stage: Optional[str]) -> bool:
34
+ def can_transition(
35
+ self,
36
+ current_status: str,
37
+ current_stage: Optional[str],
38
+ target_status: str,
39
+ target_stage: Optional[str],
40
+ ) -> bool:
35
41
  """Check if a transition path exists."""
36
42
  for t in self.transitions:
37
43
  if t.from_status and t.from_status != current_status:
38
44
  continue
39
45
  if t.from_stage and t.from_stage != current_stage:
40
46
  continue
41
-
47
+
42
48
  if t.to_status == target_status:
43
49
  if target_stage is None or t.to_stage == target_stage:
44
50
  return True
@@ -56,11 +62,11 @@ class StateMachine:
56
62
  # Match status
57
63
  if t.from_status and t.from_status != meta.status:
58
64
  continue
59
-
65
+
60
66
  # Match stage
61
67
  if t.from_stage and t.from_stage != meta.stage:
62
68
  continue
63
-
69
+
64
70
  # Special case for 'Cancel': don't show if already DONE or CLOSED
65
71
  if t.name == "cancel" and meta.stage == "done":
66
72
  continue
@@ -68,9 +74,14 @@ class StateMachine:
68
74
  allowed.append(t)
69
75
  return allowed
70
76
 
71
- def find_transition(self, from_status: str, from_stage: Optional[str],
72
- to_status: str, to_stage: Optional[str],
73
- solution: Optional[str] = None) -> Optional[TransitionConfig]:
77
+ def find_transition(
78
+ self,
79
+ from_status: str,
80
+ from_stage: Optional[str],
81
+ to_status: str,
82
+ to_stage: Optional[str],
83
+ solution: Optional[str] = None,
84
+ ) -> Optional[TransitionConfig]:
74
85
  """Find a specific transition rule."""
75
86
  candidates = []
76
87
  for t in self.transitions:
@@ -82,15 +93,15 @@ class StateMachine:
82
93
  continue
83
94
  if t.from_stage and t.from_stage != from_stage:
84
95
  continue
85
-
96
+
86
97
  # Check if this transition matches the target
87
98
  if t.to_status == to_status:
88
99
  if to_stage is None or t.to_stage == to_stage:
89
100
  candidates.append(t)
90
-
101
+
91
102
  if not candidates:
92
103
  return None
93
-
104
+
94
105
  # If we have a solution, find the transition that requires it
95
106
  if solution:
96
107
  for t in candidates:
@@ -102,84 +113,106 @@ class StateMachine:
102
113
  if t.required_solution is None:
103
114
  return t
104
115
  return None
105
-
116
+
106
117
  # Otherwise return the first one that has NO required_solution
107
118
  for t in candidates:
108
119
  if t.required_solution is None:
109
120
  return t
110
-
121
+
111
122
  return candidates[0]
112
123
 
113
- def validate_transition(self, from_status: str, from_stage: Optional[str],
114
- to_status: str, to_stage: Optional[str],
115
- solution: Optional[str] = None) -> None:
124
+ def validate_transition(
125
+ self,
126
+ from_status: str,
127
+ from_stage: Optional[str],
128
+ to_status: str,
129
+ to_stage: Optional[str],
130
+ solution: Optional[str] = None,
131
+ ) -> None:
116
132
  """
117
133
  Validate if a transition is allowed. Raises ValueError if not.
118
134
  """
119
135
  if from_status == to_status and from_stage == to_stage:
120
- return # No change is always allowed (unless we want to enforce specific updates)
136
+ return # No change is always allowed (unless we want to enforce specific updates)
137
+
138
+ transition = self.find_transition(
139
+ from_status, from_stage, to_status, to_stage, solution
140
+ )
121
141
 
122
- transition = self.find_transition(from_status, from_stage, to_status, to_stage, solution)
123
-
124
142
  if not transition:
125
- raise ValueError(f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
126
- f"to {to_status}({to_stage if to_stage else 'None'}) is not defined.")
143
+ raise ValueError(
144
+ f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
145
+ f"to {to_status}({to_stage if to_stage else 'None'}) is not defined."
146
+ )
127
147
 
128
148
  if transition.required_solution and solution != transition.required_solution:
129
- raise ValueError(f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'.")
149
+ raise ValueError(
150
+ f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'."
151
+ )
130
152
 
131
153
  def enforce_policy(self, meta: IssueMetadata) -> None:
132
154
  """
133
155
  Apply consistency rules to IssueMetadata.
134
156
  """
135
157
  from ..models import current_time
136
-
158
+
137
159
  if meta.status == "backlog":
138
160
  meta.stage = "freezed"
139
-
161
+
140
162
  elif meta.status == "closed":
141
163
  if meta.stage != "done":
142
164
  meta.stage = "done"
143
165
  if not meta.closed_at:
144
166
  meta.closed_at = current_time()
145
-
167
+
146
168
  elif meta.status == "open":
147
169
  if meta.stage is None:
148
170
  meta.stage = "draft"
149
171
 
150
- def validate_transition(self, from_status: str, from_stage: Optional[str],
151
- to_status: str, to_stage: Optional[str],
152
- solution: Optional[str] = None) -> None:
172
+ def validate_transition(
173
+ self,
174
+ from_status: str,
175
+ from_stage: Optional[str],
176
+ to_status: str,
177
+ to_stage: Optional[str],
178
+ solution: Optional[str] = None,
179
+ ) -> None:
153
180
  """
154
181
  Validate if a transition is allowed. Raises ValueError if not.
155
182
  """
156
183
  if from_status == to_status and from_stage == to_stage:
157
- return # No change is always allowed (unless we want to enforce specific updates)
184
+ return # No change is always allowed (unless we want to enforce specific updates)
185
+
186
+ transition = self.find_transition(
187
+ from_status, from_stage, to_status, to_stage, solution
188
+ )
158
189
 
159
- transition = self.find_transition(from_status, from_stage, to_status, to_stage, solution)
160
-
161
190
  if not transition:
162
- raise ValueError(f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
163
- f"to {to_status}({to_stage if to_stage else 'None'}) is not defined.")
191
+ raise ValueError(
192
+ f"Lifecycle Policy: Transition from {from_status}({from_stage if from_stage else 'None'}) "
193
+ f"to {to_status}({to_stage if to_stage else 'None'}) is not defined."
194
+ )
164
195
 
165
196
  if transition.required_solution and solution != transition.required_solution:
166
- raise ValueError(f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'.")
197
+ raise ValueError(
198
+ f"Lifecycle Policy: Transition '{transition.label}' requires solution '{transition.required_solution}'."
199
+ )
167
200
 
168
201
  def enforce_policy(self, meta: IssueMetadata) -> None:
169
202
  """
170
203
  Apply consistency rules to IssueMetadata.
171
204
  """
172
205
  from ..models import current_time
173
-
206
+
174
207
  if meta.status == "backlog":
175
208
  meta.stage = "freezed"
176
-
209
+
177
210
  elif meta.status == "closed":
178
211
  if meta.stage != "done":
179
212
  meta.stage = "done"
180
213
  if not meta.closed_at:
181
214
  meta.closed_at = current_time()
182
-
215
+
183
216
  elif meta.status == "open":
184
217
  if meta.stage is None:
185
- meta.stage = "draft"
218
+ meta.stage = "draft"
@@ -1,18 +1,20 @@
1
- from typing import List, Optional, Any
1
+ from typing import List, Optional
2
2
  from pydantic import BaseModel
3
3
 
4
+
4
5
  class Transition(BaseModel):
5
6
  name: str
6
7
  label: str
7
8
  icon: Optional[str] = None
8
9
  from_status: Optional[str] = None # None means any
9
- from_stage: Optional[str] = None # None means any
10
+ from_stage: Optional[str] = None # None means any
10
11
  to_status: str
11
12
  to_stage: Optional[str] = None
12
13
  required_solution: Optional[str] = None
13
14
  description: str = ""
14
15
  command_template: Optional[str] = None
15
16
 
17
+
16
18
  class StateMachineConfig(BaseModel):
17
19
  transitions: List[Transition]
18
20
  # We can add more config like default stages for statuses etc.