monoco-toolkit 0.2.4__py3-none-any.whl → 0.2.6__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 (54) hide show
  1. monoco/cli/project.py +15 -7
  2. monoco/cli/workspace.py +11 -3
  3. monoco/core/agent/adapters.py +24 -1
  4. monoco/core/config.py +81 -3
  5. monoco/core/integrations.py +8 -0
  6. monoco/core/lsp.py +7 -0
  7. monoco/core/output.py +8 -1
  8. monoco/core/resources/en/SKILL.md +1 -1
  9. monoco/core/setup.py +8 -1
  10. monoco/daemon/app.py +18 -12
  11. monoco/features/agent/commands.py +94 -17
  12. monoco/features/agent/core.py +48 -0
  13. monoco/features/agent/resources/en/critique.prompty +16 -0
  14. monoco/features/agent/resources/en/develop.prompty +16 -0
  15. monoco/features/agent/resources/en/investigate.prompty +16 -0
  16. monoco/features/agent/resources/en/refine.prompty +14 -0
  17. monoco/features/agent/resources/en/verify.prompty +16 -0
  18. monoco/features/agent/resources/zh/critique.prompty +18 -0
  19. monoco/features/agent/resources/zh/develop.prompty +18 -0
  20. monoco/features/agent/resources/zh/investigate.prompty +18 -0
  21. monoco/features/agent/resources/zh/refine.prompty +16 -0
  22. monoco/features/agent/resources/zh/verify.prompty +18 -0
  23. monoco/features/config/commands.py +35 -14
  24. monoco/features/i18n/commands.py +89 -10
  25. monoco/features/i18n/core.py +112 -16
  26. monoco/features/issue/commands.py +254 -85
  27. monoco/features/issue/core.py +142 -119
  28. monoco/features/issue/domain/__init__.py +0 -0
  29. monoco/features/issue/domain/lifecycle.py +126 -0
  30. monoco/features/issue/domain/models.py +170 -0
  31. monoco/features/issue/domain/parser.py +223 -0
  32. monoco/features/issue/domain/workspace.py +104 -0
  33. monoco/features/issue/engine/__init__.py +22 -0
  34. monoco/features/issue/engine/config.py +189 -0
  35. monoco/features/issue/engine/machine.py +185 -0
  36. monoco/features/issue/engine/models.py +18 -0
  37. monoco/features/issue/linter.py +32 -11
  38. monoco/features/issue/lsp/__init__.py +3 -0
  39. monoco/features/issue/lsp/definition.py +72 -0
  40. monoco/features/issue/models.py +8 -8
  41. monoco/features/issue/validator.py +204 -65
  42. monoco/features/spike/commands.py +45 -24
  43. monoco/features/spike/core.py +5 -22
  44. monoco/main.py +11 -17
  45. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
  46. monoco_toolkit-0.2.6.dist-info/RECORD +96 -0
  47. monoco/features/issue/executions/refine.md +0 -26
  48. monoco/features/pty/core.py +0 -185
  49. monoco/features/pty/router.py +0 -138
  50. monoco/features/pty/server.py +0 -56
  51. monoco_toolkit-0.2.4.dist-info/RECORD +0 -78
  52. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
  53. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
  54. {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
monoco/cli/project.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import typer
2
2
  from pathlib import Path
3
- from typing import Optional
3
+ from typing import Optional, Annotated
4
4
  from rich.console import Console
5
5
  from rich.table import Table
6
6
  import yaml
@@ -9,20 +9,21 @@ import os
9
9
 
10
10
  from monoco.core.workspace import find_projects, is_project_root
11
11
  from monoco.core.config import get_config
12
+ from monoco.core.output import AgentOutput, OutputManager
12
13
 
13
14
  app = typer.Typer(help="Manage Monoco Projects")
14
15
  console = Console()
15
16
 
16
17
  @app.command("list")
17
18
  def list_projects(
18
- json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
19
+ json: AgentOutput = False,
19
20
  root: Optional[str] = typer.Option(None, "--root", help="Workspace root")
20
21
  ):
21
22
  """List all discovered projects in the workspace."""
22
23
  cwd = Path(root).resolve() if root else Path.cwd()
23
24
  projects = find_projects(cwd)
24
25
 
25
- if json_output:
26
+ if OutputManager.is_agent_mode():
26
27
  data = [
27
28
  {
28
29
  "id": p.id,
@@ -32,7 +33,7 @@ def list_projects(
32
33
  }
33
34
  for p in projects
34
35
  ]
35
- print(json.dumps(data))
36
+ OutputManager.print(data)
36
37
  else:
37
38
  table = Table(title=f"Projects in {cwd}")
38
39
  table.add_column("ID", style="cyan")
@@ -53,14 +54,15 @@ def list_projects(
53
54
  def init_project(
54
55
  name: str = typer.Option(..., "--name", "-n", help="Project Name"),
55
56
  key: str = typer.Option(..., "--key", "-k", help="Project Key"),
56
- force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
57
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
58
+ json: AgentOutput = False,
57
59
  ):
58
60
  """Initialize a new project in the current directory."""
59
61
  cwd = Path.cwd()
60
62
  project_config_path = cwd / ".monoco" / "project.yaml"
61
63
 
62
64
  if project_config_path.exists() and not force:
63
- console.print(f"[yellow]Project already initialized in {cwd}. Use --force to overwrite.[/yellow]")
65
+ OutputManager.error(f"Project already initialized in {cwd}. Use --force to overwrite.")
64
66
  raise typer.Exit(code=1)
65
67
 
66
68
  cwd.mkdir(parents=True, exist_ok=True)
@@ -76,4 +78,10 @@ def init_project(
76
78
  with open(project_config_path, "w") as f:
77
79
  yaml.dump(config, f, default_flow_style=False)
78
80
 
79
- console.print(f"[green]Initialized project '{name}' ({key}) in {cwd}[/green]")
81
+ OutputManager.print({
82
+ "status": "initialized",
83
+ "name": name,
84
+ "key": key,
85
+ "path": str(cwd),
86
+ "config_file": str(project_config_path)
87
+ })
monoco/cli/workspace.py CHANGED
@@ -1,22 +1,26 @@
1
1
  import typer
2
2
  from pathlib import Path
3
3
  from rich.console import Console
4
+ from typing import Annotated
4
5
  import yaml
5
6
  import json
6
7
 
8
+ from monoco.core.output import AgentOutput, OutputManager
9
+
7
10
  app = typer.Typer(help="Manage Monoco Workspace")
8
11
  console = Console()
9
12
 
10
13
  @app.command("init")
11
14
  def init_workspace(
12
- force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
15
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
16
+ json: AgentOutput = False,
13
17
  ):
14
18
  """Initialize a workspace environment in the current directory."""
15
19
  cwd = Path.cwd()
16
20
  workspace_config_path = cwd / ".monoco" / "workspace.yaml"
17
21
 
18
22
  if workspace_config_path.exists() and not force:
19
- console.print(f"[yellow]Workspace already initialized in {cwd}. Use --force to overwrite.[/yellow]")
23
+ OutputManager.error(f"Workspace already initialized in {cwd}. Use --force to overwrite.")
20
24
  raise typer.Exit(code=1)
21
25
 
22
26
  cwd.mkdir(parents=True, exist_ok=True)
@@ -34,5 +38,9 @@ def init_workspace(
34
38
  with open(workspace_config_path, "w") as f:
35
39
  yaml.dump(config, f, default_flow_style=False)
36
40
 
37
- console.print(f"[green]Initialized workspace in {cwd}[/green]")
41
+ OutputManager.print({
42
+ "status": "initialized",
43
+ "path": str(cwd),
44
+ "config_file": str(workspace_config_path)
45
+ })
38
46
 
@@ -21,6 +21,16 @@ class BaseCLIClient:
21
21
 
22
22
  def _build_prompt(self, prompt: str, context_files: List[Path]) -> str:
23
23
  """Concatenate prompt and context files."""
24
+ # Inject Language Rule
25
+ try:
26
+ from monoco.core.config import get_config
27
+ settings = get_config()
28
+ lang = settings.i18n.source_lang
29
+ if lang:
30
+ prompt = f"{prompt}\n\n[SYSTEM: LANGUAGE CONSTRAINT]\nThe project source language is '{lang}'. You MUST use '{lang}' for all thinking and reporting unless explicitly instructed otherwise."
31
+ except Exception:
32
+ pass
33
+
24
34
  full_prompt = [prompt]
25
35
  if context_files:
26
36
  full_prompt.append("\n\n--- CONTEXT FILES ---")
@@ -93,10 +103,23 @@ class QwenClient(BaseCLIClient, AgentClient):
93
103
  return await self._run_command([self._executable, full_prompt])
94
104
 
95
105
 
106
+ class KimiClient(BaseCLIClient, AgentClient):
107
+ """Adapter for Moonshot Kimi CLI."""
108
+
109
+ def __init__(self):
110
+ super().__init__("kimi")
111
+
112
+ async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
113
+ full_prompt = self._build_prompt(prompt, context_files)
114
+ # Usage: kimi "prompt"
115
+ return await self._run_command([self._executable, full_prompt])
116
+
117
+
96
118
  _ADAPTERS = {
97
119
  "gemini": GeminiClient,
98
120
  "claude": ClaudeClient,
99
- "qwen": QwenClient
121
+ "qwen": QwenClient,
122
+ "kimi": KimiClient
100
123
  }
101
124
 
102
125
  def get_agent_client(name: str) -> AgentClient:
monoco/core/config.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import yaml
3
3
  from pathlib import Path
4
- from typing import Optional, Dict, Any, Callable, Awaitable
4
+ from typing import Optional, Dict, Any, Callable, Awaitable, List
5
5
  from enum import Enum
6
6
  from pydantic import BaseModel, Field
7
7
 
@@ -23,7 +23,6 @@ class PathsConfig(BaseModel):
23
23
 
24
24
  class CoreConfig(BaseModel):
25
25
  """Core system configuration."""
26
- editor: str = Field(default_factory=lambda: os.getenv("EDITOR", "vim"), description="Preferred text editor")
27
26
  log_level: str = Field(default="INFO", description="Logging verbosity")
28
27
  author: Optional[str] = Field(default=None, description="Default author for new artifacts")
29
28
 
@@ -57,6 +56,67 @@ class AgentConfig(BaseModel):
57
56
  description="Custom agent framework integrations (overrides defaults from monoco.core.integrations)"
58
57
  )
59
58
 
59
+ class IssueTypeConfig(BaseModel):
60
+ name: str
61
+ label: str
62
+ prefix: str
63
+ folder: str
64
+ description: Optional[str] = None
65
+
66
+ class TransitionConfig(BaseModel):
67
+ name: str
68
+ label: str
69
+ icon: Optional[str] = None
70
+ from_status: Optional[str] = None
71
+ from_stage: Optional[str] = None
72
+ to_status: str
73
+ to_stage: Optional[str] = None
74
+ required_solution: Optional[str] = None
75
+ description: str = ""
76
+ command_template: Optional[str] = None
77
+
78
+ class IssueSchemaConfig(BaseModel):
79
+ types: List[IssueTypeConfig] = Field(default_factory=list)
80
+ statuses: List[str] = Field(default_factory=list)
81
+ stages: List[str] = Field(default_factory=list)
82
+ solutions: List[str] = Field(default_factory=list)
83
+ workflows: List[TransitionConfig] = Field(default_factory=list)
84
+
85
+ def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
86
+ if not other:
87
+ return self
88
+
89
+ # Types: merge by name
90
+ if other.types:
91
+ type_map = {t.name: t for t in self.types}
92
+ for ot in other.types:
93
+ type_map[ot.name] = ot
94
+ self.types = list(type_map.values())
95
+
96
+ # Statuses: replace if provided
97
+ if other.statuses:
98
+ self.statuses = other.statuses
99
+
100
+ # Stages: replace if provided
101
+ if other.stages:
102
+ self.stages = other.stages
103
+
104
+ # Solutions: replace if provided
105
+ if other.solutions:
106
+ self.solutions = other.solutions
107
+
108
+ # Workflows (Transitions): merge by name
109
+ if other.workflows:
110
+ wf_map = {w.name: w for w in self.workflows}
111
+ for ow in other.workflows:
112
+ wf_map[ow.name] = ow
113
+ self.workflows = list(wf_map.values())
114
+
115
+ return self
116
+
117
+ class StateMachineConfig(BaseModel):
118
+ transitions: List[TransitionConfig]
119
+
60
120
  class MonocoConfig(BaseModel):
61
121
  """
62
122
  Main Configuration Schema.
@@ -69,6 +129,7 @@ class MonocoConfig(BaseModel):
69
129
  ui: UIConfig = Field(default_factory=UIConfig)
70
130
  telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
71
131
  agent: AgentConfig = Field(default_factory=AgentConfig)
132
+ issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
72
133
 
73
134
  @staticmethod
74
135
  def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
@@ -165,14 +226,31 @@ def get_config(project_root: Optional[str] = None, require_project: bool = False
165
226
  class ConfigScope(str, Enum):
166
227
  GLOBAL = "global"
167
228
  PROJECT = "project"
229
+ WORKSPACE = "workspace"
168
230
 
169
231
  def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> Path:
170
232
  """Get the path to the configuration file for a given scope."""
171
233
  if scope == ConfigScope.GLOBAL:
172
234
  return Path.home() / ".monoco" / "config.yaml"
235
+ elif scope == ConfigScope.WORKSPACE:
236
+ cwd = Path(project_root) if project_root else Path.cwd()
237
+ return cwd / ".monoco" / "workspace.yaml"
173
238
  else:
239
+ # ConfigScope.PROJECT
174
240
  cwd = Path(project_root) if project_root else Path.cwd()
175
- return cwd / ".monoco" / "config.yaml"
241
+ return cwd / ".monoco" / "project.yaml"
242
+
243
+ def find_monoco_root(start_path: Optional[Path] = None) -> Path:
244
+ """Recursively find the .monoco directory upwards."""
245
+ current = (start_path or Path.cwd()).resolve()
246
+ # Check if we are inside a .monoco folder (unlikely but possible)
247
+ if current.name == ".monoco":
248
+ return current.parent
249
+
250
+ for parent in [current] + list(current.parents):
251
+ if (parent / ".monoco").exists():
252
+ return parent
253
+ return current
176
254
 
177
255
  def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> Dict[str, Any]:
178
256
  """Load raw configuration dictionary from a specific scope."""
@@ -110,6 +110,14 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
110
110
  bin_name="qwen",
111
111
  version_cmd="--version",
112
112
  ),
113
+ "kimi": AgentIntegration(
114
+ key="kimi",
115
+ name="Kimi CLI",
116
+ system_prompt_file="KIMI.md",
117
+ skill_root_dir=".kimi/skills/",
118
+ bin_name="kimi",
119
+ version_cmd="--version",
120
+ ),
113
121
  "agent": AgentIntegration(
114
122
  key="agent",
115
123
  name="Antigravity",
monoco/core/lsp.py CHANGED
@@ -24,6 +24,13 @@ class Range(BaseModel):
24
24
  def __repr__(self):
25
25
  return f"{self.start.line}:{self.start.character}-{self.end.line}:{self.end.character}"
26
26
 
27
+ class Location(BaseModel):
28
+ """
29
+ Represents a location inside a resource, such as a line of code inside a text file.
30
+ """
31
+ uri: str
32
+ range: Range
33
+
27
34
  class DiagnosticSeverity(IntEnum):
28
35
  Error = 1
29
36
  Warning = 2
monoco/core/output.py CHANGED
@@ -63,8 +63,15 @@ class OutputManager:
63
63
  print(json.dumps([item.model_dump(mode='json', exclude_none=True) for item in data], separators=(',', ':')))
64
64
  else:
65
65
  # Fallback for dicts/lists/primitives
66
+ def _encoder(obj):
67
+ if isinstance(obj, BaseModel):
68
+ return obj.model_dump(mode='json', exclude_none=True)
69
+ if hasattr(obj, 'value'): # Enum support
70
+ return obj.value
71
+ return str(obj)
72
+
66
73
  try:
67
- print(json.dumps(data, separators=(',', ':'), default=str))
74
+ print(json.dumps(data, separators=(',', ':'), default=_encoder))
68
75
  except TypeError:
69
76
  print(str(data))
70
77
 
@@ -52,7 +52,7 @@ Configuration is stored in YAML format at:
52
52
 
53
53
  Key configuration sections:
54
54
 
55
- - `core`: Editor, log level, author
55
+ - `core`: Log level, author
56
56
  - `paths`: Directory paths (issues, spikes, specs)
57
57
  - `project`: Project metadata, spike repos, workspace members
58
58
  - `i18n`: Internationalization settings
monoco/core/setup.py CHANGED
@@ -162,7 +162,6 @@ def init_cli(
162
162
  user_config = {
163
163
  "core": {
164
164
  "author": author,
165
- # Editor is handled by env/config defaults, no need to prompt
166
165
  },
167
166
  "telemetry": {
168
167
  "enabled": telemetry
@@ -259,6 +258,14 @@ def init_cli(
259
258
  (cwd / "Issues").mkdir(exist_ok=True)
260
259
  (cwd / ".references").mkdir(exist_ok=True)
261
260
 
261
+ # Initialize Agent Resources
262
+ try:
263
+ from monoco.features.agent.core import init_agent_resources
264
+ init_agent_resources(cwd)
265
+ console.print(f"[dim] - Agent Resources: .monoco/actions/*.prompty[/dim]")
266
+ except Exception as e:
267
+ console.print(f"[yellow]Warning: Failed to init agent resources: {e}[/yellow]")
268
+
262
269
  console.print("\n[bold green]✓ Monoco Project Initialized![/bold green]")
263
270
  console.print(f"Access configured! issues will be created as [bold]{project_key}-XXX[/bold]")
264
271
 
monoco/daemon/app.py CHANGED
@@ -9,7 +9,6 @@ from typing import Optional, List, Dict
9
9
  from monoco.daemon.services import Broadcaster, ProjectManager
10
10
  from monoco.core.git import GitMonitor
11
11
  from monoco.core.config import get_config, ConfigMonitor, ConfigScope, get_config_path
12
- from fastapi import FastAPI, Request, HTTPException, Query
13
12
 
14
13
  # Configure logging
15
14
  logging.basicConfig(level=logging.INFO)
@@ -29,7 +28,7 @@ Monoco Daemon Process
29
28
  # Service Instances
30
29
  broadcaster = Broadcaster()
31
30
  git_monitor: GitMonitor | None = None
32
- config_monitor: ConfigMonitor | None = None
31
+ config_monitors: List[ConfigMonitor] = []
33
32
  project_manager: ProjectManager | None = None
34
33
 
35
34
  @asynccontextmanager
@@ -37,7 +36,7 @@ async def lifespan(app: FastAPI):
37
36
  # Startup
38
37
  logger.info("Starting Monoco Daemon services...")
39
38
 
40
- global project_manager, git_monitor, config_monitor
39
+ global project_manager, git_monitor, config_monitors
41
40
  # Use MONOCO_SERVER_ROOT if set, otherwise CWD
42
41
  env_root = os.getenv("MONOCO_SERVER_ROOT")
43
42
  workspace_root = Path(env_root) if env_root else Path.cwd()
@@ -50,32 +49,39 @@ async def lifespan(app: FastAPI):
50
49
  "hash": new_hash
51
50
  })
52
51
 
53
- async def on_config_change():
54
- logger.info("Config file changed, broadcasting update...")
52
+ async def on_config_change(path: str):
53
+ logger.info(f"Config file changed: {path}, broadcasting update...")
55
54
  await broadcaster.broadcast("CONFIG_UPDATED", {
56
55
  "scope": "workspace",
57
- "path": str(workspace_root / ".monoco" / "config.yaml")
56
+ "path": path
58
57
  })
59
58
 
60
59
  git_monitor = GitMonitor(workspace_root, on_git_change)
61
60
 
62
- config_path = get_config_path(ConfigScope.PROJECT, workspace_root)
63
- config_monitor = ConfigMonitor(config_path, on_config_change)
61
+ project_config_path = get_config_path(ConfigScope.PROJECT, str(workspace_root))
62
+ workspace_config_path = get_config_path(ConfigScope.WORKSPACE, str(workspace_root))
63
+
64
+ config_monitors = [
65
+ ConfigMonitor(project_config_path, lambda: on_config_change(str(project_config_path))),
66
+ ConfigMonitor(workspace_config_path, lambda: on_config_change(str(workspace_config_path)))
67
+ ]
64
68
 
65
69
  await project_manager.start_all()
66
70
  git_task = asyncio.create_task(git_monitor.start())
67
- config_task = asyncio.create_task(config_monitor.start())
71
+ config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
68
72
 
69
73
  yield
70
74
  # Shutdown
71
75
  logger.info("Shutting down Monoco Daemon services...")
72
76
  if git_monitor:
73
77
  git_monitor.stop()
74
- if config_monitor:
75
- config_monitor.stop()
78
+ for m in config_monitors:
79
+ m.stop()
76
80
  if project_manager:
77
81
  project_manager.stop_all()
78
- await asyncio.gather(git_task, config_task)
82
+
83
+ await git_task
84
+ await asyncio.gather(*config_tasks)
79
85
 
80
86
  app = FastAPI(
81
87
  title="Monoco Daemon",
@@ -1,8 +1,8 @@
1
1
 
2
2
  import typer
3
- from typing import Optional
3
+ from typing import Optional, Annotated
4
4
  from pathlib import Path
5
- from monoco.core.output import print_output, print_error
5
+ from monoco.core.output import print_output, print_error, AgentOutput, OutputManager
6
6
  from monoco.core.agent.adapters import get_agent_client
7
7
  from monoco.core.agent.state import AgentStateManager
8
8
  from monoco.core.agent.action import ActionRegistry, ActionContext
@@ -12,6 +12,8 @@ import re
12
12
  import json as j
13
13
 
14
14
  app = typer.Typer()
15
+ action_app = typer.Typer(name="action", help="Manage generic agent actions/prompts.")
16
+ app.add_typer(action_app)
15
17
 
16
18
  @app.command(name="run")
17
19
  def run_command(
@@ -19,6 +21,7 @@ def run_command(
19
21
  target: Optional[str] = typer.Argument(None, help="Target file argument for the task"),
20
22
  provider: Optional[str] = typer.Option(None, "--using", "-u", help="Override agent provider"),
21
23
  instruction: Optional[str] = typer.Option(None, "--instruction", "-i", help="Additional instruction for the agent"),
24
+ json: AgentOutput = False,
22
25
  ):
23
26
  """
24
27
  Execute a prompt or a named task using an Agent CLI.
@@ -39,7 +42,8 @@ def run_command(
39
42
 
40
43
  if action:
41
44
  # It IS an action
42
- print(f"Running action: {action.name}")
45
+ if not OutputManager.is_agent_mode():
46
+ print(f"Running action: {action.name}")
43
47
 
44
48
  # Simple template substitution
45
49
  final_prompt = action.template
@@ -72,7 +76,8 @@ def run_command(
72
76
  # 3. State Check
73
77
  state = state_manager.load()
74
78
  if not state or state.is_stale:
75
- print("Agent state stale or missing, refreshing...")
79
+ if not OutputManager.is_agent_mode():
80
+ print("Agent state stale or missing, refreshing...")
76
81
  state = state_manager.refresh()
77
82
 
78
83
  if prov_name not in state.providers:
@@ -87,15 +92,19 @@ def run_command(
87
92
  try:
88
93
  client = get_agent_client(prov_name)
89
94
  result = asyncio.run(client.execute(final_prompt, context_files=context_files))
90
- print(result)
95
+
96
+ if OutputManager.is_agent_mode():
97
+ OutputManager.print({"result": result, "provider": prov_name})
98
+ else:
99
+ print(result)
91
100
 
92
101
  except Exception as e:
93
102
  print_error(f"Execution failed: {e}")
94
103
  raise typer.Exit(1)
95
104
 
96
- @app.command()
97
- def list(
98
- json: bool = typer.Option(False, "--json", help="Output in JSON format"),
105
+ @action_app.command(name="list")
106
+ def action_list(
107
+ json: AgentOutput = False,
99
108
  context: Optional[str] = typer.Option(None, "--context", help="Context for filtering (JSON string)")
100
109
  ):
101
110
  """List available actions."""
@@ -111,29 +120,78 @@ def list(
111
120
  print_error(f"Invalid context JSON: {e}")
112
121
 
113
122
  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")
123
+
124
+ # Filter by State Machine if context implies an Issue
125
+ if action_context and (action_context.status or action_context.stage):
126
+ try:
127
+ from monoco.features.issue.engine.machine import StateMachine
128
+ from monoco.features.issue.models import IssueMetadata
129
+
130
+ cfg = settings.issue
131
+ sm = StateMachine(cfg)
132
+
133
+ # Create a mock metadata for state check
134
+ mock_meta = IssueMetadata(
135
+ id="mock-id",
136
+ title="Mock Issue",
137
+ type=action_context.type or "feature",
138
+ status=action_context.status or "open",
139
+ stage=action_context.stage
140
+ )
141
+
142
+ allowed_transitions = sm.get_available_transitions(mock_meta)
143
+ allowed_names = {t.name for t in allowed_transitions}
144
+
145
+ # Identify which actions are actually transitions
146
+ all_transition_names = {t.name for t in cfg.workflows}
147
+
148
+ filtered_actions = []
149
+ for action in actions:
150
+ # If an action is a formal transition, it must be allowed
151
+ if action.name in all_transition_names:
152
+ if action.name in allowed_names:
153
+ filtered_actions.append(action)
154
+ else:
155
+ # Generic actions are passed through (already filtered by 'when')
156
+ filtered_actions.append(action)
157
+
158
+ actions = filtered_actions
159
+
160
+ except Exception:
161
+ # Fallback if Issue feature is missing or context is invalid
162
+ pass
163
+
164
+ # OutputManager handles list of Pydantic models automatically for both JSON and Table
165
+ print_output(actions, title="Available Actions")
166
+
167
+ @app.command(name="list")
168
+ def list_providers(
169
+ json: AgentOutput = False,
170
+ force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
171
+ ):
172
+ """List available agent providers and their status."""
173
+ # Reuse status logic
174
+ status(json=json, force=force)
118
175
 
119
176
  @app.command()
120
177
  def status(
121
- json: bool = typer.Option(False, "--json", help="Output in JSON format"),
178
+ json: AgentOutput = False,
122
179
  force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
123
180
  ):
124
181
  """View status of Agent Providers."""
125
182
  state_manager = AgentStateManager()
126
183
  state = state_manager.get_or_refresh(force=force)
127
184
 
128
- if json:
129
- import json as j
185
+ if OutputManager.is_agent_mode():
130
186
  # Convert datetime to ISO string for JSON serialization
131
187
  data = state.dict()
132
188
  data["last_checked"] = data["last_checked"].isoformat()
133
- print(j.dumps(data, indent=2))
189
+ OutputManager.print(data)
134
190
  else:
135
191
  # Standard output using existing print_output or custom formatting
136
192
  from monoco.core.output import Table
193
+ from rich import print as rprint
194
+
137
195
  table = Table(title=f"Agent Status (Last Checked: {state.last_checked.strftime('%Y-%m-%d %H:%M:%S')})")
138
196
  table.add_column("Provider")
139
197
  table.add_column("Available")
@@ -147,7 +205,7 @@ def status(
147
205
  p_state.path or "-",
148
206
  p_state.error or "-"
149
207
  )
150
- print_output(table)
208
+ rprint(table)
151
209
 
152
210
  @app.command()
153
211
  def doctor(
@@ -158,3 +216,22 @@ def doctor(
158
216
  """
159
217
  from monoco.features.agent.doctor import doctor as doc_impl
160
218
  doc_impl(force)
219
+
220
+ @app.command()
221
+ def init(
222
+ force: bool = typer.Option(False, "--force", "-f", help="Force overwrite existing resources")
223
+ ):
224
+ """
225
+ Initialize Agent Resources (prompts) in the current project.
226
+ """
227
+ settings = get_config()
228
+ root = Path(settings.paths.root)
229
+
230
+ from monoco.features.agent.core import init_agent_resources
231
+ # TODO: Pass force arg when supported by core
232
+ init_agent_resources(root)
233
+
234
+ if OutputManager.is_agent_mode():
235
+ OutputManager.print({"status": "initialized"})
236
+ else:
237
+ print(f"Agent resources initialized in {root}")
@@ -0,0 +1,48 @@
1
+ import shutil
2
+ import os
3
+ from pathlib import Path
4
+ from typing import List
5
+ try:
6
+ from importlib.resources import files
7
+ except ImportError:
8
+ # Fallback for python < 3.9
9
+ from importlib_resources import files
10
+
11
+ def init_agent_resources(project_root: Path):
12
+ """
13
+ Initialize Agent resources (prompts) in the project workspace.
14
+ """
15
+ actions_dir = project_root / ".monoco" / "actions"
16
+ actions_dir.mkdir(parents=True, exist_ok=True)
17
+
18
+ # Determine language from config, default to 'en'
19
+ # We need to load config to know the language.
20
+ # But this function might be called before full config load?
21
+ # Helper to peek at config or default to 'en'
22
+ from monoco.core.config import get_config
23
+ try:
24
+ settings = get_config(str(project_root), require_project=False) # Best effort
25
+ lang = settings.core.language or "en"
26
+ except Exception:
27
+ lang = "en"
28
+
29
+ # Define source path relative to this module
30
+ try:
31
+ current_dir = Path(__file__).parent
32
+
33
+ # Try specific language, fallback to 'en'
34
+ source_dir = current_dir / "resources" / lang
35
+ if not source_dir.exists():
36
+ source_dir = current_dir / "resources" / "en"
37
+
38
+ if source_dir.exists():
39
+ for item in source_dir.glob("*.prompty"):
40
+ target = actions_dir / item.name
41
+ # Copy if not exists, or overwrite?
42
+ # Ideally init should be safe to run multiple times (idempotent)
43
+ # But user might have customized them.
44
+ if not target.exists():
45
+ shutil.copy2(item, target)
46
+ except Exception as e:
47
+ # Fallback or Log?
48
+ pass