monoco-toolkit 0.1.7__py3-none-any.whl → 0.2.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.
- monoco/core/agent/__init__.py +5 -0
- monoco/core/agent/action.py +144 -0
- monoco/core/agent/adapters.py +106 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +117 -0
- monoco/core/config.py +42 -1
- monoco/core/execution.py +62 -0
- monoco/core/git.py +51 -2
- monoco/core/output.py +13 -2
- monoco/core/state.py +53 -0
- monoco/core/workspace.py +75 -12
- monoco/daemon/app.py +120 -57
- monoco/daemon/models.py +4 -0
- monoco/daemon/services.py +56 -155
- monoco/features/agent/commands.py +160 -0
- monoco/features/agent/doctor.py +30 -0
- monoco/features/issue/core.py +80 -47
- monoco/features/issue/executions/refine.md +26 -0
- monoco/features/issue/models.py +18 -6
- monoco/features/issue/monitor.py +94 -0
- monoco/main.py +13 -0
- {monoco_toolkit-0.1.7.dist-info → monoco_toolkit-0.2.0.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.1.7.dist-info → monoco_toolkit-0.2.0.dist-info}/RECORD +26 -15
- {monoco_toolkit-0.1.7.dist-info → monoco_toolkit-0.2.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.7.dist-info → monoco_toolkit-0.2.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.7.dist-info → monoco_toolkit-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
|
|
2
|
+
import typer
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from monoco.core.output import print_output, print_error
|
|
6
|
+
from monoco.core.agent.adapters import get_agent_client
|
|
7
|
+
from monoco.core.agent.state import AgentStateManager
|
|
8
|
+
from monoco.core.agent.action import ActionRegistry, ActionContext
|
|
9
|
+
from monoco.core.config import get_config
|
|
10
|
+
import asyncio
|
|
11
|
+
import re
|
|
12
|
+
import json as j
|
|
13
|
+
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
|
|
16
|
+
@app.command(name="run")
|
|
17
|
+
def run_command(
|
|
18
|
+
prompt_or_task: str = typer.Argument(..., help="Prompt string OR execution task name (e.g. 'refine-issue')"),
|
|
19
|
+
target: Optional[str] = typer.Argument(None, help="Target file argument for the task"),
|
|
20
|
+
provider: Optional[str] = typer.Option(None, "--using", "-u", help="Override agent provider"),
|
|
21
|
+
instruction: Optional[str] = typer.Option(None, "--instruction", "-i", help="Additional instruction for the agent"),
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Execute a prompt or a named task using an Agent CLI.
|
|
25
|
+
"""
|
|
26
|
+
# 0. Setup
|
|
27
|
+
settings = get_config()
|
|
28
|
+
state_manager = AgentStateManager()
|
|
29
|
+
registry = ActionRegistry(Path(settings.paths.root))
|
|
30
|
+
|
|
31
|
+
# 1. Check if it's a named task
|
|
32
|
+
action = registry.get(prompt_or_task)
|
|
33
|
+
|
|
34
|
+
final_prompt = prompt_or_task
|
|
35
|
+
context_files = []
|
|
36
|
+
|
|
37
|
+
# Determine Provider Priority: CLI > Action Def > Config > Default
|
|
38
|
+
prov_name = provider
|
|
39
|
+
|
|
40
|
+
if action:
|
|
41
|
+
# It IS an action
|
|
42
|
+
print(f"Running action: {action.name}")
|
|
43
|
+
|
|
44
|
+
# Simple template substitution
|
|
45
|
+
final_prompt = action.template
|
|
46
|
+
|
|
47
|
+
if "{{file}}" in final_prompt:
|
|
48
|
+
if not target:
|
|
49
|
+
print_error("This task requires a target file argument.")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
target_path = Path(target).resolve()
|
|
53
|
+
if not target_path.exists():
|
|
54
|
+
print_error(f"Target file not found: {target}")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
final_prompt = final_prompt.replace("{{file}}", target_path.read_text())
|
|
58
|
+
# Also add to context files? Ideally the prompt has it.
|
|
59
|
+
# Let's add it to context files list to be safe if prompt didn't embed it fully
|
|
60
|
+
context_files.append(target_path)
|
|
61
|
+
|
|
62
|
+
if not prov_name:
|
|
63
|
+
prov_name = action.provider
|
|
64
|
+
|
|
65
|
+
# 2. Append Instruction if provided
|
|
66
|
+
if instruction:
|
|
67
|
+
final_prompt = f"{final_prompt}\n\n[USER INSTRUCTION]\n{instruction}"
|
|
68
|
+
|
|
69
|
+
# 2. Provider Resolution Fallback
|
|
70
|
+
prov_name = prov_name or settings.agent.framework or "gemini"
|
|
71
|
+
|
|
72
|
+
# 3. State Check
|
|
73
|
+
state = state_manager.load()
|
|
74
|
+
if not state or state.is_stale:
|
|
75
|
+
print("Agent state stale or missing, refreshing...")
|
|
76
|
+
state = state_manager.refresh()
|
|
77
|
+
|
|
78
|
+
if prov_name not in state.providers:
|
|
79
|
+
print_error(f"Provider '{prov_name}' unknown.")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
if not state.providers[prov_name].available:
|
|
83
|
+
print_error(f"Provider '{prov_name}' is not available. Run 'monoco doctor' to diagnose.")
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
# 4. Execute
|
|
87
|
+
try:
|
|
88
|
+
client = get_agent_client(prov_name)
|
|
89
|
+
result = asyncio.run(client.execute(final_prompt, context_files=context_files))
|
|
90
|
+
print(result)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print_error(f"Execution failed: {e}")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def list(
|
|
98
|
+
json: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
99
|
+
context: Optional[str] = typer.Option(None, "--context", help="Context for filtering (JSON string)")
|
|
100
|
+
):
|
|
101
|
+
"""List available actions."""
|
|
102
|
+
settings = get_config()
|
|
103
|
+
registry = ActionRegistry(Path(settings.paths.root))
|
|
104
|
+
|
|
105
|
+
action_context = None
|
|
106
|
+
if context:
|
|
107
|
+
try:
|
|
108
|
+
ctx_data = j.loads(context)
|
|
109
|
+
action_context = ActionContext(**ctx_data)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print_error(f"Invalid context JSON: {e}")
|
|
112
|
+
|
|
113
|
+
actions = registry.list_available(action_context)
|
|
114
|
+
if json:
|
|
115
|
+
print(j.dumps([a.dict() for a in actions], indent=2))
|
|
116
|
+
else:
|
|
117
|
+
print_output(actions, title="Available Actions")
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def status(
|
|
121
|
+
json: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
122
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
123
|
+
):
|
|
124
|
+
"""View status of Agent Providers."""
|
|
125
|
+
state_manager = AgentStateManager()
|
|
126
|
+
state = state_manager.get_or_refresh(force=force)
|
|
127
|
+
|
|
128
|
+
if json:
|
|
129
|
+
import json as j
|
|
130
|
+
# Convert datetime to ISO string for JSON serialization
|
|
131
|
+
data = state.dict()
|
|
132
|
+
data["last_checked"] = data["last_checked"].isoformat()
|
|
133
|
+
print(j.dumps(data, indent=2))
|
|
134
|
+
else:
|
|
135
|
+
# Standard output using existing print_output or custom formatting
|
|
136
|
+
from monoco.core.output import Table
|
|
137
|
+
table = Table(title=f"Agent Status (Last Checked: {state.last_checked.strftime('%Y-%m-%d %H:%M:%S')})")
|
|
138
|
+
table.add_column("Provider")
|
|
139
|
+
table.add_column("Available")
|
|
140
|
+
table.add_column("Path")
|
|
141
|
+
table.add_column("Error")
|
|
142
|
+
|
|
143
|
+
for name, p_state in state.providers.items():
|
|
144
|
+
table.add_row(
|
|
145
|
+
name,
|
|
146
|
+
"✅" if p_state.available else "❌",
|
|
147
|
+
p_state.path or "-",
|
|
148
|
+
p_state.error or "-"
|
|
149
|
+
)
|
|
150
|
+
print_output(table)
|
|
151
|
+
|
|
152
|
+
@app.command()
|
|
153
|
+
def doctor(
|
|
154
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
Diagnose Agent Environment and refresh state.
|
|
158
|
+
"""
|
|
159
|
+
from monoco.features.agent.doctor import doctor as doc_impl
|
|
160
|
+
doc_impl(force)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from monoco.core.output import print_output, print_error
|
|
3
|
+
from monoco.core.agent.state import AgentStateManager
|
|
4
|
+
|
|
5
|
+
app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@app.command()
|
|
8
|
+
def doctor(
|
|
9
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
10
|
+
):
|
|
11
|
+
"""
|
|
12
|
+
Diagnose Agent Environment and refresh state.
|
|
13
|
+
"""
|
|
14
|
+
manager = AgentStateManager()
|
|
15
|
+
try:
|
|
16
|
+
if force:
|
|
17
|
+
print("Force refreshing agent state...")
|
|
18
|
+
state = manager.refresh()
|
|
19
|
+
else:
|
|
20
|
+
state = manager.get_or_refresh()
|
|
21
|
+
|
|
22
|
+
print_output(state, title="Agent Diagnosis Report")
|
|
23
|
+
|
|
24
|
+
# Simple summary
|
|
25
|
+
available = [k for k, v in state.providers.items() if v.available]
|
|
26
|
+
print(f"\n✅ Available Agents: {', '.join(available) if available else 'None'}")
|
|
27
|
+
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print_error(f"Doctor failed: {e}")
|
|
30
|
+
raise typer.Exit(1)
|
monoco/features/issue/core.py
CHANGED
|
@@ -52,9 +52,9 @@ def parse_issue(file_path: Path) -> Optional[IssueMetadata]:
|
|
|
52
52
|
if not isinstance(data, dict):
|
|
53
53
|
return None
|
|
54
54
|
|
|
55
|
-
# Inject path before validation to ensure it persists
|
|
56
55
|
data['path'] = str(file_path.absolute())
|
|
57
56
|
meta = IssueMetadata(**data)
|
|
57
|
+
meta.actions = get_available_actions(meta)
|
|
58
58
|
return meta
|
|
59
59
|
except Exception:
|
|
60
60
|
return None
|
|
@@ -171,47 +171,36 @@ def create_issue_file(
|
|
|
171
171
|
metadata.path = str(file_path.absolute())
|
|
172
172
|
|
|
173
173
|
return metadata, file_path
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
def get_available_actions(meta: IssueMetadata) -> List[Any]:
|
|
175
|
+
from .models import IssueAction
|
|
176
|
+
actions = []
|
|
177
|
+
|
|
178
|
+
if meta.status == IssueStatus.OPEN:
|
|
179
|
+
# Stage-based movements
|
|
180
|
+
if meta.stage == IssueStage.DRAFT:
|
|
181
|
+
actions.append(IssueAction(label="Start", target_status=IssueStatus.OPEN, target_stage=IssueStage.DOING))
|
|
182
|
+
actions.append(IssueAction(label="Freeze", target_status=IssueStatus.BACKLOG))
|
|
183
|
+
elif meta.stage == IssueStage.DOING:
|
|
184
|
+
actions.append(IssueAction(label="Stop", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
185
|
+
actions.append(IssueAction(label="Submit", target_status=IssueStatus.OPEN, target_stage=IssueStage.REVIEW))
|
|
186
|
+
elif meta.stage == IssueStage.REVIEW:
|
|
187
|
+
actions.append(IssueAction(label="Approve", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.IMPLEMENTED))
|
|
188
|
+
actions.append(IssueAction(label="Reject", target_status=IssueStatus.OPEN, target_stage=IssueStage.DOING))
|
|
189
|
+
elif meta.stage == IssueStage.DONE:
|
|
190
|
+
actions.append(IssueAction(label="Reopen", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
191
|
+
actions.append(IssueAction(label="Close", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.IMPLEMENTED))
|
|
192
|
+
|
|
193
|
+
# Generic cancel
|
|
194
|
+
actions.append(IssueAction(label="Cancel", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.CANCELLED))
|
|
190
195
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# If we are transitioning TO Closed, current stage must be Review.
|
|
200
|
-
if current_stage != IssueStage.REVIEW:
|
|
201
|
-
raise ValueError(f"Lifecycle Policy: 'Implemented' issues must be submitted for review first.\nCurrent stage: {current_stage}\nAction: Run `monoco issue submit {issue_id}`.")
|
|
202
|
-
|
|
203
|
-
# Policy: No closing from DOING (General Safety)
|
|
204
|
-
if current_stage == IssueStage.DOING:
|
|
205
|
-
raise ValueError("Cannot close issue in progress (Doing). Please review (`monoco issue submit`) or stop (`monoco issue open`) first.")
|
|
206
|
-
|
|
207
|
-
# Policy: Dependencies must be closed
|
|
208
|
-
if issue_dependencies:
|
|
209
|
-
for dep_id in issue_dependencies:
|
|
210
|
-
dep_path = find_issue_path(issues_root, dep_id)
|
|
211
|
-
if dep_path:
|
|
212
|
-
dep_meta = parse_issue(dep_path)
|
|
213
|
-
if dep_meta and dep_meta.status != IssueStatus.CLOSED:
|
|
214
|
-
raise ValueError(f"Dependency Block: Cannot close {issue_id} because dependency {dep_id} is not closed.")
|
|
196
|
+
elif meta.status == IssueStatus.BACKLOG:
|
|
197
|
+
actions.append(IssueAction(label="Pull", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
198
|
+
actions.append(IssueAction(label="Cancel", target_status=IssueStatus.CLOSED, target_stage=IssueStage.DONE, target_solution=IssueSolution.CANCELLED))
|
|
199
|
+
|
|
200
|
+
elif meta.status == IssueStatus.CLOSED:
|
|
201
|
+
actions.append(IssueAction(label="Reopen", target_status=IssueStatus.OPEN, target_stage=IssueStage.DRAFT))
|
|
202
|
+
|
|
203
|
+
return actions
|
|
215
204
|
|
|
216
205
|
def find_issue_path(issues_root: Path, issue_id: str) -> Optional[Path]:
|
|
217
206
|
parsed = IssueID(issue_id)
|
|
@@ -253,7 +242,17 @@ def find_issue_path(issues_root: Path, issue_id: str) -> Optional[Path]:
|
|
|
253
242
|
return f
|
|
254
243
|
return None
|
|
255
244
|
|
|
256
|
-
def update_issue(
|
|
245
|
+
def update_issue(
|
|
246
|
+
issues_root: Path,
|
|
247
|
+
issue_id: str,
|
|
248
|
+
status: Optional[IssueStatus] = None,
|
|
249
|
+
stage: Optional[IssueStage] = None,
|
|
250
|
+
solution: Optional[IssueSolution] = None,
|
|
251
|
+
parent: Optional[str] = None,
|
|
252
|
+
dependencies: Optional[List[str]] = None,
|
|
253
|
+
related: Optional[List[str]] = None,
|
|
254
|
+
tags: Optional[List[str]] = None
|
|
255
|
+
) -> IssueMetadata:
|
|
257
256
|
path = find_issue_path(issues_root, issue_id)
|
|
258
257
|
if not path:
|
|
259
258
|
raise FileNotFoundError(f"Issue {issue_id} not found.")
|
|
@@ -262,7 +261,7 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
262
261
|
content = path.read_text()
|
|
263
262
|
|
|
264
263
|
# Split Frontmatter and Body
|
|
265
|
-
match = re.search(r"^---(.*?)---\n(.*)
|
|
264
|
+
match = re.search(r"^---(.*?)---\n(.*)\n", content, re.DOTALL | re.MULTILINE)
|
|
266
265
|
if not match:
|
|
267
266
|
# Fallback
|
|
268
267
|
match_simple = re.search(r"^---(.*?)---", content, re.DOTALL | re.MULTILINE)
|
|
@@ -313,14 +312,29 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
313
312
|
raise ValueError("Cannot close issue in progress (Doing). Please review (`monoco issue submit`) or stop (`monoco issue open`) first.")
|
|
314
313
|
|
|
315
314
|
# Policy: Dependencies must be closed
|
|
316
|
-
|
|
317
|
-
if
|
|
318
|
-
for dep_id in
|
|
315
|
+
dependencies_to_check = dependencies if dependencies is not None else data.get('dependencies', [])
|
|
316
|
+
if dependencies_to_check:
|
|
317
|
+
for dep_id in dependencies_to_check:
|
|
319
318
|
dep_path = find_issue_path(issues_root, dep_id)
|
|
320
319
|
if dep_path:
|
|
321
320
|
dep_meta = parse_issue(dep_path)
|
|
322
321
|
if dep_meta and dep_meta.status != IssueStatus.CLOSED:
|
|
323
322
|
raise ValueError(f"Dependency Block: Cannot close {issue_id} because dependency {dep_id} is [Status: {dep_meta.status.value}].")
|
|
323
|
+
|
|
324
|
+
# Validate new parent/dependencies/related exist
|
|
325
|
+
if parent is not None and parent != "":
|
|
326
|
+
if not find_issue_path(issues_root, parent):
|
|
327
|
+
raise ValueError(f"Parent issue {parent} not found.")
|
|
328
|
+
|
|
329
|
+
if dependencies is not None:
|
|
330
|
+
for dep_id in dependencies:
|
|
331
|
+
if not find_issue_path(issues_root, dep_id):
|
|
332
|
+
raise ValueError(f"Dependency issue {dep_id} not found.")
|
|
333
|
+
|
|
334
|
+
if related is not None:
|
|
335
|
+
for rel_id in related:
|
|
336
|
+
if not find_issue_path(issues_root, rel_id):
|
|
337
|
+
raise ValueError(f"Related issue {rel_id} not found.")
|
|
324
338
|
|
|
325
339
|
# Update Data
|
|
326
340
|
if status:
|
|
@@ -331,6 +345,21 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
331
345
|
if solution:
|
|
332
346
|
data['solution'] = solution.value
|
|
333
347
|
|
|
348
|
+
if parent is not None:
|
|
349
|
+
if parent == "":
|
|
350
|
+
data.pop('parent', None) # Remove parent field
|
|
351
|
+
else:
|
|
352
|
+
data['parent'] = parent
|
|
353
|
+
|
|
354
|
+
if dependencies is not None:
|
|
355
|
+
data['dependencies'] = dependencies
|
|
356
|
+
|
|
357
|
+
if related is not None:
|
|
358
|
+
data['related'] = related
|
|
359
|
+
|
|
360
|
+
if tags is not None:
|
|
361
|
+
data['tags'] = tags
|
|
362
|
+
|
|
334
363
|
# Lifecycle Hooks
|
|
335
364
|
# 1. Opened At: If transitioning to OPEN
|
|
336
365
|
if target_status == IssueStatus.OPEN and current_status != IssueStatus.OPEN:
|
|
@@ -385,11 +414,15 @@ def update_issue(issues_root: Path, issue_id: str, status: Optional[IssueStatus]
|
|
|
385
414
|
if path != target_path:
|
|
386
415
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
387
416
|
path.rename(target_path)
|
|
417
|
+
path = target_path # Update local path variable for returned meta
|
|
388
418
|
|
|
389
419
|
# Hook: Recursive Aggregation (FEAT-0003)
|
|
390
420
|
if updated_meta.parent:
|
|
391
421
|
recalculate_parent(issues_root, updated_meta.parent)
|
|
392
|
-
|
|
422
|
+
|
|
423
|
+
# Update returned metadata with final absolute path
|
|
424
|
+
updated_meta.path = str(path.absolute())
|
|
425
|
+
updated_meta.actions = get_available_actions(updated_meta)
|
|
393
426
|
return updated_meta
|
|
394
427
|
|
|
395
428
|
def start_issue_isolation(issues_root: Path, issue_id: str, mode: IsolationType, project_root: Path) -> IssueMetadata:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refine-issue
|
|
3
|
+
description: Refine a raw issue description into a structured Monoco Feature.
|
|
4
|
+
provider: claude
|
|
5
|
+
args: ["file"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an expert Technical Product Manager using the Monoco Issue System.
|
|
9
|
+
Your task is to refine the provided Issue file into a high-quality "Feature Ticket".
|
|
10
|
+
|
|
11
|
+
# Input Context
|
|
12
|
+
|
|
13
|
+
File: {{file}}
|
|
14
|
+
|
|
15
|
+
# Monoco Ontology Rules
|
|
16
|
+
|
|
17
|
+
1. **Feature**: Delivers User Value (Atomic, Shippable). Prefix: `FEAT-`.
|
|
18
|
+
2. **Architecture**: Uses `Chore` for purely technical maintenance.
|
|
19
|
+
3. **Structure**: Must have clear `Objective`, `Context`, `Strategy`, `Detailed Design`, and `Acceptance Criteria`.
|
|
20
|
+
|
|
21
|
+
# Instructions
|
|
22
|
+
|
|
23
|
+
1. Analyze the input content.
|
|
24
|
+
2. Rewrite it to be professional, clear, and comprehensive.
|
|
25
|
+
3. Fill in missing details with reasonable technical assumptions (but mark them if unsure).
|
|
26
|
+
4. OUTPUT ONLY the Markdown content of the new file.
|
monoco/features/issue/models.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import List, Optional, Any
|
|
2
|
+
from typing import List, Optional, Any, Dict
|
|
3
3
|
from pydantic import BaseModel, Field, model_validator
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import hashlib
|
|
@@ -83,6 +83,17 @@ class IssueIsolation(BaseModel):
|
|
|
83
83
|
path: Optional[str] = None # Worktree path (relative to repo root or absolute)
|
|
84
84
|
created_at: datetime = Field(default_factory=current_time)
|
|
85
85
|
|
|
86
|
+
class IssueAction(BaseModel):
|
|
87
|
+
label: str
|
|
88
|
+
target_status: Optional[IssueStatus] = None
|
|
89
|
+
target_stage: Optional[IssueStage] = None
|
|
90
|
+
target_solution: Optional[IssueSolution] = None
|
|
91
|
+
icon: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
# Generic execution extensions
|
|
94
|
+
command: Optional[str] = None
|
|
95
|
+
params: Dict[str, Any] = {}
|
|
96
|
+
|
|
86
97
|
class IssueMetadata(BaseModel):
|
|
87
98
|
model_config = {"extra": "allow"}
|
|
88
99
|
|
|
@@ -105,10 +116,11 @@ class IssueMetadata(BaseModel):
|
|
|
105
116
|
isolation: Optional[IssueIsolation] = None
|
|
106
117
|
dependencies: List[str] = []
|
|
107
118
|
related: List[str] = []
|
|
108
|
-
dependencies: List[str] = []
|
|
109
|
-
related: List[str] = []
|
|
110
119
|
tags: List[str] = []
|
|
111
120
|
path: Optional[str] = None # Absolute path to the issue file
|
|
121
|
+
|
|
122
|
+
# Proxy UI Actions (Excluded from file persistence)
|
|
123
|
+
actions: List[IssueAction] = Field(default=[], exclude=True)
|
|
112
124
|
|
|
113
125
|
|
|
114
126
|
@model_validator(mode='before')
|
|
@@ -132,9 +144,9 @@ class IssueMetadata(BaseModel):
|
|
|
132
144
|
@model_validator(mode='after')
|
|
133
145
|
def validate_lifecycle(self) -> 'IssueMetadata':
|
|
134
146
|
# Logic Definition:
|
|
135
|
-
# status: backlog -> stage:
|
|
147
|
+
# status: backlog -> stage: freezed
|
|
136
148
|
# status: closed -> stage: done
|
|
137
|
-
# status: open -> stage: draft | doing | review (default draft)
|
|
149
|
+
# status: open -> stage: draft | doing | review | done (default draft)
|
|
138
150
|
|
|
139
151
|
if self.status == IssueStatus.BACKLOG:
|
|
140
152
|
self.stage = IssueStage.FREEZED
|
|
@@ -149,7 +161,7 @@ class IssueMetadata(BaseModel):
|
|
|
149
161
|
|
|
150
162
|
elif self.status == IssueStatus.OPEN:
|
|
151
163
|
# Ensure valid stage for open status
|
|
152
|
-
if self.stage is None
|
|
164
|
+
if self.stage is None:
|
|
153
165
|
self.stage = IssueStage.DRAFT
|
|
154
166
|
|
|
155
167
|
return self
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Callable, Awaitable, Any, Optional
|
|
6
|
+
|
|
7
|
+
from watchdog.observers import Observer
|
|
8
|
+
from watchdog.events import FileSystemEventHandler
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("monoco.features.issue.monitor")
|
|
11
|
+
|
|
12
|
+
class IssueEventHandler(FileSystemEventHandler):
|
|
13
|
+
def __init__(self, loop, on_upsert: Callable[[dict], Awaitable[None]], on_delete: Callable[[dict], Awaitable[None]]):
|
|
14
|
+
self.loop = loop
|
|
15
|
+
self.on_upsert = on_upsert
|
|
16
|
+
self.on_delete = on_delete
|
|
17
|
+
|
|
18
|
+
def _process_upsert(self, path_str: str):
|
|
19
|
+
if not path_str.endswith(".md"):
|
|
20
|
+
return
|
|
21
|
+
asyncio.run_coroutine_threadsafe(self._handle_upsert(path_str), self.loop)
|
|
22
|
+
|
|
23
|
+
async def _handle_upsert(self, path_str: str):
|
|
24
|
+
try:
|
|
25
|
+
from monoco.features.issue.core import parse_issue
|
|
26
|
+
path = Path(path_str)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return
|
|
29
|
+
issue = parse_issue(path)
|
|
30
|
+
if issue:
|
|
31
|
+
await self.on_upsert(issue.model_dump(mode='json'))
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Error handling upsert for {path_str}: {e}")
|
|
34
|
+
|
|
35
|
+
def _process_delete(self, path_str: str):
|
|
36
|
+
if not path_str.endswith(".md"):
|
|
37
|
+
return
|
|
38
|
+
asyncio.run_coroutine_threadsafe(self._handle_delete(path_str), self.loop)
|
|
39
|
+
|
|
40
|
+
async def _handle_delete(self, path_str: str):
|
|
41
|
+
try:
|
|
42
|
+
filename = Path(path_str).name
|
|
43
|
+
match = re.match(r"([A-Z]+-\d{4})", filename)
|
|
44
|
+
if match:
|
|
45
|
+
issue_id = match.group(1)
|
|
46
|
+
await self.on_delete({"id": issue_id})
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error handling delete for {path_str}: {e}")
|
|
49
|
+
|
|
50
|
+
def on_created(self, event):
|
|
51
|
+
if not event.is_directory:
|
|
52
|
+
self._process_upsert(event.src_path)
|
|
53
|
+
|
|
54
|
+
def on_modified(self, event):
|
|
55
|
+
if not event.is_directory:
|
|
56
|
+
self._process_upsert(event.src_path)
|
|
57
|
+
|
|
58
|
+
def on_deleted(self, event):
|
|
59
|
+
if not event.is_directory:
|
|
60
|
+
self._process_delete(event.src_path)
|
|
61
|
+
|
|
62
|
+
def on_moved(self, event):
|
|
63
|
+
if not event.is_directory:
|
|
64
|
+
self._process_delete(event.src_path)
|
|
65
|
+
self._process_upsert(event.dest_path)
|
|
66
|
+
|
|
67
|
+
class IssueMonitor:
|
|
68
|
+
"""
|
|
69
|
+
Monitor the Issues directory for changes using Watchdog and trigger callbacks.
|
|
70
|
+
"""
|
|
71
|
+
def __init__(self, issues_root: Path, on_upsert: Callable[[dict], Awaitable[None]], on_delete: Callable[[dict], Awaitable[None]]):
|
|
72
|
+
self.issues_root = issues_root
|
|
73
|
+
self.on_upsert = on_upsert
|
|
74
|
+
self.on_delete = on_delete
|
|
75
|
+
self.observer = Observer()
|
|
76
|
+
self.loop = None
|
|
77
|
+
|
|
78
|
+
async def start(self):
|
|
79
|
+
self.loop = asyncio.get_running_loop()
|
|
80
|
+
event_handler = IssueEventHandler(self.loop, self.on_upsert, self.on_delete)
|
|
81
|
+
|
|
82
|
+
if not self.issues_root.exists():
|
|
83
|
+
logger.warning(f"Issues root {self.issues_root} does not exist. creating...")
|
|
84
|
+
self.issues_root.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
self.observer.schedule(event_handler, str(self.issues_root), recursive=True)
|
|
87
|
+
self.observer.start()
|
|
88
|
+
logger.info(f"Issue Monitor started (Watchdog). Watching {self.issues_root}")
|
|
89
|
+
|
|
90
|
+
def stop(self):
|
|
91
|
+
if self.observer.is_alive():
|
|
92
|
+
self.observer.stop()
|
|
93
|
+
self.observer.join()
|
|
94
|
+
logger.info(f"Issue Monitor stopped for {self.issues_root}")
|
monoco/main.py
CHANGED
|
@@ -44,6 +44,16 @@ from monoco.core.sync import sync_command, uninstall_command
|
|
|
44
44
|
app.command(name="sync")(sync_command)
|
|
45
45
|
app.command(name="uninstall")(uninstall_command)
|
|
46
46
|
|
|
47
|
+
@app.command(name="doctor")
|
|
48
|
+
def doctor_cmd(
|
|
49
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Diagnose Agent Environment.
|
|
53
|
+
"""
|
|
54
|
+
from monoco.features.agent.doctor import doctor
|
|
55
|
+
doctor(force)
|
|
56
|
+
|
|
47
57
|
@app.command()
|
|
48
58
|
def info():
|
|
49
59
|
"""
|
|
@@ -92,6 +102,9 @@ app.add_typer(spike_cmd.app, name="spike", help="Manage research spikes")
|
|
|
92
102
|
app.add_typer(i18n_cmd.app, name="i18n", help="Manage documentation i18n")
|
|
93
103
|
app.add_typer(config_cmd.app, name="config", help="Manage configuration")
|
|
94
104
|
|
|
105
|
+
from monoco.features.agent import commands as agent_cmd
|
|
106
|
+
app.add_typer(agent_cmd.app, name="agent", help="Delegate tasks to Agent CLIs")
|
|
107
|
+
|
|
95
108
|
from monoco.daemon.commands import serve
|
|
96
109
|
app.command(name="serve")(serve)
|
|
97
110
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monoco-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Agent Native Toolkit for Monoco - Task Management & Kanban for AI Agents
|
|
5
5
|
Project-URL: Homepage, https://monoco.io
|
|
6
6
|
Project-URL: Repository, https://github.com/IndenScale/Monoco
|