opencode-collaboration 2.2.0.post2__py3-none-any.whl → 2.2.0.post4__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.
- {opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/METADATA +1 -1
- {opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/RECORD +14 -9
- src/cli/main.py +164 -18
- src/core/brain_engine.py +4 -4
- src/core/change_compliance.py +112 -0
- src/core/session_manager.py +213 -0
- src/core/signoff.py +51 -1
- src/core/signoff_record_manager.py +94 -0
- src/core/task_executor.py +47 -10
- src/core/workflow_inference.py +169 -0
- src/utils/environment.py +64 -0
- {opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/WHEEL +0 -0
- {opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/entry_points.txt +0 -0
- {opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/top_level.txt +0 -0
{opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/RECORD
RENAMED
|
@@ -2,7 +2,7 @@ src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
2
2
|
src/main.py,sha256=W_G68r7XFyioa8wHlSsdwBK1eIyWDaIQWciyZxhZHgg,15
|
|
3
3
|
src/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
src/cli/agent.py,sha256=o8-3UpfcnUMePltpBdVBuBFMECMiKnOA7airyvi1m54,9874
|
|
5
|
-
src/cli/main.py,sha256=
|
|
5
|
+
src/cli/main.py,sha256=nzC2NCkDgqKMSv2WZPJqMk-050V-APfe_FbirOddXQM,50007
|
|
6
6
|
src/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
src/core/agent_manager.py,sha256=Mxe4G_IfEaQ1lERdGNfMvK0tIS6DuE3WJUNOxgtAD8o,16535
|
|
8
8
|
src/core/auto_doc_git.py,sha256=_yphH8tLAMq3R-yMHpc2bcmmap2p7b2So4qVmXUtCh8,1396
|
|
@@ -10,7 +10,8 @@ src/core/auto_docs.py,sha256=rQfJrrKGe8fiQK4eSq4gAp1rfFAu_o-sDV_ZloaFQVQ,11142
|
|
|
10
10
|
src/core/auto_engine.py,sha256=bV9VXa0naPpxKuE7p7xHAZKZJe9DIGfzrFwHhiaDHKA,16031
|
|
11
11
|
src/core/auto_git_sync.py,sha256=jklR_NOYBXYgRSdG0rMRgvQM3jvRBpvm-qSjc-vDK28,2077
|
|
12
12
|
src/core/auto_retry.py,sha256=qG4UY2d312PoECJKKuIq3qspe6qP1mTMEQ4M2Y9J1oo,7571
|
|
13
|
-
src/core/brain_engine.py,sha256=
|
|
13
|
+
src/core/brain_engine.py,sha256=b6PtFde-zkFfCtDihd1es-BEROA_6wQXBJV9S_fL9WE,15032
|
|
14
|
+
src/core/change_compliance.py,sha256=twFvW5gwEpzJHtPubkYJqV2xnzpHRPmczkRvGDVuRSE,3574
|
|
14
15
|
src/core/config_reloader.py,sha256=Fne2FJ-AgPNnqV7Ody-nRpLOSptde65ZXqyGoFl-PbE,8726
|
|
15
16
|
src/core/daemon.py,sha256=pu776YVEC-dw4mKFh7JZg4q7R6XGPe52wiQtwnsQxb4,6470
|
|
16
17
|
src/core/design_review_notifier.py,sha256=XlWyPXUZ7V4CeWdvgf92QBqbrSvO_VzBW7VhUWmlRqk,9826
|
|
@@ -27,22 +28,26 @@ src/core/monitor.py,sha256=3s0xuc6l7pF4MtnRjYVxbfn8MLfKk7p7mljqtsZDQZ0,8949
|
|
|
27
28
|
src/core/phase_advance.py,sha256=76UN8TI7RpJgRfjgV4bvs7uWlvNt-h_dz6gTDgK2zrY,11285
|
|
28
29
|
src/core/project_manager.py,sha256=qayc8NHy3go26LWZgw17nL57p8LXKjpprAS1_iKZHDg,16637
|
|
29
30
|
src/core/resource_lock.py,sha256=1G2yOdbqZnWqVdcIt6IKbhRLhSg23kLgJhdwiFt7jdY,13889
|
|
30
|
-
src/core/
|
|
31
|
+
src/core/session_manager.py,sha256=2Hr36C71kIPia4UYKXx8Ym86T7_9TTQxC8ZrXm65IZU,6825
|
|
32
|
+
src/core/signoff.py,sha256=Zl5rV7C6X3tg1xwZiePH5kbetmOGReJIN4ydhQIjtxY,8037
|
|
33
|
+
src/core/signoff_record_manager.py,sha256=66ZqKdVzYR2-oGprbvfu-QXEMHH0yj3Byzetsq3wPv8,3051
|
|
31
34
|
src/core/state_machine.py,sha256=L1gfdsYC6mlWcHP5RwQQen8vCTwV2spIP-Ktwz4gux8,14919
|
|
32
35
|
src/core/state_manager.py,sha256=jkgci5q71DRjmKe8igeFhOTxHJ2xqNjAHJo6THFtw_M,16672
|
|
33
36
|
src/core/state_migrator.py,sha256=OYXtwxA_ePFAS_XqOMhKR2fyU1werLePbqCyNZg8eXQ,13746
|
|
34
37
|
src/core/state_validator.py,sha256=Q86jbEO0fNdzDi3zIPB_G_ibQ0QWyXFYV_rPj6Eakjw,18827
|
|
35
38
|
src/core/story_manager.py,sha256=8b0Kelw6zdvZYr9sb0Rnlf7_XeXXOSdVar4q2vQjdbI,21867
|
|
36
39
|
src/core/supervisor.py,sha256=pT_5CkimpFgB_gyqzsUL-25l3MsRGP1TWFEnHdGJtwo,7290
|
|
37
|
-
src/core/task_executor.py,sha256=
|
|
40
|
+
src/core/task_executor.py,sha256=l3FeVe5yWZ-z8mwfucCsp5FwXhc2Rvpw5zJEu7u2Kso,24881
|
|
38
41
|
src/core/workflow.py,sha256=LpH9g6xbtCmYOhhCSxpcstRR7TptN8e6b0mEag3UcW4,5634
|
|
42
|
+
src/core/workflow_inference.py,sha256=XU8Z32H4z4MGj5AUS2x9gmS88nD9UL7xWQueXJRlvmA,6093
|
|
39
43
|
src/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
44
|
src/utils/date.py,sha256=iWS0hTaoDE2iC0jJb3lTIB5yK5xxRbrC1C98Fgb8LFc,577
|
|
45
|
+
src/utils/environment.py,sha256=zowgHHBIZ8fgVAZ_fP_I4mX7S9zSn3mvPvnFUYIethE,1509
|
|
41
46
|
src/utils/file.py,sha256=5IFKkT2m1emJUHDzIiLsa4YG9GCqOhhmiLvc6aVY9-Y,1301
|
|
42
47
|
src/utils/lock.py,sha256=soxYFsBKJHUzN-_QXkorVfgnmt0D5p1SZtqwPNqcWPI,2880
|
|
43
48
|
src/utils/yaml.py,sha256=zcbh0OP7NOqxTexEAR3akQkllUh8xeKt42O2CHIImyg,777
|
|
44
|
-
opencode_collaboration-2.2.0.
|
|
45
|
-
opencode_collaboration-2.2.0.
|
|
46
|
-
opencode_collaboration-2.2.0.
|
|
47
|
-
opencode_collaboration-2.2.0.
|
|
48
|
-
opencode_collaboration-2.2.0.
|
|
49
|
+
opencode_collaboration-2.2.0.post4.dist-info/METADATA,sha256=9kWRm0S8WLlmBDyc6OY9aQM74-aY5LxJTGxLGNR9HiI,3127
|
|
50
|
+
opencode_collaboration-2.2.0.post4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
51
|
+
opencode_collaboration-2.2.0.post4.dist-info/entry_points.txt,sha256=fYyHWa_NefMp527B7fHl-29SwZQCElRdtxm_7LoUK-Y,48
|
|
52
|
+
opencode_collaboration-2.2.0.post4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
53
|
+
opencode_collaboration-2.2.0.post4.dist-info/RECORD,,
|
src/cli/main.py
CHANGED
|
@@ -17,6 +17,7 @@ from ..core.auto_engine import AutoCollaborationEngine, TodoCommandExecutor, Wor
|
|
|
17
17
|
from ..core.auto_retry import AutoRetry, AutoRetryConfig
|
|
18
18
|
from ..core.auto_docs import AutoDocs, AutoDocsConfig
|
|
19
19
|
from ..core.phase_advance import PhaseAdvanceEngine
|
|
20
|
+
from ..core.session_manager import SessionManager
|
|
20
21
|
from ..utils.lock import LockExistsError
|
|
21
22
|
|
|
22
23
|
|
|
@@ -123,6 +124,9 @@ def status_command():
|
|
|
123
124
|
req_status = state_manager.get_signoff_status("requirements")
|
|
124
125
|
console.print(f"需求签署 - 产品经理: {'✓' if req_status['pm_signoff'] else '✗'}, 开发: {'✓' if req_status['dev_signoff'] else '✗'}")
|
|
125
126
|
|
|
127
|
+
session_manager = SessionManager(project_path)
|
|
128
|
+
session_manager.show_welcome(active_agent)
|
|
129
|
+
|
|
126
130
|
except StateFileNotFoundError:
|
|
127
131
|
click.echo("错误: 未找到项目状态文件,请先初始化项目")
|
|
128
132
|
sys.exit(1)
|
|
@@ -136,22 +140,33 @@ def status_command():
|
|
|
136
140
|
|
|
137
141
|
@main.command("switch")
|
|
138
142
|
@click.argument("agent_id", type=click.IntRange(1, 2))
|
|
139
|
-
|
|
143
|
+
@click.option("--welcome/--no-welcome", "-w", default=True, help="显示欢迎信息")
|
|
144
|
+
def switch_command(agent_id: int, welcome: bool):
|
|
140
145
|
"""切换Agent角色。"""
|
|
141
146
|
try:
|
|
142
147
|
project_path = get_project_path()
|
|
143
148
|
state_manager = StateManager(project_path)
|
|
144
|
-
|
|
149
|
+
|
|
145
150
|
current_agent = state_manager.get_active_agent()
|
|
146
151
|
if current_agent == f"agent{agent_id}":
|
|
147
152
|
click.echo(f"已经是 Agent {agent_id}")
|
|
148
153
|
return
|
|
149
|
-
|
|
154
|
+
|
|
150
155
|
state_manager.set_active_agent(f"agent{agent_id}")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
|
|
157
|
+
if welcome:
|
|
158
|
+
session_manager = SessionManager(project_path)
|
|
159
|
+
session_manager.show_welcome(f"agent{agent_id}")
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
state = state_manager.load_state()
|
|
163
|
+
agents = state.get("agents", {})
|
|
164
|
+
agent_info = agents.get(f"agent{agent_id}", {})
|
|
165
|
+
role = agent_info.get("role", "未知")
|
|
166
|
+
except Exception:
|
|
167
|
+
role = "未知"
|
|
168
|
+
click.echo(f"已切换到 Agent {agent_id} ({role})")
|
|
169
|
+
|
|
155
170
|
except Exception as e:
|
|
156
171
|
click.echo(f"错误: {e}")
|
|
157
172
|
sys.exit(1)
|
|
@@ -159,26 +174,27 @@ def switch_command(agent_id: int):
|
|
|
159
174
|
|
|
160
175
|
@main.command("review")
|
|
161
176
|
@click.argument("stage", type=click.Choice(["requirements", "design", "test"]))
|
|
177
|
+
@click.option("--file", "-f", help="指定评审文件路径")
|
|
162
178
|
@click.option("--new", is_flag=True, default=False)
|
|
163
179
|
@click.option("--list", "-l", is_flag=True, default=False)
|
|
164
|
-
def review_command(stage: str, new: bool, list: bool):
|
|
180
|
+
def review_command(stage: str, file: str, new: bool, list: bool):
|
|
165
181
|
"""管理评审流程。"""
|
|
166
182
|
try:
|
|
167
183
|
project_path = get_project_path()
|
|
168
184
|
state_manager = StateManager(project_path)
|
|
169
185
|
workflow_engine = WorkflowEngine(state_manager)
|
|
170
|
-
|
|
186
|
+
|
|
171
187
|
if new:
|
|
172
188
|
workflow_engine.start_review(stage)
|
|
173
189
|
click.echo(f"已发起 {stage} 评审")
|
|
174
|
-
|
|
190
|
+
|
|
175
191
|
if list:
|
|
176
192
|
history = state_manager.get_history()
|
|
177
193
|
console.print(f"\n[bold]{stage.upper()} 评审历史[/bold]")
|
|
178
194
|
for item in history[:10]:
|
|
179
195
|
if "review" in item["action"] or "signoff" in item["action"]:
|
|
180
196
|
console.print(f"- {item['timestamp']}: Agent {item['agent']} - {item['details']}")
|
|
181
|
-
|
|
197
|
+
|
|
182
198
|
except Exception as e:
|
|
183
199
|
click.echo(f"错误: {e}")
|
|
184
200
|
sys.exit(1)
|
|
@@ -188,26 +204,74 @@ def review_command(stage: str, new: bool, list: bool):
|
|
|
188
204
|
@click.argument("stage", type=click.Choice(["requirements", "design", "test"]))
|
|
189
205
|
@click.option("--comment", "-m", default="")
|
|
190
206
|
@click.option("--reject", "-r", default=None)
|
|
191
|
-
|
|
207
|
+
@click.option("--sync", "-s", is_flag=True, default=False, help="签署后自动同步到远程")
|
|
208
|
+
def signoff_command(stage: str, comment: str, reject: str, sync: bool):
|
|
192
209
|
"""签署确认。"""
|
|
193
210
|
try:
|
|
194
211
|
project_path = get_project_path()
|
|
195
212
|
state_manager = StateManager(project_path)
|
|
196
213
|
workflow_engine = WorkflowEngine(state_manager)
|
|
197
214
|
signoff_engine = SignoffEngine(state_manager, workflow_engine)
|
|
198
|
-
|
|
215
|
+
|
|
199
216
|
agent_id = state_manager.get_active_agent()
|
|
200
|
-
|
|
217
|
+
|
|
201
218
|
if reject:
|
|
202
219
|
result = signoff_engine.reject(stage, agent_id, reject)
|
|
203
220
|
click.echo(f"已拒签 {stage} 阶段")
|
|
204
221
|
else:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
if sync:
|
|
223
|
+
from ..core.git import GitHelper
|
|
224
|
+
git_helper = GitHelper(project_path)
|
|
225
|
+
result = signoff_engine.signoff_with_sync(stage, agent_id, comment, git_helper)
|
|
226
|
+
click.echo(result.message)
|
|
227
|
+
else:
|
|
228
|
+
result = signoff_engine.sign(stage, agent_id, comment)
|
|
229
|
+
click.echo(f"已签署 {stage} 阶段")
|
|
230
|
+
|
|
208
231
|
if state_manager.can_proceed_to_next_phase():
|
|
209
232
|
click.echo("双方已签署,可以推进到下一阶段")
|
|
210
|
-
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
click.echo(f"错误: {e}")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@main.command("signoffs")
|
|
240
|
+
@click.option("--list", "-l", is_flag=True, default=False, help="列出所有签署记录")
|
|
241
|
+
@click.option("--id", "-i", help="查看特定签署记录ID")
|
|
242
|
+
def signoffs_command(list: bool, id: str):
|
|
243
|
+
"""查看签署记录。"""
|
|
244
|
+
try:
|
|
245
|
+
from ..core.signoff_record_manager import SignoffRecordManager
|
|
246
|
+
|
|
247
|
+
project_path = get_project_path()
|
|
248
|
+
manager = SignoffRecordManager(project_path)
|
|
249
|
+
|
|
250
|
+
if id:
|
|
251
|
+
record = manager.get_signoff(id)
|
|
252
|
+
if record:
|
|
253
|
+
console.print(f"\n[bold]签署记录: {id}[/bold]")
|
|
254
|
+
console.print(f"里程碑: {record.get('milestone')}")
|
|
255
|
+
console.print(f"阶段: {record.get('phase')}")
|
|
256
|
+
console.print(f"状态: {record.get('status')}")
|
|
257
|
+
console.print("\n签署者:")
|
|
258
|
+
for signer in record.get("signers", []):
|
|
259
|
+
status_icon = "✓" if signer.get("status") == "approved" else "○"
|
|
260
|
+
console.print(f" {status_icon} {signer.get('role')} ({signer.get('agent')}) - {signer.get('timestamp')}")
|
|
261
|
+
else:
|
|
262
|
+
click.echo(f"未找到签署记录: {id}")
|
|
263
|
+
elif list:
|
|
264
|
+
signoffs = manager.list_signoffs()
|
|
265
|
+
if signoffs:
|
|
266
|
+
console.print("\n[bold]签署记录列表[/bold]")
|
|
267
|
+
for record in signoffs:
|
|
268
|
+
status_icon = "✓" if record.get("status") == "APPROVED" else "○"
|
|
269
|
+
console.print(f"{status_icon} {record.get('signoff_id')} - {record.get('milestone')} - {record.get('phase')}")
|
|
270
|
+
else:
|
|
271
|
+
click.echo("暂无签署记录")
|
|
272
|
+
else:
|
|
273
|
+
click.echo("请使用 --list 列出所有记录,或 --id 查看特定记录")
|
|
274
|
+
|
|
211
275
|
except Exception as e:
|
|
212
276
|
click.echo(f"错误: {e}")
|
|
213
277
|
sys.exit(1)
|
|
@@ -1104,5 +1168,87 @@ def advance_command(phase: str, force: bool, check: bool):
|
|
|
1104
1168
|
sys.exit(1)
|
|
1105
1169
|
|
|
1106
1170
|
|
|
1171
|
+
@main.command("workflow")
|
|
1172
|
+
@click.option("--check", "-c", is_flag=True, help="显示合规检查结果")
|
|
1173
|
+
@click.option("--suggest", "-s", is_flag=True, help="显示下一步建议")
|
|
1174
|
+
def workflow_command(check: bool, suggest: bool):
|
|
1175
|
+
"""查看当前工作流状态和推理。"""
|
|
1176
|
+
try:
|
|
1177
|
+
from ..core.workflow_inference import WorkflowInferenceEngine
|
|
1178
|
+
|
|
1179
|
+
project_path = get_project_path()
|
|
1180
|
+
state_manager = StateManager(project_path)
|
|
1181
|
+
engine = WorkflowInferenceEngine(project_path, state_manager)
|
|
1182
|
+
|
|
1183
|
+
result = engine.infer_next_action()
|
|
1184
|
+
|
|
1185
|
+
if result.unknown:
|
|
1186
|
+
click.echo("⚠️ 无法确定当前流程状态")
|
|
1187
|
+
click.echo("请先初始化项目: oc-collab project init")
|
|
1188
|
+
else:
|
|
1189
|
+
console.print(Panel(
|
|
1190
|
+
f"当前阶段: {result.current_phase}\n\n{result.suggestion}",
|
|
1191
|
+
title="工作流状态",
|
|
1192
|
+
style="blue"
|
|
1193
|
+
))
|
|
1194
|
+
|
|
1195
|
+
if check or suggest:
|
|
1196
|
+
compliance = result.compliance_check
|
|
1197
|
+
if compliance.valid:
|
|
1198
|
+
click.echo("\n✅ 流程合规")
|
|
1199
|
+
else:
|
|
1200
|
+
click.echo("\n⚠️ 流程不合规:")
|
|
1201
|
+
for violation in compliance.violations:
|
|
1202
|
+
click.echo(f" - {violation}")
|
|
1203
|
+
for suggestion in compliance.suggestions:
|
|
1204
|
+
click.echo(f" 💡 {suggestion}")
|
|
1205
|
+
|
|
1206
|
+
except Exception as e:
|
|
1207
|
+
click.echo(f"错误: {e}")
|
|
1208
|
+
sys.exit(1)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
@main.command("compliance")
|
|
1212
|
+
@click.option("--prd", "-p", help="检查 PRD 文件")
|
|
1213
|
+
@click.option("--rfc", "-r", help="检查 RFC 文件")
|
|
1214
|
+
@click.option("--detect", "-d", is_flag=True, help="检测冲突")
|
|
1215
|
+
def compliance_command(prd: str, rfc: str, detect: bool):
|
|
1216
|
+
"""检查变更合规性。"""
|
|
1217
|
+
try:
|
|
1218
|
+
from ..core.change_compliance import ChangeComplianceChecker
|
|
1219
|
+
|
|
1220
|
+
project_path = get_project_path()
|
|
1221
|
+
checker = ChangeComplianceChecker(project_path)
|
|
1222
|
+
|
|
1223
|
+
if prd:
|
|
1224
|
+
result = checker.check_prd_compliance(prd)
|
|
1225
|
+
if result.valid:
|
|
1226
|
+
click.echo("✅ PRD 合规")
|
|
1227
|
+
else:
|
|
1228
|
+
for v in result.violations:
|
|
1229
|
+
click.echo(f"❌ {v}")
|
|
1230
|
+
|
|
1231
|
+
if rfc:
|
|
1232
|
+
result = checker.check_rfc_compliance(rfc, prd)
|
|
1233
|
+
if result.valid:
|
|
1234
|
+
click.echo("✅ RFC 合规")
|
|
1235
|
+
else:
|
|
1236
|
+
for v in result.violations:
|
|
1237
|
+
click.echo(f"❌ {v}")
|
|
1238
|
+
|
|
1239
|
+
if detect and prd and rfc:
|
|
1240
|
+
conflicts = checker.detect_conflicts(prd, rfc)
|
|
1241
|
+
if conflicts:
|
|
1242
|
+
click.echo(f"⚠️ 发现 {len(conflicts)} 个冲突:")
|
|
1243
|
+
for c in conflicts:
|
|
1244
|
+
click.echo(f" - {c.type}: {c.description}")
|
|
1245
|
+
else:
|
|
1246
|
+
click.echo("✅ 未检测到冲突")
|
|
1247
|
+
|
|
1248
|
+
except Exception as e:
|
|
1249
|
+
click.echo(f"错误: {e}")
|
|
1250
|
+
sys.exit(1)
|
|
1251
|
+
|
|
1252
|
+
|
|
1107
1253
|
if __name__ == "__main__":
|
|
1108
1254
|
main()
|
src/core/brain_engine.py
CHANGED
|
@@ -141,11 +141,11 @@ class BrainEngine:
|
|
|
141
141
|
agent_type=AgentType.AGENT_1,
|
|
142
142
|
conditions=[
|
|
143
143
|
Condition("phase_equals", {"value": "requirements_review"}),
|
|
144
|
-
Condition("
|
|
144
|
+
Condition("custom", {"function": lambda ctx: not ctx.get("signoff", {}).get("requirements", {}).get("pm_signoff", False)})
|
|
145
145
|
],
|
|
146
146
|
action=ActionType.SIGNOFF_REQUIREMENTS,
|
|
147
147
|
priority=90,
|
|
148
|
-
description="当阶段为requirements_review
|
|
148
|
+
description="当阶段为requirements_review且Agent1未签署时,签署需求"
|
|
149
149
|
),
|
|
150
150
|
Rule(
|
|
151
151
|
id="agent1-review-design",
|
|
@@ -191,11 +191,11 @@ class BrainEngine:
|
|
|
191
191
|
agent_type=AgentType.AGENT_2,
|
|
192
192
|
conditions=[
|
|
193
193
|
Condition("phase_equals", {"value": "requirements_review"}),
|
|
194
|
-
Condition("
|
|
194
|
+
Condition("custom", {"function": lambda ctx: not ctx.get("signoff", {}).get("requirements", {}).get("dev_signoff", False)})
|
|
195
195
|
],
|
|
196
196
|
action=ActionType.SIGNOFF_REQUIREMENTS,
|
|
197
197
|
priority=95,
|
|
198
|
-
description="当阶段为requirements_review
|
|
198
|
+
description="当阶段为requirements_review且Agent2未签署时,签署需求"
|
|
199
199
|
),
|
|
200
200
|
Rule(
|
|
201
201
|
id="agent2-create-design",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""变更合规检查器模块。"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import re
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ComplianceResult:
|
|
14
|
+
"""合规检查结果"""
|
|
15
|
+
valid: bool
|
|
16
|
+
violations: List[str] = field(default_factory=list)
|
|
17
|
+
warnings: List[str] = field(default_factory=list)
|
|
18
|
+
suggestions: List[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Conflict:
|
|
23
|
+
"""冲突信息"""
|
|
24
|
+
type: str
|
|
25
|
+
file: str
|
|
26
|
+
description: str
|
|
27
|
+
details: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChangeComplianceChecker:
|
|
31
|
+
"""变更合规检查器"""
|
|
32
|
+
|
|
33
|
+
PRD_PATTERN = r'(?:FR|UR|NFR|BUG)-[\w]+(?:[-][\w]+)*'
|
|
34
|
+
RFC_PATTERN = r'RFC-[\d]+'
|
|
35
|
+
|
|
36
|
+
def __init__(self, project_path: str):
|
|
37
|
+
self.project_path = Path(project_path)
|
|
38
|
+
|
|
39
|
+
def check_prd_compliance(self, prd_file: str) -> ComplianceResult:
|
|
40
|
+
"""检查 PRD 合规性"""
|
|
41
|
+
result = ComplianceResult(valid=True)
|
|
42
|
+
|
|
43
|
+
if not Path(prd_file).exists():
|
|
44
|
+
result.valid = False
|
|
45
|
+
result.violations.append(f"PRD 文件不存在: {prd_file}")
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
with open(prd_file, 'r') as f:
|
|
49
|
+
content = f.read()
|
|
50
|
+
|
|
51
|
+
if "## 签署确认" not in content:
|
|
52
|
+
result.violations.append("PRD 缺少签署确认章节")
|
|
53
|
+
result.suggestions.append("请添加签署确认表格")
|
|
54
|
+
|
|
55
|
+
prd_ids = re.findall(self.PRD_PATTERN, content)
|
|
56
|
+
if prd_ids:
|
|
57
|
+
result.warnings.append(f"发现 {len(set(prd_ids))} 个需求 ID,建议检查追溯性")
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def check_rfc_compliance(self, rfc_file: str, prd_file: str = None) -> ComplianceResult:
|
|
62
|
+
"""检查 RFC 合规性"""
|
|
63
|
+
result = ComplianceResult(valid=True)
|
|
64
|
+
|
|
65
|
+
if not Path(rfc_file).exists():
|
|
66
|
+
result.valid = False
|
|
67
|
+
result.violations.append(f"RFC 文件不存在: {rfc_file}")
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
with open(rfc_file, 'r') as f:
|
|
71
|
+
content = f.read()
|
|
72
|
+
|
|
73
|
+
rfc_ids = re.findall(self.RFC_PATTERN, content)
|
|
74
|
+
if rfc_ids:
|
|
75
|
+
result.suggestions.append(f"发现 {len(set(rfc_ids))} 个 RFC 引用")
|
|
76
|
+
|
|
77
|
+
if prd_file and Path(prd_file).exists():
|
|
78
|
+
with open(prd_file, 'r') as f:
|
|
79
|
+
prd_content = f.read()
|
|
80
|
+
|
|
81
|
+
if content.strip()[-100:] in prd_content[-200:]:
|
|
82
|
+
result.warnings.append("RFC 内容可能已包含在 PRD 中,可作为记录无需单独评审")
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
def detect_conflicts(self, prd_file: str, rfc_file: str) -> List[Conflict]:
|
|
87
|
+
"""检测 PRD 与 RFC 之间的冲突"""
|
|
88
|
+
conflicts = []
|
|
89
|
+
|
|
90
|
+
if not Path(prd_file).exists() or not Path(rfc_file).exists():
|
|
91
|
+
return conflicts
|
|
92
|
+
|
|
93
|
+
with open(prd_file, 'r') as f:
|
|
94
|
+
prd_content = f.read()
|
|
95
|
+
|
|
96
|
+
with open(rfc_file, 'r') as f:
|
|
97
|
+
rfc_content = f.read()
|
|
98
|
+
|
|
99
|
+
prd_ids = set(re.findall(self.PRD_PATTERN, prd_content))
|
|
100
|
+
rfc_refs = set(re.findall(self.RFC_PATTERN, rfc_content))
|
|
101
|
+
|
|
102
|
+
for prd_id in prd_ids:
|
|
103
|
+
for rfc_ref in rfc_refs:
|
|
104
|
+
if prd_id in rfc_content and prd_id not in rfc_ref:
|
|
105
|
+
conflicts.append(Conflict(
|
|
106
|
+
type="内容冲突",
|
|
107
|
+
file=f"PRD vs RFC",
|
|
108
|
+
description=f"需求 {prd_id} 在 RFC 中被引用,但 RFC 编号不一致",
|
|
109
|
+
details=f"PRD 引用: {prd_id}, RFC 内容: 包含 {prd_id}"
|
|
110
|
+
))
|
|
111
|
+
|
|
112
|
+
return conflicts
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from .state_manager import StateManager
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
AGENT_ROLES = {
|
|
12
|
+
"agent1": {
|
|
13
|
+
"role": "产品经理",
|
|
14
|
+
"responsibilities": [
|
|
15
|
+
"编写和评审需求文档",
|
|
16
|
+
"定义验收标准",
|
|
17
|
+
"签署需求确认",
|
|
18
|
+
"评审设计文档",
|
|
19
|
+
"评审测试报告"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"agent2": {
|
|
23
|
+
"role": "开发负责人",
|
|
24
|
+
"responsibilities": [
|
|
25
|
+
"评审需求文档",
|
|
26
|
+
"编写详细设计",
|
|
27
|
+
"代码实现",
|
|
28
|
+
"编写单元测试",
|
|
29
|
+
"签署技术确认"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
COMMON_COMMANDS = [
|
|
35
|
+
("oc-collab status", "查看项目状态"),
|
|
36
|
+
("oc-collab todo", "查看待办事项"),
|
|
37
|
+
("oc-collab review", "评审文档"),
|
|
38
|
+
("oc-collab signoff", "签署确认"),
|
|
39
|
+
("oc-collab history", "查看协作历史"),
|
|
40
|
+
("oc-collab switch <1|2>", "切换Agent角色")
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SessionConfig:
|
|
45
|
+
def __init__(self, project_path: str):
|
|
46
|
+
self.project_path = project_path
|
|
47
|
+
self.enabled = True
|
|
48
|
+
self.show_responsibilities = True
|
|
49
|
+
self.show_todo = True
|
|
50
|
+
self.show_pending = True
|
|
51
|
+
self.load_config()
|
|
52
|
+
|
|
53
|
+
def load_config(self):
|
|
54
|
+
state_file = Path(self.project_path) / "state" / "project_state.yaml"
|
|
55
|
+
if state_file.exists():
|
|
56
|
+
try:
|
|
57
|
+
import yaml
|
|
58
|
+
with open(state_file) as f:
|
|
59
|
+
state = yaml.safe_load(f) or {}
|
|
60
|
+
session_config = state.get("session_start", {})
|
|
61
|
+
self.enabled = session_config.get("enabled", True)
|
|
62
|
+
self.show_responsibilities = session_config.get("show_responsibilities", True)
|
|
63
|
+
self.show_todo = session_config.get("show_todo", True)
|
|
64
|
+
self.show_pending = session_config.get("show_pending", True)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SessionManager:
|
|
70
|
+
def __init__(self, project_path: str):
|
|
71
|
+
self.project_path = project_path
|
|
72
|
+
self.state_manager = StateManager(project_path)
|
|
73
|
+
self.config = SessionConfig(project_path)
|
|
74
|
+
|
|
75
|
+
def get_project_info(self) -> dict:
|
|
76
|
+
try:
|
|
77
|
+
state = self.state_manager.load_state()
|
|
78
|
+
metadata = state.get("metadata", {})
|
|
79
|
+
project_info = state.get("project", {})
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"name": metadata.get("project_name") or project_info.get("name", "未配置"),
|
|
83
|
+
"phase": project_info.get("phase") or state.get("phase", "未知"),
|
|
84
|
+
"milestone": state.get("current_milestone", "待定义")
|
|
85
|
+
}
|
|
86
|
+
except Exception:
|
|
87
|
+
return {"name": "未配置", "phase": "未知", "milestone": "待定义"}
|
|
88
|
+
|
|
89
|
+
def get_agent_info(self, agent_id: str) -> dict:
|
|
90
|
+
agent_config = AGENT_ROLES.get(agent_id, {
|
|
91
|
+
"role": "未知",
|
|
92
|
+
"responsibilities": []
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
state = self.state_manager.load_state()
|
|
97
|
+
agents = state.get("agents", {})
|
|
98
|
+
agent_state = agents.get(agent_id, {})
|
|
99
|
+
except KeyError:
|
|
100
|
+
agent_state = {}
|
|
101
|
+
except Exception:
|
|
102
|
+
agent_state = {}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"id": agent_id,
|
|
106
|
+
"role": agent_config["role"],
|
|
107
|
+
"current_task": agent_state.get("current_task", ""),
|
|
108
|
+
"responsibilities": agent_config["responsibilities"]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def get_responsibilities_text(self, agent_id: str) -> str:
|
|
112
|
+
agent = self.get_agent_info(agent_id)
|
|
113
|
+
if not self.config.show_responsibilities:
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
lines = ["你的职责:"]
|
|
117
|
+
for resp in agent["responsibilities"]:
|
|
118
|
+
lines.append(f" - {resp}")
|
|
119
|
+
return "\n".join(lines)
|
|
120
|
+
|
|
121
|
+
def get_todo_items(self) -> str:
|
|
122
|
+
if not self.config.show_todo:
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
from .auto_engine import TodoCommandExecutor
|
|
127
|
+
executor = TodoCommandExecutor(self.project_path)
|
|
128
|
+
todo_list = executor.get_todo_list()
|
|
129
|
+
|
|
130
|
+
if not todo_list:
|
|
131
|
+
return "待办事项:\n 暂无待办事项"
|
|
132
|
+
|
|
133
|
+
lines = ["待办事项:"]
|
|
134
|
+
for item in todo_list[:5]:
|
|
135
|
+
task = item.get("task", "")
|
|
136
|
+
lines.append(f" [ ] {task}")
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
except Exception:
|
|
139
|
+
return "待办事项:\n 暂无待办事项"
|
|
140
|
+
|
|
141
|
+
def get_pending_issues(self) -> str:
|
|
142
|
+
if not self.config.show_pending:
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
pending_file = Path(self.project_path) / "state" / "memory" / "pending.yaml"
|
|
146
|
+
if not pending_file.exists():
|
|
147
|
+
return "上次会话遗留:\n 无遗留问题"
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
import yaml
|
|
151
|
+
with open(pending_file) as f:
|
|
152
|
+
pending = yaml.safe_load(f) or []
|
|
153
|
+
|
|
154
|
+
if not pending:
|
|
155
|
+
return "上次会话遗留:\n 无遗留问题"
|
|
156
|
+
|
|
157
|
+
lines = ["上次会话遗留:"]
|
|
158
|
+
for item in pending[:5]:
|
|
159
|
+
desc = item.get("description", item)
|
|
160
|
+
lines.append(f" - {desc}")
|
|
161
|
+
return "\n".join(lines)
|
|
162
|
+
except Exception:
|
|
163
|
+
return "上次会话遗留:\n 无遗留问题"
|
|
164
|
+
|
|
165
|
+
def get_common_commands(self) -> str:
|
|
166
|
+
lines = ["常用命令:"]
|
|
167
|
+
for cmd, desc in COMMON_COMMANDS:
|
|
168
|
+
lines.append(f" - {cmd}: {desc}")
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
|
|
171
|
+
def get_welcome_message(self, agent_id: str) -> str:
|
|
172
|
+
if not self.config.enabled:
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
project = self.get_project_info()
|
|
176
|
+
agent = self.get_agent_info(agent_id)
|
|
177
|
+
|
|
178
|
+
parts = [
|
|
179
|
+
f"=== Agent {agent_id.replace('agent', '')} ({agent['role']}) ===",
|
|
180
|
+
"",
|
|
181
|
+
f"当前项目: {project['name']}",
|
|
182
|
+
f"当前阶段: {project['phase']}",
|
|
183
|
+
f"当前里程碑: {project['milestone']}",
|
|
184
|
+
""
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
resp_text = self.get_responsibilities_text(agent_id)
|
|
188
|
+
if resp_text:
|
|
189
|
+
parts.append(resp_text)
|
|
190
|
+
parts.append("")
|
|
191
|
+
|
|
192
|
+
todo_text = self.get_todo_items()
|
|
193
|
+
if todo_text:
|
|
194
|
+
parts.append(todo_text)
|
|
195
|
+
parts.append("")
|
|
196
|
+
|
|
197
|
+
pending_text = self.get_pending_issues()
|
|
198
|
+
if pending_text:
|
|
199
|
+
parts.append(pending_text)
|
|
200
|
+
parts.append("")
|
|
201
|
+
|
|
202
|
+
parts.append(self.get_common_commands())
|
|
203
|
+
|
|
204
|
+
return "\n".join(parts)
|
|
205
|
+
|
|
206
|
+
def show_welcome(self, agent_id: str):
|
|
207
|
+
message = self.get_welcome_message(agent_id)
|
|
208
|
+
if message:
|
|
209
|
+
console.print(Panel(
|
|
210
|
+
Text(message, justify="left"),
|
|
211
|
+
title="会话引导",
|
|
212
|
+
style="blue"
|
|
213
|
+
))
|
src/core/signoff.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""签署引擎模块。"""
|
|
2
|
-
from typing import Tuple
|
|
2
|
+
from typing import Tuple, Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class SignoffError(Exception):
|
|
@@ -27,6 +28,22 @@ class RejectionError(SignoffError):
|
|
|
27
28
|
pass
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
@dataclass
|
|
32
|
+
class SignoffResult:
|
|
33
|
+
"""签署结果"""
|
|
34
|
+
success: bool
|
|
35
|
+
message: str
|
|
36
|
+
synced: bool = False
|
|
37
|
+
sync_error: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SyncResult:
|
|
42
|
+
"""同步结果"""
|
|
43
|
+
success: bool
|
|
44
|
+
message: str
|
|
45
|
+
|
|
46
|
+
|
|
30
47
|
class SignoffEngine:
|
|
31
48
|
"""签署引擎。"""
|
|
32
49
|
|
|
@@ -194,3 +211,36 @@ class SignoffEngine:
|
|
|
194
211
|
return False
|
|
195
212
|
|
|
196
213
|
return True
|
|
214
|
+
|
|
215
|
+
def signoff_with_sync(self, stage: str, agent: str, comment: str = "", git_helper=None) -> SignoffResult:
|
|
216
|
+
"""执行签署并同步到远程"""
|
|
217
|
+
try:
|
|
218
|
+
sign_result = self.sign(stage, agent, comment)
|
|
219
|
+
|
|
220
|
+
synced = False
|
|
221
|
+
sync_error = None
|
|
222
|
+
|
|
223
|
+
if git_helper:
|
|
224
|
+
try:
|
|
225
|
+
git_helper.push()
|
|
226
|
+
synced = True
|
|
227
|
+
except Exception as e:
|
|
228
|
+
sync_error = str(e)
|
|
229
|
+
|
|
230
|
+
message = f"签署成功: {stage} 阶段"
|
|
231
|
+
if synced:
|
|
232
|
+
message += "\n✓ 已同步到远程仓库"
|
|
233
|
+
elif sync_error:
|
|
234
|
+
message += f"\n⚠ 同步失败: {sync_error}"
|
|
235
|
+
|
|
236
|
+
return SignoffResult(
|
|
237
|
+
success=True,
|
|
238
|
+
message=message,
|
|
239
|
+
synced=synced,
|
|
240
|
+
sync_error=sync_error
|
|
241
|
+
)
|
|
242
|
+
except SignoffError as e:
|
|
243
|
+
return SignoffResult(
|
|
244
|
+
success=False,
|
|
245
|
+
message=f"签署失败: {e}"
|
|
246
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""签署记录管理器模块。"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
import yaml
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from dataclasses import dataclass, asdict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SignoffRecord:
|
|
11
|
+
"""签署记录"""
|
|
12
|
+
signoff_id: str
|
|
13
|
+
milestone: str
|
|
14
|
+
phase: str
|
|
15
|
+
signers: List[Dict[str, Any]]
|
|
16
|
+
status: str
|
|
17
|
+
created_at: str
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
20
|
+
return asdict(self)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SignoffRecordManager:
|
|
24
|
+
"""签署记录管理器"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_path: str):
|
|
27
|
+
self.project_path = Path(project_path)
|
|
28
|
+
self.signoffs_dir = self.project_path / "state" / "signoffs"
|
|
29
|
+
self.signoffs_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
def _generate_signoff_id(self, milestone: str) -> str:
|
|
32
|
+
"""生成签署记录ID"""
|
|
33
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
34
|
+
return f"SIG-{milestone}-{timestamp}"
|
|
35
|
+
|
|
36
|
+
def save_signoff(self, milestone: str, phase: str, signers: List[Dict[str, Any]], status: str = "PENDING") -> str:
|
|
37
|
+
"""保存签署记录"""
|
|
38
|
+
signoff_id = self._generate_signoff_id(milestone)
|
|
39
|
+
|
|
40
|
+
record = SignoffRecord(
|
|
41
|
+
signoff_id=signoff_id,
|
|
42
|
+
milestone=milestone,
|
|
43
|
+
phase=phase,
|
|
44
|
+
signers=signers,
|
|
45
|
+
status=status,
|
|
46
|
+
created_at=datetime.now().isoformat()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
file_path = self.signoffs_dir / f"{signoff_id}.yaml"
|
|
50
|
+
with open(file_path, 'w') as f:
|
|
51
|
+
yaml.dump(record.to_dict(), f)
|
|
52
|
+
|
|
53
|
+
return signoff_id
|
|
54
|
+
|
|
55
|
+
def get_signoff(self, signoff_id: str) -> Optional[Dict[str, Any]]:
|
|
56
|
+
"""获取签署记录"""
|
|
57
|
+
file_path = self.signoffs_dir / f"{signoff_id}.yaml"
|
|
58
|
+
|
|
59
|
+
if file_path.exists():
|
|
60
|
+
with open(file_path) as f:
|
|
61
|
+
return yaml.safe_load(f)
|
|
62
|
+
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def list_signoffs(self) -> List[Dict[str, Any]]:
|
|
66
|
+
"""列出所有签署记录"""
|
|
67
|
+
signoffs = []
|
|
68
|
+
for file_path in self.signoffs_dir.glob("*.yaml"):
|
|
69
|
+
with open(file_path) as f:
|
|
70
|
+
signoffs.append(yaml.safe_load(f))
|
|
71
|
+
return signoffs
|
|
72
|
+
|
|
73
|
+
def update_signoff_status(self, signoff_id: str, status: str, signer: Dict[str, Any] = None):
|
|
74
|
+
"""更新签署记录状态"""
|
|
75
|
+
record = self.get_signoff(signoff_id)
|
|
76
|
+
if record:
|
|
77
|
+
record["status"] = status
|
|
78
|
+
|
|
79
|
+
if signer:
|
|
80
|
+
for i, s in enumerate(record["signers"]):
|
|
81
|
+
if s.get("agent") == signer.get("agent"):
|
|
82
|
+
record["signers"][i] = signer
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
file_path = self.signoffs_dir / f"{signoff_id}.yaml"
|
|
86
|
+
with open(file_path, 'w') as f:
|
|
87
|
+
yaml.dump(record, f)
|
|
88
|
+
|
|
89
|
+
def check_all_signed(self, signoff_id: str) -> bool:
|
|
90
|
+
"""检查签署记录中所有签署者是否都已签署"""
|
|
91
|
+
record = self.get_signoff(signoff_id)
|
|
92
|
+
if record:
|
|
93
|
+
return all(s.get("status") == "approved" for s in record.get("signers", []))
|
|
94
|
+
return False
|
src/core/task_executor.py
CHANGED
|
@@ -442,7 +442,7 @@ class ExecuteBlackboxTestStrategy(TaskStrategy):
|
|
|
442
442
|
test_result_path = f"docs/03-test/test_report_{project_name}_v1.md"
|
|
443
443
|
|
|
444
444
|
result = subprocess.run(
|
|
445
|
-
["
|
|
445
|
+
["python3", "-m", "pytest", "-v", "--tb=short"],
|
|
446
446
|
capture_output=True,
|
|
447
447
|
text=True,
|
|
448
448
|
timeout=300
|
|
@@ -646,13 +646,20 @@ class TaskExecutor:
|
|
|
646
646
|
FixBugsStrategy()
|
|
647
647
|
]
|
|
648
648
|
|
|
649
|
-
def __init__(self, max_retries: int = 3, default_timeout: int = 300):
|
|
650
|
-
"""初始化任务执行器。
|
|
649
|
+
def __init__(self, max_retries: int = 3, default_timeout: int = 300, mock_mode: bool = False):
|
|
650
|
+
"""初始化任务执行器。
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
max_retries: 最大重试次数
|
|
654
|
+
default_timeout: 默认超时时间(秒)
|
|
655
|
+
mock_mode: 是否使用 Mock 模式(E2E 测试时使用)
|
|
656
|
+
"""
|
|
651
657
|
self.strategies: Dict[str, TaskStrategy] = {}
|
|
652
658
|
self.task_history: List[Task] = []
|
|
653
659
|
self.max_retries = max_retries
|
|
654
660
|
self.default_timeout = default_timeout
|
|
655
|
-
|
|
661
|
+
self.mock_mode = mock_mode
|
|
662
|
+
|
|
656
663
|
for strategy in self.DEFAULT_STRATEGIES:
|
|
657
664
|
self.register_strategy(strategy)
|
|
658
665
|
|
|
@@ -742,31 +749,61 @@ class TaskExecutor:
|
|
|
742
749
|
return result
|
|
743
750
|
|
|
744
751
|
def execute_action(self, action_type: str, context: Dict[str, Any]) -> TaskResult:
|
|
745
|
-
"""根据动作类型执行任务。
|
|
752
|
+
"""根据动作类型执行任务。
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
action_type: 动作类型
|
|
756
|
+
context: 上下文信息
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
任务执行结果
|
|
760
|
+
"""
|
|
746
761
|
action_to_task_type = {
|
|
747
762
|
"create_requirements": "create_requirements",
|
|
748
763
|
"review_requirements": "review_requirements",
|
|
749
764
|
"signoff_requirements": "signoff_requirements",
|
|
750
765
|
"create_design": "create_design",
|
|
751
766
|
"review_design": "review_design",
|
|
767
|
+
"implement_code": "implement_code",
|
|
768
|
+
"fix_bugs": "fix_bugs",
|
|
752
769
|
"execute_blackbox_test": "execute_blackbox_test",
|
|
753
|
-
"execute_deployment": "execute_deployment"
|
|
754
|
-
"implement_code": "implement_code"
|
|
770
|
+
"execute_deployment": "execute_deployment"
|
|
755
771
|
}
|
|
756
|
-
|
|
772
|
+
|
|
757
773
|
task_type = action_to_task_type.get(action_type)
|
|
758
774
|
if not task_type:
|
|
759
775
|
return TaskResult(
|
|
760
776
|
success=False,
|
|
761
777
|
message=f"未知的动作类型: {action_type}"
|
|
762
778
|
)
|
|
763
|
-
|
|
779
|
+
|
|
780
|
+
if self.mock_mode and action_type == "execute_blackbox_test":
|
|
781
|
+
return TaskResult(
|
|
782
|
+
success=True,
|
|
783
|
+
message="黑盒测试通过(Mock 模式)",
|
|
784
|
+
files_created=[],
|
|
785
|
+
files_modified=[],
|
|
786
|
+
duration=0.0,
|
|
787
|
+
quality_score=1.0
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
if self.mock_mode and action_type == "fix_bugs":
|
|
791
|
+
pending_issues = context.get("pending_issues", 0)
|
|
792
|
+
return TaskResult(
|
|
793
|
+
success=True,
|
|
794
|
+
message=f"已修复 {pending_issues} 个 Bug(Mock 模式)",
|
|
795
|
+
files_created=[],
|
|
796
|
+
files_modified=[],
|
|
797
|
+
duration=0.0,
|
|
798
|
+
quality_score=1.0
|
|
799
|
+
)
|
|
800
|
+
|
|
764
801
|
task = self.create_task(
|
|
765
802
|
name=f"执行{action_type}",
|
|
766
803
|
task_type=task_type,
|
|
767
804
|
params=context
|
|
768
805
|
)
|
|
769
|
-
|
|
806
|
+
|
|
770
807
|
return self.execute_task(task, context)
|
|
771
808
|
|
|
772
809
|
def get_pending_tasks(self) -> List[Task]:
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""主动流程推理引擎模块。"""
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComplianceLevel(Enum):
|
|
12
|
+
"""合规级别"""
|
|
13
|
+
VALID = "valid"
|
|
14
|
+
WARNING = "warning"
|
|
15
|
+
VIOLATION = "violation"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ComplianceResult:
|
|
20
|
+
"""合规检查结果"""
|
|
21
|
+
valid: bool
|
|
22
|
+
level: ComplianceLevel
|
|
23
|
+
violations: List[str] = field(default_factory=list)
|
|
24
|
+
warnings: List[str] = field(default_factory=list)
|
|
25
|
+
suggestions: List[str] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class InferenceResult:
|
|
30
|
+
"""流程推理结果"""
|
|
31
|
+
current_phase: str
|
|
32
|
+
next_phases: List[str]
|
|
33
|
+
suggestion: str
|
|
34
|
+
compliance_check: ComplianceResult
|
|
35
|
+
unknown: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WorkflowInferenceEngine:
|
|
39
|
+
"""主动流程推理引擎"""
|
|
40
|
+
|
|
41
|
+
PHASE_SEQUENCE = {
|
|
42
|
+
"requirements": ["design", "development", "testing"],
|
|
43
|
+
"design": ["development", "testing"],
|
|
44
|
+
"development": ["testing"],
|
|
45
|
+
"testing": ["deployment"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
PHASE_NEXT = {
|
|
49
|
+
"requirements": "设计阶段",
|
|
50
|
+
"design": "开发阶段",
|
|
51
|
+
"development": "测试阶段",
|
|
52
|
+
"testing": "部署阶段",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def __init__(self, project_path: str, state_manager):
|
|
56
|
+
"""初始化流程推理引擎"""
|
|
57
|
+
self.project_path = project_path
|
|
58
|
+
self.state_manager = state_manager
|
|
59
|
+
|
|
60
|
+
def infer_next_action(self) -> InferenceResult:
|
|
61
|
+
"""根据当前状态推理下一步应该执行的操作"""
|
|
62
|
+
try:
|
|
63
|
+
current_state = self.state_manager.load_state()
|
|
64
|
+
phase = current_state.get("phase", current_state.get("project", {}).get("phase", "unknown"))
|
|
65
|
+
|
|
66
|
+
if phase == "unknown":
|
|
67
|
+
return InferenceResult(
|
|
68
|
+
current_phase="unknown",
|
|
69
|
+
next_phases=[],
|
|
70
|
+
suggestion="请先初始化项目: oc-collab project init",
|
|
71
|
+
compliance_check=ComplianceResult(
|
|
72
|
+
valid=False,
|
|
73
|
+
level=ComplianceLevel.VIOLATION,
|
|
74
|
+
violations=["无法确定当前流程状态"],
|
|
75
|
+
suggestions=["请先初始化项目"]
|
|
76
|
+
),
|
|
77
|
+
unknown=True
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
next_phases = self.PHASE_SEQUENCE.get(phase, [])
|
|
81
|
+
|
|
82
|
+
compliance = self._run_compliance_check(phase, current_state)
|
|
83
|
+
|
|
84
|
+
if next_phases:
|
|
85
|
+
suggestion = f"当前阶段: {phase}\n建议: 进入 {self.PHASE_NEXT.get(phase, phase)}"
|
|
86
|
+
else:
|
|
87
|
+
suggestion = f"当前阶段: {phase}\n可选: 无(流程已完成)"
|
|
88
|
+
|
|
89
|
+
return InferenceResult(
|
|
90
|
+
current_phase=phase,
|
|
91
|
+
next_phases=next_phases,
|
|
92
|
+
suggestion=suggestion,
|
|
93
|
+
compliance_check=compliance
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"流程推理失败: {e}")
|
|
98
|
+
return InferenceResult(
|
|
99
|
+
current_phase="unknown",
|
|
100
|
+
next_phases=[],
|
|
101
|
+
suggestion=f"推理失败: {e}",
|
|
102
|
+
compliance_check=ComplianceResult(
|
|
103
|
+
valid=False,
|
|
104
|
+
level=ComplianceLevel.VIOLATION,
|
|
105
|
+
violations=[str(e)],
|
|
106
|
+
suggestions=["请检查项目状态"]
|
|
107
|
+
),
|
|
108
|
+
unknown=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _run_compliance_check(self, phase: str, current_state: Dict[str, Any]) -> ComplianceResult:
|
|
112
|
+
"""运行合规检查"""
|
|
113
|
+
violations = []
|
|
114
|
+
warnings = []
|
|
115
|
+
suggestions = []
|
|
116
|
+
|
|
117
|
+
if phase == "unknown":
|
|
118
|
+
violations.append("无法确定当前流程状态")
|
|
119
|
+
suggestions.append("请先初始化项目")
|
|
120
|
+
|
|
121
|
+
if phase == "requirements":
|
|
122
|
+
requirements_status = current_state.get("requirements", {})
|
|
123
|
+
if isinstance(requirements_status, dict):
|
|
124
|
+
if not requirements_status.get("pm_signoff") or not requirements_status.get("dev_signoff"):
|
|
125
|
+
violations.append("需求阶段尚未完成签署")
|
|
126
|
+
suggestions.append("请先完成需求评审和签署")
|
|
127
|
+
|
|
128
|
+
elif phase == "design":
|
|
129
|
+
design_status = current_state.get("design", {})
|
|
130
|
+
if isinstance(design_status, dict):
|
|
131
|
+
if not design_status.get("pm_signoff") or not design_status.get("dev_signoff"):
|
|
132
|
+
violations.append("设计阶段尚未完成签署")
|
|
133
|
+
suggestions.append("请先完成设计评审和签署")
|
|
134
|
+
elif isinstance(design_status, list):
|
|
135
|
+
if not design_status:
|
|
136
|
+
violations.append("没有设计文档")
|
|
137
|
+
suggestions.append("请先创建设计文档")
|
|
138
|
+
|
|
139
|
+
return ComplianceResult(
|
|
140
|
+
valid=len(violations) == 0,
|
|
141
|
+
level=ComplianceLevel.VIOLATION if violations else ComplianceLevel.VALID,
|
|
142
|
+
violations=violations,
|
|
143
|
+
warnings=warnings,
|
|
144
|
+
suggestions=suggestions
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def validate_phase_transition(self, from_phase: str, to_phase: str) -> ComplianceResult:
|
|
148
|
+
"""验证阶段转换是否合规"""
|
|
149
|
+
valid_transitions = self.PHASE_SEQUENCE.get(from_phase, [])
|
|
150
|
+
|
|
151
|
+
if to_phase not in valid_transitions:
|
|
152
|
+
return ComplianceResult(
|
|
153
|
+
valid=False,
|
|
154
|
+
level=ComplianceLevel.VIOLATION,
|
|
155
|
+
violations=[f"阶段顺序不正确: {from_phase} → {to_phase}"],
|
|
156
|
+
suggestions=[f"从 {from_phase} 阶段只能转换到: {', '.join(valid_transitions)}"]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return ComplianceResult(
|
|
160
|
+
valid=True,
|
|
161
|
+
level=ComplianceLevel.VALID,
|
|
162
|
+
violations=[],
|
|
163
|
+
suggestions=[f"✓ {from_phase} → {to_phase} 符合流程规范"]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def get_workflow_status(self) -> str:
|
|
167
|
+
"""获取工作流状态"""
|
|
168
|
+
result = self.infer_next_action()
|
|
169
|
+
return f"[{result.current_phase.upper()}] - {result.suggestion}"
|
src/utils/environment.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""环境检测工具模块。"""
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvironmentError(Exception):
|
|
8
|
+
"""环境异常基类。"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PythonNotFoundError(EnvironmentError):
|
|
13
|
+
"""Python 解释器未找到异常。"""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_python_command() -> str:
|
|
18
|
+
"""检测并返回可用的 Python 命令。
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
可用的 Python 命令(优先使用 python3)
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
PythonNotFoundError: 未找到 Python 解释器
|
|
25
|
+
"""
|
|
26
|
+
if shutil.which("python3"):
|
|
27
|
+
return "python3"
|
|
28
|
+
elif shutil.which("python"):
|
|
29
|
+
return "python"
|
|
30
|
+
else:
|
|
31
|
+
raise PythonNotFoundError("未找到 Python 解释器(python 或 python3)")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_python_executable() -> str:
|
|
35
|
+
"""返回当前使用的 Python 解释器路径。
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Python 解释器的绝对路径
|
|
39
|
+
"""
|
|
40
|
+
return sys.executable
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_test_environment() -> bool:
|
|
44
|
+
"""检测是否在测试环境中运行。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
如果在测试环境中返回 True,否则返回 False
|
|
48
|
+
"""
|
|
49
|
+
return "pytest" in sys.modules or "test" in sys.argv[0].lower()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_environment_info() -> dict:
|
|
53
|
+
"""获取当前环境信息。
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
包含环境信息的字典
|
|
57
|
+
"""
|
|
58
|
+
return {
|
|
59
|
+
"python_command": get_python_command(),
|
|
60
|
+
"python_executable": get_python_executable(),
|
|
61
|
+
"is_test_environment": is_test_environment(),
|
|
62
|
+
"python_version": sys.version,
|
|
63
|
+
"platform": sys.platform
|
|
64
|
+
}
|
{opencode_collaboration-2.2.0.post2.dist-info → opencode_collaboration-2.2.0.post4.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|