super-engineer-workflow 0.1.6 → 0.1.7
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 +13 -0
- package/README.md +6 -10
- package/docs/se/345/221/275/344/273/244/345/215/217/350/256/256.md +42 -297
- package/docs//350/267/250/345/271/263/345/217/260/346/224/257/346/214/201/347/237/251/351/230/265.md +34 -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 +3 -2
- package/package.json +1 -1
- package/scripts/se-cli.py +146 -2
- package/scripts/se-e2e-test.py +89 -0
- package/scripts/se-setup.py +13 -2
- package/super-engineer-workflow/SKILL.md +20 -5
- package/super-engineer-workflow/references/commands/apply.md +28 -0
- package/super-engineer-workflow/references/commands/archive.md +23 -0
- package/super-engineer-workflow/references/commands/bridge.md +25 -0
- package/super-engineer-workflow/references/commands/common.md +32 -0
- package/super-engineer-workflow/references/commands/plan.md +25 -0
- package/super-engineer-workflow/references/commands/propose.md +25 -0
- package/super-engineer-workflow/references/commands/review.md +22 -0
- package/super-engineer-workflow/references/commands/status.md +22 -0
- package/super-engineer-workflow/references/commands/verify.md +23 -0
- package/super-engineer-workflow/scripts/bootstrap-openspec.py +3 -1
- package/super-engineer-workflow/scripts/common.py +128 -0
- package/super-engineer-workflow/scripts/generate-discovery.py +44 -0
- package/super-engineer-workflow/scripts/generate-smart-plan.py +12 -2
- package/super-engineer-workflow/scripts/prepare-archive-openspec.py +4 -0
- package/super-engineer-workflow/scripts/run-workflow.py +90 -23
- package/super-engineer-workflow/scripts/writeback-openspec.py +5 -1
- package/super-engineer-workflow/references/se-commands.md +0 -586
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import argparse
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from common import ensure_workflow_inputs, load_workspace_config, todo_path, update_se_state, workflow_source, workspace_root
|
|
7
|
+
from common import ensure_workflow_inputs, load_workspace_config, openspec_tasks_hash, todo_path, update_se_state, workflow_source, workspace_root
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def main() -> None:
|
|
@@ -30,12 +30,14 @@ def main() -> None:
|
|
|
30
30
|
artifacts={
|
|
31
31
|
"todo": str(todo_path(config)),
|
|
32
32
|
"bridge_source": str(result.get("bridge_source", "")),
|
|
33
|
+
"tasks_sha256": openspec_tasks_hash(config),
|
|
33
34
|
},
|
|
34
35
|
)
|
|
35
36
|
print(f"workflow_source={result.get('workflow_source', '')}")
|
|
36
37
|
print(f"todo={todo_path(config)}")
|
|
37
38
|
print(f"bridge_generated={'true' if result.get('bridge_generated') else 'false'}")
|
|
38
39
|
print(f"bridge_source={result.get('bridge_source', '')}")
|
|
40
|
+
print(f"tasks_sha256={openspec_tasks_hash(config)}")
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
if __name__ == "__main__":
|
|
@@ -23,6 +23,61 @@ def now_iso() -> str:
|
|
|
23
23
|
return datetime.now(timezone.utc).isoformat()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def workflow_lock_path(config: dict[str, Any]) -> Path:
|
|
27
|
+
return artifacts_dir(config) / "workflow.lock"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def process_is_running(pid: int) -> bool:
|
|
31
|
+
if pid <= 0:
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
os.kill(pid, 0)
|
|
35
|
+
except OSError:
|
|
36
|
+
return False
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def acquire_workflow_lock(config: dict[str, Any], command: str, stale_seconds: int = 1800) -> Path:
|
|
41
|
+
ensure_runtime_dirs(config)
|
|
42
|
+
path = workflow_lock_path(config)
|
|
43
|
+
payload = {
|
|
44
|
+
"pid": os.getpid(),
|
|
45
|
+
"command": command,
|
|
46
|
+
"created_at": now_iso(),
|
|
47
|
+
}
|
|
48
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
|
|
49
|
+
try:
|
|
50
|
+
fd = os.open(str(path), flags)
|
|
51
|
+
except FileExistsError:
|
|
52
|
+
existing = read_json(path, {})
|
|
53
|
+
pid = int(existing.get("pid", 0) or 0) if isinstance(existing, dict) else 0
|
|
54
|
+
created_at = parse_iso_datetime(str(existing.get("created_at", ""))) if isinstance(existing, dict) else None
|
|
55
|
+
age = (datetime.now(timezone.utc) - created_at).total_seconds() if created_at else stale_seconds + 1
|
|
56
|
+
if age > stale_seconds or not process_is_running(pid):
|
|
57
|
+
try:
|
|
58
|
+
path.unlink()
|
|
59
|
+
except FileNotFoundError:
|
|
60
|
+
pass
|
|
61
|
+
return acquire_workflow_lock(config, command, stale_seconds=stale_seconds)
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
f"检测到工作流正在执行:pid={pid}, command={existing.get('command', '') if isinstance(existing, dict) else ''}。"
|
|
64
|
+
"请等待当前命令结束后重试。"
|
|
65
|
+
)
|
|
66
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
67
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
68
|
+
handle.write("\n")
|
|
69
|
+
return path
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def release_workflow_lock(path: Path | None) -> None:
|
|
73
|
+
if not path:
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
path.unlink()
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
26
81
|
def parse_iso_datetime(value: str) -> datetime | None:
|
|
27
82
|
normalized = str(value).strip()
|
|
28
83
|
if not normalized:
|
|
@@ -842,6 +897,71 @@ def update_se_state(
|
|
|
842
897
|
return state
|
|
843
898
|
|
|
844
899
|
|
|
900
|
+
def openspec_tasks_hash(config: dict[str, Any]) -> str:
|
|
901
|
+
if workflow_source(config) != "openspec":
|
|
902
|
+
return ""
|
|
903
|
+
return file_sha256(openspec_tasks_path(config))
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def openspec_artifact_hashes(config: dict[str, Any]) -> dict[str, Any]:
|
|
907
|
+
if workflow_source(config) != "openspec":
|
|
908
|
+
return {}
|
|
909
|
+
result: dict[str, Any] = {}
|
|
910
|
+
openspec = config.get("openspec", {})
|
|
911
|
+
for key in ("proposal_file", "design_file", "tasks_file"):
|
|
912
|
+
path_text = str(openspec.get(key, "")).strip()
|
|
913
|
+
if not path_text:
|
|
914
|
+
continue
|
|
915
|
+
path = Path(path_text)
|
|
916
|
+
if path.exists() and path.is_file():
|
|
917
|
+
result[key.replace("_file", "")] = {
|
|
918
|
+
"path": str(path.resolve()),
|
|
919
|
+
"sha256": file_sha256(path),
|
|
920
|
+
}
|
|
921
|
+
specs: list[dict[str, str]] = []
|
|
922
|
+
specs_dir_text = str(openspec.get("specs_dir", "")).strip()
|
|
923
|
+
if specs_dir_text:
|
|
924
|
+
specs_dir = Path(specs_dir_text)
|
|
925
|
+
if specs_dir.exists() and specs_dir.is_dir():
|
|
926
|
+
for path in sorted(specs_dir.rglob("*.md")):
|
|
927
|
+
specs.append(
|
|
928
|
+
{
|
|
929
|
+
"path": str(path.resolve()),
|
|
930
|
+
"relative_path": str(path.relative_to(specs_dir)),
|
|
931
|
+
"sha256": file_sha256(path),
|
|
932
|
+
}
|
|
933
|
+
)
|
|
934
|
+
result["specs"] = specs
|
|
935
|
+
return result
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def openspec_hash_drift(config: dict[str, Any], baseline: dict[str, Any]) -> list[str]:
|
|
939
|
+
current = openspec_artifact_hashes(config)
|
|
940
|
+
drifts: list[str] = []
|
|
941
|
+
for key in ("proposal", "design", "tasks"):
|
|
942
|
+
old_item = baseline.get(key, {}) if isinstance(baseline, dict) else {}
|
|
943
|
+
new_item = current.get(key, {}) if isinstance(current, dict) else {}
|
|
944
|
+
old_hash = str(old_item.get("sha256", "")).strip() if isinstance(old_item, dict) else ""
|
|
945
|
+
new_hash = str(new_item.get("sha256", "")).strip() if isinstance(new_item, dict) else ""
|
|
946
|
+
if old_hash and new_hash and old_hash != new_hash:
|
|
947
|
+
drifts.append(f"{key}.md 自执行回写后已变化")
|
|
948
|
+
old_specs = {
|
|
949
|
+
str(item.get("relative_path", "")): str(item.get("sha256", ""))
|
|
950
|
+
for item in baseline.get("specs", [])
|
|
951
|
+
if isinstance(item, dict)
|
|
952
|
+
} if isinstance(baseline, dict) else {}
|
|
953
|
+
new_specs = {
|
|
954
|
+
str(item.get("relative_path", "")): str(item.get("sha256", ""))
|
|
955
|
+
for item in current.get("specs", [])
|
|
956
|
+
if isinstance(item, dict)
|
|
957
|
+
} if isinstance(current, dict) else {}
|
|
958
|
+
for rel, old_hash in old_specs.items():
|
|
959
|
+
new_hash = new_specs.get(rel, "")
|
|
960
|
+
if old_hash and new_hash and old_hash != new_hash:
|
|
961
|
+
drifts.append(f"specs/{rel} 自执行回写后已变化")
|
|
962
|
+
return drifts
|
|
963
|
+
|
|
964
|
+
|
|
845
965
|
def _se_state_artifact_exists(path_text: str) -> bool:
|
|
846
966
|
return bool(path_text and Path(path_text).exists())
|
|
847
967
|
|
|
@@ -1125,6 +1245,10 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
|
|
|
1125
1245
|
errors.append("/se:propose 后不能直接进入交付,必须先执行 /se:bridge。")
|
|
1126
1246
|
if not _se_state_artifact_exists(str(artifacts.get("todo", ""))):
|
|
1127
1247
|
errors.append("缺少桥接 todo.md,请先执行 /se:bridge。")
|
|
1248
|
+
bridged_hash = str(artifacts.get("tasks_sha256", "")).strip()
|
|
1249
|
+
current_hash = openspec_tasks_hash(config)
|
|
1250
|
+
if bridged_hash and current_hash and bridged_hash != current_hash:
|
|
1251
|
+
errors.append("OpenSpec tasks.md 已变化,请重新执行 /se:bridge 生成待审核 todo.md。")
|
|
1128
1252
|
elif se_command == "/se:apply":
|
|
1129
1253
|
if phase not in ("bridged", "planned", "implementing", "reviewed", "blocked") and se_command not in allowed_next:
|
|
1130
1254
|
errors.append("当前状态不允许进入交付,请先完成 /se:bridge 并人工审核 todo.md。")
|
|
@@ -1132,6 +1256,10 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
|
|
|
1132
1256
|
errors.append("/se:propose 后不能直接进入交付,必须先执行 /se:bridge。")
|
|
1133
1257
|
if not _se_state_artifact_exists(str(artifacts.get("todo", ""))):
|
|
1134
1258
|
errors.append("缺少桥接 todo.md,请先执行 /se:bridge。")
|
|
1259
|
+
bridged_hash = str(artifacts.get("tasks_sha256", "")).strip()
|
|
1260
|
+
current_hash = openspec_tasks_hash(config)
|
|
1261
|
+
if bridged_hash and current_hash and bridged_hash != current_hash:
|
|
1262
|
+
errors.append("OpenSpec tasks.md 已变化,请重新执行 /se:bridge 并审核新的 todo.md。")
|
|
1135
1263
|
elif se_command == "/se:review":
|
|
1136
1264
|
if phase not in ("self_checked", "reviewed", "blocked"):
|
|
1137
1265
|
errors.append("当前状态不允许 review,请先完成实现和自查。")
|
|
@@ -107,6 +107,49 @@ def build_discovery_markdown(discovery: dict) -> str:
|
|
|
107
107
|
return "\n".join(lines)
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def build_discovery_summary(discovery: dict) -> dict[str, object]:
|
|
111
|
+
codebase_summaries: list[dict[str, object]] = []
|
|
112
|
+
evidence: list[dict[str, object]] = []
|
|
113
|
+
total_matches = 0
|
|
114
|
+
for codebase in discovery.get("codebases", []):
|
|
115
|
+
matches = list(codebase.get("matches", []))
|
|
116
|
+
total_matches += len(matches)
|
|
117
|
+
codebase_summaries.append(
|
|
118
|
+
{
|
|
119
|
+
"name": codebase.get("name", ""),
|
|
120
|
+
"path": codebase.get("path", ""),
|
|
121
|
+
"detected_project": codebase.get("detected_project", {}),
|
|
122
|
+
"match_count": len(matches),
|
|
123
|
+
"top_files": list(dict.fromkeys(str(item.get("file", "")) for item in matches if item.get("file")))[:12],
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
for item in matches[:6]:
|
|
127
|
+
evidence.append(
|
|
128
|
+
{
|
|
129
|
+
"codebase": codebase.get("name", ""),
|
|
130
|
+
"keyword": item.get("keyword", ""),
|
|
131
|
+
"file": item.get("file", ""),
|
|
132
|
+
"line": item.get("line", 0),
|
|
133
|
+
"snippet": item.get("snippet", ""),
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
if len(evidence) >= 20:
|
|
137
|
+
break
|
|
138
|
+
return {
|
|
139
|
+
"session_id": discovery.get("session_id", ""),
|
|
140
|
+
"source": "run-workflow.py discovery-summary",
|
|
141
|
+
"schema_version": 1,
|
|
142
|
+
"task_count": len(discovery.get("tasks", [])),
|
|
143
|
+
"keywords": discovery.get("keywords", [])[:30],
|
|
144
|
+
"service_resolution": discovery.get("service_resolution", {}),
|
|
145
|
+
"codebases": codebase_summaries,
|
|
146
|
+
"total_match_count": total_matches,
|
|
147
|
+
"evidence": evidence,
|
|
148
|
+
"planning_hints": discovery.get("planning_hints", [])[:10],
|
|
149
|
+
"created_at": discovery.get("created_at", ""),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
110
153
|
def main() -> None:
|
|
111
154
|
parser = argparse.ArgumentParser(description="根据 todo 关键词定位代码证据,生成 discovery 产物。")
|
|
112
155
|
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
@@ -165,6 +208,7 @@ def main() -> None:
|
|
|
165
208
|
"created_at": now_iso(),
|
|
166
209
|
}
|
|
167
210
|
write_managed_json(config, data_artifact_path(config, "discovery.json", session_meta), discovery)
|
|
211
|
+
write_managed_json(config, data_artifact_path(config, "discovery-summary.json", session_meta), build_discovery_summary(discovery))
|
|
168
212
|
write_managed_text(config, report_artifact_path(config, "discovery.md", session_meta), build_discovery_markdown(discovery))
|
|
169
213
|
|
|
170
214
|
status = ensure_status(config, session_meta, read_json(data_artifact_path(config, "status.json", session_meta), {}))
|
|
@@ -122,6 +122,8 @@ def build_acceptance_criteria(task_breakdown: list[dict[str, object]], detected:
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def discovery_evidence(discovery: dict, limit: int = 16) -> list[dict[str, object]]:
|
|
125
|
+
if discovery.get("source") == "run-workflow.py discovery-summary":
|
|
126
|
+
return list(discovery.get("evidence", []))[:limit]
|
|
125
127
|
evidence: list[dict[str, object]] = []
|
|
126
128
|
for codebase in discovery.get("codebases", []):
|
|
127
129
|
for match in codebase.get("matches", [])[:limit]:
|
|
@@ -140,7 +142,10 @@ def discovery_evidence(discovery: dict, limit: int = 16) -> list[dict[str, objec
|
|
|
140
142
|
|
|
141
143
|
|
|
142
144
|
def confidence_from_discovery(discovery: dict, impacted_files: list[str]) -> str:
|
|
143
|
-
|
|
145
|
+
if discovery.get("source") == "run-workflow.py discovery-summary":
|
|
146
|
+
match_count = int(discovery.get("total_match_count", 0) or 0)
|
|
147
|
+
else:
|
|
148
|
+
match_count = sum(len(codebase.get("matches", [])) for codebase in discovery.get("codebases", []))
|
|
144
149
|
if match_count >= 5 and impacted_files:
|
|
145
150
|
return "high"
|
|
146
151
|
if match_count > 0 or impacted_files:
|
|
@@ -317,6 +322,11 @@ def collect_target_plan_data(config: dict, codebases: list[Path]) -> tuple[list[
|
|
|
317
322
|
|
|
318
323
|
def merge_discovery_files(discovery: dict, fallback_files: list[str]) -> list[str]:
|
|
319
324
|
files: list[str] = []
|
|
325
|
+
if discovery.get("source") == "run-workflow.py discovery-summary":
|
|
326
|
+
for codebase in discovery.get("codebases", []):
|
|
327
|
+
files.extend(str(item) for item in codebase.get("top_files", []) if item)
|
|
328
|
+
files.extend(fallback_files)
|
|
329
|
+
return unique(files)[:24]
|
|
320
330
|
for codebase in discovery.get("codebases", []):
|
|
321
331
|
for match in codebase.get("matches", []):
|
|
322
332
|
file_path = str(match.get("file", "")).strip()
|
|
@@ -369,7 +379,7 @@ def main() -> None:
|
|
|
369
379
|
summary = summarize_todo(todo_text)
|
|
370
380
|
docs = existing_reference_files(config)
|
|
371
381
|
target_codebases, impacted_files, impacted_modules = collect_target_plan_data(config, codebases)
|
|
372
|
-
discovery = read_json(data_artifact_path(config, "discovery.json", session_meta), {})
|
|
382
|
+
discovery = read_json(data_artifact_path(config, "discovery-summary.json", session_meta), {}) or read_json(data_artifact_path(config, "discovery.json", session_meta), {})
|
|
373
383
|
bridge_context = read_json(openspec_bridge_context_path(config), {})
|
|
374
384
|
impacted_files = merge_discovery_files(discovery, impacted_files)
|
|
375
385
|
impacted_modules = infer_java_modules(impacted_files) or impacted_modules
|
|
@@ -12,6 +12,7 @@ from common import (
|
|
|
12
12
|
is_standard_workflow_notification,
|
|
13
13
|
load_workspace_config,
|
|
14
14
|
openspec_change_dir,
|
|
15
|
+
openspec_hash_drift,
|
|
15
16
|
openspec_writeback_dir,
|
|
16
17
|
collect_openspec_cli_context,
|
|
17
18
|
read_json,
|
|
@@ -133,6 +134,8 @@ def main() -> None:
|
|
|
133
134
|
acceptance_result = summary.get("acceptance_result", [])
|
|
134
135
|
if any(item.get("status") != "passed" for item in acceptance_result):
|
|
135
136
|
blockers.append("存在未通过的验收项")
|
|
137
|
+
hash_drifts = openspec_hash_drift(config, summary.get("openspec_hashes", {}))
|
|
138
|
+
blockers.extend(hash_drifts)
|
|
136
139
|
blockers.extend(notification_blockers(config, summary))
|
|
137
140
|
spec_conflicts = detect_spec_conflicts(summary)
|
|
138
141
|
if spec_conflicts:
|
|
@@ -159,6 +162,7 @@ def main() -> None:
|
|
|
159
162
|
"verify_result": summary.get("verify", {}).get("result", ""),
|
|
160
163
|
"spec_impacts": summary.get("spec_impacts", []),
|
|
161
164
|
"spec_conflicts": spec_conflicts,
|
|
165
|
+
"openspec_hash_drifts": hash_drifts,
|
|
162
166
|
"merge_mode": merge_mode,
|
|
163
167
|
"acceptance_result": acceptance_result,
|
|
164
168
|
"residual_risks": summary.get("residual_risks", []),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
import json
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
@@ -11,6 +12,7 @@ from common import (
|
|
|
11
12
|
current_session_is_stale,
|
|
12
13
|
current_session_meta,
|
|
13
14
|
data_artifact_path,
|
|
15
|
+
acquire_workflow_lock,
|
|
14
16
|
ensure_plan_can_run,
|
|
15
17
|
ensure_status,
|
|
16
18
|
load_workspace_config,
|
|
@@ -20,6 +22,7 @@ from common import (
|
|
|
20
22
|
planned_codebase,
|
|
21
23
|
read_json,
|
|
22
24
|
read_se_state,
|
|
25
|
+
release_workflow_lock,
|
|
23
26
|
require_se_state,
|
|
24
27
|
report_artifact_path,
|
|
25
28
|
recover_se_state_from_artifacts,
|
|
@@ -165,7 +168,7 @@ def command_validate_state(workspace: Path | None, command: str | None) -> None:
|
|
|
165
168
|
raise SystemExit(1)
|
|
166
169
|
|
|
167
170
|
|
|
168
|
-
def command_route_se(workspace: Path | None, command_text: str | None, timeout_seconds: int, force: bool = False) -> None:
|
|
171
|
+
def command_route_se(workspace: Path | None, command_text: str | None, timeout_seconds: int, force: bool = False, output_json: bool = False) -> None:
|
|
169
172
|
if not command_text:
|
|
170
173
|
raise SystemExit("缺少 /se:* 命令文本。")
|
|
171
174
|
parsed = parse_se_command(command_text)
|
|
@@ -178,27 +181,88 @@ def command_route_se(workspace: Path | None, command_text: str | None, timeout_s
|
|
|
178
181
|
print(f"argument={argument}")
|
|
179
182
|
if se_command == "/se:init":
|
|
180
183
|
command_init(workspace)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
184
|
+
print_route_reply_constraint(se_command)
|
|
185
|
+
return
|
|
186
|
+
config = load_workspace_config(workspace)
|
|
187
|
+
lock_path = None
|
|
188
|
+
if se_command != "/se:status":
|
|
189
|
+
try:
|
|
190
|
+
lock_path = acquire_workflow_lock(config, se_command)
|
|
191
|
+
print(f"workflow_lock={lock_path}")
|
|
192
|
+
except RuntimeError as error:
|
|
193
|
+
if output_json:
|
|
194
|
+
print(json.dumps({"se_command": se_command, "run_command": run_command, "result": "blocked", "error": str(error)}, ensure_ascii=False, indent=2))
|
|
195
|
+
raise SystemExit(str(error))
|
|
196
|
+
try:
|
|
197
|
+
if se_command == "/se:propose":
|
|
198
|
+
command_propose_openspec(workspace, argument or None)
|
|
199
|
+
elif se_command == "/se:bridge":
|
|
200
|
+
command_bootstrap_openspec(workspace, explicit_se_bridge=True)
|
|
201
|
+
elif se_command == "/se:plan":
|
|
202
|
+
command_plan(workspace)
|
|
203
|
+
elif se_command == "/se:apply":
|
|
204
|
+
command_apply(workspace, timeout_seconds)
|
|
205
|
+
elif se_command == "/se:review":
|
|
206
|
+
command_review(workspace)
|
|
207
|
+
elif se_command == "/se:verify":
|
|
208
|
+
command_verify(workspace, timeout_seconds, force)
|
|
209
|
+
elif se_command == "/se:archive-check":
|
|
210
|
+
command_prepare_archive_openspec(workspace)
|
|
211
|
+
elif se_command == "/se:archive":
|
|
212
|
+
command_archive_openspec(workspace)
|
|
213
|
+
elif se_command == "/se:status":
|
|
214
|
+
command_status(workspace)
|
|
215
|
+
else:
|
|
216
|
+
raise SystemExit(f"不支持的 /se:* 命令:{se_command}")
|
|
217
|
+
finally:
|
|
218
|
+
release_workflow_lock(lock_path)
|
|
201
219
|
print_route_reply_constraint(se_command)
|
|
220
|
+
if output_json:
|
|
221
|
+
state = validate_se_state(load_workspace_config(workspace), run_command)
|
|
222
|
+
payload = {
|
|
223
|
+
"se_command": se_command,
|
|
224
|
+
"run_command": run_command,
|
|
225
|
+
"argument": argument,
|
|
226
|
+
"result": "ok",
|
|
227
|
+
"phase": state.get("phase", ""),
|
|
228
|
+
"allowed_next": state.get("allowed_next", []),
|
|
229
|
+
}
|
|
230
|
+
try:
|
|
231
|
+
payload["session_id"] = current_session_meta(load_workspace_config(workspace)).get("session_id", "")
|
|
232
|
+
except FileNotFoundError:
|
|
233
|
+
payload["session_id"] = ""
|
|
234
|
+
print("route_result_json_begin")
|
|
235
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
236
|
+
print("route_result_json_end")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def command_route_check(workspace: Path | None, command_text: str | None) -> None:
|
|
240
|
+
if not command_text:
|
|
241
|
+
raise SystemExit("缺少 /se:* 命令文本。")
|
|
242
|
+
config = load_workspace_config(workspace)
|
|
243
|
+
parsed = parse_se_command(command_text)
|
|
244
|
+
se_command = parsed["se_command"]
|
|
245
|
+
run_command = parsed["run_command"]
|
|
246
|
+
result = validate_se_state(config, run_command)
|
|
247
|
+
payload = {
|
|
248
|
+
"se_command": se_command,
|
|
249
|
+
"run_command": run_command,
|
|
250
|
+
"argument": str(parsed.get("argument", "")).strip(),
|
|
251
|
+
"workflow_source": workflow_source(config),
|
|
252
|
+
"allowed": bool(result.get("valid")),
|
|
253
|
+
"phase": result.get("phase", ""),
|
|
254
|
+
"allowed_next": result.get("allowed_next", []),
|
|
255
|
+
"errors": result.get("errors", []),
|
|
256
|
+
"state_path": result.get("state_path", ""),
|
|
257
|
+
}
|
|
258
|
+
try:
|
|
259
|
+
session = current_session_meta(config)
|
|
260
|
+
payload["session_id"] = session.get("session_id", "")
|
|
261
|
+
except FileNotFoundError:
|
|
262
|
+
payload["session_id"] = ""
|
|
263
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
264
|
+
if not payload["allowed"]:
|
|
265
|
+
raise SystemExit(1)
|
|
202
266
|
|
|
203
267
|
|
|
204
268
|
def print_route_reply_constraint(se_command: str) -> None:
|
|
@@ -473,19 +537,22 @@ def command_apply(workspace: Path | None, timeout_seconds: int) -> None:
|
|
|
473
537
|
|
|
474
538
|
def main() -> None:
|
|
475
539
|
parser = argparse.ArgumentParser(description="super-engineer 统一工作流入口。")
|
|
476
|
-
parser.add_argument("command", choices=["route-se", "init", "propose-openspec", "bootstrap-openspec", "writeback-openspec", "prepare-archive-openspec", "archive-openspec", "discover", "plan", "apply", "start-implement", "finish-implement", "self-check", "review", "verify", "status", "next", "validate-state", "assert-standard-session"])
|
|
540
|
+
parser.add_argument("command", choices=["route-se", "route-check", "init", "propose-openspec", "bootstrap-openspec", "writeback-openspec", "prepare-archive-openspec", "archive-openspec", "discover", "plan", "apply", "start-implement", "finish-implement", "self-check", "review", "verify", "status", "next", "validate-state", "assert-standard-session"])
|
|
477
541
|
parser.add_argument("change_name", nargs="?", help="配合 propose-openspec 或 validate-state 使用。")
|
|
478
542
|
parser.add_argument("--command-text", help="配合 route-se 使用,传入完整 /se:* 命令文本。")
|
|
479
543
|
parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
|
|
480
544
|
parser.add_argument("--timeout-seconds", type=int, default=300)
|
|
481
545
|
parser.add_argument("--force", action="store_true", help="配合 verify 使用,强制重跑验证并覆盖结果。")
|
|
546
|
+
parser.add_argument("--json", action="store_true", help="输出机器可读摘要。")
|
|
482
547
|
parser.add_argument("--explicit-se-bridge", action="store_true", help="确认本次 bootstrap-openspec 来自用户显式 /se:bridge 命令。")
|
|
483
548
|
args = parser.parse_args()
|
|
484
549
|
|
|
485
550
|
workspace = Path(args.workspace).expanduser() if args.workspace else None
|
|
486
551
|
|
|
487
552
|
if args.command == "route-se":
|
|
488
|
-
command_route_se(workspace, args.command_text or args.change_name, args.timeout_seconds, args.force)
|
|
553
|
+
command_route_se(workspace, args.command_text or args.change_name, args.timeout_seconds, args.force, args.json)
|
|
554
|
+
elif args.command == "route-check":
|
|
555
|
+
command_route_check(workspace, args.command_text or args.change_name)
|
|
489
556
|
elif args.command == "init":
|
|
490
557
|
command_init(workspace)
|
|
491
558
|
elif args.command == "propose-openspec":
|
|
@@ -10,6 +10,7 @@ from common import (
|
|
|
10
10
|
format_duration,
|
|
11
11
|
load_workspace_config,
|
|
12
12
|
now_iso,
|
|
13
|
+
openspec_artifact_hashes,
|
|
13
14
|
openspec_bridge_context_path,
|
|
14
15
|
openspec_change_dir,
|
|
15
16
|
openspec_writeback_dir,
|
|
@@ -265,6 +266,7 @@ def main() -> None:
|
|
|
265
266
|
if str(status.get("phase", "")).strip() != "done":
|
|
266
267
|
archive_blockers.append(f"当前状态不是 done:{status.get('phase', '')}")
|
|
267
268
|
|
|
269
|
+
mapping = task_mapping(plan, verify, todo_text)
|
|
268
270
|
payload = {
|
|
269
271
|
"change_name": str(config.get("openspec", {}).get("change_name", "")),
|
|
270
272
|
"change_dir": str(openspec_change_dir(config)),
|
|
@@ -295,7 +297,8 @@ def main() -> None:
|
|
|
295
297
|
"sections": verify.get("sections", []),
|
|
296
298
|
},
|
|
297
299
|
"acceptance_result": summarize_acceptance(plan, verify),
|
|
298
|
-
"task_mapping":
|
|
300
|
+
"task_mapping": mapping,
|
|
301
|
+
"openspec_hashes": openspec_artifact_hashes(config),
|
|
299
302
|
"spec_impacts": infer_spec_impacts(plan, plan_bridge_context),
|
|
300
303
|
"residual_risks": residual_risks(plan, review, verify),
|
|
301
304
|
"manual_decisions": [],
|
|
@@ -310,6 +313,7 @@ def main() -> None:
|
|
|
310
313
|
|
|
311
314
|
output_dir = openspec_writeback_dir(config)
|
|
312
315
|
write_managed_json(config, output_dir / "execution-summary.json", payload)
|
|
316
|
+
write_managed_json(config, output_dir / "task-mapping.json", {"source": "run-workflow.py writeback-openspec", "items": mapping})
|
|
313
317
|
write_managed_text(config, output_dir / "execution-summary.md", build_markdown(payload))
|
|
314
318
|
print(f"writeback_dir={output_dir}")
|
|
315
319
|
print(f"change_dir={openspec_change_dir(config)}")
|