monoco-toolkit 0.1.0__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.
Files changed (69) hide show
  1. monoco/cli/__init__.py +0 -0
  2. monoco/cli/project.py +87 -0
  3. monoco/cli/workspace.py +46 -0
  4. monoco/core/agent/__init__.py +5 -0
  5. monoco/core/agent/action.py +144 -0
  6. monoco/core/agent/adapters.py +106 -0
  7. monoco/core/agent/protocol.py +31 -0
  8. monoco/core/agent/state.py +106 -0
  9. monoco/core/config.py +152 -17
  10. monoco/core/execution.py +62 -0
  11. monoco/core/feature.py +58 -0
  12. monoco/core/git.py +51 -2
  13. monoco/core/injection.py +196 -0
  14. monoco/core/integrations.py +234 -0
  15. monoco/core/lsp.py +61 -0
  16. monoco/core/output.py +13 -2
  17. monoco/core/registry.py +36 -0
  18. monoco/core/resources/en/AGENTS.md +8 -0
  19. monoco/core/resources/en/SKILL.md +66 -0
  20. monoco/core/resources/zh/AGENTS.md +8 -0
  21. monoco/core/resources/zh/SKILL.md +66 -0
  22. monoco/core/setup.py +88 -110
  23. monoco/core/skills.py +444 -0
  24. monoco/core/state.py +53 -0
  25. monoco/core/sync.py +224 -0
  26. monoco/core/telemetry.py +4 -1
  27. monoco/core/workspace.py +85 -20
  28. monoco/daemon/app.py +127 -58
  29. monoco/daemon/models.py +4 -0
  30. monoco/daemon/services.py +56 -155
  31. monoco/features/agent/commands.py +166 -0
  32. monoco/features/agent/doctor.py +30 -0
  33. monoco/features/config/commands.py +125 -44
  34. monoco/features/i18n/adapter.py +29 -0
  35. monoco/features/i18n/commands.py +89 -10
  36. monoco/features/i18n/core.py +113 -27
  37. monoco/features/i18n/resources/en/AGENTS.md +8 -0
  38. monoco/features/i18n/resources/en/SKILL.md +94 -0
  39. monoco/features/i18n/resources/zh/AGENTS.md +8 -0
  40. monoco/features/i18n/resources/zh/SKILL.md +94 -0
  41. monoco/features/issue/adapter.py +34 -0
  42. monoco/features/issue/commands.py +183 -65
  43. monoco/features/issue/core.py +172 -77
  44. monoco/features/issue/linter.py +215 -116
  45. monoco/features/issue/migration.py +134 -0
  46. monoco/features/issue/models.py +23 -19
  47. monoco/features/issue/monitor.py +94 -0
  48. monoco/features/issue/resources/en/AGENTS.md +15 -0
  49. monoco/features/issue/resources/en/SKILL.md +87 -0
  50. monoco/features/issue/resources/zh/AGENTS.md +15 -0
  51. monoco/features/issue/resources/zh/SKILL.md +114 -0
  52. monoco/features/issue/validator.py +269 -0
  53. monoco/features/pty/core.py +185 -0
  54. monoco/features/pty/router.py +138 -0
  55. monoco/features/pty/server.py +56 -0
  56. monoco/features/spike/adapter.py +30 -0
  57. monoco/features/spike/commands.py +45 -24
  58. monoco/features/spike/core.py +4 -21
  59. monoco/features/spike/resources/en/AGENTS.md +7 -0
  60. monoco/features/spike/resources/en/SKILL.md +74 -0
  61. monoco/features/spike/resources/zh/AGENTS.md +7 -0
  62. monoco/features/spike/resources/zh/SKILL.md +74 -0
  63. monoco/main.py +115 -2
  64. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +10 -3
  65. monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
  66. monoco_toolkit-0.1.0.dist-info/RECORD +0 -33
  67. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
  68. {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
  69. {monoco_toolkit-0.1.0.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)