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.
- monoco/core/agent/adapters.py +24 -1
- monoco/core/config.py +77 -17
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/resources/zh/SKILL.md +6 -7
- monoco/core/setup.py +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +5 -5
- monoco/features/issue/commands.py +135 -55
- monoco/features/issue/core.py +157 -122
- monoco/features/issue/domain/__init__.py +0 -0
- monoco/features/issue/domain/lifecycle.py +126 -0
- monoco/features/issue/domain/models.py +170 -0
- monoco/features/issue/domain/parser.py +223 -0
- monoco/features/issue/domain/workspace.py +104 -0
- monoco/features/issue/engine/__init__.py +22 -0
- monoco/features/issue/engine/config.py +172 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +32 -11
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/models.py +26 -9
- monoco/features/issue/resources/zh/SKILL.md +8 -9
- monoco/features/issue/validator.py +181 -65
- monoco/features/spike/core.py +5 -22
- monoco/features/spike/resources/zh/SKILL.md +2 -2
- monoco/main.py +2 -26
- monoco_toolkit-0.2.7.dist-info/METADATA +129 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/RECORD +33 -27
- monoco/features/agent/commands.py +0 -166
- monoco/features/agent/doctor.py +0 -30
- monoco/features/pty/core.py +0 -185
- monoco/features/pty/router.py +0 -138
- monoco/features/pty/server.py +0 -56
- monoco_toolkit-0.2.5.dist-info/METADATA +0 -93
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.7.dist-info}/licenses/LICENSE +0 -0
monoco/core/agent/adapters.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
242
|
+
|
|
243
|
+
# Check if we are inside a .monoco folder
|
|
185
244
|
if current.name == ".monoco":
|
|
186
245
|
return current.parent
|
|
187
246
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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]:
|
monoco/core/integrations.py
CHANGED
|
@@ -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=
|
|
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:
|
|
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[
|
|
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 =
|
|
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.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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[
|
|
81
|
-
stage: Optional[
|
|
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=
|
|
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=
|
|
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,
|
|
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,
|
|
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=
|
|
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[
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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[
|
|
462
|
-
stage: Optional[
|
|
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
|
|
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
|
|
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
|
|
539
|
-
f"[{s_color}]{i.status
|
|
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 ==
|
|
609
|
-
stories = [i for i in issues if i.type ==
|
|
610
|
-
tasks = [i for i in issues if i.type in [
|
|
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 = {
|
|
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
|
-
#
|
|
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]))
|