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,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from common import (
|
|
10
|
+
current_session_meta,
|
|
11
|
+
data_artifact_path,
|
|
12
|
+
detect_project,
|
|
13
|
+
ensure_status,
|
|
14
|
+
extract_todo_keywords,
|
|
15
|
+
load_workspace_config,
|
|
16
|
+
now_iso,
|
|
17
|
+
parse_task_blocks,
|
|
18
|
+
read_json,
|
|
19
|
+
read_text,
|
|
20
|
+
relative_to,
|
|
21
|
+
report_artifact_path,
|
|
22
|
+
resolve_target_codebases,
|
|
23
|
+
todo_path,
|
|
24
|
+
workspace_root,
|
|
25
|
+
write_managed_json,
|
|
26
|
+
write_managed_text,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_rg(keyword: str, codebase: Path, max_count: int = 12) -> list[dict[str, object]]:
|
|
31
|
+
if not shutil.which("rg"):
|
|
32
|
+
return []
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
[
|
|
35
|
+
"rg",
|
|
36
|
+
"--line-number",
|
|
37
|
+
"--column",
|
|
38
|
+
"--no-heading",
|
|
39
|
+
"--fixed-strings",
|
|
40
|
+
"--glob",
|
|
41
|
+
"!target/**",
|
|
42
|
+
"--glob",
|
|
43
|
+
"!build/**",
|
|
44
|
+
"--glob",
|
|
45
|
+
"!node_modules/**",
|
|
46
|
+
"--glob",
|
|
47
|
+
"!.git/**",
|
|
48
|
+
keyword,
|
|
49
|
+
str(codebase),
|
|
50
|
+
],
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
check=False,
|
|
54
|
+
)
|
|
55
|
+
if result.returncode not in (0, 1):
|
|
56
|
+
return []
|
|
57
|
+
matches: list[dict[str, object]] = []
|
|
58
|
+
for line in result.stdout.splitlines()[:max_count]:
|
|
59
|
+
parts = line.split(":", 3)
|
|
60
|
+
if len(parts) < 4:
|
|
61
|
+
continue
|
|
62
|
+
file_path, line_no, column_no, snippet = parts
|
|
63
|
+
matches.append(
|
|
64
|
+
{
|
|
65
|
+
"file": str(Path(file_path).resolve()),
|
|
66
|
+
"relative_file": relative_to(file_path, codebase),
|
|
67
|
+
"line": int(line_no) if line_no.isdigit() else 0,
|
|
68
|
+
"column": int(column_no) if column_no.isdigit() else 0,
|
|
69
|
+
"snippet": snippet.strip()[:240],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
return matches
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_discovery_markdown(discovery: dict) -> str:
|
|
76
|
+
lines = [
|
|
77
|
+
"# 上下文定位",
|
|
78
|
+
"",
|
|
79
|
+
"## 任务",
|
|
80
|
+
]
|
|
81
|
+
tasks = discovery.get("tasks", [])
|
|
82
|
+
if tasks:
|
|
83
|
+
lines.extend(f"- {task.get('title')}" for task in tasks)
|
|
84
|
+
else:
|
|
85
|
+
lines.append("- 暂无")
|
|
86
|
+
lines.extend(["", "## 关键词"])
|
|
87
|
+
keywords = discovery.get("keywords", [])
|
|
88
|
+
lines.extend(f"- `{item}`" for item in keywords) if keywords else lines.append("- 暂无")
|
|
89
|
+
lines.extend(["", "## 代码命中"])
|
|
90
|
+
for codebase in discovery.get("codebases", []):
|
|
91
|
+
lines.append(f"### {codebase.get('name')}")
|
|
92
|
+
lines.append(f"- 路径:{codebase.get('path')}")
|
|
93
|
+
lines.append(f"- 构建工具:{codebase.get('detected_project', {}).get('build_tool') or '未知'}")
|
|
94
|
+
matches = codebase.get("matches", [])
|
|
95
|
+
if not matches:
|
|
96
|
+
lines.append("- 未命中关键词")
|
|
97
|
+
continue
|
|
98
|
+
for item in matches[:20]:
|
|
99
|
+
lines.append(
|
|
100
|
+
f"- `{item.get('relative_file')}`:{item.get('line')} "
|
|
101
|
+
f"命中 `{item.get('keyword')}`:{item.get('snippet')}"
|
|
102
|
+
)
|
|
103
|
+
lines.extend(["", "## 计划提示"])
|
|
104
|
+
for hint in discovery.get("planning_hints", []):
|
|
105
|
+
lines.append(f"- {hint}")
|
|
106
|
+
lines.append("")
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main() -> None:
|
|
111
|
+
parser = argparse.ArgumentParser(description="根据 todo 关键词定位代码证据,生成 discovery 产物。")
|
|
112
|
+
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
|
|
115
|
+
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
116
|
+
config = load_workspace_config(workspace)
|
|
117
|
+
session_meta = current_session_meta(config)
|
|
118
|
+
todo_text = read_text(todo_path(config))
|
|
119
|
+
codebases, resolution = resolve_target_codebases(config, todo_text)
|
|
120
|
+
keywords = extract_todo_keywords(todo_text)
|
|
121
|
+
if not keywords:
|
|
122
|
+
keywords = [task["title"] for task in parse_task_blocks(todo_text)[:8]]
|
|
123
|
+
|
|
124
|
+
codebase_sections: list[dict] = []
|
|
125
|
+
all_matches: list[dict] = []
|
|
126
|
+
for codebase in codebases:
|
|
127
|
+
matches: list[dict] = []
|
|
128
|
+
for keyword in keywords:
|
|
129
|
+
for match in run_rg(keyword, codebase):
|
|
130
|
+
match["keyword"] = keyword
|
|
131
|
+
matches.append(match)
|
|
132
|
+
seen: set[tuple[str, int, str]] = set()
|
|
133
|
+
deduped: list[dict] = []
|
|
134
|
+
for match in matches:
|
|
135
|
+
key = (str(match.get("file", "")), int(match.get("line", 0)), str(match.get("keyword", "")))
|
|
136
|
+
if key in seen:
|
|
137
|
+
continue
|
|
138
|
+
seen.add(key)
|
|
139
|
+
deduped.append(match)
|
|
140
|
+
all_matches.extend(deduped)
|
|
141
|
+
codebase_sections.append(
|
|
142
|
+
{
|
|
143
|
+
"name": codebase.name,
|
|
144
|
+
"path": str(codebase),
|
|
145
|
+
"detected_project": detect_project(codebase, config),
|
|
146
|
+
"matches": deduped[:80],
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
planning_hints = []
|
|
151
|
+
if all_matches:
|
|
152
|
+
planning_hints.append("优先把命中关键词的文件作为影响范围候选,并结合调用链继续定位。")
|
|
153
|
+
else:
|
|
154
|
+
planning_hints.append("关键词未命中代码,计划阶段需要基于项目结构继续定位真实入口。")
|
|
155
|
+
if len(codebases) > 1:
|
|
156
|
+
planning_hints.append("本轮涉及多个仓库,后续 review 和 verify 必须逐仓执行。")
|
|
157
|
+
|
|
158
|
+
discovery = {
|
|
159
|
+
"session_id": session_meta["session_id"],
|
|
160
|
+
"tasks": parse_task_blocks(todo_text),
|
|
161
|
+
"keywords": keywords,
|
|
162
|
+
"service_resolution": resolution,
|
|
163
|
+
"codebases": codebase_sections,
|
|
164
|
+
"planning_hints": planning_hints,
|
|
165
|
+
"created_at": now_iso(),
|
|
166
|
+
}
|
|
167
|
+
write_managed_json(config, data_artifact_path(config, "discovery.json", session_meta), discovery)
|
|
168
|
+
write_managed_text(config, report_artifact_path(config, "discovery.md", session_meta), build_discovery_markdown(discovery))
|
|
169
|
+
|
|
170
|
+
status = ensure_status(config, session_meta, read_json(data_artifact_path(config, "status.json", session_meta), {}))
|
|
171
|
+
status.update(
|
|
172
|
+
{
|
|
173
|
+
"phase": "discover",
|
|
174
|
+
"current_task": "已完成上下文定位。",
|
|
175
|
+
"progress": 15,
|
|
176
|
+
"next_action": "基于 discovery.json 生成可执行计划。",
|
|
177
|
+
"completed_tasks": list(dict.fromkeys(status.get("completed_tasks", []) + ["已完成上下文定位"])),
|
|
178
|
+
"updated_at": now_iso(),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
write_managed_json(config, data_artifact_path(config, "status.json", session_meta), status)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from common import (
|
|
9
|
+
current_session_meta,
|
|
10
|
+
data_artifact_path,
|
|
11
|
+
ensure_status,
|
|
12
|
+
load_workspace_config,
|
|
13
|
+
now_iso,
|
|
14
|
+
phase_after,
|
|
15
|
+
planned_codebases,
|
|
16
|
+
read_json,
|
|
17
|
+
report_artifact_path,
|
|
18
|
+
workspace_root,
|
|
19
|
+
write_managed_json,
|
|
20
|
+
write_managed_text,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_git(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
25
|
+
return subprocess.run(args, cwd=cwd, capture_output=True, text=True, check=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_git_repo(workspace: Path) -> bool:
|
|
29
|
+
result = run_git(["git", "rev-parse", "--is-inside-work-tree"], workspace)
|
|
30
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def changed_files(workspace: Path) -> list[str]:
|
|
34
|
+
result = run_git(["git", "status", "--porcelain"], workspace)
|
|
35
|
+
if result.returncode != 0:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
files: list[str] = []
|
|
39
|
+
for line in result.stdout.splitlines():
|
|
40
|
+
if not line.strip():
|
|
41
|
+
continue
|
|
42
|
+
path = line[3:]
|
|
43
|
+
if " -> " in path:
|
|
44
|
+
path = path.split(" -> ", 1)[1]
|
|
45
|
+
normalized = path.strip()
|
|
46
|
+
if normalized.startswith(".super-engineer/"):
|
|
47
|
+
continue
|
|
48
|
+
files.append(str((workspace / normalized).resolve()))
|
|
49
|
+
return files
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def diff_summary(workspace: Path) -> list[str]:
|
|
53
|
+
result = run_git(["git", "diff", "--stat"], workspace)
|
|
54
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
55
|
+
return []
|
|
56
|
+
lines: list[str] = []
|
|
57
|
+
for line in result.stdout.splitlines():
|
|
58
|
+
stripped = line.rstrip()
|
|
59
|
+
if not stripped or "file changed" in stripped or "files changed" in stripped:
|
|
60
|
+
continue
|
|
61
|
+
if ".super-engineer/" in stripped:
|
|
62
|
+
continue
|
|
63
|
+
lines.append(stripped)
|
|
64
|
+
return lines
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_tests(files: list[str]) -> list[str]:
|
|
68
|
+
return [item for item in files if item.endswith("Test.java") or "/test/" in item]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_findings(plan_files: list[str], changed: list[str], repo_mode: str, repo_name: str) -> list[dict[str, object]]:
|
|
72
|
+
findings: list[dict[str, object]] = []
|
|
73
|
+
planned = set(plan_files)
|
|
74
|
+
changed_set = set(changed)
|
|
75
|
+
|
|
76
|
+
if repo_mode == "git" and not changed:
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
"severity": "blocker",
|
|
80
|
+
"scope": repo_name,
|
|
81
|
+
"title": "未检测到代码差异",
|
|
82
|
+
"detail": f"{repo_name}:未检测到本地代码差异,本次审查只能围绕计划范围进行。",
|
|
83
|
+
"blocking": True,
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if repo_mode == "fallback":
|
|
88
|
+
findings.append(
|
|
89
|
+
{
|
|
90
|
+
"severity": "warning",
|
|
91
|
+
"scope": repo_name,
|
|
92
|
+
"title": "无法读取 Git 差异",
|
|
93
|
+
"detail": f"{repo_name}:当前无法读取 Git 代码差异,本次审查只能基于计划范围做兜底判断。",
|
|
94
|
+
"blocking": False,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
unplanned = sorted(item for item in changed_set if item not in planned and not item.startswith(".super-engineer/"))
|
|
99
|
+
if unplanned:
|
|
100
|
+
findings.append(
|
|
101
|
+
{
|
|
102
|
+
"severity": "warning",
|
|
103
|
+
"scope": repo_name,
|
|
104
|
+
"title": "计划外改动",
|
|
105
|
+
"detail": f"{repo_name}:发现计划外改动文件:{', '.join(unplanned)}。请确认是否需要扩展计划范围。",
|
|
106
|
+
"blocking": False,
|
|
107
|
+
"files": unplanned,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if changed and planned:
|
|
112
|
+
unchanged_planned = sorted(item for item in planned if item not in changed_set)
|
|
113
|
+
if unchanged_planned:
|
|
114
|
+
findings.append(
|
|
115
|
+
{
|
|
116
|
+
"severity": "info",
|
|
117
|
+
"scope": repo_name,
|
|
118
|
+
"title": "计划内文件未改动",
|
|
119
|
+
"detail": f"{repo_name}:部分计划内文件尚未发生改动:{', '.join(unchanged_planned[:4])}。请确认是尚未实现,还是计划范围过宽。",
|
|
120
|
+
"blocking": False,
|
|
121
|
+
"files": unchanged_planned[:8],
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
touches_app_code = any(item.endswith((".java", ".kt", ".xml", ".yml")) and "Test" not in item for item in changed)
|
|
126
|
+
if touches_app_code and not find_tests(changed):
|
|
127
|
+
findings.append(
|
|
128
|
+
{
|
|
129
|
+
"severity": "warning",
|
|
130
|
+
"scope": repo_name,
|
|
131
|
+
"title": "缺少测试改动",
|
|
132
|
+
"detail": f"{repo_name}:应用代码已经变更,但没有检测到对应测试改动。请确认是否需要补充测试或人工验证。",
|
|
133
|
+
"blocking": False,
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if not findings:
|
|
138
|
+
findings.append(
|
|
139
|
+
{
|
|
140
|
+
"severity": "info",
|
|
141
|
+
"scope": repo_name,
|
|
142
|
+
"title": "审查通过",
|
|
143
|
+
"detail": f"{repo_name}:当前范围内未发现明显的计划与代码差异不一致问题。",
|
|
144
|
+
"blocking": False,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
return findings
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_review_markdown(plan: dict, sections: list[dict], findings: list[dict[str, object]]) -> str:
|
|
151
|
+
lines = [
|
|
152
|
+
"# 代码审查",
|
|
153
|
+
"",
|
|
154
|
+
"## 审查范围",
|
|
155
|
+
plan.get("requirement_summary") or "请补充审查范围。",
|
|
156
|
+
"",
|
|
157
|
+
"## 目标仓库",
|
|
158
|
+
]
|
|
159
|
+
targets = plan.get("target_codebases", [])
|
|
160
|
+
if targets:
|
|
161
|
+
lines.extend(f"- {target.get('name')}:{target.get('path')}" for target in targets)
|
|
162
|
+
else:
|
|
163
|
+
lines.append(f"- {plan.get('resolved_code_path') or '未知'}")
|
|
164
|
+
lines.extend([
|
|
165
|
+
"",
|
|
166
|
+
"## 审查结论",
|
|
167
|
+
])
|
|
168
|
+
lines.extend(f"- [{item.get('severity')}] {item.get('title')}:{item.get('detail')}" for item in findings)
|
|
169
|
+
lines.extend(["", "## 审查门禁"])
|
|
170
|
+
blocking = [item for item in findings if item.get("blocking")]
|
|
171
|
+
if blocking:
|
|
172
|
+
lines.append("- blocked:存在阻塞项,进入验证前必须处理。")
|
|
173
|
+
else:
|
|
174
|
+
lines.append("- passed:未发现阻塞级问题。")
|
|
175
|
+
lines.extend(["", "## 仓库审查明细"])
|
|
176
|
+
for section in sections:
|
|
177
|
+
lines.extend(
|
|
178
|
+
[
|
|
179
|
+
f"### {section['name']}",
|
|
180
|
+
f"- 审查模式:{'基于 Git 代码差异的审查' if section['repo_mode'] == 'git' else '基于计划范围的兜底审查'}",
|
|
181
|
+
"- 变更文件:",
|
|
182
|
+
]
|
|
183
|
+
)
|
|
184
|
+
if section["changed"]:
|
|
185
|
+
lines.extend(f" - `{item}`" for item in section["changed"])
|
|
186
|
+
else:
|
|
187
|
+
lines.append(" - 暂无")
|
|
188
|
+
lines.append("- 差异摘要:")
|
|
189
|
+
if section["summary"]:
|
|
190
|
+
lines.extend(f" - {item}" for item in section["summary"])
|
|
191
|
+
else:
|
|
192
|
+
lines.append(" - 暂无")
|
|
193
|
+
lines.extend(["", "## 已检查的计划步骤"])
|
|
194
|
+
if plan.get("change_steps"):
|
|
195
|
+
lines.extend(f"- {step['title']}" for step in plan["change_steps"])
|
|
196
|
+
else:
|
|
197
|
+
lines.append("- 暂无")
|
|
198
|
+
lines.extend(["", "## 待复核风险"])
|
|
199
|
+
if plan.get("risks"):
|
|
200
|
+
lines.extend(f"- {item}" for item in plan["risks"])
|
|
201
|
+
else:
|
|
202
|
+
lines.append("- 暂无")
|
|
203
|
+
lines.extend(["", "## 测试覆盖说明"])
|
|
204
|
+
if plan.get("test_plan"):
|
|
205
|
+
lines.extend(f"- {item}" for item in plan["test_plan"])
|
|
206
|
+
else:
|
|
207
|
+
lines.append("- 暂无")
|
|
208
|
+
lines.append("")
|
|
209
|
+
return "\n".join(lines)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> None:
|
|
213
|
+
parser = argparse.ArgumentParser(description="基于计划和真实代码差异生成 review.md。")
|
|
214
|
+
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
215
|
+
args = parser.parse_args()
|
|
216
|
+
|
|
217
|
+
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
218
|
+
config = load_workspace_config(workspace)
|
|
219
|
+
session_meta = current_session_meta(config)
|
|
220
|
+
plan = read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
221
|
+
codebases = planned_codebases(config, session_meta)
|
|
222
|
+
target_plans = plan.get("target_codebases", [])
|
|
223
|
+
target_map = {str(item.get("path")): item for item in target_plans}
|
|
224
|
+
|
|
225
|
+
sections: list[dict] = []
|
|
226
|
+
findings: list[dict[str, object]] = []
|
|
227
|
+
for codebase in codebases:
|
|
228
|
+
repo_name = codebase.name
|
|
229
|
+
target_plan = target_map.get(str(codebase), {})
|
|
230
|
+
plan_files = target_plan.get("impacted_files", [])
|
|
231
|
+
if is_git_repo(codebase):
|
|
232
|
+
changed = changed_files(codebase)
|
|
233
|
+
summary = diff_summary(codebase)
|
|
234
|
+
repo_mode = "git"
|
|
235
|
+
else:
|
|
236
|
+
changed = plan_files
|
|
237
|
+
summary = []
|
|
238
|
+
repo_mode = "fallback"
|
|
239
|
+
sections.append(
|
|
240
|
+
{
|
|
241
|
+
"name": repo_name,
|
|
242
|
+
"path": str(codebase),
|
|
243
|
+
"changed": changed,
|
|
244
|
+
"summary": summary,
|
|
245
|
+
"repo_mode": repo_mode,
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
findings.extend(build_findings(plan_files, changed, repo_mode, repo_name))
|
|
249
|
+
review_result = "blocked" if any(item.get("blocking") for item in findings) else "passed"
|
|
250
|
+
write_managed_json(
|
|
251
|
+
config,
|
|
252
|
+
data_artifact_path(config, "review.json", session_meta),
|
|
253
|
+
{
|
|
254
|
+
"session_id": session_meta["session_id"],
|
|
255
|
+
"source": "run-workflow.py review",
|
|
256
|
+
"schema_version": 1,
|
|
257
|
+
"result": review_result,
|
|
258
|
+
"sections": sections,
|
|
259
|
+
"findings": findings,
|
|
260
|
+
"created_at": now_iso(),
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
write_managed_text(
|
|
264
|
+
config,
|
|
265
|
+
report_artifact_path(config, "review.md", session_meta),
|
|
266
|
+
build_review_markdown(plan, sections, findings) + "\n",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
status = ensure_status(config, session_meta, read_json(data_artifact_path(config, "status.json", session_meta), {}))
|
|
270
|
+
phase, awaiting_confirmation, pending_for, next_action = phase_after("review", config["mode"])
|
|
271
|
+
if review_result == "blocked":
|
|
272
|
+
phase = "blocked"
|
|
273
|
+
awaiting_confirmation = False
|
|
274
|
+
pending_for = ""
|
|
275
|
+
next_action = "请先处理 review 阻塞项,再重新执行 review。"
|
|
276
|
+
status.update(
|
|
277
|
+
{
|
|
278
|
+
"phase": phase,
|
|
279
|
+
"current_task": "代码审查已完成。" if review_result == "passed" else "代码审查发现阻塞项。",
|
|
280
|
+
"progress": 75,
|
|
281
|
+
"awaiting_confirmation": awaiting_confirmation,
|
|
282
|
+
"pending_confirmation_for": pending_for,
|
|
283
|
+
"next_action": next_action,
|
|
284
|
+
"completed_tasks": list(dict.fromkeys(status.get("completed_tasks", []) + ["已完成代码审查"])),
|
|
285
|
+
"blocked_tasks": list(dict.fromkeys(status.get("blocked_tasks", []) + [item["title"] for item in findings if item.get("blocking")])),
|
|
286
|
+
"started_at": status.get("started_at") or session_meta.get("started_at", ""),
|
|
287
|
+
"updated_at": now_iso(),
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
write_managed_json(config, data_artifact_path(config, "status.json", session_meta), status)
|
|
291
|
+
if review_result == "blocked":
|
|
292
|
+
raise SystemExit("代码审查发现阻塞项,请查看 review.md。")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from common import (
|
|
9
|
+
current_session_meta,
|
|
10
|
+
data_artifact_path,
|
|
11
|
+
ensure_status,
|
|
12
|
+
load_workspace_config,
|
|
13
|
+
now_iso,
|
|
14
|
+
planned_codebases,
|
|
15
|
+
read_json,
|
|
16
|
+
report_artifact_path,
|
|
17
|
+
workspace_root,
|
|
18
|
+
write_managed_json,
|
|
19
|
+
write_managed_text,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_git(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
24
|
+
return subprocess.run(args, cwd=cwd, capture_output=True, text=True, check=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_git_repo(path: Path) -> bool:
|
|
28
|
+
result = run_git(["git", "rev-parse", "--is-inside-work-tree"], path)
|
|
29
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def changed_files(path: Path) -> list[str]:
|
|
33
|
+
result = run_git(["git", "status", "--porcelain"], path)
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
return []
|
|
36
|
+
files: list[str] = []
|
|
37
|
+
for line in result.stdout.splitlines():
|
|
38
|
+
if not line.strip():
|
|
39
|
+
continue
|
|
40
|
+
normalized = line[3:].strip()
|
|
41
|
+
if " -> " in normalized:
|
|
42
|
+
normalized = normalized.split(" -> ", 1)[1]
|
|
43
|
+
if normalized.startswith(".super-engineer/"):
|
|
44
|
+
continue
|
|
45
|
+
files.append(str((path / normalized).resolve()))
|
|
46
|
+
return files
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_self_check(plan: dict, sections: list[dict]) -> dict:
|
|
50
|
+
findings: list[dict] = []
|
|
51
|
+
plan_files = set(str(item) for item in plan.get("impacted_files", []))
|
|
52
|
+
changed = {item for section in sections for item in section.get("changed_files", [])}
|
|
53
|
+
has_git_repo = any(section.get("repo_mode") == "git" for section in sections)
|
|
54
|
+
changed_app = [item for item in changed if item.endswith((".java", ".kt", ".xml", ".yml", ".yaml", ".ts", ".tsx", ".js"))]
|
|
55
|
+
changed_tests = [item for item in changed if "/test/" in item or item.endswith(("Test.java", ".test.ts", ".spec.ts", ".test.js"))]
|
|
56
|
+
|
|
57
|
+
if not changed and has_git_repo:
|
|
58
|
+
findings.append(
|
|
59
|
+
{
|
|
60
|
+
"severity": "blocker",
|
|
61
|
+
"title": "未检测到代码差异",
|
|
62
|
+
"detail": "实现阶段结束后没有发现本地代码变更,无法进入有效审查。",
|
|
63
|
+
"blocking": True,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
elif not changed:
|
|
67
|
+
findings.append(
|
|
68
|
+
{
|
|
69
|
+
"severity": "warning",
|
|
70
|
+
"title": "无法读取代码差异",
|
|
71
|
+
"detail": "目标目录不是 Git 仓库,自查只能依赖后续人工 review 与验证命令。",
|
|
72
|
+
"blocking": False,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
unplanned = sorted(item for item in changed if plan_files and item not in plan_files)
|
|
76
|
+
if unplanned:
|
|
77
|
+
findings.append(
|
|
78
|
+
{
|
|
79
|
+
"severity": "warning",
|
|
80
|
+
"title": "存在计划外改动",
|
|
81
|
+
"detail": "请确认这些文件是否应补入计划:" + ",".join(unplanned[:8]),
|
|
82
|
+
"blocking": False,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
if changed_app and not changed_tests:
|
|
86
|
+
findings.append(
|
|
87
|
+
{
|
|
88
|
+
"severity": "warning",
|
|
89
|
+
"title": "应用代码改动缺少测试改动",
|
|
90
|
+
"detail": "如果本轮改动无法自动化测试,需要在 verify.md 中补充人工验证项。",
|
|
91
|
+
"blocking": False,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
if not findings:
|
|
95
|
+
findings.append(
|
|
96
|
+
{
|
|
97
|
+
"severity": "info",
|
|
98
|
+
"title": "自查通过",
|
|
99
|
+
"detail": "未发现会阻断 review 的基础问题。",
|
|
100
|
+
"blocking": False,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
return {
|
|
104
|
+
"result": "blocked" if any(item.get("blocking") for item in findings) else "passed",
|
|
105
|
+
"findings": findings,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_markdown(self_check: dict, sections: list[dict]) -> str:
|
|
110
|
+
lines = [
|
|
111
|
+
"# 实现自查",
|
|
112
|
+
"",
|
|
113
|
+
"## 总体结果",
|
|
114
|
+
f"- {self_check.get('result')}",
|
|
115
|
+
"",
|
|
116
|
+
"## 自查发现",
|
|
117
|
+
]
|
|
118
|
+
for finding in self_check.get("findings", []):
|
|
119
|
+
lines.append(
|
|
120
|
+
f"- [{finding.get('severity')}] {finding.get('title')}:{finding.get('detail')}"
|
|
121
|
+
)
|
|
122
|
+
lines.extend(["", "## 变更文件"])
|
|
123
|
+
for section in sections:
|
|
124
|
+
lines.append(f"### {section.get('name')}")
|
|
125
|
+
changed = section.get("changed_files", [])
|
|
126
|
+
if changed:
|
|
127
|
+
lines.extend(f"- `{item}`" for item in changed)
|
|
128
|
+
else:
|
|
129
|
+
lines.append("- 暂无")
|
|
130
|
+
lines.append("")
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
parser = argparse.ArgumentParser(description="实现完成后进行基础自查并生成 self-check 产物。")
|
|
136
|
+
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
137
|
+
args = parser.parse_args()
|
|
138
|
+
|
|
139
|
+
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
140
|
+
config = load_workspace_config(workspace)
|
|
141
|
+
session_meta = current_session_meta(config)
|
|
142
|
+
plan = read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
143
|
+
|
|
144
|
+
sections: list[dict] = []
|
|
145
|
+
for codebase in planned_codebases(config, session_meta):
|
|
146
|
+
sections.append(
|
|
147
|
+
{
|
|
148
|
+
"name": codebase.name,
|
|
149
|
+
"path": str(codebase),
|
|
150
|
+
"repo_mode": "git" if is_git_repo(codebase) else "fallback",
|
|
151
|
+
"changed_files": changed_files(codebase) if is_git_repo(codebase) else [],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
self_check = build_self_check(plan, sections)
|
|
155
|
+
payload = {
|
|
156
|
+
"session_id": session_meta["session_id"],
|
|
157
|
+
"source": "run-workflow.py self-check",
|
|
158
|
+
"schema_version": 1,
|
|
159
|
+
"result": self_check["result"],
|
|
160
|
+
"sections": sections,
|
|
161
|
+
"findings": self_check["findings"],
|
|
162
|
+
"created_at": now_iso(),
|
|
163
|
+
}
|
|
164
|
+
write_managed_json(config, data_artifact_path(config, "self-check.json", session_meta), payload)
|
|
165
|
+
write_managed_text(config, report_artifact_path(config, "self-check.md", session_meta), build_markdown(self_check, sections))
|
|
166
|
+
|
|
167
|
+
status = ensure_status(config, session_meta, read_json(data_artifact_path(config, "status.json", session_meta), {}))
|
|
168
|
+
status.update(
|
|
169
|
+
{
|
|
170
|
+
"phase": "self_check" if self_check["result"] == "passed" else "blocked",
|
|
171
|
+
"current_task": "实现自查已完成。",
|
|
172
|
+
"progress": 65 if self_check["result"] == "passed" else 62,
|
|
173
|
+
"next_action": "继续执行代码审查。" if self_check["result"] == "passed" else "请先处理自查阻塞项。",
|
|
174
|
+
"completed_tasks": list(dict.fromkeys(status.get("completed_tasks", []) + ["已完成实现自查"])),
|
|
175
|
+
"blocked_tasks": list(dict.fromkeys(status.get("blocked_tasks", []) + [item["title"] for item in self_check["findings"] if item.get("blocking")])),
|
|
176
|
+
"updated_at": now_iso(),
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
write_managed_json(config, data_artifact_path(config, "status.json", session_meta), status)
|
|
180
|
+
if self_check["result"] == "blocked":
|
|
181
|
+
raise SystemExit("实现自查发现阻塞项,请查看 self-check.md。")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|