super-engineer-workflow 0.1.2 → 0.1.5
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 +25 -0
- package/README.md +11 -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 +13 -0
- package/docs//346/250/241/346/235/277/344/275/277/347/224/250/346/214/207/345/215/227.md +55 -0
- package/package.json +4 -2
- package/scripts/se-cli.py +77 -0
- package/scripts/se-e2e-test.py +369 -0
- package/scripts/se-setup.py +12 -83
- package/super-engineer-workflow/SKILL.md +50 -405
- package/super-engineer-workflow/references/se-commands.md +5 -1
- package/super-engineer-workflow/references/workflow.md +3 -1
- package/super-engineer-workflow/scripts/common.py +103 -1
- package/super-engineer-workflow/scripts/generate-review-report.py +18 -2
- package/super-engineer-workflow/scripts/generate-self-check.py +1 -1
- package/super-engineer-workflow/scripts/generate-smart-plan.py +24 -0
- package/super-engineer-workflow/scripts/propose-openspec.py +20 -11
- package/super-engineer-workflow/scripts/run-verify-and-report.py +28 -5
- package/super-engineer-workflow/scripts/run-workflow.py +18 -4
- package/super-engineer-workflow/scripts/writeback-openspec.py +9 -1
- package/templates/workspaces/frontend.yml +18 -0
- package/templates/workspaces/java-microservice.yml +19 -0
- package/templates/workspaces/multi-repo.yml +21 -0
- package/templates/workspaces/openspec-auto.yml +18 -0
- package/templates/workspaces/openspec-manual.yml +16 -0
- package/templates/workspaces/todo-auto.yml +13 -0
- package/templates/workspaces/todo-manual.yml +13 -0
|
@@ -161,7 +161,9 @@ OpenSpec change 名称必须通过 `/se:propose <change-name>` 显式指定。
|
|
|
161
161
|
- `/se:verify` 通过后状态为 `verified`,只允许 `/se:archive-check`
|
|
162
162
|
- `/se:archive-check` 通过后状态为 `archive_ready`,只允许 `/se:archive`
|
|
163
163
|
- 所有阶段推进必须先通过 `run-workflow.py validate-state <command>` 等价校验,不能只依赖 AI 回复
|
|
164
|
-
-
|
|
164
|
+
- `plan` 只有在没有有效计划会话、当前会话已完成/归档/阻塞,或当前会话失效时才创建新的 `session_id`
|
|
165
|
+
- 如果当前 session 已有 `plan.json` 且仍停留在计划确认阶段,重复执行 `plan` 必须复用当前 session,不能创建新 session
|
|
166
|
+
- 如果当前 session 已进入 `implement`、`self_check`、`review`、`verify` 等交付阶段,重复执行 `plan` 必须被脚本拒绝
|
|
165
167
|
- 新会话不能覆盖历史会话目录
|
|
166
168
|
- `current-session.json` 只指向当前正在推进的会话
|
|
167
169
|
- 后续 `start-implement`、`finish-implement`、`review`、`verify`、`status` 都基于当前会话执行
|
|
@@ -665,6 +665,47 @@ def current_session_meta(config: dict[str, Any]) -> dict[str, Any]:
|
|
|
665
665
|
return normalized
|
|
666
666
|
|
|
667
667
|
|
|
668
|
+
def current_session_status(config: dict[str, Any], session_meta: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
669
|
+
meta = session_meta or current_session_meta(config)
|
|
670
|
+
status = read_json(data_artifact_path(config, "status.json", meta), {})
|
|
671
|
+
return status if isinstance(status, dict) else {}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def active_session_for_plan(config: dict[str, Any]) -> dict[str, Any] | None:
|
|
675
|
+
if current_session_is_stale(config):
|
|
676
|
+
return None
|
|
677
|
+
try:
|
|
678
|
+
session_meta = current_session_meta(config)
|
|
679
|
+
except FileNotFoundError:
|
|
680
|
+
return None
|
|
681
|
+
if not data_artifact_path(config, "plan.json", session_meta).exists():
|
|
682
|
+
return None
|
|
683
|
+
status = current_session_status(config, session_meta)
|
|
684
|
+
status_phase = str(status.get("phase", "") or "").strip()
|
|
685
|
+
if status_phase in ("done", "archived", "blocked"):
|
|
686
|
+
return None
|
|
687
|
+
return {
|
|
688
|
+
"session": session_meta,
|
|
689
|
+
"status": status,
|
|
690
|
+
"phase": status_phase or "plan",
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def ensure_plan_can_run(config: dict[str, Any]) -> dict[str, Any] | None:
|
|
695
|
+
active = active_session_for_plan(config)
|
|
696
|
+
if not active:
|
|
697
|
+
return None
|
|
698
|
+
phase = str(active.get("phase", "")).strip()
|
|
699
|
+
session = active.get("session", {})
|
|
700
|
+
if phase in ("plan", "wait_confirm_plan"):
|
|
701
|
+
return active
|
|
702
|
+
raise RuntimeError(
|
|
703
|
+
f"当前已有活跃 session 正在交付中,禁止重新执行 /se:plan:"
|
|
704
|
+
f"session_id={session.get('session_id', '')}, phase={phase}。"
|
|
705
|
+
"请继续当前 /se:apply 链路,不要创建新的 plan/session。"
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
668
709
|
def data_artifact_path(config: dict[str, Any], name: str, session_meta: dict[str, Any] | None = None) -> Path:
|
|
669
710
|
meta = _normalize_session_meta(config, session_meta or current_session_meta(config))
|
|
670
711
|
return Path(meta["data_dir"]) / name
|
|
@@ -1016,6 +1057,8 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
|
|
|
1016
1057
|
elif run_command in ("plan", "apply"):
|
|
1017
1058
|
if not todo_path(config).exists():
|
|
1018
1059
|
errors.append("缺少 todo_file,请先执行 /se:init 或补充 todo.md。")
|
|
1060
|
+
if run_command == "plan" and phase in ("implementing", "self_checked", "reviewed"):
|
|
1061
|
+
errors.append("当前已有活跃 session 正在交付中,不能重新执行 /se:plan。请继续当前 /se:apply、/se:review 或 /se:verify。")
|
|
1019
1062
|
elif run_command == "start-implement":
|
|
1020
1063
|
if phase not in ("planned", "implementing", "blocked"):
|
|
1021
1064
|
errors.append("当前状态不允许进入实现,请先执行 /se:plan。")
|
|
@@ -1065,7 +1108,16 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
|
|
|
1065
1108
|
for key in ("proposal", "design", "tasks"):
|
|
1066
1109
|
if not _se_state_artifact_exists(str(artifacts.get(key, ""))):
|
|
1067
1110
|
errors.append(f"缺少 OpenSpec 产物:{key}")
|
|
1068
|
-
elif se_command
|
|
1111
|
+
elif se_command == "/se:plan":
|
|
1112
|
+
if phase not in ("bridged", "planned", "blocked") and se_command not in allowed_next:
|
|
1113
|
+
errors.append("当前状态不允许重新计划,请先完成 /se:bridge,或继续当前活跃交付会话。")
|
|
1114
|
+
if phase in ("implementing", "self_checked", "reviewed", "verified"):
|
|
1115
|
+
errors.append("当前已有活跃 session 正在交付中,不能重新执行 /se:plan。请继续当前 /se:apply、/se:review 或 /se:verify。")
|
|
1116
|
+
if phase == "proposed":
|
|
1117
|
+
errors.append("/se:propose 后不能直接进入交付,必须先执行 /se:bridge。")
|
|
1118
|
+
if not _se_state_artifact_exists(str(artifacts.get("todo", ""))):
|
|
1119
|
+
errors.append("缺少桥接 todo.md,请先执行 /se:bridge。")
|
|
1120
|
+
elif se_command == "/se:apply":
|
|
1069
1121
|
if phase not in ("bridged", "planned", "implementing", "reviewed", "blocked") and se_command not in allowed_next:
|
|
1070
1122
|
errors.append("当前状态不允许进入交付,请先完成 /se:bridge 并人工审核 todo.md。")
|
|
1071
1123
|
if phase == "proposed":
|
|
@@ -1151,6 +1203,54 @@ def file_sha256(path: Path) -> str:
|
|
|
1151
1203
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
1152
1204
|
|
|
1153
1205
|
|
|
1206
|
+
def markdown_headings(text: str, limit: int = 16) -> list[str]:
|
|
1207
|
+
headings: list[str] = []
|
|
1208
|
+
for line in text.splitlines():
|
|
1209
|
+
stripped = line.strip()
|
|
1210
|
+
if stripped.startswith("#"):
|
|
1211
|
+
headings.append(stripped[:160])
|
|
1212
|
+
if len(headings) >= limit:
|
|
1213
|
+
break
|
|
1214
|
+
return headings
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def compact_text_excerpt(text: str, keywords: list[str] | None = None, max_chars: int = 6000) -> str:
|
|
1218
|
+
normalized = text.strip()
|
|
1219
|
+
if len(normalized) <= max_chars:
|
|
1220
|
+
return normalized
|
|
1221
|
+
|
|
1222
|
+
keywords = [item.lower() for item in (keywords or []) if item]
|
|
1223
|
+
lines = normalized.splitlines()
|
|
1224
|
+
selected: list[str] = []
|
|
1225
|
+
selected.extend(lines[:60])
|
|
1226
|
+
for index, line in enumerate(lines):
|
|
1227
|
+
lowered = line.lower()
|
|
1228
|
+
if keywords and not any(keyword in lowered for keyword in keywords):
|
|
1229
|
+
continue
|
|
1230
|
+
start = max(0, index - 2)
|
|
1231
|
+
end = min(len(lines), index + 3)
|
|
1232
|
+
selected.append("")
|
|
1233
|
+
selected.extend(lines[start:end])
|
|
1234
|
+
if len("\n".join(selected)) >= max_chars:
|
|
1235
|
+
break
|
|
1236
|
+
excerpt = "\n".join(selected).strip()
|
|
1237
|
+
if len(excerpt) > max_chars:
|
|
1238
|
+
excerpt = excerpt[:max_chars].rstrip()
|
|
1239
|
+
return excerpt + "\n\n...[已摘要,按需读取原文件全文]..."
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def summarize_markdown_file(path: Path, keywords: list[str] | None = None, max_excerpt_chars: int = 6000) -> dict[str, Any]:
|
|
1243
|
+
text = read_text(path)
|
|
1244
|
+
return {
|
|
1245
|
+
"path": str(path.resolve()),
|
|
1246
|
+
"bytes": path.stat().st_size if path.exists() else 0,
|
|
1247
|
+
"sha256": file_sha256(path),
|
|
1248
|
+
"headings": markdown_headings(text),
|
|
1249
|
+
"excerpt": compact_text_excerpt(text, keywords=keywords, max_chars=max_excerpt_chars),
|
|
1250
|
+
"truncated": len(text.strip()) > max_excerpt_chars,
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
|
|
1154
1254
|
def write_text(path: Path, content: str) -> None:
|
|
1155
1255
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1156
1256
|
path.write_text(content, encoding="utf-8")
|
|
@@ -1627,6 +1727,8 @@ def build_openspec_bridge_context(config: dict[str, Any], tasks_text: str) -> di
|
|
|
1627
1727
|
"spec_reference_files": specs,
|
|
1628
1728
|
"proposal_headings": extract_headings(proposal_text),
|
|
1629
1729
|
"design_headings": extract_headings(design_text),
|
|
1730
|
+
"proposal_excerpt": compact_text_excerpt(proposal_text, max_chars=3000) if proposal_text else "",
|
|
1731
|
+
"design_excerpt": compact_text_excerpt(design_text, max_chars=3000) if design_text else "",
|
|
1630
1732
|
"business_constraints": [
|
|
1631
1733
|
f"需求来源是 OpenSpec change:{openspec.get('change_name', '')}",
|
|
1632
1734
|
"优先以 proposal.md、design.md 和 specs/ 下的 delta specs 作为业务边界",
|
|
@@ -64,6 +64,22 @@ def diff_summary(workspace: Path) -> list[str]:
|
|
|
64
64
|
return lines
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def compact_lines(lines: list[str], limit: int = 80) -> list[str]:
|
|
68
|
+
if len(lines) <= limit:
|
|
69
|
+
return lines
|
|
70
|
+
head = max(1, limit // 2)
|
|
71
|
+
tail = max(1, limit - head)
|
|
72
|
+
return lines[:head] + [f"... 已省略 {len(lines) - limit} 行,详见 git diff。"] + lines[-tail:]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_plan_context(config: dict, session_meta: dict) -> dict:
|
|
76
|
+
summary = read_json(data_artifact_path(config, "plan-summary.json", session_meta), {})
|
|
77
|
+
if summary:
|
|
78
|
+
summary["compact"] = True
|
|
79
|
+
return summary
|
|
80
|
+
return read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
81
|
+
|
|
82
|
+
|
|
67
83
|
def find_tests(files: list[str]) -> list[str]:
|
|
68
84
|
return [item for item in files if item.endswith("Test.java") or "/test/" in item]
|
|
69
85
|
|
|
@@ -217,7 +233,7 @@ def main() -> None:
|
|
|
217
233
|
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
218
234
|
config = load_workspace_config(workspace)
|
|
219
235
|
session_meta = current_session_meta(config)
|
|
220
|
-
plan =
|
|
236
|
+
plan = read_plan_context(config, session_meta)
|
|
221
237
|
codebases = planned_codebases(config, session_meta)
|
|
222
238
|
target_plans = plan.get("target_codebases", [])
|
|
223
239
|
target_map = {str(item.get("path")): item for item in target_plans}
|
|
@@ -230,7 +246,7 @@ def main() -> None:
|
|
|
230
246
|
plan_files = target_plan.get("impacted_files", [])
|
|
231
247
|
if is_git_repo(codebase):
|
|
232
248
|
changed = changed_files(codebase)
|
|
233
|
-
summary = diff_summary(codebase)
|
|
249
|
+
summary = compact_lines(diff_summary(codebase))
|
|
234
250
|
repo_mode = "git"
|
|
235
251
|
else:
|
|
236
252
|
changed = plan_files
|
|
@@ -139,7 +139,7 @@ def main() -> None:
|
|
|
139
139
|
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
140
140
|
config = load_workspace_config(workspace)
|
|
141
141
|
session_meta = current_session_meta(config)
|
|
142
|
-
plan = read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
142
|
+
plan = read_json(data_artifact_path(config, "plan-summary.json", session_meta), {}) or read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
143
143
|
|
|
144
144
|
sections: list[dict] = []
|
|
145
145
|
for codebase in planned_codebases(config, session_meta):
|
|
@@ -266,6 +266,29 @@ def build_plan_markdown(plan: dict) -> str:
|
|
|
266
266
|
return "\n".join(lines)
|
|
267
267
|
|
|
268
268
|
|
|
269
|
+
def build_plan_summary(plan: dict) -> dict[str, object]:
|
|
270
|
+
return {
|
|
271
|
+
"session_id": plan.get("session_id", ""),
|
|
272
|
+
"source": "run-workflow.py plan-summary",
|
|
273
|
+
"schema_version": 1,
|
|
274
|
+
"requirement_summary": plan.get("requirement_summary", ""),
|
|
275
|
+
"target_codebases": [
|
|
276
|
+
{
|
|
277
|
+
"name": item.get("name", ""),
|
|
278
|
+
"path": item.get("path", ""),
|
|
279
|
+
"verify_command": (item.get("detected_project") or {}).get("verify_command", ""),
|
|
280
|
+
}
|
|
281
|
+
for item in plan.get("target_codebases", [])
|
|
282
|
+
],
|
|
283
|
+
"impacted_files": plan.get("impacted_files", [])[:80],
|
|
284
|
+
"change_steps": plan.get("change_steps", [])[:20],
|
|
285
|
+
"acceptance_criteria": plan.get("acceptance_criteria", [])[:40],
|
|
286
|
+
"test_plan": plan.get("test_plan", [])[:20],
|
|
287
|
+
"risks": plan.get("risks", [])[:10],
|
|
288
|
+
"unknowns": plan.get("unknowns", [])[:10],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
269
292
|
def collect_target_plan_data(config: dict, codebases: list[Path]) -> tuple[list[dict], list[str], list[str]]:
|
|
270
293
|
targets: list[dict] = []
|
|
271
294
|
all_impacted_files: list[str] = []
|
|
@@ -404,6 +427,7 @@ def main() -> None:
|
|
|
404
427
|
}
|
|
405
428
|
|
|
406
429
|
write_managed_json(config, data_artifact_path(config, "plan.json", session_meta), plan)
|
|
430
|
+
write_managed_json(config, data_artifact_path(config, "plan-summary.json", session_meta), build_plan_summary(plan))
|
|
407
431
|
write_managed_text(config, report_artifact_path(config, "plan.md", session_meta), build_plan_markdown(plan) + "\n")
|
|
408
432
|
|
|
409
433
|
status = ensure_status(config, session_meta, read_json(data_artifact_path(config, "status.json", session_meta), {}))
|
|
@@ -12,9 +12,9 @@ from common import (
|
|
|
12
12
|
openspec_cli_available,
|
|
13
13
|
openspec_writeback_dir,
|
|
14
14
|
read_demand_source,
|
|
15
|
-
read_text,
|
|
16
15
|
run_openspec_cli,
|
|
17
16
|
select_openspec_change,
|
|
17
|
+
summarize_markdown_file,
|
|
18
18
|
update_se_state,
|
|
19
19
|
validate_openspec_change_name,
|
|
20
20
|
workflow_source,
|
|
@@ -51,11 +51,9 @@ def main() -> None:
|
|
|
51
51
|
raise SystemExit(str(error))
|
|
52
52
|
demand_file = str(demand_source.get("source", "")).strip()
|
|
53
53
|
demand_text = str(demand_source.get("content", "")).strip()
|
|
54
|
+
demand_keywords = [item for item in [change_name, *change_name.replace("-", " ").split()] if item]
|
|
54
55
|
reference_contexts = [
|
|
55
|
-
|
|
56
|
-
"path": item,
|
|
57
|
-
"content": read_text(Path(item)),
|
|
58
|
-
}
|
|
56
|
+
summarize_markdown_file(Path(item), keywords=demand_keywords)
|
|
59
57
|
for item in existing_reference_files(config)
|
|
60
58
|
]
|
|
61
59
|
|
|
@@ -92,15 +90,16 @@ def main() -> None:
|
|
|
92
90
|
"demand_file": demand_file,
|
|
93
91
|
"demand_source_type": demand_source.get("source_type", ""),
|
|
94
92
|
"demand_fetch_command": demand_source.get("command", []),
|
|
95
|
-
"
|
|
93
|
+
"demand_text_available_at": demand_file,
|
|
94
|
+
"demand_excerpt": demand_text[:12000] + ("\n\n...[已摘要,按需读取 demand_file 全文]..." if len(demand_text) > 12000 else ""),
|
|
96
95
|
"reference_files": reference_contexts,
|
|
97
96
|
"openspec_cli_available": openspec_cli_available(),
|
|
98
97
|
"commands": commands,
|
|
99
|
-
"next_action": "Use
|
|
98
|
+
"next_action": "Use demand_excerpt, reference file summaries, and OpenSpec instructions to create or update proposal.md, design.md, tasks.md, and specs/. Read full source files only when necessary.",
|
|
100
99
|
"workflow_phase_after_completion": "proposed",
|
|
101
100
|
"allowed_next_after_completion": ["/se:bridge"],
|
|
102
101
|
"forbidden_next_after_completion": ["/se:plan", "/se:apply"],
|
|
103
|
-
"final_reply_constraint": "
|
|
102
|
+
"final_reply_constraint": "代码未修改。下一步只能执行 /se:bridge。",
|
|
104
103
|
}
|
|
105
104
|
write_managed_json(config, writeback_dir / "propose-input.json", payload)
|
|
106
105
|
write_managed_text(
|
|
@@ -119,7 +118,7 @@ def main() -> None:
|
|
|
119
118
|
"",
|
|
120
119
|
"## Demand",
|
|
121
120
|
"",
|
|
122
|
-
|
|
121
|
+
payload["demand_excerpt"] or "未配置或未找到 demand_file。",
|
|
123
122
|
"",
|
|
124
123
|
"## Reference Files",
|
|
125
124
|
"",
|
|
@@ -129,7 +128,17 @@ def main() -> None:
|
|
|
129
128
|
[
|
|
130
129
|
f"### {item['path']}",
|
|
131
130
|
"",
|
|
132
|
-
|
|
131
|
+
f"- bytes: {item.get('bytes', 0)}",
|
|
132
|
+
f"- sha256: {item.get('sha256', '')}",
|
|
133
|
+
f"- truncated: {item.get('truncated', False)}",
|
|
134
|
+
"",
|
|
135
|
+
"#### Headings",
|
|
136
|
+
"",
|
|
137
|
+
"\n".join(f"- {heading}" for heading in item.get("headings", [])) or "暂无标题",
|
|
138
|
+
"",
|
|
139
|
+
"#### Excerpt",
|
|
140
|
+
"",
|
|
141
|
+
item.get("excerpt", "") or "文件为空或无法读取。",
|
|
133
142
|
]
|
|
134
143
|
)
|
|
135
144
|
for item in reference_contexts
|
|
@@ -163,7 +172,7 @@ def main() -> None:
|
|
|
163
172
|
print("workflow_phase_after_completion=proposed")
|
|
164
173
|
print("allowed_next_after_completion=/se:bridge")
|
|
165
174
|
print("forbidden_next_after_completion=/se:plan,/se:apply")
|
|
166
|
-
print("final_reply_must
|
|
175
|
+
print("final_reply_must=代码未修改。下一步只能执行 /se:bridge。")
|
|
167
176
|
|
|
168
177
|
|
|
169
178
|
if __name__ == "__main__":
|
|
@@ -28,6 +28,29 @@ from common import (
|
|
|
28
28
|
workspace_root,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
MAX_VERIFY_LOG_CHARS = 12000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compact_command_output(text: str, max_chars: int = MAX_VERIFY_LOG_CHARS) -> str:
|
|
36
|
+
if len(text) <= max_chars:
|
|
37
|
+
return text
|
|
38
|
+
head = max_chars // 2
|
|
39
|
+
tail = max_chars - head
|
|
40
|
+
omitted = len(text) - max_chars
|
|
41
|
+
return text[:head].rstrip() + f"\n\n...[已省略 {omitted} 字符]...\n\n" + text[-tail:].lstrip()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compact_command_records(value):
|
|
45
|
+
if isinstance(value, dict):
|
|
46
|
+
return {
|
|
47
|
+
key: compact_command_output(item) if key in ("stdout", "stderr", "output", "logs") and isinstance(item, str) else compact_command_records(item)
|
|
48
|
+
for key, item in value.items()
|
|
49
|
+
}
|
|
50
|
+
if isinstance(value, list):
|
|
51
|
+
return [compact_command_records(item) for item in value]
|
|
52
|
+
return value
|
|
53
|
+
|
|
31
54
|
VERIFY_BLOCKERS = {"未识别到验证命令", "验证失败", "验证执行超时"}
|
|
32
55
|
|
|
33
56
|
|
|
@@ -248,7 +271,7 @@ def main() -> None:
|
|
|
248
271
|
workspace = workspace_root(Path(args.workspace).expanduser() if args.workspace else None)
|
|
249
272
|
config = load_workspace_config(workspace)
|
|
250
273
|
session_meta = current_session_meta(config)
|
|
251
|
-
plan = read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
274
|
+
plan = read_json(data_artifact_path(config, "plan-summary.json", session_meta), {}) or read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
252
275
|
status_path = data_artifact_path(config, "status.json", session_meta)
|
|
253
276
|
existing_status = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
254
277
|
if (
|
|
@@ -319,8 +342,8 @@ def main() -> None:
|
|
|
319
342
|
"result": repo_result,
|
|
320
343
|
"exit_code": str(result.returncode),
|
|
321
344
|
"duration": repo_duration,
|
|
322
|
-
"stdout": result.stdout,
|
|
323
|
-
"stderr": result.stderr,
|
|
345
|
+
"stdout": compact_command_output(result.stdout),
|
|
346
|
+
"stderr": compact_command_output(result.stderr),
|
|
324
347
|
}
|
|
325
348
|
)
|
|
326
349
|
if repo_result != "通过":
|
|
@@ -363,8 +386,8 @@ def main() -> None:
|
|
|
363
386
|
"result": "超时",
|
|
364
387
|
"exit_code": "timeout",
|
|
365
388
|
"duration": duration,
|
|
366
|
-
"stdout": error.stdout or "",
|
|
367
|
-
"stderr": error.stderr or "",
|
|
389
|
+
"stdout": compact_command_output(error.stdout or ""),
|
|
390
|
+
"stderr": compact_command_output(error.stderr or ""),
|
|
368
391
|
}
|
|
369
392
|
)
|
|
370
393
|
status_for_duration = ensure_status(config, session_meta, read_json(status_path, {}))
|
|
@@ -11,6 +11,7 @@ from common import (
|
|
|
11
11
|
current_session_is_stale,
|
|
12
12
|
current_session_meta,
|
|
13
13
|
data_artifact_path,
|
|
14
|
+
ensure_plan_can_run,
|
|
14
15
|
ensure_status,
|
|
15
16
|
load_workspace_config,
|
|
16
17
|
now_iso,
|
|
@@ -40,19 +41,19 @@ SE_ROUTE_REPLY_CONSTRAINTS: dict[str, dict[str, str]] = {
|
|
|
40
41
|
"phase": "proposed",
|
|
41
42
|
"allowed_next": "/se:bridge",
|
|
42
43
|
"forbidden_next": "/se:plan,/se:apply",
|
|
43
|
-
"final_reply_must": "
|
|
44
|
+
"final_reply_must": "代码未修改。下一步只能执行 /se:bridge。",
|
|
44
45
|
},
|
|
45
46
|
"/se:bridge": {
|
|
46
47
|
"phase": "bridged",
|
|
47
48
|
"allowed_next": "人工审核 todo.md 后 /se:apply",
|
|
48
49
|
"forbidden_next": "自动执行 /se:plan,自动执行 /se:apply,代码实现",
|
|
49
|
-
"final_reply_must": "桥接 todo
|
|
50
|
+
"final_reply_must": "桥接 todo 已生成。请审核 todo.md,审核通过后发送 /se:apply。",
|
|
50
51
|
},
|
|
51
52
|
"/se:plan": {
|
|
52
53
|
"phase": "planned",
|
|
53
54
|
"allowed_next": "/se:apply",
|
|
54
55
|
"forbidden_next": "代码实现,review,verify",
|
|
55
|
-
"final_reply_must": "
|
|
56
|
+
"final_reply_must": "计划已生成。下一步执行 /se:apply。",
|
|
56
57
|
},
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -287,9 +288,22 @@ def command_archive_openspec(workspace: Path | None) -> None:
|
|
|
287
288
|
def command_plan(workspace: Path | None) -> None:
|
|
288
289
|
config = load_workspace_config(workspace)
|
|
289
290
|
require_se_state(config, "plan")
|
|
291
|
+
try:
|
|
292
|
+
active = ensure_plan_can_run(config)
|
|
293
|
+
except RuntimeError as error:
|
|
294
|
+
raise SystemExit(str(error))
|
|
295
|
+
if active:
|
|
296
|
+
session = active.get("session", {})
|
|
297
|
+
print("session_action=reused")
|
|
298
|
+
print(f"session_id={session.get('session_id', '')}")
|
|
299
|
+
print(f"phase={active.get('phase', '')}")
|
|
300
|
+
print("next_action=当前计划已存在,继续执行 /se:apply。")
|
|
301
|
+
return
|
|
290
302
|
command_init(workspace)
|
|
291
303
|
config = load_workspace_config(workspace)
|
|
292
|
-
create_session(config)
|
|
304
|
+
session_meta = create_session(config)
|
|
305
|
+
print("session_action=created")
|
|
306
|
+
print(f"session_id={session_meta.get('session_id', '')}")
|
|
293
307
|
command_discover(workspace)
|
|
294
308
|
args = ["--workspace", str(workspace)] if workspace else []
|
|
295
309
|
run_python("generate-smart-plan.py", args)
|
|
@@ -223,6 +223,14 @@ def build_markdown(payload: dict) -> str:
|
|
|
223
223
|
return "\n".join(lines)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def read_plan_context(config: dict, session_meta: dict) -> dict:
|
|
227
|
+
summary = read_json(data_artifact_path(config, "plan-summary.json", session_meta), {})
|
|
228
|
+
if summary:
|
|
229
|
+
summary["compact"] = True
|
|
230
|
+
return summary
|
|
231
|
+
return read_json(data_artifact_path(config, "plan.json", session_meta), {})
|
|
232
|
+
|
|
233
|
+
|
|
226
234
|
def main() -> None:
|
|
227
235
|
parser = argparse.ArgumentParser(description="把当前会话执行结果回写到 OpenSpec change 目录。")
|
|
228
236
|
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
@@ -234,7 +242,7 @@ def main() -> None:
|
|
|
234
242
|
raise SystemExit("当前 workspace.yml 未启用 OpenSpec 模式,无需执行 writeback-openspec。")
|
|
235
243
|
|
|
236
244
|
session_meta = current_session_meta(config)
|
|
237
|
-
plan =
|
|
245
|
+
plan = read_plan_context(config, session_meta)
|
|
238
246
|
review = read_json(data_artifact_path(config, "review.json", session_meta), {})
|
|
239
247
|
verify = read_json(data_artifact_path(config, "verify.json", session_meta), {})
|
|
240
248
|
status = read_json(data_artifact_path(config, "status.json", session_meta), {})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: auto
|
|
3
|
+
workflow_source: openspec
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
demand_file: superengineer/${demand_name}/需求.md
|
|
7
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
8
|
+
reference_files:
|
|
9
|
+
- docs/需求分析与实现指南.md
|
|
10
|
+
- docs/前端规范.md
|
|
11
|
+
code_path: __CODE_PATH__
|
|
12
|
+
output_dir: superengineer/${demand_name}/output
|
|
13
|
+
openspec:
|
|
14
|
+
changes_dir: openspec/changes
|
|
15
|
+
verify_commands:
|
|
16
|
+
default: pnpm test && pnpm build
|
|
17
|
+
|
|
18
|
+
# 如果团队使用 npm,可把 verify_commands.default 改成 npm test && npm run build。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: auto
|
|
3
|
+
workflow_source: openspec
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
demand_file: superengineer/${demand_name}/需求.md
|
|
7
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
8
|
+
reference_files:
|
|
9
|
+
- docs/需求分析与实现指南.md
|
|
10
|
+
- docs/开发规范.md
|
|
11
|
+
- docs/接口规范.md
|
|
12
|
+
code_path: __CODE_PATH__
|
|
13
|
+
output_dir: superengineer/${demand_name}/output
|
|
14
|
+
openspec:
|
|
15
|
+
changes_dir: openspec/changes
|
|
16
|
+
verify_commands:
|
|
17
|
+
default: mvn -q test
|
|
18
|
+
|
|
19
|
+
# Spring Boot 多模块项目可把 code_path 指向具体服务目录,或使用 multi-repo 模板。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: auto
|
|
3
|
+
workflow_source: openspec
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
demand_file: superengineer/${demand_name}/需求.md
|
|
7
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
8
|
+
reference_files:
|
|
9
|
+
- docs/需求分析与实现指南.md
|
|
10
|
+
- docs/开发规范.md
|
|
11
|
+
- docs/系统架构.md
|
|
12
|
+
code_path: __CODE_PATH__
|
|
13
|
+
output_dir: superengineer/${demand_name}/output
|
|
14
|
+
openspec:
|
|
15
|
+
changes_dir: openspec/changes
|
|
16
|
+
verify_commands:
|
|
17
|
+
frontend-app: pnpm test && pnpm build
|
|
18
|
+
user-service: mvn -q test
|
|
19
|
+
pricing-service: go test ./...
|
|
20
|
+
|
|
21
|
+
# code_path 指向多个仓库的上层目录时,工作流会结合 todo 与代码结构推断目标仓库。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: auto
|
|
3
|
+
workflow_source: openspec
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
demand_file: superengineer/${demand_name}/需求.md
|
|
7
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
8
|
+
reference_files:
|
|
9
|
+
- docs/需求分析与实现指南.md
|
|
10
|
+
- docs/开发规范.md
|
|
11
|
+
code_path: __CODE_PATH__
|
|
12
|
+
output_dir: superengineer/${demand_name}/output
|
|
13
|
+
openspec:
|
|
14
|
+
changes_dir: openspec/changes
|
|
15
|
+
|
|
16
|
+
# 可选:自动识别不准确时覆盖验证命令
|
|
17
|
+
# verify_commands:
|
|
18
|
+
# default: mvn -q test
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: manual
|
|
3
|
+
workflow_source: openspec
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
demand_file: superengineer/${demand_name}/需求.md
|
|
7
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
8
|
+
reference_files:
|
|
9
|
+
- docs/需求分析与实现指南.md
|
|
10
|
+
- docs/开发规范.md
|
|
11
|
+
code_path: __CODE_PATH__
|
|
12
|
+
output_dir: superengineer/${demand_name}/output
|
|
13
|
+
openspec:
|
|
14
|
+
changes_dir: openspec/changes
|
|
15
|
+
|
|
16
|
+
# manual 模式会在 plan、implement、review 等关键阶段停下来等待确认。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: auto
|
|
3
|
+
workflow_source: todo
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
7
|
+
reference_files:
|
|
8
|
+
- docs/需求分析与实现指南.md
|
|
9
|
+
- docs/开发规范.md
|
|
10
|
+
code_path: __CODE_PATH__
|
|
11
|
+
output_dir: superengineer/${demand_name}/output
|
|
12
|
+
|
|
13
|
+
# todo 模式不使用 OpenSpec change,适合任务已经明确的小需求。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
mode: manual
|
|
3
|
+
workflow_source: todo
|
|
4
|
+
vars:
|
|
5
|
+
demand_name: __DEMAND_NAME__
|
|
6
|
+
todo_file: superengineer/${demand_name}/todo.md
|
|
7
|
+
reference_files:
|
|
8
|
+
- docs/需求分析与实现指南.md
|
|
9
|
+
- docs/开发规范.md
|
|
10
|
+
code_path: __CODE_PATH__
|
|
11
|
+
output_dir: superengineer/${demand_name}/output
|
|
12
|
+
|
|
13
|
+
# manual 模式适合首次接入、复杂需求或需要逐步确认的团队。
|