super-engineer-workflow 0.1.5 → 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.
Files changed (27) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -10
  3. package/docs/se/345/221/275/344/273/244/345/215/217/350/256/256.md +42 -297
  4. 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
  5. 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
  6. package/package.json +1 -1
  7. package/scripts/se-cli.py +146 -2
  8. package/scripts/se-e2e-test.py +168 -0
  9. package/scripts/se-setup.py +13 -2
  10. package/super-engineer-workflow/SKILL.md +20 -5
  11. package/super-engineer-workflow/references/commands/apply.md +28 -0
  12. package/super-engineer-workflow/references/commands/archive.md +23 -0
  13. package/super-engineer-workflow/references/commands/bridge.md +25 -0
  14. package/super-engineer-workflow/references/commands/common.md +32 -0
  15. package/super-engineer-workflow/references/commands/plan.md +25 -0
  16. package/super-engineer-workflow/references/commands/propose.md +25 -0
  17. package/super-engineer-workflow/references/commands/review.md +22 -0
  18. package/super-engineer-workflow/references/commands/status.md +22 -0
  19. package/super-engineer-workflow/references/commands/verify.md +23 -0
  20. package/super-engineer-workflow/scripts/bootstrap-openspec.py +3 -1
  21. package/super-engineer-workflow/scripts/common.py +143 -7
  22. package/super-engineer-workflow/scripts/generate-discovery.py +44 -0
  23. package/super-engineer-workflow/scripts/generate-smart-plan.py +12 -2
  24. package/super-engineer-workflow/scripts/prepare-archive-openspec.py +4 -0
  25. package/super-engineer-workflow/scripts/run-workflow.py +108 -33
  26. package/super-engineer-workflow/scripts/writeback-openspec.py +5 -1
  27. package/super-engineer-workflow/references/se-commands.md +0 -586
@@ -0,0 +1,25 @@
1
+ # `/se:propose <change-name>`
2
+
3
+ 用途:生成或修正当前 OpenSpec change 的规格文档,不改代码,不生成 todo。
4
+
5
+ ## 前置
6
+
7
+ - `workflow_source=openspec`。
8
+ - 用户必须显式提供 `<change-name>`。
9
+ - 必须读取 `demand_file`,并读取真实存在的 `reference_files` 摘要作为上下文。
10
+
11
+ ## 执行
12
+
13
+ 1. 先执行公共 `route-check`。
14
+ 2. 再执行:
15
+ `python3 scripts/run-workflow.py route-se --command-text "/se:propose <change-name>"`
16
+ 3. 根据 `propose-input.json` 更新或生成 `proposal.md`、`design.md`、`tasks.md`、`specs/`。
17
+ 4. 停在 `proposed` 阶段。
18
+
19
+ ## 禁止
20
+
21
+ - 禁止生成或覆盖 `todo.md`。
22
+ - 禁止执行 `/se:bridge`、`/se:plan`、`/se:apply`。
23
+ - 禁止提示“确认后执行 /se:apply”。
24
+
25
+ 最终回复必须表达:代码未修改,下一步只能执行 `/se:bridge`。
@@ -0,0 +1,22 @@
1
+ # `/se:review`
2
+
3
+ 用途:基于标准 session 和真实代码差异生成 review 结果。
4
+
5
+ ## 前置
6
+
7
+ - 已完成实现和 self-check。
8
+ - 存在标准 `plan.json` / `plan-summary.json`。
9
+
10
+ ## 执行
11
+
12
+ 1. 先执行公共 `route-check`。
13
+ 2. 再执行:
14
+ `python3 scripts/run-workflow.py route-se --command-text "/se:review"`
15
+ 3. 优先读取 `plan-summary.json`,输出 compact review。
16
+
17
+ ## 禁止
18
+
19
+ - 禁止在未实现完成时 review。
20
+ - 禁止跳过 blocking finding 进入 verify。
21
+
22
+ review 通过时下一步 `/se:verify`;存在 blocking finding 时下一步 `/se:apply` 修复。
@@ -0,0 +1,22 @@
1
+ # `/se:status`
2
+
3
+ 用途:查看当前工作流状态,不修改代码和产物。
4
+
5
+ ## 执行
6
+
7
+ 1. 先执行公共 `route-check`。
8
+ 2. 再执行:
9
+ `python3 scripts/run-workflow.py route-se --command-text "/se:status"`
10
+
11
+ ## 回复
12
+
13
+ 只汇报:
14
+
15
+ - 当前 workflow_source / mode
16
+ - 当前 phase
17
+ - 当前 session
18
+ - 关键产物路径
19
+ - allowed_next
20
+ - blockers
21
+
22
+ 禁止顺带执行下一阶段命令。
@@ -0,0 +1,23 @@
1
+ # `/se:verify`
2
+
3
+ 用途:运行验证命令、生成 verify 报告,并由脚本发送通知。
4
+
5
+ ## 前置
6
+
7
+ - 已完成 review。
8
+ - 存在标准 session。
9
+
10
+ ## 执行
11
+
12
+ 1. 先执行公共 `route-check`。
13
+ 2. 再执行:
14
+ `python3 scripts/run-workflow.py route-se --command-text "/se:verify"`
15
+ 3. 验证日志由脚本截断摘要。
16
+ 4. 通知只能由 verify 脚本发送,成功证据是 `notification.json`。
17
+
18
+ ## 禁止
19
+
20
+ - 禁止 AI 直接调用任何 webhook。
21
+ - verify 失败时禁止提示 archive-check。
22
+
23
+ OpenSpec 模式 verify 通过后,下一步 `/se:archive-check`。
@@ -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:
@@ -652,17 +707,13 @@ def create_session(config: dict[str, Any]) -> dict[str, Any]:
652
707
  },
653
708
  )
654
709
  Path(session_meta["data_dir"]).mkdir(parents=True, exist_ok=True)
655
- Path(session_meta["report_dir"]).mkdir(parents=True, exist_ok=True)
656
710
  write_managed_json(config, current_session_file(config), session_meta)
657
711
  return session_meta
658
712
 
659
713
 
660
714
  def current_session_meta(config: dict[str, Any]) -> dict[str, Any]:
661
715
  session_meta = read_json(current_session_file(config), {})
662
- normalized = _normalize_session_meta(config, session_meta)
663
- Path(normalized["data_dir"]).mkdir(parents=True, exist_ok=True)
664
- Path(normalized["report_dir"]).mkdir(parents=True, exist_ok=True)
665
- return normalized
716
+ return _normalize_session_meta(config, session_meta)
666
717
 
667
718
 
668
719
  def current_session_status(config: dict[str, Any], session_meta: dict[str, Any] | None = None) -> dict[str, Any]:
@@ -678,16 +729,26 @@ def active_session_for_plan(config: dict[str, Any]) -> dict[str, Any] | None:
678
729
  session_meta = current_session_meta(config)
679
730
  except FileNotFoundError:
680
731
  return None
681
- if not data_artifact_path(config, "plan.json", session_meta).exists():
682
- return None
732
+ data_dir = Path(str(session_meta["data_dir"]))
733
+ has_plan = data_artifact_path(config, "plan.json", session_meta).exists()
683
734
  status = current_session_status(config, session_meta)
684
735
  status_phase = str(status.get("phase", "") or "").strip()
685
736
  if status_phase in ("done", "archived", "blocked"):
686
737
  return None
738
+ if not has_plan and data_dir.exists():
739
+ return {
740
+ "session": session_meta,
741
+ "status": status,
742
+ "phase": status_phase or "draft",
743
+ "incomplete": True,
744
+ }
745
+ if not has_plan:
746
+ return None
687
747
  return {
688
748
  "session": session_meta,
689
749
  "status": status,
690
750
  "phase": status_phase or "plan",
751
+ "incomplete": False,
691
752
  }
692
753
 
693
754
 
@@ -697,6 +758,8 @@ def ensure_plan_can_run(config: dict[str, Any]) -> dict[str, Any] | None:
697
758
  return None
698
759
  phase = str(active.get("phase", "")).strip()
699
760
  session = active.get("session", {})
761
+ if active.get("incomplete"):
762
+ return active
700
763
  if phase in ("plan", "wait_confirm_plan"):
701
764
  return active
702
765
  raise RuntimeError(
@@ -834,6 +897,71 @@ def update_se_state(
834
897
  return state
835
898
 
836
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
+
837
965
  def _se_state_artifact_exists(path_text: str) -> bool:
838
966
  return bool(path_text and Path(path_text).exists())
839
967
 
@@ -1117,6 +1245,10 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
1117
1245
  errors.append("/se:propose 后不能直接进入交付,必须先执行 /se:bridge。")
1118
1246
  if not _se_state_artifact_exists(str(artifacts.get("todo", ""))):
1119
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。")
1120
1252
  elif se_command == "/se:apply":
1121
1253
  if phase not in ("bridged", "planned", "implementing", "reviewed", "blocked") and se_command not in allowed_next:
1122
1254
  errors.append("当前状态不允许进入交付,请先完成 /se:bridge 并人工审核 todo.md。")
@@ -1124,6 +1256,10 @@ def validate_se_state(config: dict[str, Any], run_command: str) -> dict[str, Any
1124
1256
  errors.append("/se:propose 后不能直接进入交付,必须先执行 /se:bridge。")
1125
1257
  if not _se_state_artifact_exists(str(artifacts.get("todo", ""))):
1126
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。")
1127
1263
  elif se_command == "/se:review":
1128
1264
  if phase not in ("self_checked", "reviewed", "blocked"):
1129
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
- match_count = sum(len(codebase.get("matches", [])) for codebase in discovery.get("codebases", []))
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
- elif se_command == "/se:propose":
182
- command_propose_openspec(workspace, argument or None)
183
- elif se_command == "/se:bridge":
184
- command_bootstrap_openspec(workspace, explicit_se_bridge=True)
185
- elif se_command == "/se:plan":
186
- command_plan(workspace)
187
- elif se_command == "/se:apply":
188
- command_apply(workspace, timeout_seconds)
189
- elif se_command == "/se:review":
190
- command_review(workspace)
191
- elif se_command == "/se:verify":
192
- command_verify(workspace, timeout_seconds, force)
193
- elif se_command == "/se:archive-check":
194
- command_prepare_archive_openspec(workspace)
195
- elif se_command == "/se:archive":
196
- command_archive_openspec(workspace)
197
- elif se_command == "/se:status":
198
- command_status(workspace)
199
- else:
200
- raise SystemExit(f"不支持的 /se:* 命令:{se_command}")
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:
@@ -294,16 +358,24 @@ def command_plan(workspace: Path | None) -> None:
294
358
  raise SystemExit(str(error))
295
359
  if active:
296
360
  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
302
- command_init(workspace)
303
- config = load_workspace_config(workspace)
304
- session_meta = create_session(config)
305
- print("session_action=created")
306
- print(f"session_id={session_meta.get('session_id', '')}")
361
+ session_meta = session
362
+ if active.get("incomplete"):
363
+ print("session_action=reused_incomplete")
364
+ print(f"session_id={session_meta.get('session_id', '')}")
365
+ print(f"phase={active.get('phase', '')}")
366
+ print("next_action=复用未完成计划会话,继续生成 discovery/plan。")
367
+ else:
368
+ print("session_action=reused")
369
+ print(f"session_id={session_meta.get('session_id', '')}")
370
+ print(f"phase={active.get('phase', '')}")
371
+ print("next_action=当前计划已存在,继续执行 /se:apply。")
372
+ return
373
+ else:
374
+ command_init(workspace)
375
+ config = load_workspace_config(workspace)
376
+ session_meta = create_session(config)
377
+ print("session_action=created")
378
+ print(f"session_id={session_meta.get('session_id', '')}")
307
379
  command_discover(workspace)
308
380
  args = ["--workspace", str(workspace)] if workspace else []
309
381
  run_python("generate-smart-plan.py", args)
@@ -465,19 +537,22 @@ def command_apply(workspace: Path | None, timeout_seconds: int) -> None:
465
537
 
466
538
  def main() -> None:
467
539
  parser = argparse.ArgumentParser(description="super-engineer 统一工作流入口。")
468
- 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"])
469
541
  parser.add_argument("change_name", nargs="?", help="配合 propose-openspec 或 validate-state 使用。")
470
542
  parser.add_argument("--command-text", help="配合 route-se 使用,传入完整 /se:* 命令文本。")
471
543
  parser.add_argument("--workspace", help="工作空间路径,默认读取当前目录")
472
544
  parser.add_argument("--timeout-seconds", type=int, default=300)
473
545
  parser.add_argument("--force", action="store_true", help="配合 verify 使用,强制重跑验证并覆盖结果。")
546
+ parser.add_argument("--json", action="store_true", help="输出机器可读摘要。")
474
547
  parser.add_argument("--explicit-se-bridge", action="store_true", help="确认本次 bootstrap-openspec 来自用户显式 /se:bridge 命令。")
475
548
  args = parser.parse_args()
476
549
 
477
550
  workspace = Path(args.workspace).expanduser() if args.workspace else None
478
551
 
479
552
  if args.command == "route-se":
480
- 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)
481
556
  elif args.command == "init":
482
557
  command_init(workspace)
483
558
  elif args.command == "propose-openspec":