monoco-toolkit 0.1.7__py3-none-any.whl → 0.2.2__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.
@@ -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)
@@ -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 validate_transition(
175
- current_status: IssueStatus,
176
- current_stage: Optional[IssueStage],
177
- target_status: IssueStatus,
178
- target_stage: Optional[IssueStage],
179
- target_solution: Optional[str],
180
- issue_dependencies: List[str],
181
- issues_root: Path,
182
- issue_id: str
183
- ):
184
- """
185
- Centralized validation logic for state transitions.
186
- """
187
- # Policy: Prevent Backlog -> Review
188
- if target_stage == IssueStage.REVIEW and current_status == IssueStatus.BACKLOG:
189
- raise ValueError(f"Lifecycle Policy: Cannot submit Backlog issue directly. Run `monoco issue pull {issue_id}` first.")
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
- if target_status == IssueStatus.CLOSED:
192
- if not target_solution:
193
- raise ValueError(f"Closing an issue requires a solution. Please provide --solution or edit the file metadata.")
194
-
195
- # Policy: IMPLEMENTED requires REVIEW stage (unless we are already in REVIEW)
196
- # Check current stage.
197
- if target_solution == IssueSolution.IMPLEMENTED.value:
198
- # If we are transitioning FROM Review, it's fine.
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(issues_root: Path, issue_id: str, status: Optional[IssueStatus] = None, stage: Optional[IssueStage] = None, solution: Optional[IssueSolution] = None) -> IssueMetadata:
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(.*)$\n", content, re.DOTALL | re.MULTILINE)
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
- dependencies = data.get('dependencies', [])
317
- if dependencies:
318
- for dep_id in dependencies:
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.
@@ -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: null
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 or self.stage == IssueStage.DONE:
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.1.7
3
+ Version: 0.2.2
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