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.
Files changed (26) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +11 -0
  3. 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
  4. package/docs//346/250/241/346/235/277/344/275/277/347/224/250/346/214/207/345/215/227.md +55 -0
  5. package/package.json +4 -2
  6. package/scripts/se-cli.py +77 -0
  7. package/scripts/se-e2e-test.py +369 -0
  8. package/scripts/se-setup.py +12 -83
  9. package/super-engineer-workflow/SKILL.md +50 -405
  10. package/super-engineer-workflow/references/se-commands.md +5 -1
  11. package/super-engineer-workflow/references/workflow.md +3 -1
  12. package/super-engineer-workflow/scripts/common.py +103 -1
  13. package/super-engineer-workflow/scripts/generate-review-report.py +18 -2
  14. package/super-engineer-workflow/scripts/generate-self-check.py +1 -1
  15. package/super-engineer-workflow/scripts/generate-smart-plan.py +24 -0
  16. package/super-engineer-workflow/scripts/propose-openspec.py +20 -11
  17. package/super-engineer-workflow/scripts/run-verify-and-report.py +28 -5
  18. package/super-engineer-workflow/scripts/run-workflow.py +18 -4
  19. package/super-engineer-workflow/scripts/writeback-openspec.py +9 -1
  20. package/templates/workspaces/frontend.yml +18 -0
  21. package/templates/workspaces/java-microservice.yml +19 -0
  22. package/templates/workspaces/multi-repo.yml +21 -0
  23. package/templates/workspaces/openspec-auto.yml +18 -0
  24. package/templates/workspaces/openspec-manual.yml +16 -0
  25. package/templates/workspaces/todo-auto.yml +13 -0
  26. 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
- - 每次执行 `plan` 都必须创建新的 `session_id`
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 in ("/se:plan", "/se:apply"):
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 = read_json(data_artifact_path(config, "plan.json", session_meta), {})
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
- "demand_text": demand_text,
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 demand_text, reference_files, and OpenSpec instructions to create or update proposal.md, design.md, tasks.md, and specs/.",
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": "代码暂未修改。下一步只能执行 /se:bridge,把当前 OpenSpec tasks.md 桥接为待审核 todo.md。",
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
- demand_text or "未配置或未找到 demand_file。",
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
- item["content"] or "文件为空或无法读取。",
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=代码暂未修改。下一步只能执行 /se:bridge,把当前 OpenSpec tasks.md 桥接为待审核 todo.md。")
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": "代码暂未修改。下一步只能执行 /se:bridge,把当前 OpenSpec tasks.md 桥接为待审核 todo.md。",
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 已生成。请先人工审核 todo.md,审核通过后再发送 /se:apply。",
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": "计划已生成。下一步只有在确认计划后执行 /se:apply。",
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 = read_json(data_artifact_path(config, "plan.json", session_meta), {})
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 模式适合首次接入、复杂需求或需要逐步确认的团队。