monoco-toolkit 0.2.5__py3-none-any.whl → 0.2.7__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 (39) hide show
  1. monoco/core/agent/adapters.py +24 -1
  2. monoco/core/config.py +77 -17
  3. monoco/core/integrations.py +8 -0
  4. monoco/core/lsp.py +7 -0
  5. monoco/core/output.py +8 -1
  6. monoco/core/resources/zh/SKILL.md +6 -7
  7. monoco/core/setup.py +8 -0
  8. monoco/features/i18n/resources/zh/SKILL.md +5 -5
  9. monoco/features/issue/commands.py +135 -55
  10. monoco/features/issue/core.py +157 -122
  11. monoco/features/issue/domain/__init__.py +0 -0
  12. monoco/features/issue/domain/lifecycle.py +126 -0
  13. monoco/features/issue/domain/models.py +170 -0
  14. monoco/features/issue/domain/parser.py +223 -0
  15. monoco/features/issue/domain/workspace.py +104 -0
  16. monoco/features/issue/engine/__init__.py +22 -0
  17. monoco/features/issue/engine/config.py +172 -0
  18. monoco/features/issue/engine/machine.py +185 -0
  19. monoco/features/issue/engine/models.py +18 -0
  20. monoco/features/issue/linter.py +32 -11
  21. monoco/features/issue/lsp/__init__.py +3 -0
  22. monoco/features/issue/lsp/definition.py +72 -0
  23. monoco/features/issue/models.py +26 -9
  24. monoco/features/issue/resources/zh/SKILL.md +8 -9
  25. monoco/features/issue/validator.py +181 -65
  26. monoco/features/spike/core.py +5 -22
  27. monoco/features/spike/resources/zh/SKILL.md +2 -2
  28. monoco/main.py +2 -26
  29. monoco_toolkit-0.2.7.dist-info/METADATA +129 -0
  30. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/RECORD +33 -27
  31. monoco/features/agent/commands.py +0 -166
  32. monoco/features/agent/doctor.py +0 -30
  33. monoco/features/pty/core.py +0 -185
  34. monoco/features/pty/router.py +0 -138
  35. monoco/features/pty/server.py +0 -56
  36. monoco_toolkit-0.2.5.dist-info/METADATA +0 -93
  37. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/WHEEL +0 -0
  38. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/entry_points.txt +0 -0
  39. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -10,7 +10,7 @@ import asyncio
10
10
  from watchdog.observers import Observer
11
11
  from watchdog.events import FileSystemEventHandler
12
12
 
13
- from monoco.core.integrations import AgentIntegration
13
+
14
14
 
15
15
  logger = logging.getLogger("monoco.core.config")
16
16
 
@@ -46,15 +46,68 @@ class TelemetryConfig(BaseModel):
46
46
  """Configuration for Telemetry."""
47
47
  enabled: Optional[bool] = Field(default=None, description="Whether telemetry is enabled")
48
48
 
49
- class AgentConfig(BaseModel):
50
- """Configuration for Agent Environment Integration."""
51
- targets: Optional[list[str]] = Field(default=None, description="Specific target files to inject into (e.g. .cursorrules)")
52
- framework: Optional[str] = Field(default=None, description="Manually specified agent framework (cursor, windsurf, etc.)")
53
- includes: Optional[list[str]] = Field(default=None, description="List of specific features to include in injection")
54
- integrations: Optional[Dict[str, "AgentIntegration"]] = Field(
55
- default=None,
56
- description="Custom agent framework integrations (overrides defaults from monoco.core.integrations)"
57
- )
49
+
50
+
51
+ class IssueTypeConfig(BaseModel):
52
+ name: str
53
+ label: str
54
+ prefix: str
55
+ folder: str
56
+ description: Optional[str] = None
57
+
58
+ class TransitionConfig(BaseModel):
59
+ name: str
60
+ label: str
61
+ icon: Optional[str] = None
62
+ from_status: Optional[str] = None
63
+ from_stage: Optional[str] = None
64
+ to_status: str
65
+ to_stage: Optional[str] = None
66
+ required_solution: Optional[str] = None
67
+ description: str = ""
68
+ command_template: Optional[str] = None
69
+
70
+ class IssueSchemaConfig(BaseModel):
71
+ types: List[IssueTypeConfig] = Field(default_factory=list)
72
+ statuses: List[str] = Field(default_factory=list)
73
+ stages: List[str] = Field(default_factory=list)
74
+ solutions: List[str] = Field(default_factory=list)
75
+ workflows: List[TransitionConfig] = Field(default_factory=list)
76
+
77
+ def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
78
+ if not other:
79
+ return self
80
+
81
+ # Types: merge by name
82
+ if other.types:
83
+ type_map = {t.name: t for t in self.types}
84
+ for ot in other.types:
85
+ type_map[ot.name] = ot
86
+ self.types = list(type_map.values())
87
+
88
+ # Statuses: replace if provided
89
+ if other.statuses:
90
+ self.statuses = other.statuses
91
+
92
+ # Stages: replace if provided
93
+ if other.stages:
94
+ self.stages = other.stages
95
+
96
+ # Solutions: replace if provided
97
+ if other.solutions:
98
+ self.solutions = other.solutions
99
+
100
+ # Workflows (Transitions): merge by name
101
+ if other.workflows:
102
+ wf_map = {w.name: w for w in self.workflows}
103
+ for ow in other.workflows:
104
+ wf_map[ow.name] = ow
105
+ self.workflows = list(wf_map.values())
106
+
107
+ return self
108
+
109
+ class StateMachineConfig(BaseModel):
110
+ transitions: List[TransitionConfig]
58
111
 
59
112
  class MonocoConfig(BaseModel):
60
113
  """
@@ -67,7 +120,8 @@ class MonocoConfig(BaseModel):
67
120
  i18n: I18nConfig = Field(default_factory=I18nConfig)
68
121
  ui: UIConfig = Field(default_factory=UIConfig)
69
122
  telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
70
- agent: AgentConfig = Field(default_factory=AgentConfig)
123
+
124
+ issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
71
125
 
72
126
  @staticmethod
73
127
  def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
@@ -179,15 +233,21 @@ def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> P
179
233
  return cwd / ".monoco" / "project.yaml"
180
234
 
181
235
  def find_monoco_root(start_path: Optional[Path] = None) -> Path:
182
- """Recursively find the .monoco directory upwards."""
236
+ """
237
+ Find the Monoco Workspace root.
238
+ Strictly restricted to checking the current directory (or its parent if CWD is .monoco).
239
+ Recursive upward lookup is disabled per FIX-0009.
240
+ """
183
241
  current = (start_path or Path.cwd()).resolve()
184
- # Check if we are inside a .monoco folder (unlikely but possible)
242
+
243
+ # Check if we are inside a .monoco folder
185
244
  if current.name == ".monoco":
186
245
  return current.parent
187
246
 
188
- for parent in [current] + list(current.parents):
189
- if (parent / ".monoco").exists():
190
- return parent
247
+ # Check if current directory has .monoco
248
+ if (current / ".monoco").exists():
249
+ return current
250
+
191
251
  return current
192
252
 
193
253
  def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> Dict[str, Any]:
@@ -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
 
@@ -9,11 +9,11 @@ Monoco Toolkit 的核心功能和命令。
9
9
 
10
10
  ## 概述
11
11
 
12
- Monoco 是一个开发者生产力工具包,提供:
12
+ Monoco 是一个开发者生产力工具包,提供:
13
13
 
14
- - **项目初始化**:标准化的项目结构
15
- - **配置管理**:全局和项目级别的配置
16
- - **工作空间管理**:多项目设置
14
+ - **项目初始化**: 标准化的项目结构
15
+ - **配置管理**: 全局和项目级别的配置
16
+ - **工作空间管理**: 多项目设置
17
17
 
18
18
  ## 核心命令
19
19
 
@@ -34,7 +34,6 @@ Monoco 是一个开发者生产力工具包,提供:
34
34
  ### Agent 集成
35
35
 
36
36
  - **`monoco sync`**: 与 agent 环境同步
37
-
38
37
  - 将系统提示注入到 agent 配置文件(GEMINI.md, CLAUDE.md 等)
39
38
  - 分发 skills 到 agent 框架目录
40
39
  - 遵循 `i18n.source_lang` 的语言配置
@@ -45,12 +44,12 @@ Monoco 是一个开发者生产力工具包,提供:
45
44
 
46
45
  ## 配置结构
47
46
 
48
- 配置以 YAML 格式存储在:
47
+ 配置以 YAML 格式存储在:
49
48
 
50
49
  - **全局**: `~/.monoco/config.yaml`
51
50
  - **项目**: `.monoco/config.yaml`
52
51
 
53
- 关键配置段:
52
+ 关键配置段:
54
53
 
55
54
  - `core`: 编辑器、日志级别、作者
56
55
  - `paths`: 目录路径(issues, spikes, specs)
monoco/core/setup.py CHANGED
@@ -258,6 +258,14 @@ def init_cli(
258
258
  (cwd / "Issues").mkdir(exist_ok=True)
259
259
  (cwd / ".references").mkdir(exist_ok=True)
260
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
+
261
269
  console.print("\n[bold green]✓ Monoco Project Initialized![/bold green]")
262
270
  console.print(f"Access configured! issues will be created as [bold]{project_key}-XXX[/bold]")
263
271
 
@@ -9,7 +9,7 @@ description: 文档国际化质量控制。确保多语言文档保持同步。
9
9
 
10
10
  ## 概述
11
11
 
12
- I18n 功能提供:
12
+ I18n 功能提供:
13
13
 
14
14
  - **自动扫描**缺失的翻译
15
15
  - **标准化结构**用于多语言文档
@@ -33,7 +33,7 @@ monoco i18n scan
33
33
 
34
34
  ## 配置
35
35
 
36
- I18n 设置在 `.monoco/config.yaml` 中配置:
36
+ I18n 设置在 `.monoco/config.yaml` 中配置:
37
37
 
38
38
  ```yaml
39
39
  i18n:
@@ -47,7 +47,7 @@ i18n:
47
47
 
48
48
  ### 根文件(后缀模式)
49
49
 
50
- 对于项目根目录中的文件:
50
+ 对于项目根目录中的文件:
51
51
 
52
52
  - 源文件: `README.md`
53
53
  - 中文: `README_ZH.md`
@@ -55,7 +55,7 @@ i18n:
55
55
 
56
56
  ### 子目录文件(目录模式)
57
57
 
58
- 对于 `docs/` 或其他目录中的文件:
58
+ 对于 `docs/` 或其他目录中的文件:
59
59
 
60
60
  ```
61
61
  docs/
@@ -72,7 +72,7 @@ docs/
72
72
 
73
73
  ## 排除规则
74
74
 
75
- 以下内容会自动从 i18n 扫描中排除:
75
+ 以下内容会自动从 i18n 扫描中排除:
76
76
 
77
77
  - `.gitignore` 模式(自动遵循)
78
78
  - `.references/` 目录
@@ -14,16 +14,18 @@ from . import core
14
14
 
15
15
  app = typer.Typer(help="Agent-Native Issue Management.")
16
16
  backlog_app = typer.Typer(help="Manage backlog operations.")
17
+ lsp_app = typer.Typer(help="LSP Server commands.")
17
18
  app.add_typer(backlog_app, name="backlog")
19
+ app.add_typer(lsp_app, name="lsp")
18
20
  console = Console()
19
21
 
20
22
  @app.command("create")
21
23
  def create(
22
- type: IssueType = typer.Argument(..., help="Issue type (epic, feature, chore, fix)"),
24
+ type: str = typer.Argument(..., help="Issue type (epic, feature, chore, fix, etc.)"),
23
25
  title: str = typer.Option(..., "--title", "-t", help="Issue title"),
24
26
  parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
25
27
  is_backlog: bool = typer.Option(False, "--backlog", help="Create as backlog item"),
26
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Issue stage (draft, doing, review)"),
28
+ stage: Optional[str] = typer.Option(None, "--stage", help="Issue stage"),
27
29
  dependencies: List[str] = typer.Option([], "--dependency", "-d", help="Issue dependency ID(s)"),
28
30
  related: List[str] = typer.Option([], "--related", "-r", help="Related Issue ID(s)"),
29
31
  subdir: Optional[str] = typer.Option(None, "--subdir", "-s", help="Subdirectory for organization (e.g. 'Backend/Auth')"),
@@ -32,10 +34,11 @@ def create(
32
34
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
33
35
  json: AgentOutput = False,
34
36
  ):
37
+ """Create a new issue."""
35
38
  """Create a new issue."""
36
39
  config = get_config()
37
40
  issues_root = _resolve_issues_root(config, root)
38
- status = IssueStatus.BACKLOG if is_backlog else IssueStatus.OPEN
41
+ status = "backlog" if is_backlog else "open"
39
42
 
40
43
  if parent:
41
44
  parent_path = core.find_issue_path(issues_root, parent)
@@ -63,11 +66,15 @@ def create(
63
66
  except ValueError:
64
67
  rel_path = path
65
68
 
66
- OutputManager.print({
67
- "issue": issue,
68
- "path": str(rel_path),
69
- "status": "created"
70
- })
69
+ if OutputManager.is_agent_mode():
70
+ OutputManager.print({
71
+ "issue": issue,
72
+ "path": str(rel_path),
73
+ "status": "created"
74
+ })
75
+ else:
76
+ console.print(f"[green]✔ Created {issue.id} in status {issue.status}.[/green]")
77
+ console.print(f"Path: {rel_path}")
71
78
 
72
79
  except ValueError as e:
73
80
  OutputManager.error(str(e))
@@ -77,8 +84,8 @@ def create(
77
84
  def update(
78
85
  issue_id: str = typer.Argument(..., help="Issue ID to update"),
79
86
  title: Optional[str] = typer.Option(None, "--title", "-t", help="New title"),
80
- status: Optional[IssueStatus] = typer.Option(None, "--status", help="New status"),
81
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="New stage"),
87
+ status: Optional[str] = typer.Option(None, "--status", help="New status"),
88
+ stage: Optional[str] = typer.Option(None, "--stage", help="New stage"),
82
89
  parent: Optional[str] = typer.Option(None, "--parent", "-p", help="Parent Issue ID"),
83
90
  sprint: Optional[str] = typer.Option(None, "--sprint", help="Sprint ID"),
84
91
  dependencies: Optional[List[str]] = typer.Option(None, "--dependency", "-d", help="Issue dependency ID(s)"),
@@ -125,7 +132,7 @@ def move_open(
125
132
  issues_root = _resolve_issues_root(config, root)
126
133
  try:
127
134
  # Pull operation: Force stage to TODO
128
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
135
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
129
136
  OutputManager.print({
130
137
  "issue": issue,
131
138
  "status": "opened"
@@ -153,13 +160,13 @@ def start(
153
160
 
154
161
  try:
155
162
  # Implicitly ensure status is Open
156
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DOING)
163
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="doing")
157
164
 
158
165
  isolation_info = None
159
166
 
160
167
  if branch:
161
168
  try:
162
- issue = core.start_issue_isolation(issues_root, issue_id, IsolationType.BRANCH, project_root)
169
+ issue = core.start_issue_isolation(issues_root, issue_id, "branch", project_root)
163
170
  isolation_info = {"type": "branch", "ref": issue.isolation.ref}
164
171
  except Exception as e:
165
172
  OutputManager.error(f"Failed to create branch: {e}")
@@ -167,7 +174,7 @@ def start(
167
174
 
168
175
  if worktree:
169
176
  try:
170
- issue = core.start_issue_isolation(issues_root, issue_id, IsolationType.WORKTREE, project_root)
177
+ issue = core.start_issue_isolation(issues_root, issue_id, "worktree", project_root)
171
178
  isolation_info = {"type": "worktree", "path": issue.isolation.path, "ref": issue.isolation.ref}
172
179
  except Exception as e:
173
180
  OutputManager.error(f"Failed to create worktree: {e}")
@@ -196,7 +203,7 @@ def submit(
196
203
  project_root = _resolve_project_root(config)
197
204
  try:
198
205
  # Implicitly ensure status is Open
199
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.REVIEW)
206
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="review")
200
207
 
201
208
  # Delivery Report Generation
202
209
  report_status = "skipped"
@@ -228,7 +235,7 @@ def submit(
228
235
  @app.command("close")
229
236
  def move_close(
230
237
  issue_id: str = typer.Argument(..., help="Issue ID to close"),
231
- solution: Optional[IssueSolution] = typer.Option(None, "--solution", "-s", help="Solution type"),
238
+ solution: Optional[str] = typer.Option(None, "--solution", "-s", help="Solution type"),
232
239
  prune: bool = typer.Option(False, "--prune", help="Delete branch/worktree after close"),
233
240
  force: bool = typer.Option(False, "--force", help="Force delete branch/worktree"),
234
241
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
@@ -241,12 +248,15 @@ def move_close(
241
248
 
242
249
  # Pre-flight check for interactive guidance (Requirement FEAT-0082 #6)
243
250
  if solution is None:
244
- valid_solutions = [e.value for e in IssueSolution]
251
+ # Resolve options from engine
252
+ from .engine import get_engine
253
+ engine = get_engine(str(issues_root.parent))
254
+ valid_solutions = engine.issue_config.solutions or []
245
255
  OutputManager.error(f"Closing an issue requires a solution. Options: {', '.join(valid_solutions)}")
246
256
  raise typer.Exit(code=1)
247
257
 
248
258
  try:
249
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=solution)
259
+ issue = core.update_issue(issues_root, issue_id, status="closed", solution=solution)
250
260
 
251
261
  pruned_resources = []
252
262
  if prune:
@@ -276,7 +286,7 @@ def push(
276
286
  config = get_config()
277
287
  issues_root = _resolve_issues_root(config, root)
278
288
  try:
279
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.BACKLOG)
289
+ issue = core.update_issue(issues_root, issue_id, status="backlog")
280
290
  OutputManager.print({
281
291
  "issue": issue,
282
292
  "status": "pushed_to_backlog"
@@ -295,7 +305,7 @@ def pull(
295
305
  config = get_config()
296
306
  issues_root = _resolve_issues_root(config, root)
297
307
  try:
298
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.OPEN, stage=IssueStage.DRAFT)
308
+ issue = core.update_issue(issues_root, issue_id, status="open", stage="draft")
299
309
  OutputManager.print({
300
310
  "issue": issue,
301
311
  "status": "pulled_from_backlog"
@@ -314,7 +324,7 @@ def cancel(
314
324
  config = get_config()
315
325
  issues_root = _resolve_issues_root(config, root)
316
326
  try:
317
- issue = core.update_issue(issues_root, issue_id, status=IssueStatus.CLOSED, solution=IssueSolution.CANCELLED)
327
+ issue = core.update_issue(issues_root, issue_id, status="closed", solution="cancelled")
318
328
  OutputManager.print({
319
329
  "issue": issue,
320
330
  "status": "cancelled"
@@ -427,10 +437,10 @@ def board(
427
437
  issue_list = []
428
438
  for issue in sorted(issues, key=lambda x: x.updated_at, reverse=True):
429
439
  type_color = {
430
- IssueType.FEATURE: "green",
431
- IssueType.CHORE: "blue",
432
- IssueType.FIX: "red",
433
- IssueType.EPIC: "magenta"
440
+ "feature": "green",
441
+ "chore": "blue",
442
+ "fix": "red",
443
+ "epic": "magenta"
434
444
  }.get(issue.type, "white")
435
445
 
436
446
  issue_list.append(
@@ -458,8 +468,8 @@ def board(
458
468
  @app.command("list")
459
469
  def list_cmd(
460
470
  status: Optional[str] = typer.Option(None, "--status", "-s", help="Filter by status (open, closed, backlog, all)"),
461
- type: Optional[IssueType] = typer.Option(None, "--type", "-t", help="Filter by type"),
462
- stage: Optional[IssueStage] = typer.Option(None, "--stage", help="Filter by stage"),
471
+ type: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type"),
472
+ stage: Optional[str] = typer.Option(None, "--stage", help="Filter by stage"),
463
473
  root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
464
474
  workspace: bool = typer.Option(False, "--workspace", "-w", help="Include issues from workspace members"),
465
475
  json: AgentOutput = False,
@@ -481,7 +491,7 @@ def list_cmd(
481
491
  for i in issues:
482
492
  # Status Filter
483
493
  if target_status != "all":
484
- if i.status.value != target_status:
494
+ if i.status != target_status:
485
495
  continue
486
496
 
487
497
  # Type Filter
@@ -530,13 +540,13 @@ def _render_issues_table(issues: List[IssueMetadata], title: str = "Issues"):
530
540
  t_color = type_colors.get(i.type, "white")
531
541
  s_color = status_colors.get(i.status, "white")
532
542
 
533
- stage_str = i.stage.value if i.stage else "-"
543
+ stage_str = i.stage if i.stage else "-"
534
544
  updated_str = i.updated_at.strftime("%Y-%m-%d %H:%M")
535
545
 
536
546
  table.add_row(
537
547
  i.id,
538
- f"[{t_color}]{i.type.value}[/{t_color}]",
539
- f"[{s_color}]{i.status.value}[/{s_color}]",
548
+ f"[{t_color}]{i.type}[/{t_color}]",
549
+ f"[{s_color}]{i.status}[/{s_color}]",
540
550
  stage_str,
541
551
  i.title,
542
552
  updated_str
@@ -605,11 +615,11 @@ def scope(
605
615
  return
606
616
 
607
617
  tree = Tree(f"[bold blue]Monoco Issue Scope[/bold blue]")
608
- epics = sorted([i for i in issues if i.type == IssueType.EPIC], key=lambda x: x.id)
609
- stories = [i for i in issues if i.type == IssueType.FEATURE]
610
- tasks = [i for i in issues if i.type in [IssueType.CHORE, IssueType.FIX]]
618
+ epics = sorted([i for i in issues if i.type == "epic"], key=lambda x: x.id)
619
+ stories = [i for i in issues if i.type == "feature"]
620
+ tasks = [i for i in issues if i.type in ["chore", "fix"]]
611
621
 
612
- status_map = {IssueStatus.OPEN: "[blue]●[/blue]", IssueStatus.CLOSED: "[green]✔[/green]", IssueStatus.BACKLOG: "[dim]💤[/dim]"}
622
+ status_map = {"open": "[blue]●[/blue]", "closed": "[green]✔[/green]", "backlog": "[dim]💤[/dim]"}
613
623
 
614
624
  for epic in epics:
615
625
  epic_node = tree.add(f"{status_map[epic.status]} [bold]{epic.id}[/bold]: {epic.title}")
@@ -622,6 +632,57 @@ def scope(
622
632
 
623
633
  console.print(Panel(tree, expand=False))
624
634
 
635
+ @app.command("inspect")
636
+ def inspect(
637
+ target: str = typer.Argument(..., help="Issue ID or File Path"),
638
+ root: Optional[str] = typer.Option(None, "--root", help="Override issues root directory"),
639
+ ast: bool = typer.Option(False, "--ast", help="Output JSON AST structure for debugging"),
640
+ json: AgentOutput = False,
641
+ ):
642
+ """
643
+ Inspect a specific issue and return its metadata (including actions).
644
+ """
645
+ config = get_config()
646
+ issues_root = _resolve_issues_root(config, root)
647
+
648
+ # Try as Path
649
+ target_path = Path(target)
650
+ if target_path.exists() and target_path.is_file():
651
+ path = target_path
652
+ else:
653
+ # Try as ID
654
+ # Search path logic is needed? Or core.find_issue_path
655
+ path = core.find_issue_path(issues_root, target)
656
+ if not path:
657
+ OutputManager.error(f"Issue or file {target} not found.")
658
+ raise typer.Exit(code=1)
659
+
660
+ # AST Debug Mode
661
+ if ast:
662
+ from .domain.parser import MarkdownParser
663
+ content = path.read_text()
664
+ try:
665
+ domain_issue = MarkdownParser.parse(content, path=str(path))
666
+ print(domain_issue.model_dump_json(indent=2))
667
+ except Exception as e:
668
+ OutputManager.error(f"Failed to parse AST: {e}")
669
+ raise typer.Exit(code=1)
670
+ return
671
+
672
+ # Normal Mode
673
+ meta = core.parse_issue(path)
674
+
675
+ if not meta:
676
+ OutputManager.error(f"Could not parse issue {target}.")
677
+ raise typer.Exit(code=1)
678
+
679
+ # In JSON mode (AgentOutput), we might want to return rich data
680
+ if OutputManager.is_agent_mode():
681
+ OutputManager.print(meta)
682
+ else:
683
+ # For human, print yaml-like or table
684
+ console.print(meta)
685
+
625
686
  @app.command("lint")
626
687
  def lint(
627
688
  recursive: bool = typer.Option(False, "--recursive", "-r", help="Recursively scan subdirectories"),
@@ -662,27 +723,9 @@ def _resolve_issues_root(config, cli_root: Optional[str]) -> Path:
662
723
  return path
663
724
 
664
725
  # 2. Handle Default / Contextual Execution (No --root)
665
- # We need to detect if we are in a Workspace Root with multiple projects
726
+ # Strict Workspace Check: If not in a project root, we rely on the config root.
727
+ # (The global app callback already enforces presence of .monoco for most commands)
666
728
  cwd = Path.cwd()
667
-
668
- # If CWD is NOT a project root (no .monoco/), scan for subprojects
669
- if not is_project_root(cwd):
670
- subprojects = find_projects(cwd)
671
- if len(subprojects) > 1:
672
- console.print(f"[yellow]Workspace detected with {len(subprojects)} projects:[/yellow]")
673
- for p in subprojects:
674
- console.print(f" - [bold]{p.name}[/bold]")
675
- console.print("\n[yellow]Please specify a project using --root <PATH>.[/yellow]")
676
- # We don't exit here strictly, but usually this means we can't find 'Issues' in CWD anyway
677
- # so the config fallbacks below will likely fail or point to non-existent CWD/Issues.
678
- # But let's fail fast to be helpful.
679
- raise typer.Exit(code=1)
680
- elif len(subprojects) == 1:
681
- # Auto-select the only child project?
682
- # It's safer to require explicit intent, but let's try to be helpful if it's obvious.
683
- # However, standard behavior is usually "operate on current dir".
684
- # Let's stick to standard config resolution, but maybe warn.
685
- pass
686
729
 
687
730
  # 3. Config Fallback
688
731
  config_issues_path = Path(config.paths.issues)
@@ -826,3 +869,40 @@ def commit(
826
869
  except Exception as e:
827
870
  console.print(f"[red]Git Error:[/red] {e}")
828
871
  raise typer.Exit(code=1)
872
+
873
+ @lsp_app.command("definition")
874
+ def lsp_definition(
875
+ file: str = typer.Option(..., "--file", "-f", help="Abs path to file"),
876
+ line: int = typer.Option(..., "--line", "-l", help="0-indexed line number"),
877
+ character: int = typer.Option(..., "--char", "-c", help="0-indexed character number"),
878
+ ):
879
+ """
880
+ Handle textDocument/definition request.
881
+ Output: JSON Location | null
882
+ """
883
+ import json
884
+ from monoco.core.lsp import Position
885
+ from monoco.features.issue.lsp import DefinitionProvider
886
+
887
+ config = get_config()
888
+ # Workspace Root resolution is key here.
889
+ # If we are in a workspace, we want the workspace root, not just issue root.
890
+ # _resolve_project_root returns the closest project root or monoco root.
891
+ workspace_root = _resolve_project_root(config)
892
+ # Search for topmost workspace root to enable cross-project navigation
893
+ current_best = workspace_root
894
+ for parent in [workspace_root] + list(workspace_root.parents):
895
+ if (parent / ".monoco" / "workspace.yaml").exists() or (parent / ".monoco" / "project.yaml").exists():
896
+ current_best = parent
897
+ workspace_root = current_best
898
+
899
+ provider = DefinitionProvider(workspace_root)
900
+ file_path = Path(file)
901
+
902
+ locations = provider.provide_definition(
903
+ file_path,
904
+ Position(line=line, character=character)
905
+ )
906
+
907
+ # helper to serialize
908
+ print(json.dumps([l.model_dump(mode='json') for l in locations]))