opencode-collaboration 2.0.0__py3-none-any.whl → 2.1.0__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.
@@ -0,0 +1,352 @@
1
+ """Git 工作流强制约束模块。
2
+
3
+ 功能:
4
+ 1. 验证 Agent 通过 Git pull 获取最新文件
5
+ 2. 强制执行 Git 操作
6
+ 3. 检测工作流违规
7
+ """
8
+ import subprocess
9
+ import logging
10
+ from typing import Tuple, Optional, Dict, Any
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ import os
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class WorkflowViolation:
21
+ """工作流违规。"""
22
+ agent_id: str
23
+ action: str
24
+ reason: str
25
+ suggestion: str
26
+
27
+ def to_dict(self) -> Dict[str, str]:
28
+ return {
29
+ "agent_id": self.agent_id,
30
+ "action": self.action,
31
+ "reason": self.reason,
32
+ "suggestion": self.suggestion
33
+ }
34
+
35
+
36
+ class GitWorkflowEnforcer:
37
+ """Git 工作流强制执行器。"""
38
+
39
+ REQUIRED_GIT_OPERATIONS = [
40
+ "READ_REQUIREMENTS",
41
+ "READ_DESIGN",
42
+ "READ_TEST_REPORT",
43
+ "READ_SIGNOFF",
44
+ "READ_CODE",
45
+ ]
46
+
47
+ def __init__(self, project_path: str = "."):
48
+ """初始化。
49
+
50
+ Args:
51
+ project_path: 项目路径
52
+ """
53
+ self.project_path = Path(project_path)
54
+
55
+ def is_git_repository(self) -> bool:
56
+ """检查是否为 Git 仓库。"""
57
+ return (self.project_path / ".git").exists()
58
+
59
+ def get_git_root(self) -> Optional[Path]:
60
+ """获取 Git 根目录。"""
61
+ try:
62
+ result = subprocess.run(
63
+ ["git", "rev-parse", "--show-toplevel"],
64
+ cwd=self.project_path,
65
+ capture_output=True,
66
+ text=True,
67
+ timeout=5
68
+ )
69
+ if result.returncode == 0:
70
+ return Path(result.stdout.strip())
71
+ except (subprocess.TimeoutExpired, FileNotFoundError):
72
+ pass
73
+ return None
74
+
75
+ def verify_git_pull(self, agent_id: str, file_path: str) -> Tuple[bool, Optional[WorkflowViolation]]:
76
+ """验证 Agent 是否通过 Git pull 获取最新文件。
77
+
78
+ Args:
79
+ agent_id: Agent ID
80
+ file_path: 要读取的文件路径
81
+
82
+ Returns:
83
+ (是否通过, 违规信息)
84
+ """
85
+ abs_path = self.project_path / file_path
86
+
87
+ if not abs_path.exists():
88
+ return True, None
89
+
90
+ local_content = self._read_local_file(str(abs_path))
91
+
92
+ git_content = self._git_show(f"HEAD:{file_path}")
93
+
94
+ if git_content is None:
95
+ logger.warning(f"文件不在 Git 中: {file_path}")
96
+ return True, None
97
+
98
+ if local_content != git_content:
99
+ return False, WorkflowViolation(
100
+ agent_id=agent_id,
101
+ action="READ_FILE",
102
+ reason=f"本地文件与 Git HEAD 不一致",
103
+ suggestion="请先执行 'git pull' 获取最新版本"
104
+ )
105
+
106
+ return True, None
107
+
108
+ def verify_git_status(self, agent_id: str) -> Tuple[bool, Optional[WorkflowViolation]]:
109
+ """验证工作区是否干净。
110
+
111
+ Args:
112
+ agent_id: Agent ID
113
+
114
+ Returns:
115
+ (是否干净, 违规信息)
116
+ """
117
+ try:
118
+ result = subprocess.run(
119
+ ["git", "status", "--porcelain"],
120
+ cwd=self.project_path,
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=5
124
+ )
125
+
126
+ if result.stdout.strip():
127
+ return False, WorkflowViolation(
128
+ agent_id=agent_id,
129
+ action="GIT_STATUS",
130
+ reason="工作区有未提交的更改",
131
+ suggestion="请先提交或暂存更改"
132
+ )
133
+
134
+ return True, None
135
+
136
+ except subprocess.TimeoutExpired:
137
+ return False, WorkflowViolation(
138
+ agent_id=agent_id,
139
+ action="GIT_STATUS",
140
+ reason="Git 命令超时",
141
+ suggestion="检查网络连接和 Git 配置"
142
+ )
143
+
144
+ def enforce_git_operation(self, agent_id: str, operation: str) -> Tuple[bool, Optional[WorkflowViolation]]:
145
+ """强制执行 Git 操作。
146
+
147
+ Args:
148
+ agent_id: Agent ID
149
+ operation: 操作类型
150
+
151
+ Returns:
152
+ (是否通过, 违规信息)
153
+ """
154
+ if operation in self.REQUIRED_GIT_OPERATIONS:
155
+ success, error = self._run_git_pull()
156
+ if not success:
157
+ return False, WorkflowViolation(
158
+ agent_id=agent_id,
159
+ action=operation,
160
+ reason="Git pull 失败",
161
+ suggestion=str(error) if error else "请检查 Git 配置"
162
+ )
163
+
164
+ return True, None
165
+
166
+ def check_file_in_git(self, file_path: str) -> bool:
167
+ """检查文件是否在 Git 版本控制中。"""
168
+ try:
169
+ result = subprocess.run(
170
+ ["git", "ls-files", "--error-unmatch", file_path],
171
+ cwd=self.project_path,
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=5
175
+ )
176
+ return result.returncode == 0
177
+ except (subprocess.TimeoutExpired, FileNotFoundError):
178
+ return False
179
+
180
+ def get_file_version(self, file_path: str) -> Optional[str]:
181
+ """获取文件的 Git HEAD 版本哈希。"""
182
+ try:
183
+ result = subprocess.run(
184
+ ["git", "log", "-1", "--format=%H", "--", file_path],
185
+ cwd=self.project_path,
186
+ capture_output=True,
187
+ text=True,
188
+ timeout=5
189
+ )
190
+ if result.returncode == 0:
191
+ return result.stdout.strip()
192
+ except (subprocess.TimeoutExpired, FileNotFoundError):
193
+ pass
194
+ return None
195
+
196
+ def get_commit_info(self, commit_hash: str = "HEAD") -> Dict[str, Any]:
197
+ """获取提交信息。"""
198
+ try:
199
+ result = subprocess.run(
200
+ ["git", "log", "-1", "--format=%H%n%an%n%ae%n%ad%n%s", "--date=iso",
201
+ commit_hash],
202
+ cwd=self.project_path,
203
+ capture_output=True,
204
+ text=True,
205
+ timeout=5
206
+ )
207
+ if result.returncode == 0:
208
+ lines = result.stdout.strip().split('\n')
209
+ return {
210
+ "hash": lines[0] if len(lines) > 0 else "",
211
+ "author": lines[1] if len(lines) > 1 else "",
212
+ "email": lines[2] if len(lines) > 2 else "",
213
+ "date": lines[3] if len(lines) > 3 else "",
214
+ "message": '\n'.join(lines[4:]) if len(lines) > 4 else ""
215
+ }
216
+ except (subprocess.TimeoutExpired, FileNotFoundError):
217
+ pass
218
+ return {}
219
+
220
+ def _run_git_pull(self) -> Tuple[bool, Optional[Exception]]:
221
+ """执行 git pull。"""
222
+ try:
223
+ result = subprocess.run(
224
+ ["git", "pull", "--ff-only"],
225
+ cwd=self.project_path,
226
+ capture_output=True,
227
+ text=True,
228
+ timeout=30
229
+ )
230
+
231
+ if result.returncode == 0:
232
+ logger.info(f"Git pull 成功: {result.stdout}")
233
+ return True, None
234
+ else:
235
+ logger.error(f"Git pull 失败: {result.stderr}")
236
+ return False, Exception(result.stderr)
237
+
238
+ except subprocess.TimeoutExpired:
239
+ error = Exception("Git pull 超时")
240
+ logger.error(str(error))
241
+ return False, error
242
+
243
+ def _read_local_file(self, file_path: str) -> str:
244
+ """读取本地文件内容。"""
245
+ try:
246
+ with open(file_path, 'r', encoding='utf-8') as f:
247
+ return f.read()
248
+ except (IOError, UnicodeDecodeError) as e:
249
+ logger.warning(f"读取文件失败: {file_path}, {e}")
250
+ return ""
251
+
252
+ def _git_show(self, file_path: str) -> Optional[str]:
253
+ """从 Git 获取文件内容。"""
254
+ try:
255
+ result = subprocess.run(
256
+ ["git", "show", file_path],
257
+ cwd=self.project_path,
258
+ capture_output=True,
259
+ text=True,
260
+ timeout=10
261
+ )
262
+
263
+ if result.returncode == 0:
264
+ return result.stdout
265
+ else:
266
+ logger.debug(f"Git show 失败: {result.stderr}")
267
+ return None
268
+
269
+ except (subprocess.TimeoutExpired, FileNotFoundError):
270
+ logger.warning(f"Git show 超时: {file_path}")
271
+ return None
272
+
273
+
274
+ class GitConfigChecker:
275
+ """Git 配置检查器。"""
276
+
277
+ def __init__(self, project_path: str = "."):
278
+ self.project_path = Path(project_path)
279
+
280
+ def check_git_installed(self) -> bool:
281
+ """检查 Git 是否安装。"""
282
+ try:
283
+ subprocess.run(
284
+ ["git", "--version"],
285
+ capture_output=True,
286
+ timeout=5
287
+ )
288
+ return True
289
+ except FileNotFoundError:
290
+ return False
291
+
292
+ def check_git_configured(self) -> Tuple[bool, str]:
293
+ """检查 Git 是否已配置。"""
294
+ try:
295
+ result = subprocess.run(
296
+ ["git", "config", "--global", "--list"],
297
+ capture_output=True,
298
+ text=True,
299
+ timeout=5
300
+ )
301
+ if "user.name" in result.stdout and "user.email" in result.stdout:
302
+ return True, "Git 已配置用户信息"
303
+ else:
304
+ return False, "Git 未配置用户信息 (user.name/user.email)"
305
+ except FileNotFoundError:
306
+ return False, "Git 未安装"
307
+
308
+ def check_remote_configured(self) -> Tuple[bool, str]:
309
+ """检查远程仓库是否配置。"""
310
+ try:
311
+ result = subprocess.run(
312
+ ["git", "remote", "-v"],
313
+ cwd=self.project_path,
314
+ capture_output=True,
315
+ text=True,
316
+ timeout=5
317
+ )
318
+ if result.stdout.strip():
319
+ return True, f"远程仓库已配置: {result.stdout.strip()}"
320
+ else:
321
+ return False, "未配置远程仓库"
322
+ except subprocess.TimeoutExpired:
323
+ return False, "Git 命令超时"
324
+
325
+ def get_full_status(self) -> Dict[str, Any]:
326
+ """获取完整的 Git 状态。"""
327
+ enforcer = GitWorkflowEnforcer(str(self.project_path))
328
+
329
+ return {
330
+ "git_installed": self.check_git_installed(),
331
+ "git_configured": self.check_git_configured()[0],
332
+ "is_repository": enforcer.is_git_repository(),
333
+ "remote_configured": self.check_remote_configured()[0],
334
+ "git_root": str(enforcer.get_git_root()) if enforcer.get_git_root() else None
335
+ }
336
+
337
+
338
+ if __name__ == "__main__":
339
+ logging.basicConfig(level=logging.INFO)
340
+
341
+ checker = GitConfigChecker(".")
342
+ print("Git 配置检查:")
343
+ status = checker.get_full_status()
344
+ for key, value in status.items():
345
+ print(f" {key}: {value}")
346
+
347
+ print("\nGit 工作流强制执行:")
348
+ enforcer = GitWorkflowEnforcer(".")
349
+ is_clean, violation = enforcer.verify_git_status("test_agent")
350
+ print(f" 工作区干净: {is_clean}")
351
+ if violation:
352
+ print(f" 违规: {violation.reason}")
@@ -0,0 +1,290 @@
1
+ """迭代状态管理模块。
2
+
3
+ 功能:
4
+ 1. 管理多迭代状态隔离
5
+ 2. 验证迭代状态与实际进度一致
6
+ 3. 重置指定阶段状态
7
+ """
8
+ import logging
9
+ from typing import Dict, Optional, List
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ import yaml
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class IterationStatus(Enum):
20
+ """迭代状态。"""
21
+ PENDING = "pending"
22
+ IN_PROGRESS = "in_progress"
23
+ COMPLETED = "completed"
24
+ ARCHIVED = "archived"
25
+
26
+
27
+ class PhaseStatus(Enum):
28
+ """阶段状态。"""
29
+ PENDING = "pending"
30
+ IN_PROGRESS = "in_progress"
31
+ COMPLETED = "completed"
32
+ APPROVED = "approved"
33
+
34
+
35
+ @dataclass
36
+ class IterationState:
37
+ """迭代状态。"""
38
+ version: str
39
+ status: str = IterationStatus.PENDING.value
40
+ start_date: str = ""
41
+ end_date: str = ""
42
+ requirements: str = PhaseStatus.PENDING.value
43
+ design: str = PhaseStatus.PENDING.value
44
+ development: str = PhaseStatus.PENDING.value
45
+ testing: str = PhaseStatus.PENDING.value
46
+ deployment: str = PhaseStatus.PENDING.value
47
+ history: List[Dict] = field(default_factory=list)
48
+
49
+ def to_dict(self) -> Dict:
50
+ return {
51
+ "version": self.version,
52
+ "status": self.status,
53
+ "start_date": self.start_date,
54
+ "end_date": self.end_date,
55
+ "requirements": self.requirements,
56
+ "design": self.design,
57
+ "development": self.development,
58
+ "testing": self.testing,
59
+ "deployment": self.deployment,
60
+ "history": self.history
61
+ }
62
+
63
+
64
+ class IterationStatusManager:
65
+ """迭代状态管理器。"""
66
+
67
+ PHASES = ["requirements", "design", "development", "testing", "deployment"]
68
+
69
+ def __init__(self, state_path: str):
70
+ """初始化。
71
+
72
+ Args:
73
+ state_path: state.yaml 文件路径
74
+ """
75
+ self.state_path = state_path
76
+ self.state = self._load_state()
77
+ self.current_iteration = self._get_current_iteration()
78
+
79
+ def _load_state(self) -> Dict:
80
+ """加载状态文件。"""
81
+ try:
82
+ with open(self.state_path, 'r', encoding='utf-8') as f:
83
+ return yaml.safe_load(f) or {}
84
+ except (IOError, yaml.YAMLError) as e:
85
+ logger.error(f"加载状态文件失败: {e}")
86
+ return {}
87
+
88
+ def _save_state(self):
89
+ """保存状态文件。"""
90
+ try:
91
+ with open(self.state_path, 'w', encoding='utf-8') as f:
92
+ yaml.dump(self.state, f, allow_unicode=True, sort_keys=False)
93
+ except IOError as e:
94
+ logger.error(f"保存状态文件失败: {e}")
95
+
96
+ def _get_current_iteration(self) -> Optional[str]:
97
+ """获取当前迭代版本。"""
98
+ return self.state.get("iteration", {}).get("current")
99
+
100
+ def get_iteration_state(self, version: str = None) -> Optional[IterationState]:
101
+ """获取指定迭代的状态。"""
102
+ target_version = version or self.current_iteration
103
+ if not target_version:
104
+ return None
105
+
106
+ iterations = self.state.get("iterations", {})
107
+ if target_version in iterations:
108
+ data = iterations[target_version]
109
+ return IterationState(**data)
110
+ return None
111
+
112
+ def get_current_state(self) -> Optional[IterationState]:
113
+ """获取当前迭代状态。"""
114
+ return self.get_iteration_state()
115
+
116
+ def start_iteration(self, version: str) -> IterationState:
117
+ """开始新迭代,初始化状态。"""
118
+ if "iterations" not in self.state:
119
+ self.state["iterations"] = {}
120
+
121
+ now = datetime.now().isoformat()
122
+ new_iteration = IterationState(
123
+ version=version,
124
+ status=IterationStatus.IN_PROGRESS.value,
125
+ start_date=now
126
+ )
127
+
128
+ self.state["iterations"][version] = new_iteration.to_dict()
129
+ self.state["iteration"] = {
130
+ "current": version,
131
+ "status": "in_progress"
132
+ }
133
+
134
+ self.current_iteration = version
135
+ self._save_state()
136
+
137
+ logger.info(f"开始新迭代: {version}")
138
+ return new_iteration
139
+
140
+ def complete_iteration(self, version: str = None) -> bool:
141
+ """完成迭代。"""
142
+ target_version = version or self.current_iteration
143
+ if not target_version:
144
+ logger.error("没有当前迭代")
145
+ return False
146
+
147
+ now = datetime.now().isoformat()
148
+ if target_version in self.state.get("iterations", {}):
149
+ self.state["iterations"][target_version]["status"] = IterationStatus.COMPLETED.value
150
+ self.state["iterations"][target_version]["end_date"] = now
151
+ self.state["iterations"][target_version]["status"] = IterationStatus.COMPLETED.value
152
+ self.state["iteration"]["status"] = "completed"
153
+
154
+ self._save_state()
155
+ logger.info(f"迭代已完成: {target_version}")
156
+ return True
157
+
158
+ return False
159
+
160
+ def update_phase_status(self, phase: str, status: str, version: str = None) -> bool:
161
+ """更新阶段状态。"""
162
+ if phase not in self.PHASES:
163
+ logger.error(f"未知阶段: {phase}")
164
+ return False
165
+
166
+ target_version = version or self.current_iteration
167
+ if not target_version:
168
+ logger.error("没有当前迭代")
169
+ return False
170
+
171
+ if target_version in self.state.get("iterations", {}):
172
+ self.state["iterations"][target_version][phase] = status
173
+
174
+ self._save_state()
175
+ logger.info(f"迭代 {target_version} 的 {phase} 状态已更新为 {status}")
176
+ return True
177
+
178
+ return False
179
+
180
+ def reset_phase(self, phase: str, version: str = None) -> bool:
181
+ """重置指定阶段状态为 pending。"""
182
+ return self.update_phase_status(phase, PhaseStatus.PENDING.value, version)
183
+
184
+ def archive_iteration(self, version: str) -> bool:
185
+ """归档迭代。"""
186
+ if version in self.state.get("iterations", {}):
187
+ self.state["iterations"][version]["status"] = IterationStatus.ARCHIVED.value
188
+
189
+ if self.state.get("iteration", {}).get("current") == version:
190
+ self.state["iteration"]["current"] = None
191
+ self.state["iteration"]["status"] = "archived"
192
+
193
+ self._save_state()
194
+ logger.info(f"迭代已归档: {version}")
195
+ return True
196
+
197
+ return False
198
+
199
+ def switch_iteration(self, version: str) -> bool:
200
+ """切换到指定迭代。"""
201
+ if version in self.state.get("iterations", {}):
202
+ self.state["iteration"]["current"] = version
203
+ self.state["iteration"]["status"] = self.state["iterations"][version]["status"]
204
+ self.current_iteration = version
205
+
206
+ self._save_state()
207
+ logger.info(f"已切换到迭代: {version}")
208
+ return True
209
+
210
+ logger.error(f"迭代不存在: {version}")
211
+ return False
212
+
213
+ def get_progress(self, version: str = None) -> Dict:
214
+ """获取迭代进度。"""
215
+ target_version = version or self.current_iteration
216
+ if not target_version:
217
+ return {"error": "没有当前迭代"}
218
+
219
+ iteration = self.get_iteration_state(target_version)
220
+ if not iteration:
221
+ return {"error": f"迭代不存在: {target_version}"}
222
+
223
+ completed_phases = 0
224
+ total_phases = len(self.PHASES)
225
+
226
+ for phase in self.PHASES:
227
+ status = getattr(iteration, phase)
228
+ if status in [PhaseStatus.COMPLETED.value, PhaseStatus.APPROVED.value]:
229
+ completed_phases += 1
230
+
231
+ return {
232
+ "version": target_version,
233
+ "overall_status": iteration.status,
234
+ "phases": {
235
+ phase: getattr(iteration, phase) for phase in self.PHASES
236
+ },
237
+ "progress_percent": (completed_phases / total_phases) * 100,
238
+ "completed_phases": completed_phases,
239
+ "total_phases": total_phases
240
+ }
241
+
242
+ def get_all_iterations(self) -> List[Dict]:
243
+ """获取所有迭代信息。"""
244
+ iterations = self.state.get("iterations", {})
245
+ return [
246
+ {"version": k, **v} for k, v in iterations.items()
247
+ ]
248
+
249
+ def validate_consistency(self, version: str = None) -> Dict:
250
+ """验证迭代状态与实际进度一致。"""
251
+ target_version = version or self.current_iteration
252
+ if not target_version:
253
+ return {"valid": False, "reason": "没有当前迭代"}
254
+
255
+ iteration = self.get_iteration_state(target_version)
256
+ if not iteration:
257
+ return {"valid": False, "reason": f"迭代不存在: {target_version}"}
258
+
259
+ issues = []
260
+
261
+ for phase in self.PHASES:
262
+ status = getattr(iteration, phase)
263
+
264
+ if status == PhaseStatus.IN_PROGRESS.value:
265
+ issues.append({
266
+ "phase": phase,
267
+ "status": status,
268
+ "issue": "阶段状态为进行中,请确保工作已完成后再更新状态"
269
+ })
270
+
271
+ return {
272
+ "valid": len(issues) == 0,
273
+ "version": target_version,
274
+ "issues": issues
275
+ }
276
+
277
+
278
+ if __name__ == "__main__":
279
+ logging.basicConfig(level=logging.INFO)
280
+
281
+ manager = IterationStatusManager("state/project_state.yaml")
282
+
283
+ print("当前迭代状态:")
284
+ print(manager.get_current_state())
285
+
286
+ print("\n进度:")
287
+ print(manager.get_progress())
288
+
289
+ print("\n一致性验证:")
290
+ print(manager.validate_consistency())