super-engineer-workflow 0.1.0
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.
- package/CHANGELOG.md +9 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/SECURITY.md +21 -0
- package/bin/super-engineer.js +19 -0
- package/docs/se/345/221/275/344/273/244/345/215/217/350/256/256.md +335 -0
- package/docs//344/270/255/346/226/207/344/275/277/347/224/250/346/211/213/345/206/214.md +707 -0
- package/docs//345/205/254/345/274/200/345/217/221/345/270/203/346/243/200/346/237/245/346/270/205/345/215/225.md +43 -0
- package/docs//345/277/253/351/200/237/345/210/235/345/247/213/345/214/226/345/267/245/344/275/234/345/214/272.md +419 -0
- package/docs//351/241/271/347/233/256/346/236/266/346/236/204/344/270/216/350/256/276/350/256/241/350/257/264/346/230/216.md +657 -0
- package/package.json +55 -0
- package/scripts/se-cli.py +301 -0
- package/scripts/se-setup.py +331 -0
- package/scripts/se-smoke-test.py +86 -0
- package/super-engineer-workflow/SKILL.md +439 -0
- package/super-engineer-workflow/adapters/go.yml +8 -0
- package/super-engineer-workflow/adapters/java-gradle.yml +8 -0
- package/super-engineer-workflow/adapters/java-maven.yml +8 -0
- package/super-engineer-workflow/adapters/node-react.yml +8 -0
- package/super-engineer-workflow/adapters/node-vue.yml +8 -0
- package/super-engineer-workflow/adapters/python.yml +8 -0
- package/super-engineer-workflow/agents/openai.yaml +4 -0
- package/super-engineer-workflow/assets/config-schema.json +100 -0
- package/super-engineer-workflow/assets/config.example.yml +12 -0
- package/super-engineer-workflow/assets/plan-schema.json +362 -0
- package/super-engineer-workflow/assets/status-schema.json +83 -0
- package/super-engineer-workflow/assets/workspace.example.yml +25 -0
- package/super-engineer-workflow/config.example.yml +12 -0
- package/super-engineer-workflow/references/contracts.md +39 -0
- package/super-engineer-workflow/references/execution-modes.md +38 -0
- package/super-engineer-workflow/references/java.md +21 -0
- package/super-engineer-workflow/references/planning.md +45 -0
- package/super-engineer-workflow/references/platform-openclaw.md +10 -0
- package/super-engineer-workflow/references/project-docs.md +7 -0
- package/super-engineer-workflow/references/review-checklist.md +26 -0
- package/super-engineer-workflow/references/se-commands.md +582 -0
- package/super-engineer-workflow/references/verify-checklist.md +45 -0
- package/super-engineer-workflow/references/workflow.md +208 -0
- package/super-engineer-workflow/scripts/archive-openspec.py +110 -0
- package/super-engineer-workflow/scripts/bootstrap-openspec.py +42 -0
- package/super-engineer-workflow/scripts/common.py +3285 -0
- package/super-engineer-workflow/scripts/generate-discovery.py +185 -0
- package/super-engineer-workflow/scripts/generate-review-report.py +296 -0
- package/super-engineer-workflow/scripts/generate-self-check.py +185 -0
- package/super-engineer-workflow/scripts/generate-smart-plan.py +429 -0
- package/super-engineer-workflow/scripts/init-workspace.py +68 -0
- package/super-engineer-workflow/scripts/prepare-archive-openspec.py +186 -0
- package/super-engineer-workflow/scripts/propose-openspec.py +170 -0
- package/super-engineer-workflow/scripts/run-verify-and-report.py +399 -0
- package/super-engineer-workflow/scripts/run-workflow.py +506 -0
- package/super-engineer-workflow/scripts/update-status.py +43 -0
- package/super-engineer-workflow/scripts/writeback-openspec.py +311 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from common import (
|
|
8
|
+
existing_reference_files,
|
|
9
|
+
load_workspace_config,
|
|
10
|
+
openspec_change_dir,
|
|
11
|
+
openspec_change_name,
|
|
12
|
+
openspec_cli_available,
|
|
13
|
+
openspec_writeback_dir,
|
|
14
|
+
read_demand_source,
|
|
15
|
+
read_text,
|
|
16
|
+
run_openspec_cli,
|
|
17
|
+
select_openspec_change,
|
|
18
|
+
update_se_state,
|
|
19
|
+
validate_openspec_change_name,
|
|
20
|
+
workflow_source,
|
|
21
|
+
workspace_root,
|
|
22
|
+
write_active_openspec_change,
|
|
23
|
+
write_managed_json,
|
|
24
|
+
write_managed_text,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
parser = argparse.ArgumentParser(description="OpenSpec-native propose preparation for current workspace.")
|
|
30
|
+
parser.add_argument("change_name", nargs="?", help="OpenSpec change 名称,例如 demand-addition-rate")
|
|
31
|
+
parser.add_argument("--change", dest="change_name_option", help="OpenSpec change 名称,例如 demand-addition-rate")
|
|
32
|
+
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
36
|
+
config = load_workspace_config(workspace)
|
|
37
|
+
if workflow_source(config) != "openspec":
|
|
38
|
+
raise SystemExit("当前 workspace.yml 未启用 OpenSpec 模式,无法执行 propose-openspec。")
|
|
39
|
+
|
|
40
|
+
explicit_change_name = args.change_name_option or args.change_name
|
|
41
|
+
if not explicit_change_name:
|
|
42
|
+
raise SystemExit("缺少 OpenSpec change 名称。请使用 /se:propose <change-name> 显式指定。")
|
|
43
|
+
explicit_change_name = validate_openspec_change_name(explicit_change_name)
|
|
44
|
+
config = select_openspec_change(config, explicit_change_name)
|
|
45
|
+
change_name = openspec_change_name(config)
|
|
46
|
+
change_dir = openspec_change_dir(config)
|
|
47
|
+
writeback_dir = openspec_writeback_dir(config)
|
|
48
|
+
try:
|
|
49
|
+
demand_source = read_demand_source(config)
|
|
50
|
+
except RuntimeError as error:
|
|
51
|
+
raise SystemExit(str(error))
|
|
52
|
+
demand_file = str(demand_source.get("source", "")).strip()
|
|
53
|
+
demand_text = str(demand_source.get("content", "")).strip()
|
|
54
|
+
reference_contexts = [
|
|
55
|
+
{
|
|
56
|
+
"path": item,
|
|
57
|
+
"content": read_text(Path(item)),
|
|
58
|
+
}
|
|
59
|
+
for item in existing_reference_files(config)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
commands: list[dict] = []
|
|
63
|
+
if openspec_cli_available():
|
|
64
|
+
if not change_dir.exists():
|
|
65
|
+
commands.append(run_openspec_cli(config, ["new", "change", change_name]))
|
|
66
|
+
commands.append(run_openspec_cli(config, ["status", "--change", change_name, "--json"]))
|
|
67
|
+
status_json = commands[-1].get("json") or {}
|
|
68
|
+
artifacts = status_json.get("artifacts", []) if isinstance(status_json, dict) else []
|
|
69
|
+
for artifact in artifacts:
|
|
70
|
+
artifact_id = str(artifact.get("id") or artifact.get("artifact") or artifact.get("name") or "").strip()
|
|
71
|
+
if artifact_id:
|
|
72
|
+
commands.append(run_openspec_cli(config, ["instructions", artifact_id, "--change", change_name, "--json"]))
|
|
73
|
+
else:
|
|
74
|
+
change_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
(change_dir / "specs").mkdir(parents=True, exist_ok=True)
|
|
76
|
+
commands.append(
|
|
77
|
+
{
|
|
78
|
+
"available": False,
|
|
79
|
+
"args": [],
|
|
80
|
+
"returncode": None,
|
|
81
|
+
"stdout": "",
|
|
82
|
+
"stderr": "openspec CLI not found in PATH; created change directory only",
|
|
83
|
+
"json": None,
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
active_change_file = write_active_openspec_change(config, change_name)
|
|
88
|
+
payload = {
|
|
89
|
+
"change_name": change_name,
|
|
90
|
+
"change_dir": str(change_dir),
|
|
91
|
+
"active_change_file": str(active_change_file),
|
|
92
|
+
"demand_file": demand_file,
|
|
93
|
+
"demand_source_type": demand_source.get("source_type", ""),
|
|
94
|
+
"demand_fetch_command": demand_source.get("command", []),
|
|
95
|
+
"demand_text": demand_text,
|
|
96
|
+
"reference_files": reference_contexts,
|
|
97
|
+
"openspec_cli_available": openspec_cli_available(),
|
|
98
|
+
"commands": commands,
|
|
99
|
+
"next_action": "Use demand_text, reference_files, and OpenSpec instructions to create or update proposal.md, design.md, tasks.md, and specs/.",
|
|
100
|
+
"workflow_phase_after_completion": "proposed",
|
|
101
|
+
"allowed_next_after_completion": ["/se:bridge"],
|
|
102
|
+
"forbidden_next_after_completion": ["/se:plan", "/se:apply"],
|
|
103
|
+
"final_reply_constraint": "代码暂未修改。下一步只能执行 /se:bridge,把当前 OpenSpec tasks.md 桥接为待审核 todo.md。",
|
|
104
|
+
}
|
|
105
|
+
write_managed_json(config, writeback_dir / "propose-input.json", payload)
|
|
106
|
+
write_managed_text(
|
|
107
|
+
config,
|
|
108
|
+
writeback_dir / "propose-input.md",
|
|
109
|
+
"\n".join(
|
|
110
|
+
[
|
|
111
|
+
"# Propose Input",
|
|
112
|
+
"",
|
|
113
|
+
f"- change: {change_name}",
|
|
114
|
+
f"- change_dir: {change_dir}",
|
|
115
|
+
f"- demand_file: {demand_file or ''}",
|
|
116
|
+
f"- demand_source_type: {demand_source.get('source_type', '')}",
|
|
117
|
+
f"- demand_fetch_command: {' '.join(str(item) for item in demand_source.get('command', [])) if demand_source.get('command') else ''}",
|
|
118
|
+
f"- openspec_cli_available: {openspec_cli_available()}",
|
|
119
|
+
"",
|
|
120
|
+
"## Demand",
|
|
121
|
+
"",
|
|
122
|
+
demand_text or "未配置或未找到 demand_file。",
|
|
123
|
+
"",
|
|
124
|
+
"## Reference Files",
|
|
125
|
+
"",
|
|
126
|
+
"\n\n".join(
|
|
127
|
+
[
|
|
128
|
+
"\n".join(
|
|
129
|
+
[
|
|
130
|
+
f"### {item['path']}",
|
|
131
|
+
"",
|
|
132
|
+
item["content"] or "文件为空或无法读取。",
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
for item in reference_contexts
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
or "未配置或未找到 reference_files。",
|
|
139
|
+
"",
|
|
140
|
+
]
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
update_se_state(
|
|
144
|
+
config,
|
|
145
|
+
phase="proposed",
|
|
146
|
+
last_command="/se:propose",
|
|
147
|
+
artifacts={
|
|
148
|
+
"proposal": str(change_dir / "proposal.md"),
|
|
149
|
+
"design": str(change_dir / "design.md"),
|
|
150
|
+
"tasks": str(change_dir / "tasks.md"),
|
|
151
|
+
"change_dir": str(change_dir),
|
|
152
|
+
"propose_input": str(writeback_dir / "propose-input.json"),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
print(f"change_name={change_name}")
|
|
156
|
+
print(f"change_dir={change_dir}")
|
|
157
|
+
print(f"active_change_file={active_change_file}")
|
|
158
|
+
print(f"demand_file={demand_file or ''}")
|
|
159
|
+
print(f"demand_source_type={demand_source.get('source_type', '')}")
|
|
160
|
+
print(f"reference_files={len(reference_contexts)}")
|
|
161
|
+
print(f"openspec_cli_available={str(openspec_cli_available()).lower()}")
|
|
162
|
+
print(f"propose_input={writeback_dir / 'propose-input.json'}")
|
|
163
|
+
print("workflow_phase_after_completion=proposed")
|
|
164
|
+
print("allowed_next_after_completion=/se:bridge")
|
|
165
|
+
print("forbidden_next_after_completion=/se:plan,/se:apply")
|
|
166
|
+
print("final_reply_must=代码暂未修改。下一步只能执行 /se:bridge,把当前 OpenSpec tasks.md 桥接为待审核 todo.md。")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from common import (
|
|
11
|
+
current_session_meta,
|
|
12
|
+
data_artifact_path,
|
|
13
|
+
detect_project,
|
|
14
|
+
ensure_status,
|
|
15
|
+
format_duration,
|
|
16
|
+
load_workspace_config,
|
|
17
|
+
is_standard_workflow_notification,
|
|
18
|
+
feishu_config,
|
|
19
|
+
notify_workflow_result,
|
|
20
|
+
now_iso,
|
|
21
|
+
phase_after,
|
|
22
|
+
planned_codebases,
|
|
23
|
+
read_json,
|
|
24
|
+
report_artifact_path,
|
|
25
|
+
workflow_duration_seconds,
|
|
26
|
+
write_managed_json,
|
|
27
|
+
write_managed_text,
|
|
28
|
+
workspace_root,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
VERIFY_BLOCKERS = {"未识别到验证命令", "验证失败", "验证执行超时"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def has_sent_standard_notification(
|
|
35
|
+
config: dict,
|
|
36
|
+
session_meta: dict,
|
|
37
|
+
status: dict,
|
|
38
|
+
) -> bool:
|
|
39
|
+
verify_path = data_artifact_path(config, "verify.json", session_meta)
|
|
40
|
+
notification_path = data_artifact_path(config, "notification.json", session_meta)
|
|
41
|
+
verify = read_json(verify_path, {})
|
|
42
|
+
notification = read_json(notification_path, {})
|
|
43
|
+
if not isinstance(verify, dict) or not isinstance(notification, dict):
|
|
44
|
+
return False
|
|
45
|
+
overall_result = str(verify.get("result", "")).strip()
|
|
46
|
+
if not overall_result:
|
|
47
|
+
return False
|
|
48
|
+
return is_standard_workflow_notification(config, session_meta, status, overall_result, notification)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tail(text: str, lines: int = 20) -> list[str]:
|
|
52
|
+
items = [line.rstrip() for line in text.splitlines() if line.strip()]
|
|
53
|
+
return items[-lines:]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def summarize_stdout(stdout_text: str) -> list[str]:
|
|
57
|
+
summaries: list[str] = []
|
|
58
|
+
seen: set[str] = set()
|
|
59
|
+
for line in stdout_text.splitlines():
|
|
60
|
+
stripped = line.strip()
|
|
61
|
+
if not stripped:
|
|
62
|
+
continue
|
|
63
|
+
match = re.search(r"Tests run: (\d+), Failures: (\d+), Errors: (\d+), Skipped: (\d+)", stripped)
|
|
64
|
+
if match:
|
|
65
|
+
total, failures, errors, skipped = match.groups()
|
|
66
|
+
summary = f"测试统计:共 {total} 条,用例失败 {failures},错误 {errors},跳过 {skipped}。"
|
|
67
|
+
if summary not in seen:
|
|
68
|
+
summaries.append(summary)
|
|
69
|
+
seen.add(summary)
|
|
70
|
+
if "BUILD SUCCESS" in stripped:
|
|
71
|
+
summary = "构建结果:成功。"
|
|
72
|
+
if summary not in seen:
|
|
73
|
+
summaries.append(summary)
|
|
74
|
+
seen.add(summary)
|
|
75
|
+
if "BUILD FAILURE" in stripped:
|
|
76
|
+
summary = "构建结果:失败。"
|
|
77
|
+
if summary not in seen:
|
|
78
|
+
summaries.append(summary)
|
|
79
|
+
seen.add(summary)
|
|
80
|
+
if "default message [" in stripped:
|
|
81
|
+
messages = re.findall(r"default message \[([^\]]+)\]", stripped)
|
|
82
|
+
if messages:
|
|
83
|
+
summary = f"参数校验提示:{messages[-1]}"
|
|
84
|
+
if summary not in seen:
|
|
85
|
+
summaries.append(summary)
|
|
86
|
+
seen.add(summary)
|
|
87
|
+
return summaries[:10] if summaries else tail(stdout_text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def write_report(
|
|
91
|
+
config: dict,
|
|
92
|
+
output: Path,
|
|
93
|
+
sections: list[dict],
|
|
94
|
+
checks: list[str],
|
|
95
|
+
result: str,
|
|
96
|
+
verify_duration: float,
|
|
97
|
+
workflow_duration: float,
|
|
98
|
+
) -> None:
|
|
99
|
+
lines = [
|
|
100
|
+
"# 验证结果",
|
|
101
|
+
"",
|
|
102
|
+
"## 总体结果",
|
|
103
|
+
f"- {result}",
|
|
104
|
+
f"- 验证阶段耗时:{format_duration(verify_duration)}",
|
|
105
|
+
f"- 整个工作流耗时:{format_duration(workflow_duration)}",
|
|
106
|
+
"",
|
|
107
|
+
"## 计划检查项",
|
|
108
|
+
]
|
|
109
|
+
lines.extend(f"- {item}" for item in checks) if checks else lines.append("- 暂无")
|
|
110
|
+
lines.extend(["", "## 仓库验证明细"])
|
|
111
|
+
for section in sections:
|
|
112
|
+
lines.extend(
|
|
113
|
+
[
|
|
114
|
+
f"### {section['name']}",
|
|
115
|
+
f"- 路径:{section['path']}",
|
|
116
|
+
f"- 执行结果:{section['result']}",
|
|
117
|
+
f"- 退出码:{section['exit_code']}",
|
|
118
|
+
f"- 耗时(秒):{section['duration']:.2f}",
|
|
119
|
+
"- 执行命令:",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
if section["commands"]:
|
|
123
|
+
for item in section["commands"]:
|
|
124
|
+
if isinstance(item, dict):
|
|
125
|
+
lines.append(f" - [{item.get('kind')}] {item.get('command')}")
|
|
126
|
+
else:
|
|
127
|
+
lines.append(f" - {item}")
|
|
128
|
+
else:
|
|
129
|
+
lines.append(" - 暂无,请补充可执行的验证命令。")
|
|
130
|
+
lines.append("- 标准输出摘要:")
|
|
131
|
+
stdout_summary = summarize_stdout(section["stdout"])
|
|
132
|
+
if section["stdout"].strip():
|
|
133
|
+
lines.extend(f" - {item}" for item in stdout_summary)
|
|
134
|
+
else:
|
|
135
|
+
lines.append(" - 暂无")
|
|
136
|
+
lines.append("- 标准错误摘要:")
|
|
137
|
+
stderr_summary = tail(section["stderr"])
|
|
138
|
+
if section["stderr"].strip():
|
|
139
|
+
lines.extend(f" - {item}" for item in stderr_summary)
|
|
140
|
+
else:
|
|
141
|
+
lines.append(" - 暂无")
|
|
142
|
+
lines.extend(["", "## 人工补充验证"])
|
|
143
|
+
if result == "通过":
|
|
144
|
+
lines.append("- 如果本次改动涉及服务启动或接口行为,建议补一次人工 smoke test。")
|
|
145
|
+
elif result == "未识别命令":
|
|
146
|
+
lines.append("- 当前仓库未能自动识别验证命令,请人工补充后再执行。")
|
|
147
|
+
elif result == "超时":
|
|
148
|
+
lines.append("- 验证执行超时,请检查命令是否卡住或是否需要更长超时时间。")
|
|
149
|
+
else:
|
|
150
|
+
lines.append("- 请根据失败输出修复问题后重新执行验证。")
|
|
151
|
+
lines.append("")
|
|
152
|
+
write_managed_text(config, output, "\n".join(lines))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def merge_result(current: str, new: str) -> str:
|
|
156
|
+
order = {"通过": 0, "未识别命令": 1, "失败": 2, "超时": 3}
|
|
157
|
+
return new if order.get(new, 0) > order.get(current, 0) else current
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def finalize_status(
|
|
161
|
+
config: dict,
|
|
162
|
+
session_meta: dict,
|
|
163
|
+
plan: dict,
|
|
164
|
+
overall_result: str,
|
|
165
|
+
blocked_reason: str,
|
|
166
|
+
) -> None:
|
|
167
|
+
status_path = data_artifact_path(config, "status.json", session_meta)
|
|
168
|
+
status = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
169
|
+
phase, awaiting_confirmation, pending_for, _ = phase_after("verify", config["mode"])
|
|
170
|
+
existing_blockers = [item for item in status.get("blocked_tasks", []) if item not in VERIFY_BLOCKERS]
|
|
171
|
+
finished_at = now_iso()
|
|
172
|
+
duration_seconds = workflow_duration_seconds(session_meta, status, finished_at)
|
|
173
|
+
|
|
174
|
+
if overall_result == "通过":
|
|
175
|
+
status.update(
|
|
176
|
+
{
|
|
177
|
+
"phase": "done",
|
|
178
|
+
"current_task": "验证通过。",
|
|
179
|
+
"progress": 100,
|
|
180
|
+
"awaiting_confirmation": False,
|
|
181
|
+
"pending_confirmation_for": "",
|
|
182
|
+
"next_action": "工作流已完成。",
|
|
183
|
+
"completed_tasks": list(dict.fromkeys(status.get("completed_tasks", []) + ["已执行验证"])),
|
|
184
|
+
"blocked_tasks": existing_blockers,
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
status.update(
|
|
189
|
+
{
|
|
190
|
+
"phase": "blocked",
|
|
191
|
+
"current_task": (
|
|
192
|
+
"未识别到验证命令。"
|
|
193
|
+
if overall_result == "未识别命令"
|
|
194
|
+
else ("验证执行超时。" if overall_result == "超时" else "验证失败。")
|
|
195
|
+
),
|
|
196
|
+
"progress": 95,
|
|
197
|
+
"awaiting_confirmation": awaiting_confirmation if overall_result != "超时" else False,
|
|
198
|
+
"pending_confirmation_for": pending_for if overall_result not in ("通过", "超时") else "",
|
|
199
|
+
"next_action": (
|
|
200
|
+
"请人工补充验证命令后重新执行验证。"
|
|
201
|
+
if overall_result == "未识别命令"
|
|
202
|
+
else ("请检查命令是否卡住,或延长超时时间后重试。" if overall_result == "超时" else "请修复验证失败项后重新执行。")
|
|
203
|
+
),
|
|
204
|
+
"completed_tasks": list(dict.fromkeys(status.get("completed_tasks", []) + ["已执行验证"])),
|
|
205
|
+
"blocked_tasks": list(dict.fromkeys(existing_blockers + [blocked_reason or "验证失败"])),
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
status.update(
|
|
210
|
+
{
|
|
211
|
+
"started_at": status.get("started_at") or session_meta.get("started_at", ""),
|
|
212
|
+
"finished_at": finished_at,
|
|
213
|
+
"duration_seconds": duration_seconds,
|
|
214
|
+
"updated_at": finished_at,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
notification_result = notify_workflow_result(config, session_meta, plan, status, overall_result)
|
|
219
|
+
status["notification_status"] = str(notification_result.get("status", "skipped"))
|
|
220
|
+
status["notification_message"] = str(notification_result.get("message", ""))
|
|
221
|
+
if (
|
|
222
|
+
overall_result == "通过"
|
|
223
|
+
and feishu_config(config).get("enabled")
|
|
224
|
+
and not is_standard_workflow_notification(config, session_meta, status, overall_result, notification_result)
|
|
225
|
+
):
|
|
226
|
+
status.update(
|
|
227
|
+
{
|
|
228
|
+
"phase": "blocked",
|
|
229
|
+
"current_task": "验证通过但飞书通知发送失败。",
|
|
230
|
+
"progress": 95,
|
|
231
|
+
"awaiting_confirmation": False,
|
|
232
|
+
"pending_confirmation_for": "",
|
|
233
|
+
"next_action": "请修复飞书通知配置或网络后重新执行 /se:verify。",
|
|
234
|
+
"blocked_tasks": list(dict.fromkeys(status.get("blocked_tasks", []) + ["飞书通知发送失败"])),
|
|
235
|
+
"updated_at": now_iso(),
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
write_managed_json(config, status_path, status)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main() -> None:
|
|
242
|
+
parser = argparse.ArgumentParser(description="执行验证命令并生成 verify.md。")
|
|
243
|
+
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
244
|
+
parser.add_argument("--timeout-seconds", type=int, default=300)
|
|
245
|
+
parser.add_argument("--force", action="store_true", help="即使当前会话已完成,也强制重新执行验证并覆盖结果。")
|
|
246
|
+
args = parser.parse_args()
|
|
247
|
+
|
|
248
|
+
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
249
|
+
config = load_workspace_config(workspace)
|
|
250
|
+
session_meta = current_session_meta(config)
|
|
251
|
+
plan = read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
252
|
+
status_path = data_artifact_path(config, "status.json", session_meta)
|
|
253
|
+
existing_status = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
254
|
+
if (
|
|
255
|
+
not args.force
|
|
256
|
+
and str(existing_status.get("phase", "")).strip() == "done"
|
|
257
|
+
and str(existing_status.get("notification_status", "")).strip() == "sent"
|
|
258
|
+
and has_sent_standard_notification(config, session_meta, existing_status)
|
|
259
|
+
):
|
|
260
|
+
print("当前会话已完成且通知已发送,跳过重复验证。需要重跑请显式传入 --force。")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
codebases = planned_codebases(config, session_meta)
|
|
264
|
+
target_plans = plan.get("target_codebases", [])
|
|
265
|
+
target_map = {str(item.get("path")): item for item in target_plans}
|
|
266
|
+
|
|
267
|
+
started = time.time()
|
|
268
|
+
sections: list[dict] = []
|
|
269
|
+
overall_result = "通过"
|
|
270
|
+
blocked_reason = ""
|
|
271
|
+
try:
|
|
272
|
+
for codebase in codebases:
|
|
273
|
+
target_plan = target_map.get(str(codebase), {})
|
|
274
|
+
detected = target_plan.get("detected_project") or detect_project(codebase, config)
|
|
275
|
+
commands = [
|
|
276
|
+
{"kind": kind, "command": item}
|
|
277
|
+
for kind, item in [
|
|
278
|
+
("test", detected.get("test_command", "")),
|
|
279
|
+
("start", detected.get("start_command", "")),
|
|
280
|
+
("verify", detected.get("verify_command", "")),
|
|
281
|
+
]
|
|
282
|
+
if item
|
|
283
|
+
]
|
|
284
|
+
verify_command = detected.get("verify_command", "")
|
|
285
|
+
if not verify_command:
|
|
286
|
+
sections.append(
|
|
287
|
+
{
|
|
288
|
+
"name": codebase.name,
|
|
289
|
+
"path": str(codebase),
|
|
290
|
+
"commands": commands,
|
|
291
|
+
"result": "未识别命令",
|
|
292
|
+
"exit_code": "n/a",
|
|
293
|
+
"duration": 0.0,
|
|
294
|
+
"stdout": "",
|
|
295
|
+
"stderr": "",
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
overall_result = merge_result(overall_result, "未识别命令")
|
|
299
|
+
blocked_reason = "未识别到验证命令"
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
repo_started = time.time()
|
|
303
|
+
result = subprocess.run(
|
|
304
|
+
verify_command,
|
|
305
|
+
cwd=codebase,
|
|
306
|
+
shell=True,
|
|
307
|
+
capture_output=True,
|
|
308
|
+
text=True,
|
|
309
|
+
timeout=args.timeout_seconds,
|
|
310
|
+
check=False,
|
|
311
|
+
)
|
|
312
|
+
repo_duration = time.time() - repo_started
|
|
313
|
+
repo_result = "通过" if result.returncode == 0 else "失败"
|
|
314
|
+
sections.append(
|
|
315
|
+
{
|
|
316
|
+
"name": codebase.name,
|
|
317
|
+
"path": str(codebase),
|
|
318
|
+
"commands": commands,
|
|
319
|
+
"result": repo_result,
|
|
320
|
+
"exit_code": str(result.returncode),
|
|
321
|
+
"duration": repo_duration,
|
|
322
|
+
"stdout": result.stdout,
|
|
323
|
+
"stderr": result.stderr,
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
if repo_result != "通过":
|
|
327
|
+
overall_result = merge_result(overall_result, "失败")
|
|
328
|
+
blocked_reason = "验证失败"
|
|
329
|
+
duration = time.time() - started
|
|
330
|
+
status_for_duration = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
331
|
+
workflow_duration = workflow_duration_seconds(session_meta, status_for_duration, now_iso())
|
|
332
|
+
write_report(
|
|
333
|
+
config,
|
|
334
|
+
report_artifact_path(config, "verify.md", session_meta),
|
|
335
|
+
sections,
|
|
336
|
+
plan.get("test_plan", []),
|
|
337
|
+
overall_result,
|
|
338
|
+
duration,
|
|
339
|
+
workflow_duration,
|
|
340
|
+
)
|
|
341
|
+
write_managed_json(
|
|
342
|
+
config,
|
|
343
|
+
data_artifact_path(config, "verify.json", session_meta),
|
|
344
|
+
{
|
|
345
|
+
"session_id": session_meta["session_id"],
|
|
346
|
+
"source": "run-workflow.py verify",
|
|
347
|
+
"schema_version": 1,
|
|
348
|
+
"result": overall_result,
|
|
349
|
+
"sections": sections,
|
|
350
|
+
"duration_seconds": duration,
|
|
351
|
+
"workflow_duration_seconds": workflow_duration,
|
|
352
|
+
"created_at": now_iso(),
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
finalize_status(config, session_meta, plan, overall_result, blocked_reason)
|
|
356
|
+
except subprocess.TimeoutExpired as error:
|
|
357
|
+
duration = time.time() - started
|
|
358
|
+
sections.append(
|
|
359
|
+
{
|
|
360
|
+
"name": codebases[min(len(sections), len(codebases) - 1)].name if codebases else "未知仓库",
|
|
361
|
+
"path": str(codebases[min(len(sections), len(codebases) - 1)]) if codebases else "",
|
|
362
|
+
"commands": [],
|
|
363
|
+
"result": "超时",
|
|
364
|
+
"exit_code": "timeout",
|
|
365
|
+
"duration": duration,
|
|
366
|
+
"stdout": error.stdout or "",
|
|
367
|
+
"stderr": error.stderr or "",
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
status_for_duration = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
371
|
+
workflow_duration = workflow_duration_seconds(session_meta, status_for_duration, now_iso())
|
|
372
|
+
write_report(
|
|
373
|
+
config,
|
|
374
|
+
report_artifact_path(config, "verify.md", session_meta),
|
|
375
|
+
sections,
|
|
376
|
+
plan.get("test_plan", []),
|
|
377
|
+
"超时",
|
|
378
|
+
duration,
|
|
379
|
+
workflow_duration,
|
|
380
|
+
)
|
|
381
|
+
write_managed_json(
|
|
382
|
+
config,
|
|
383
|
+
data_artifact_path(config, "verify.json", session_meta),
|
|
384
|
+
{
|
|
385
|
+
"session_id": session_meta["session_id"],
|
|
386
|
+
"source": "run-workflow.py verify",
|
|
387
|
+
"schema_version": 1,
|
|
388
|
+
"result": "超时",
|
|
389
|
+
"sections": sections,
|
|
390
|
+
"duration_seconds": duration,
|
|
391
|
+
"workflow_duration_seconds": workflow_duration,
|
|
392
|
+
"created_at": now_iso(),
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
finalize_status(config, session_meta, plan, "超时", "验证执行超时")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
if __name__ == "__main__":
|
|
399
|
+
main()
|