monoco-toolkit 0.1.1__py3-none-any.whl → 0.2.5__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/cli/__init__.py +0 -0
- monoco/cli/project.py +87 -0
- monoco/cli/workspace.py +46 -0
- monoco/core/agent/__init__.py +5 -0
- monoco/core/agent/action.py +144 -0
- monoco/core/agent/adapters.py +106 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +106 -0
- monoco/core/config.py +152 -17
- monoco/core/execution.py +62 -0
- monoco/core/feature.py +58 -0
- monoco/core/git.py +51 -2
- monoco/core/injection.py +196 -0
- monoco/core/integrations.py +234 -0
- monoco/core/lsp.py +61 -0
- monoco/core/output.py +13 -2
- monoco/core/registry.py +36 -0
- monoco/core/resources/en/AGENTS.md +8 -0
- monoco/core/resources/en/SKILL.md +66 -0
- monoco/core/resources/zh/AGENTS.md +8 -0
- monoco/core/resources/zh/SKILL.md +66 -0
- monoco/core/setup.py +88 -110
- monoco/core/skills.py +444 -0
- monoco/core/state.py +53 -0
- monoco/core/sync.py +224 -0
- monoco/core/telemetry.py +4 -1
- monoco/core/workspace.py +85 -20
- monoco/daemon/app.py +127 -58
- monoco/daemon/models.py +4 -0
- monoco/daemon/services.py +56 -155
- monoco/features/agent/commands.py +166 -0
- monoco/features/agent/doctor.py +30 -0
- monoco/features/config/commands.py +125 -44
- monoco/features/i18n/adapter.py +29 -0
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +113 -27
- monoco/features/i18n/resources/en/AGENTS.md +8 -0
- monoco/features/i18n/resources/en/SKILL.md +94 -0
- monoco/features/i18n/resources/zh/AGENTS.md +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +94 -0
- monoco/features/issue/adapter.py +34 -0
- monoco/features/issue/commands.py +183 -65
- monoco/features/issue/core.py +172 -77
- monoco/features/issue/linter.py +215 -116
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +23 -19
- monoco/features/issue/monitor.py +94 -0
- monoco/features/issue/resources/en/AGENTS.md +15 -0
- monoco/features/issue/resources/en/SKILL.md +87 -0
- monoco/features/issue/resources/zh/AGENTS.md +15 -0
- monoco/features/issue/resources/zh/SKILL.md +114 -0
- monoco/features/issue/validator.py +269 -0
- monoco/features/pty/core.py +185 -0
- monoco/features/pty/router.py +138 -0
- monoco/features/pty/server.py +56 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +4 -21
- monoco/features/spike/resources/en/AGENTS.md +7 -0
- monoco/features/spike/resources/en/SKILL.md +74 -0
- monoco/features/spike/resources/zh/AGENTS.md +7 -0
- monoco/features/spike/resources/zh/SKILL.md +74 -0
- monoco/main.py +115 -2
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +2 -2
- monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
- monoco_toolkit-0.1.1.dist-info/RECORD +0 -33
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.1.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Issue Management (Agent Guidance)
|
|
2
|
+
|
|
3
|
+
## Issue Management
|
|
4
|
+
|
|
5
|
+
System for managing tasks using `monoco issue`.
|
|
6
|
+
|
|
7
|
+
- **Create**: `monoco issue create <type> -t "Title"` (types: epic, feature, chore, fix)
|
|
8
|
+
- **Status**: `monoco issue open|close|backlog <id>`
|
|
9
|
+
- **Check**: `monoco issue lint` (Must run after manual edits)
|
|
10
|
+
- **Lifecycle**: `monoco issue start|submit|delete <id>`
|
|
11
|
+
- **Structure**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (e.g. `Issues/Features/open/`). Do not deviate.
|
|
12
|
+
- **Rules**:
|
|
13
|
+
1. **Heading**: Must have `## {ID}: {Title}` (matches metadata).
|
|
14
|
+
2. **Checkboxes**: Min 2 using `- [ ]`, `- [x]`, `- [-]`, `- [/]`.
|
|
15
|
+
3. **Review**: `## Review Comments` section required for Review/Done stages.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monoco-issue
|
|
3
|
+
description: Official skill for Monoco Issue System. Treats Issues as Universal Atoms, managing the lifecycle of Epic/Feature/Chore/Fix.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Issue Management
|
|
7
|
+
|
|
8
|
+
Use this skill to create and manage **Issues** (Universal Atoms) in Monoco projects.
|
|
9
|
+
|
|
10
|
+
## Core Ontology
|
|
11
|
+
|
|
12
|
+
### 1. Strategy Layer
|
|
13
|
+
|
|
14
|
+
- **🏆 EPIC**: Grand goals, vision containers. Mindset: Architect.
|
|
15
|
+
|
|
16
|
+
### 2. Value Layer
|
|
17
|
+
|
|
18
|
+
- **✨ FEATURE**: Value increments from user perspective. Mindset: Product Owner.
|
|
19
|
+
- **Atomicity Principle**: Feature = Design + Dev + Test + Doc + i18n. They are one.
|
|
20
|
+
|
|
21
|
+
### 3. Execution Layer
|
|
22
|
+
|
|
23
|
+
- **🧹 CHORE**: Engineering maintenance, no direct user value. Mindset: Builder.
|
|
24
|
+
- **🐞 FIX**: Correcting deviations. Mindset: Debugger.
|
|
25
|
+
|
|
26
|
+
## Guidelines
|
|
27
|
+
|
|
28
|
+
### Directory Structure & Naming
|
|
29
|
+
|
|
30
|
+
`Issues/{CapitalizedPluralType}/{lowercase_status}/`
|
|
31
|
+
|
|
32
|
+
- **Types**: `Epics`, `Features`, `Chores`, `Fixes`
|
|
33
|
+
- **Statuses**: `open`, `backlog`, `closed`
|
|
34
|
+
|
|
35
|
+
### Structural Integrity
|
|
36
|
+
|
|
37
|
+
Issues are validated via `monoco issue lint`. key constraints:
|
|
38
|
+
|
|
39
|
+
1. **Mandatory Heading**: `## {ID}: {Title}` must match front matter.
|
|
40
|
+
2. **Min Checkboxes**: At least 2 checkboxes (AC/Tasks).
|
|
41
|
+
3. **Review Protocol**: `## Review Comments` required for `review` or `done` stages.
|
|
42
|
+
|
|
43
|
+
### Path Transitions
|
|
44
|
+
|
|
45
|
+
Use `monoco issue`:
|
|
46
|
+
|
|
47
|
+
1. **Create**: `monoco issue create <type> --title "..."`
|
|
48
|
+
|
|
49
|
+
- Params: `--parent <id>`, `--dependency <id>`, `--related <id>`, `--sprint <id>`, `--tags <tag>`
|
|
50
|
+
|
|
51
|
+
2. **Transition**: `monoco issue open/close/backlog <id>`
|
|
52
|
+
|
|
53
|
+
3. **View**: `monoco issue scope`
|
|
54
|
+
|
|
55
|
+
4. **Validation**: `monoco issue lint`
|
|
56
|
+
|
|
57
|
+
5. **Modification**: `monoco issue start/submit/delete <id>`
|
|
58
|
+
|
|
59
|
+
6. **Commit**: `monoco issue commit` (Atomic commit for issue files)
|
|
60
|
+
7. **Validation**: `monoco issue lint` (Enforces compliance)
|
|
61
|
+
|
|
62
|
+
## Validation Rules (FEAT-0082)
|
|
63
|
+
|
|
64
|
+
To ensure data integrity, all Issue tickets must follow these strict rules:
|
|
65
|
+
|
|
66
|
+
### 1. Structural Consistency
|
|
67
|
+
|
|
68
|
+
- Must contain a Level 2 Heading matching exactly: `## {ID}: {Title}`.
|
|
69
|
+
- Example: `## FEAT-0082: Issue Ticket Validator`
|
|
70
|
+
|
|
71
|
+
### 2. Content Completeness
|
|
72
|
+
|
|
73
|
+
- **Checkboxes**: Minimum of 2 checkboxes required (one for AC, one for Tasks).
|
|
74
|
+
- **Review Comments**: If `stage` is `review` or `done`, a `## Review Comments` section is mandatory and must not be empty.
|
|
75
|
+
|
|
76
|
+
### 3. Checkbox Syntax & Hierarchy
|
|
77
|
+
|
|
78
|
+
- Use only `- [ ]`, `- [x]`, `- [-]`, or `- [/]`.
|
|
79
|
+
- **Inheritance**: If nested checkboxes exist, the parent state must reflect child states (e.g., if any child is `[/]`, parent must be `[/]`; if all children are `[x]`, parent must be `[x]`).
|
|
80
|
+
|
|
81
|
+
### 4. State Matrix
|
|
82
|
+
|
|
83
|
+
The `status` (folder) and `stage` (front matter) must be compatible:
|
|
84
|
+
|
|
85
|
+
- **open**: Draft, Doing, Review, Done
|
|
86
|
+
- **backlog**: Draft, Doing, Review
|
|
87
|
+
- **closed**: Done
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Issue 管理 (Agent 指引)
|
|
2
|
+
|
|
3
|
+
## Issue 管理
|
|
4
|
+
|
|
5
|
+
使用 `monoco issue` 管理任务的系统。
|
|
6
|
+
|
|
7
|
+
- **创建**: `monoco issue create <type> -t "标题"` (类型: epic, feature, chore, fix)
|
|
8
|
+
- **状态**: `monoco issue open|close|backlog <id>`
|
|
9
|
+
- **检查**: `monoco issue lint` (手动编辑后必须运行)
|
|
10
|
+
- **生命周期**: `monoco issue start|submit|delete <id>`
|
|
11
|
+
- **结构**: `Issues/{CapitalizedPluralType}/{lowercase_status}/` (如 `Issues/Features/open/`)。
|
|
12
|
+
- **强制规则**:
|
|
13
|
+
1. **标题**: 必须包含 `## {ID}: {Title}` 标题(与 Front Matter 一致)。
|
|
14
|
+
2. **内容**: 至少 2 个 Checkbox,使用 `- [ ]`, `- [x]`, `- [-]`, `- [/]`。
|
|
15
|
+
3. **评审**: `review`/`done` 阶段必须包含 `## Review Comments` 章节且内容不为空。
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monoco-issue
|
|
3
|
+
description: Monoco Issue System 的官方技能定义。将 Issue 视为通用原子 (Universal Atom),管理 Epic/Feature/Chore/Fix 的生命周期。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 自我管理 (Monoco Issue System)
|
|
7
|
+
|
|
8
|
+
使用此技能在 Monoco 项目中创建和管理 **Issue** (通用原子)。该系统参考 Jira 表达体系,同时保持 "建设者 (Builder)" 和 "调试者 (Debugger)" 思维模式的隔离。
|
|
9
|
+
|
|
10
|
+
## 核心本体论 (Core Ontology)
|
|
11
|
+
|
|
12
|
+
Monoco 不仅仅复刻 Jira,而是基于 **"思维模式 (Mindset)"** 重新定义工作单元。
|
|
13
|
+
|
|
14
|
+
### 1. 战略层 (Strategy)
|
|
15
|
+
|
|
16
|
+
#### 🏆 EPIC (史诗)
|
|
17
|
+
|
|
18
|
+
- **Mindset**: _Architect_ (架构师)
|
|
19
|
+
- **定义**: 跨越多个周期的宏大目标。它不是单纯的"大任务",而是"愿景的容器"。
|
|
20
|
+
- **产出**: 定义了系统的边界和核心价值。
|
|
21
|
+
|
|
22
|
+
### 2. 价值层 (Value)
|
|
23
|
+
|
|
24
|
+
#### ✨ FEATURE (特性)
|
|
25
|
+
|
|
26
|
+
- **Mindset**: _Product Owner_ (产品负责人)
|
|
27
|
+
- **定义**: 用户视角的价值增量。必须是可独立交付 (Shippable) 的垂直切片。
|
|
28
|
+
- **Focus**: "Why" & "What" (用户想要什么?)。
|
|
29
|
+
- **Prefix**: `FEAT-`
|
|
30
|
+
|
|
31
|
+
### 3. 执行层 (Execution)
|
|
32
|
+
|
|
33
|
+
#### 🧹 CHORE (杂务)
|
|
34
|
+
|
|
35
|
+
- **Mindset**: _Builder_ (建设者)
|
|
36
|
+
- **定义**: **不产生**直接用户价值的工程性事务。
|
|
37
|
+
- **场景**: 架构升级、写构建脚本、修复 CI/CD 流水线。
|
|
38
|
+
- **Focus**: "How" (为了支撑系统运转,必须做什么)。
|
|
39
|
+
- **Prefix**: `CHORE-`
|
|
40
|
+
|
|
41
|
+
> 注:取代了传统的 Task 概念。
|
|
42
|
+
|
|
43
|
+
#### 🐞 FIX (修复)
|
|
44
|
+
|
|
45
|
+
- **Mindset**: _Debugger_ (调试者)
|
|
46
|
+
- **定义**: 预期与现实的偏差。它是负价值的修正。
|
|
47
|
+
- **Focus**: "Fix" (恢复原状)。
|
|
48
|
+
- **Prefix**: `FIX-`
|
|
49
|
+
|
|
50
|
+
> 注:取代了传统的 Bug 概念。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**关系链**:
|
|
55
|
+
|
|
56
|
+
- **主要**: `EPIC` (愿景) -> `FEATURE` (价值交付单元)
|
|
57
|
+
- **次要**: `CHORE` (工程维护/支撑) - 通常独立存在。
|
|
58
|
+
- **原子性原则**: Feature = Design + Dev + Test + Doc + i18n。它们是一体的。
|
|
59
|
+
|
|
60
|
+
## 准则 (Guidelines)
|
|
61
|
+
|
|
62
|
+
### 目录结构
|
|
63
|
+
|
|
64
|
+
`Issues/{CapitalizedPluralType}/{lowercase_status}/`
|
|
65
|
+
|
|
66
|
+
- `{TYPE}`: `Epics`, `Features`, `Chores`, `Fixes`
|
|
67
|
+
- `{STATUS}`: `open`, `backlog`, `closed`
|
|
68
|
+
|
|
69
|
+
### 路径流转
|
|
70
|
+
|
|
71
|
+
使用 `monoco issue`:
|
|
72
|
+
|
|
73
|
+
1. **Create**: `monoco issue create <type> --title "..."`
|
|
74
|
+
|
|
75
|
+
- Params: `--parent <id>`, `--dependency <id>`, `--related <id>`, `--sprint <id>`, `--tags <tag>`
|
|
76
|
+
|
|
77
|
+
2. **Transition**: `monoco issue open/close/backlog <id>`
|
|
78
|
+
|
|
79
|
+
3. **View**: `monoco issue scope`
|
|
80
|
+
|
|
81
|
+
4. **Validation**: `monoco issue lint`
|
|
82
|
+
|
|
83
|
+
5. **Modification**: `monoco issue start/submit/delete <id>`
|
|
84
|
+
|
|
85
|
+
6. **Commit**: `monoco issue commit` (原子化提交 Issue 文件)
|
|
86
|
+
7. **Validation**: `monoco issue lint` (强制执行合规性检查)
|
|
87
|
+
|
|
88
|
+
## 合规与结构校验 (Validation Rules)
|
|
89
|
+
|
|
90
|
+
为了确保数据严谨性,所有 Issue Ticket 必须遵循以下强制规则:
|
|
91
|
+
|
|
92
|
+
### 1. 结构一致性 (Structural Consistency)
|
|
93
|
+
|
|
94
|
+
- 必须包含一个二级标题 (`##`),内容必须与 Front Matter 中的 ID 和 Title 严格匹配。
|
|
95
|
+
- 格式:`## {ID}: {Title}`
|
|
96
|
+
- 示例:`## FEAT-0082: Issue Ticket Validator`
|
|
97
|
+
|
|
98
|
+
### 2. 内容完整性 (Content Completeness)
|
|
99
|
+
|
|
100
|
+
- **Checkbox 数量**: 每个 Ticket 必须包含至少 2 个 Checkbox(通常代表 AC 和 Tasks)。
|
|
101
|
+
- **评审记录**: 当 `stage` 为 `review` 或 `done` 时,必须包含 `## Review Comments` 标题且内容不能为空。
|
|
102
|
+
|
|
103
|
+
### 3. Checkbox 语法与层级 (Checkbox Matrix)
|
|
104
|
+
|
|
105
|
+
- 仅限使用: `- [ ]`, `- [x]`, `- [-]`, `- [/]`。
|
|
106
|
+
- **层级继承**: 若存在嵌套 Checkbox,父项状态必须正确反映子项的聚合结果(例如:任一子项为 `[/]` 则父项必为 `[/]`;子项全选则父项为 `[x]`)。
|
|
107
|
+
|
|
108
|
+
### 4. 状态矩阵 (State Matrix)
|
|
109
|
+
|
|
110
|
+
`status` (物理存放目录) 与 `stage` (Front Matter 字段) 必须兼容:
|
|
111
|
+
|
|
112
|
+
- **open**: Draft, Doing, Review, Done
|
|
113
|
+
- **backlog**: Draft, Doing, Review
|
|
114
|
+
- **closed**: Done
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import yaml
|
|
3
|
+
from typing import List, Set, Optional, Dict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from monoco.core.lsp import Diagnostic, DiagnosticSeverity, Range, Position
|
|
7
|
+
from monoco.core.config import get_config
|
|
8
|
+
from monoco.features.i18n.core import detect_language
|
|
9
|
+
from .models import IssueMetadata, IssueStatus, IssueStage, IssueType
|
|
10
|
+
|
|
11
|
+
class IssueValidator:
|
|
12
|
+
"""
|
|
13
|
+
Centralized validation logic for Issue Tickets.
|
|
14
|
+
Returns LSP-compatible Diagnostics.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, issue_root: Optional[Path] = None):
|
|
18
|
+
self.issue_root = issue_root
|
|
19
|
+
|
|
20
|
+
def validate(self, meta: IssueMetadata, content: str, all_issue_ids: Set[str] = set()) -> List[Diagnostic]:
|
|
21
|
+
diagnostics = []
|
|
22
|
+
|
|
23
|
+
# 1. State Matrix Validation
|
|
24
|
+
diagnostics.extend(self._validate_state_matrix(meta, content))
|
|
25
|
+
|
|
26
|
+
# 2. Content Completeness (Checkbox check)
|
|
27
|
+
diagnostics.extend(self._validate_content_completeness(meta, content))
|
|
28
|
+
|
|
29
|
+
# 3. Structure Consistency (Headings)
|
|
30
|
+
diagnostics.extend(self._validate_structure(meta, content))
|
|
31
|
+
|
|
32
|
+
# 4. Lifecycle/Integrity (Solution, etc.)
|
|
33
|
+
diagnostics.extend(self._validate_integrity(meta, content))
|
|
34
|
+
|
|
35
|
+
# 5. Reference Integrity
|
|
36
|
+
diagnostics.extend(self._validate_references(meta, content, all_issue_ids))
|
|
37
|
+
|
|
38
|
+
# 6. Time Consistency
|
|
39
|
+
diagnostics.extend(self._validate_time_consistency(meta, content))
|
|
40
|
+
|
|
41
|
+
# 7. Checkbox Syntax
|
|
42
|
+
diagnostics.extend(self._validate_checkbox_logic(content))
|
|
43
|
+
|
|
44
|
+
# 8. Language Consistency
|
|
45
|
+
diagnostics.extend(self._validate_language_consistency(meta, content))
|
|
46
|
+
|
|
47
|
+
return diagnostics
|
|
48
|
+
|
|
49
|
+
def _validate_language_consistency(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
50
|
+
diagnostics = []
|
|
51
|
+
try:
|
|
52
|
+
config = get_config()
|
|
53
|
+
source_lang = config.i18n.source_lang
|
|
54
|
+
|
|
55
|
+
# Check for language mismatch (specifically zh vs en)
|
|
56
|
+
if source_lang.lower() == 'zh':
|
|
57
|
+
detected = detect_language(content)
|
|
58
|
+
if detected == 'en':
|
|
59
|
+
diagnostics.append(self._create_diagnostic(
|
|
60
|
+
"Language Mismatch: Project source language is 'zh' but content appears to be 'en'.",
|
|
61
|
+
DiagnosticSeverity.Warning
|
|
62
|
+
))
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return diagnostics
|
|
66
|
+
|
|
67
|
+
def _create_diagnostic(self, message: str, severity: DiagnosticSeverity, line: int = 0) -> Diagnostic:
|
|
68
|
+
"""Helper to create a diagnostic object."""
|
|
69
|
+
return Diagnostic(
|
|
70
|
+
range=Range(
|
|
71
|
+
start=Position(line=line, character=0),
|
|
72
|
+
end=Position(line=line, character=100) # Arbitrary end
|
|
73
|
+
),
|
|
74
|
+
severity=severity,
|
|
75
|
+
message=message
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _get_field_line(self, content: str, field_name: str) -> int:
|
|
79
|
+
"""Helper to find the line number of a field in the front matter."""
|
|
80
|
+
lines = content.split('\n')
|
|
81
|
+
in_fm = False
|
|
82
|
+
for i, line in enumerate(lines):
|
|
83
|
+
stripped = line.strip()
|
|
84
|
+
if stripped == "---":
|
|
85
|
+
if not in_fm:
|
|
86
|
+
in_fm = True
|
|
87
|
+
continue
|
|
88
|
+
else:
|
|
89
|
+
break # End of FM
|
|
90
|
+
if in_fm:
|
|
91
|
+
# Match "field:", "field :", or "field: value"
|
|
92
|
+
if re.match(rf"^{re.escape(field_name)}\s*:", stripped):
|
|
93
|
+
return i
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
def _validate_state_matrix(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
97
|
+
diagnostics = []
|
|
98
|
+
|
|
99
|
+
# Check based on parsed metadata (now that auto-correction is disabled)
|
|
100
|
+
if meta.status == IssueStatus.CLOSED and meta.stage != IssueStage.DONE:
|
|
101
|
+
line = self._get_field_line(content, "status")
|
|
102
|
+
diagnostics.append(self._create_diagnostic(
|
|
103
|
+
f"State Mismatch: Closed issues must be in 'Done' stage (found: {meta.stage.value if meta.stage else 'None'})",
|
|
104
|
+
DiagnosticSeverity.Error,
|
|
105
|
+
line=line
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
if meta.status == IssueStatus.BACKLOG and meta.stage != IssueStage.FREEZED:
|
|
109
|
+
line = self._get_field_line(content, "status")
|
|
110
|
+
diagnostics.append(self._create_diagnostic(
|
|
111
|
+
f"State Mismatch: Backlog issues must be in 'Freezed' stage (found: {meta.stage.value if meta.stage else 'None'})",
|
|
112
|
+
DiagnosticSeverity.Error,
|
|
113
|
+
line=line
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
return diagnostics
|
|
117
|
+
|
|
118
|
+
def _validate_content_completeness(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
119
|
+
diagnostics = []
|
|
120
|
+
# Checkbox regex: - [ ] or - [x] or - [-] or - [/]
|
|
121
|
+
checkboxes = re.findall(r"-\s*\[([ x\-/])\]", content)
|
|
122
|
+
|
|
123
|
+
if len(checkboxes) < 2:
|
|
124
|
+
diagnostics.append(self._create_diagnostic(
|
|
125
|
+
"Content Incomplete: Ticket must contain at least 2 checkboxes (AC & Tasks).",
|
|
126
|
+
DiagnosticSeverity.Warning
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
if meta.stage in [IssueStage.REVIEW, IssueStage.DONE]:
|
|
130
|
+
# No empty checkboxes allowed
|
|
131
|
+
if ' ' in checkboxes:
|
|
132
|
+
# Find the first occurrence line
|
|
133
|
+
lines = content.split('\n')
|
|
134
|
+
first_line = 0
|
|
135
|
+
for i, line in enumerate(lines):
|
|
136
|
+
if re.search(r"-\s*\[ \]", line):
|
|
137
|
+
first_line = i
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
diagnostics.append(self._create_diagnostic(
|
|
141
|
+
f"Incomplete Tasks: Issue in {meta.stage} cannot have unchecked boxes.",
|
|
142
|
+
DiagnosticSeverity.Error,
|
|
143
|
+
line=first_line
|
|
144
|
+
))
|
|
145
|
+
return diagnostics
|
|
146
|
+
|
|
147
|
+
def _validate_structure(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
148
|
+
diagnostics = []
|
|
149
|
+
lines = content.split('\n')
|
|
150
|
+
|
|
151
|
+
# 1. Heading check: ## {issue-id}: {issue-title}
|
|
152
|
+
expected_header = f"## {meta.id}: {meta.title}"
|
|
153
|
+
header_found = False
|
|
154
|
+
|
|
155
|
+
# 2. Review Comments Check
|
|
156
|
+
review_header_found = False
|
|
157
|
+
review_content_found = False
|
|
158
|
+
|
|
159
|
+
for i, line in enumerate(lines):
|
|
160
|
+
line_stripped = line.strip()
|
|
161
|
+
if line_stripped == expected_header:
|
|
162
|
+
header_found = True
|
|
163
|
+
|
|
164
|
+
if line_stripped == "## Review Comments":
|
|
165
|
+
review_header_found = True
|
|
166
|
+
# Check near lines for content
|
|
167
|
+
# This is a naive check (next line is not empty)
|
|
168
|
+
if i + 1 < len(lines) and lines[i+1].strip():
|
|
169
|
+
review_content_found = True
|
|
170
|
+
elif i + 2 < len(lines) and lines[i+2].strip():
|
|
171
|
+
review_content_found = True
|
|
172
|
+
|
|
173
|
+
if not header_found:
|
|
174
|
+
diagnostics.append(self._create_diagnostic(
|
|
175
|
+
f"Structure Error: Missing Level 2 Heading '{expected_header}'",
|
|
176
|
+
DiagnosticSeverity.Warning
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
if meta.stage in [IssueStage.REVIEW, IssueStage.DONE]:
|
|
180
|
+
if not review_header_found:
|
|
181
|
+
diagnostics.append(self._create_diagnostic(
|
|
182
|
+
"Review Requirement: Missing '## Review Comments' section.",
|
|
183
|
+
DiagnosticSeverity.Error
|
|
184
|
+
))
|
|
185
|
+
elif not review_content_found:
|
|
186
|
+
diagnostics.append(self._create_diagnostic(
|
|
187
|
+
"Review Requirement: '## Review Comments' section is empty.",
|
|
188
|
+
DiagnosticSeverity.Error
|
|
189
|
+
))
|
|
190
|
+
return diagnostics
|
|
191
|
+
|
|
192
|
+
def _validate_integrity(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
193
|
+
diagnostics = []
|
|
194
|
+
if meta.status == IssueStatus.CLOSED and not meta.solution:
|
|
195
|
+
line = self._get_field_line(content, "status")
|
|
196
|
+
diagnostics.append(self._create_diagnostic(
|
|
197
|
+
f"Data Integrity: Closed issue {meta.id} missing 'solution' field.",
|
|
198
|
+
DiagnosticSeverity.Error,
|
|
199
|
+
line=line
|
|
200
|
+
))
|
|
201
|
+
return diagnostics
|
|
202
|
+
|
|
203
|
+
def _validate_references(self, meta: IssueMetadata, content: str, all_ids: Set[str]) -> List[Diagnostic]:
|
|
204
|
+
diagnostics = []
|
|
205
|
+
if not all_ids:
|
|
206
|
+
return diagnostics
|
|
207
|
+
|
|
208
|
+
if meta.parent and meta.parent not in all_ids:
|
|
209
|
+
line = self._get_field_line(content, "parent")
|
|
210
|
+
diagnostics.append(self._create_diagnostic(
|
|
211
|
+
f"Broken Reference: Parent '{meta.parent}' not found.",
|
|
212
|
+
DiagnosticSeverity.Error,
|
|
213
|
+
line=line
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
for dep in meta.dependencies:
|
|
217
|
+
if dep not in all_ids:
|
|
218
|
+
line = self._get_field_line(content, "dependencies")
|
|
219
|
+
diagnostics.append(self._create_diagnostic(
|
|
220
|
+
f"Broken Reference: Dependency '{dep}' not found.",
|
|
221
|
+
DiagnosticSeverity.Error,
|
|
222
|
+
line=line
|
|
223
|
+
))
|
|
224
|
+
return diagnostics
|
|
225
|
+
|
|
226
|
+
def _validate_time_consistency(self, meta: IssueMetadata, content: str) -> List[Diagnostic]:
|
|
227
|
+
diagnostics = []
|
|
228
|
+
c = meta.created_at
|
|
229
|
+
o = meta.opened_at
|
|
230
|
+
u = meta.updated_at
|
|
231
|
+
cl = meta.closed_at
|
|
232
|
+
|
|
233
|
+
created_line = self._get_field_line(content, "created_at")
|
|
234
|
+
opened_line = self._get_field_line(content, "opened_at")
|
|
235
|
+
updated_line = self._get_field_line(content, "updated_at")
|
|
236
|
+
closed_line = self._get_field_line(content, "closed_at")
|
|
237
|
+
|
|
238
|
+
if o and c > o:
|
|
239
|
+
diagnostics.append(self._create_diagnostic("Time Travel: created_at > opened_at", DiagnosticSeverity.Warning, line=created_line))
|
|
240
|
+
|
|
241
|
+
if u and c > u:
|
|
242
|
+
diagnostics.append(self._create_diagnostic("Time Travel: created_at > updated_at", DiagnosticSeverity.Warning, line=created_line))
|
|
243
|
+
|
|
244
|
+
if cl:
|
|
245
|
+
if c > cl:
|
|
246
|
+
diagnostics.append(self._create_diagnostic("Time Travel: created_at > closed_at", DiagnosticSeverity.Error, line=created_line))
|
|
247
|
+
if o and o > cl:
|
|
248
|
+
diagnostics.append(self._create_diagnostic("Time Travel: opened_at > closed_at", DiagnosticSeverity.Error, line=opened_line))
|
|
249
|
+
|
|
250
|
+
return diagnostics
|
|
251
|
+
|
|
252
|
+
def _validate_checkbox_logic(self, content: str) -> List[Diagnostic]:
|
|
253
|
+
diagnostics = []
|
|
254
|
+
lines = content.split('\n')
|
|
255
|
+
|
|
256
|
+
for i, line in enumerate(lines):
|
|
257
|
+
stripped = line.lstrip()
|
|
258
|
+
|
|
259
|
+
# Syntax Check: - [?]
|
|
260
|
+
if stripped.startswith("- ["):
|
|
261
|
+
match = re.match(r"- \[([ x\-/])\]", stripped)
|
|
262
|
+
if not match:
|
|
263
|
+
# Check for Common errors
|
|
264
|
+
if re.match(r"- \[.{2,}\]", stripped): # [xx] or [ ]
|
|
265
|
+
diagnostics.append(self._create_diagnostic("Invalid Checkbox: Use single character [ ], [x], [-], [/]", DiagnosticSeverity.Error, i))
|
|
266
|
+
elif re.match(r"- \[([^ x\-/])\]", stripped): # [v], [o]
|
|
267
|
+
diagnostics.append(self._create_diagnostic("Invalid Checkbox Status: Use [ ], [x], [-], [/]", DiagnosticSeverity.Error, i))
|
|
268
|
+
|
|
269
|
+
return diagnostics
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import pty
|
|
5
|
+
import select
|
|
6
|
+
import signal
|
|
7
|
+
import struct
|
|
8
|
+
import fcntl
|
|
9
|
+
import termios
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, Optional, Tuple, Any
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("monoco.pty")
|
|
14
|
+
|
|
15
|
+
class PTYSession:
|
|
16
|
+
"""
|
|
17
|
+
Manages a single PTY session connected to a subprocess (shell).
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, session_id: str, cmd: list[str], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None):
|
|
20
|
+
self.session_id = session_id
|
|
21
|
+
self.cmd = cmd
|
|
22
|
+
self.env = env or os.environ.copy()
|
|
23
|
+
self.cwd = cwd or os.getcwd()
|
|
24
|
+
|
|
25
|
+
self.fd: Optional[int] = None
|
|
26
|
+
self.pid: Optional[int] = None
|
|
27
|
+
self.proc = None # subprocess.Popen object
|
|
28
|
+
self.running = False
|
|
29
|
+
self.loop = asyncio.get_running_loop()
|
|
30
|
+
|
|
31
|
+
def start(self, cols: int = 80, rows: int = 24):
|
|
32
|
+
"""
|
|
33
|
+
Spawn a subprocess connected to a new PTY using subprocess.Popen.
|
|
34
|
+
This provides better safety in threaded/asyncio environments than pty.fork().
|
|
35
|
+
"""
|
|
36
|
+
import subprocess
|
|
37
|
+
|
|
38
|
+
# 1. Open PTY pair
|
|
39
|
+
master_fd, slave_fd = pty.openpty()
|
|
40
|
+
|
|
41
|
+
# 2. Set initial size
|
|
42
|
+
self._set_winsize(master_fd, rows, cols)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 3. Spawn process
|
|
46
|
+
# start_new_session=True executes setsid()
|
|
47
|
+
self.proc = subprocess.Popen(
|
|
48
|
+
self.cmd,
|
|
49
|
+
stdin=slave_fd,
|
|
50
|
+
stdout=slave_fd,
|
|
51
|
+
stderr=slave_fd,
|
|
52
|
+
cwd=self.cwd,
|
|
53
|
+
env=self.env,
|
|
54
|
+
start_new_session=True,
|
|
55
|
+
close_fds=True # Important to close other FDs in child
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.pid = self.proc.pid
|
|
59
|
+
self.fd = master_fd
|
|
60
|
+
self.running = True
|
|
61
|
+
|
|
62
|
+
# 4. Close slave fd in parent (child has it open now)
|
|
63
|
+
os.close(slave_fd)
|
|
64
|
+
|
|
65
|
+
logger.info(f"Started session {self.session_id} (PID: {self.pid})")
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to spawn process: {e}")
|
|
69
|
+
# Ensure we clean up fds if spawn fails
|
|
70
|
+
try:
|
|
71
|
+
os.close(master_fd)
|
|
72
|
+
except: pass
|
|
73
|
+
try:
|
|
74
|
+
os.close(slave_fd)
|
|
75
|
+
except: pass
|
|
76
|
+
raise e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resize(self, cols: int, rows: int):
|
|
80
|
+
"""
|
|
81
|
+
Resize the PTY.
|
|
82
|
+
"""
|
|
83
|
+
if self.fd and self.running:
|
|
84
|
+
self._set_winsize(self.fd, rows, cols)
|
|
85
|
+
|
|
86
|
+
def write(self, data: bytes):
|
|
87
|
+
"""
|
|
88
|
+
Write input data (from websocket) to the PTY master fd.
|
|
89
|
+
"""
|
|
90
|
+
if self.fd and self.running:
|
|
91
|
+
os.write(self.fd, data)
|
|
92
|
+
|
|
93
|
+
async def read(self) -> bytes:
|
|
94
|
+
"""
|
|
95
|
+
Read output data from PTY master fd (to forward to websocket).
|
|
96
|
+
"""
|
|
97
|
+
if not self.fd or not self.running:
|
|
98
|
+
return b""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Run in executor to avoid blocking the event loop
|
|
102
|
+
# pty read is blocking
|
|
103
|
+
return await self.loop.run_in_executor(None, self._read_blocking)
|
|
104
|
+
except OSError:
|
|
105
|
+
return b""
|
|
106
|
+
|
|
107
|
+
def _read_blocking(self) -> bytes:
|
|
108
|
+
try:
|
|
109
|
+
return os.read(self.fd, 1024)
|
|
110
|
+
except OSError:
|
|
111
|
+
return b""
|
|
112
|
+
|
|
113
|
+
def terminate(self):
|
|
114
|
+
"""
|
|
115
|
+
Terminate the process and close the PTY.
|
|
116
|
+
"""
|
|
117
|
+
self.running = False
|
|
118
|
+
|
|
119
|
+
# Use Popen object if available
|
|
120
|
+
if self.proc:
|
|
121
|
+
try:
|
|
122
|
+
self.proc.terminate()
|
|
123
|
+
try:
|
|
124
|
+
self.proc.wait(timeout=1.0)
|
|
125
|
+
except:
|
|
126
|
+
# Force kill if not terminated
|
|
127
|
+
self.proc.kill()
|
|
128
|
+
self.proc.wait()
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Error terminating process: {e}")
|
|
131
|
+
self.proc = None
|
|
132
|
+
self.pid = None
|
|
133
|
+
elif self.pid:
|
|
134
|
+
# Fallback for legacy or if Popen obj lost
|
|
135
|
+
try:
|
|
136
|
+
os.kill(self.pid, signal.SIGTERM)
|
|
137
|
+
os.waitpid(self.pid, 0) # Reap zombie
|
|
138
|
+
except OSError:
|
|
139
|
+
pass
|
|
140
|
+
self.pid = None
|
|
141
|
+
|
|
142
|
+
if self.fd:
|
|
143
|
+
try:
|
|
144
|
+
os.close(self.fd)
|
|
145
|
+
except OSError:
|
|
146
|
+
pass
|
|
147
|
+
self.fd = None
|
|
148
|
+
logger.info(f"Terminated session {self.session_id}")
|
|
149
|
+
|
|
150
|
+
def _set_winsize(self, fd: int, row: int, col: int, xpix: int = 0, ypix: int = 0):
|
|
151
|
+
winsize = struct.pack("HHHH", row, col, xpix, ypix)
|
|
152
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class PTYManager:
|
|
156
|
+
"""
|
|
157
|
+
Singleton to manage multiple PTY sessions.
|
|
158
|
+
"""
|
|
159
|
+
def __init__(self):
|
|
160
|
+
self.sessions: Dict[str, PTYSession] = {}
|
|
161
|
+
|
|
162
|
+
def create_session(self, session_id: str, cwd: str, cmd: list[str] = ["/bin/zsh"], env: Dict = None) -> PTYSession:
|
|
163
|
+
if session_id in self.sessions:
|
|
164
|
+
# In a real app, we might want to attach to existing?
|
|
165
|
+
# For now, kill and recreate (or error)
|
|
166
|
+
self.close_session(session_id)
|
|
167
|
+
|
|
168
|
+
session = PTYSession(session_id, cmd, env, cwd)
|
|
169
|
+
self.sessions[session_id] = session
|
|
170
|
+
return session
|
|
171
|
+
|
|
172
|
+
def get_session(self, session_id: str) -> Optional[PTYSession]:
|
|
173
|
+
return self.sessions.get(session_id)
|
|
174
|
+
|
|
175
|
+
def close_session(self, session_id: str):
|
|
176
|
+
if session_id in self.sessions:
|
|
177
|
+
self.sessions[session_id].terminate()
|
|
178
|
+
del self.sessions[session_id]
|
|
179
|
+
|
|
180
|
+
def close_all_sessions(self):
|
|
181
|
+
"""
|
|
182
|
+
Terminate all active PTY sessions.
|
|
183
|
+
"""
|
|
184
|
+
for session_id in list(self.sessions.keys()):
|
|
185
|
+
self.close_session(session_id)
|