super-engineer-workflow 0.1.4 → 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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ - 修复重复执行 `/se:plan` 会创建多个 session 的问题:计划阶段复用当前 session,交付中重复 plan 会被拒绝。
6
+ - 增加 E2E 测试,覆盖模板 CLI、OpenSpec 状态机与桥接、todo auto 会话、verify 长日志截断。
7
+ - 增加 `se templates`、`se template show`、`se template copy`,支持复制内置 `workspace.yml` 模板。
8
+ - 增加 OpenSpec、todo、Java 微服务、前端、多仓库等工作区模板。
9
+ - 增加模板使用指南,并在 README 和快速初始化文档中补充模板入口。
10
+
3
11
  ## 0.1.4
4
12
 
5
13
  - Review 阶段优先读取 `plan-summary.json`,仅在缺失时回退到 `plan.json`。
package/README.md CHANGED
@@ -111,12 +111,23 @@ se init
111
111
  ```bash
112
112
  se init # 交互式安装 skill 并初始化工作区
113
113
  se doctor # 检查本机环境和 workspace.yml
114
+ se templates # 查看内置 workspace.yml 模板
114
115
  se install # 安装 skill 到 Codex / Claude
115
116
  se sync # 强制同步最新 skill 到 Codex / Claude
116
117
  se migrate # 补齐旧工作区缺失配置
117
118
  se version # 查看版本
118
119
  ```
119
120
 
121
+ 模板入口:
122
+
123
+ ```bash
124
+ se templates
125
+ se template show openspec-auto
126
+ se template copy openspec-auto --workspace /path/to/ai-workspace --demand-name 13-your-demand --code-path ../code
127
+ ```
128
+
129
+ 模板说明见 [docs/模板使用指南.md](/Users/muke/Documents/personal/codex/super-engineer/docs/模板使用指南.md)。
130
+
120
131
  本地源码开发时,也可以直接使用引导脚本。默认是一步一步的交互式向导:
121
132
 
122
133
  ```bash
@@ -176,12 +176,25 @@ se init
176
176
  ```bash
177
177
  se init # 交互式安装 skill 并初始化工作区
178
178
  se doctor # 检查本机环境和 workspace.yml
179
+ se templates # 查看内置 workspace.yml 模板
179
180
  se install # 安装 skill 到 Codex / Claude
180
181
  se sync # 强制同步最新 skill 到 Codex / Claude
181
182
  se migrate # 补齐旧工作区缺失配置
182
183
  se version # 查看版本
183
184
  ```
184
185
 
186
+ 如果你不想从零写 `workspace.yml`,可以先复制内置模板:
187
+
188
+ ```bash
189
+ se templates
190
+ se template copy openspec-auto \
191
+ --workspace /path/to/ai-workspace \
192
+ --demand-name 10-your-demand \
193
+ --code-path ../code
194
+ ```
195
+
196
+ 模板不会覆盖已有 `workspace.yml`,除非显式增加 `--force`。模板说明见 `docs/模板使用指南.md`。
197
+
185
198
  npm 安装的核心要求:
186
199
 
187
200
  - `package.json` 声明 `bin.super-engineer` 和 `bin.se`。
@@ -0,0 +1,55 @@
1
+ # 模板使用指南
2
+
3
+ `super-engineer` 内置了一组 `workspace.yml` 模板,用于减少团队初始化成本,并让不同项目按统一结构接入工作流。
4
+
5
+ ## 查看模板
6
+
7
+ ```bash
8
+ se templates
9
+ se template show openspec-auto
10
+ ```
11
+
12
+ ## 复制模板到工作区
13
+
14
+ ```bash
15
+ se template copy openspec-auto \
16
+ --workspace /path/to/ai-workspace \
17
+ --demand-name 13-your-demand \
18
+ --code-path ../code
19
+ ```
20
+
21
+ 默认不会覆盖已有 `workspace.yml`。确实需要覆盖时加:
22
+
23
+ ```bash
24
+ se template copy openspec-auto --workspace /path/to/ai-workspace --force
25
+ ```
26
+
27
+ ## 模板选择
28
+
29
+ | 模板 | 适用场景 |
30
+ | --- | --- |
31
+ | `openspec-auto` | 推荐默认模板。先生成 OpenSpec change,再桥接 todo,审核后自动交付。 |
32
+ | `openspec-manual` | 高风险需求。每个关键阶段都需要人工确认。 |
33
+ | `todo-auto` | 小需求、任务已经明确、不需要规格沉淀。 |
34
+ | `todo-manual` | 首次接入、培训、新手熟悉工作流。 |
35
+ | `java-microservice` | Java / Spring 微服务,默认使用 Maven 验证。 |
36
+ | `frontend` | Vue / React 前端项目,默认使用 pnpm 验证。 |
37
+ | `multi-repo` | 多仓库聚合目录,适合中台系统跨服务需求。 |
38
+
39
+ ## 初始化后需要补齐什么
40
+
41
+ 模板只负责生成配置骨架。使用前还需要确认:
42
+
43
+ - `code_path` 指向真实代码目录。
44
+ - `vars.demand_name` 是当前需求目录名。
45
+ - `demand_file` 或 `todo_file` 对应文件已经存在。
46
+ - `reference_files` 中的文档存在且与当前项目相关。
47
+ - `verify_commands` 与团队实际构建命令一致。
48
+
49
+ ## 推荐实践
50
+
51
+ 存量中台系统优先使用 `openspec-auto` 或 `multi-repo`。
52
+
53
+ 小范围试点时使用 `openspec-manual`,确认流程稳定后再切换到 `openspec-auto`。
54
+
55
+ 如果需求本身已经拆成明确任务,不需要长期规格沉淀,可以使用 `todo-auto`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-engineer-workflow",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Super Engineer workflow skill and CLI for demand-driven AI delivery with OpenSpec bridge support.",
5
5
  "license": "MIT",
6
6
  "author": "Gary-Coding",
@@ -31,6 +31,7 @@
31
31
  "super-engineer-workflow/",
32
32
  "!super-engineer-workflow/scripts/__pycache__/",
33
33
  "!super-engineer-workflow/scripts/**/*.pyc",
34
+ "templates/",
34
35
  "docs/",
35
36
  "README.md",
36
37
  "LICENSE",
@@ -40,8 +41,9 @@
40
41
  ],
41
42
  "scripts": {
42
43
  "setup": "python3 scripts/se-setup.py",
43
- "check": "python3 -m py_compile scripts/se-cli.py scripts/se-setup.py scripts/se-smoke-test.py super-engineer-workflow/scripts/*.py",
44
+ "check": "python3 -m py_compile scripts/se-cli.py scripts/se-setup.py scripts/se-smoke-test.py scripts/se-e2e-test.py super-engineer-workflow/scripts/*.py",
44
45
  "smoke": "python3 scripts/se-smoke-test.py",
46
+ "e2e": "python3 scripts/se-e2e-test.py",
45
47
  "pack:check": "npm pack --dry-run"
46
48
  },
47
49
  "keywords": [
package/scripts/se-cli.py CHANGED
@@ -14,6 +14,18 @@ from pathlib import Path
14
14
  REPO_ROOT = Path(__file__).resolve().parents[1]
15
15
  SKILL_DIR = REPO_ROOT / "super-engineer-workflow"
16
16
  PACKAGE_JSON = REPO_ROOT / "package.json"
17
+ TEMPLATE_DIR = REPO_ROOT / "templates" / "workspaces"
18
+
19
+
20
+ WORKSPACE_TEMPLATES: dict[str, str] = {
21
+ "openspec-auto": "OpenSpec 规格先行 + 自动交付,推荐用于需求迭代主流程。",
22
+ "openspec-manual": "OpenSpec 规格先行 + 分阶段人工确认,推荐用于高风险变更。",
23
+ "todo-auto": "todo.md 直接驱动 + 自动交付,推荐用于小需求或已有明确任务清单。",
24
+ "todo-manual": "todo.md 直接驱动 + 分阶段人工确认,推荐用于首次接入或新手练习。",
25
+ "java-microservice": "Java / Spring 微服务模板,内置 Maven 验证命令示例。",
26
+ "frontend": "Vue / React 前端模板,内置 pnpm/npm 验证命令示例。",
27
+ "multi-repo": "多仓库聚合目录模板,适用于中台或跨服务需求。",
28
+ }
17
29
 
18
30
 
19
31
  def main() -> None:
@@ -49,6 +61,19 @@ def main() -> None:
49
61
  migrate_parser.add_argument("--workspace", default=".", help="工作区目录,默认当前目录。")
50
62
  migrate_parser.add_argument("--dry-run", action="store_true", help="只展示计划,不写入文件。")
51
63
 
64
+ subparsers.add_parser("templates", help="列出内置 workspace.yml 模板。")
65
+
66
+ template_parser = subparsers.add_parser("template", help="查看或复制内置 workspace.yml 模板。")
67
+ template_subparsers = template_parser.add_subparsers(dest="template_action")
68
+ template_show = template_subparsers.add_parser("show", help="打印指定模板内容。")
69
+ template_show.add_argument("name", choices=sorted(WORKSPACE_TEMPLATES))
70
+ template_copy = template_subparsers.add_parser("copy", help="复制指定模板到工作区 workspace.yml。")
71
+ template_copy.add_argument("name", choices=sorted(WORKSPACE_TEMPLATES))
72
+ template_copy.add_argument("--workspace", default=".", help="工作区目录,默认当前目录。")
73
+ template_copy.add_argument("--demand-name", default="1-your-demand", help="写入 vars.demand_name 的需求目录名。")
74
+ template_copy.add_argument("--code-path", default="../code", help="写入 code_path 的代码目录。")
75
+ template_copy.add_argument("--force", action="store_true", help="允许覆盖已有 workspace.yml。")
76
+
52
77
  subparsers.add_parser("version", help="显示版本号。")
53
78
 
54
79
  args = parser.parse_args()
@@ -67,6 +92,24 @@ def main() -> None:
67
92
  if args.command == "migrate":
68
93
  exit_code = migrate(Path(args.workspace).expanduser().resolve(), dry_run=args.dry_run)
69
94
  raise SystemExit(exit_code)
95
+ if args.command == "templates":
96
+ list_templates()
97
+ return
98
+ if args.command == "template":
99
+ if args.template_action == "show":
100
+ show_template(args.name)
101
+ return
102
+ if args.template_action == "copy":
103
+ copy_template(
104
+ args.name,
105
+ Path(args.workspace).expanduser().resolve(),
106
+ demand_name=args.demand_name,
107
+ code_path=args.code_path,
108
+ force=args.force,
109
+ )
110
+ return
111
+ template_parser.print_help()
112
+ return
70
113
 
71
114
  parser.print_help()
72
115
 
@@ -119,6 +162,40 @@ def install_skill(target: Path, force: bool) -> None:
119
162
  print(f"✓ 已同步 skill: {target}")
120
163
 
121
164
 
165
+ def template_path(name: str) -> Path:
166
+ path = TEMPLATE_DIR / f"{name}.yml"
167
+ if not path.exists():
168
+ raise SystemExit(f"模板不存在:{name}")
169
+ return path
170
+
171
+
172
+ def list_templates() -> None:
173
+ print("内置 workspace.yml 模板:")
174
+ for name in sorted(WORKSPACE_TEMPLATES):
175
+ marker = "✓" if template_path(name).exists() else "!"
176
+ print(f"{marker} {name}: {WORKSPACE_TEMPLATES[name]}")
177
+
178
+
179
+ def render_template(name: str, demand_name: str, code_path: str) -> str:
180
+ text = template_path(name).read_text(encoding="utf-8")
181
+ return text.replace("__DEMAND_NAME__", demand_name).replace("__CODE_PATH__", code_path)
182
+
183
+
184
+ def show_template(name: str) -> None:
185
+ print(render_template(name, demand_name="1-your-demand", code_path="../code"))
186
+
187
+
188
+ def copy_template(name: str, workspace: Path, demand_name: str, code_path: str, force: bool) -> None:
189
+ workspace.mkdir(parents=True, exist_ok=True)
190
+ target = workspace / "workspace.yml"
191
+ if target.exists() and not force:
192
+ raise SystemExit(f"workspace.yml 已存在:{target}。如需覆盖请加 --force。")
193
+ target.write_text(render_template(name, demand_name=demand_name, code_path=code_path), encoding="utf-8")
194
+ (workspace / "docs").mkdir(parents=True, exist_ok=True)
195
+ (workspace / "superengineer" / demand_name).mkdir(parents=True, exist_ok=True)
196
+ print(f"✓ 已写入模板:{target}")
197
+
198
+
122
199
  def doctor(workspace: Path, output_json: bool) -> int:
123
200
  checks: list[dict[str, str]] = []
124
201
  add_check(checks, "python", "ok", sys.version.split()[0])
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+
13
+ REPO_ROOT = Path(__file__).resolve().parents[1]
14
+ RUN_WORKFLOW = REPO_ROOT / "super-engineer-workflow" / "scripts" / "run-workflow.py"
15
+ CLI = REPO_ROOT / "bin" / "super-engineer.js"
16
+
17
+
18
+ def main() -> None:
19
+ with tempfile.TemporaryDirectory(prefix="se-e2e-") as tmp:
20
+ root = Path(tmp)
21
+ home = root / "home"
22
+ home.mkdir()
23
+ os.environ["HOME"] = str(home)
24
+ os.environ["USERPROFILE"] = str(home)
25
+ test_templates_cli(root)
26
+ test_openspec_state_and_bridge(root)
27
+ test_todo_auto_session_and_verify_compaction(root)
28
+ print("e2e_test=ok")
29
+
30
+
31
+ def test_templates_cli(root: Path) -> None:
32
+ workspace = root / "template-workspace"
33
+ output = run(["node", str(CLI), "templates"])
34
+ if "openspec-auto" not in output or "todo-auto" not in output:
35
+ raise AssertionError("templates list did not include expected templates")
36
+
37
+ show_output = run(["node", str(CLI), "template", "show", "openspec-auto"])
38
+ if "workflow_source: openspec" not in show_output or "mode: auto" not in show_output:
39
+ raise AssertionError("template show returned unexpected content")
40
+
41
+ run(
42
+ [
43
+ "node",
44
+ str(CLI),
45
+ "template",
46
+ "copy",
47
+ "todo-auto",
48
+ "--workspace",
49
+ str(workspace),
50
+ "--demand-name",
51
+ "9-e2e-template",
52
+ "--code-path",
53
+ "../code",
54
+ ]
55
+ )
56
+ workspace_yml = read_text(workspace / "workspace.yml")
57
+ if "workflow_source: todo" not in workspace_yml or "9-e2e-template" not in workspace_yml:
58
+ raise AssertionError("template copy did not render workspace.yml")
59
+
60
+
61
+ def test_openspec_state_and_bridge(root: Path) -> None:
62
+ workspace = root / "openspec-workspace"
63
+ code = root / "code" / "demo-service"
64
+ demand_dir = workspace / "superengineer" / "8-demo"
65
+ demand_dir.mkdir(parents=True)
66
+ code.mkdir(parents=True)
67
+ (code / "package.json").write_text(
68
+ json.dumps(
69
+ {
70
+ "name": "demo-service",
71
+ "version": "1.0.0",
72
+ "scripts": {"test": "node -e \"process.exit(0)\""},
73
+ },
74
+ ensure_ascii=False,
75
+ indent=2,
76
+ ),
77
+ encoding="utf-8",
78
+ )
79
+ (workspace / "docs").mkdir(parents=True)
80
+ (workspace / "openspec" / "changes").mkdir(parents=True)
81
+ (workspace / "openspec" / "specs").mkdir(parents=True)
82
+ (demand_dir / "需求.md").write_text(
83
+ "# 需求\n\n为 demo-service 增加状态查询接口。\n\n## 验收\n\n- 查询接口返回 ok。\n",
84
+ encoding="utf-8",
85
+ )
86
+ (workspace / "workspace.yml").write_text(
87
+ "\n".join(
88
+ [
89
+ "version: 1",
90
+ "mode: auto",
91
+ "workflow_source: openspec",
92
+ "vars:",
93
+ " demand_name: 8-demo",
94
+ "demand_file: superengineer/${demand_name}/需求.md",
95
+ "todo_file: superengineer/${demand_name}/todo.md",
96
+ "reference_files: []",
97
+ "code_path: ../code/demo-service",
98
+ "output_dir: superengineer/${demand_name}/output",
99
+ "openspec:",
100
+ " changes_dir: openspec/changes",
101
+ "",
102
+ ]
103
+ ),
104
+ encoding="utf-8",
105
+ )
106
+
107
+ invalid = run(
108
+ [
109
+ sys.executable,
110
+ str(RUN_WORKFLOW),
111
+ "route-se",
112
+ "--workspace",
113
+ str(workspace),
114
+ "--command-text",
115
+ "/se:bridge",
116
+ ],
117
+ check=False,
118
+ )
119
+ if invalid.returncode == 0 or "请先执行 /se:propose" not in invalid.output:
120
+ raise AssertionError("/se:bridge before /se:propose should be rejected")
121
+
122
+ env_without_openspec = os.environ.copy()
123
+ env_without_openspec["PATH"] = ""
124
+ propose = run(
125
+ [
126
+ sys.executable,
127
+ str(RUN_WORKFLOW),
128
+ "route-se",
129
+ "--workspace",
130
+ str(workspace),
131
+ "--command-text",
132
+ "/se:propose demo-change",
133
+ ],
134
+ env=env_without_openspec,
135
+ )
136
+ if "final_reply_must=代码未修改。下一步只能执行 /se:bridge。" not in propose:
137
+ raise AssertionError("/se:propose did not print strict next-step constraint")
138
+
139
+ change_dir = workspace / "openspec" / "changes" / "demo-change"
140
+ (change_dir / "specs").mkdir(parents=True, exist_ok=True)
141
+ (change_dir / "proposal.md").write_text("# Proposal\n\n增加状态查询接口。\n", encoding="utf-8")
142
+ (change_dir / "design.md").write_text("# Design\n\n目标服务 demo-service。\n", encoding="utf-8")
143
+ (change_dir / "tasks.md").write_text(
144
+ "# Tasks\n\n"
145
+ "- [ ] 修改 demo-service controller 增加状态查询接口\n"
146
+ "- [ ] 补充验证,确认接口返回 ok\n",
147
+ encoding="utf-8",
148
+ )
149
+
150
+ bridge = run(
151
+ [
152
+ sys.executable,
153
+ str(RUN_WORKFLOW),
154
+ "route-se",
155
+ "--workspace",
156
+ str(workspace),
157
+ "--command-text",
158
+ "/se:bridge",
159
+ ]
160
+ )
161
+ if "bridge_generated=true" not in bridge:
162
+ raise AssertionError("/se:bridge did not generate todo.md")
163
+ todo = read_text(workspace / "superengineer" / "8-demo" / "todo.md")
164
+ if "demo-change" not in todo or "demo-service" not in todo:
165
+ raise AssertionError("bridged todo.md missing expected OpenSpec context")
166
+
167
+ first_plan = run(
168
+ [
169
+ sys.executable,
170
+ str(RUN_WORKFLOW),
171
+ "route-se",
172
+ "--workspace",
173
+ str(workspace),
174
+ "--command-text",
175
+ "/se:plan",
176
+ ]
177
+ )
178
+ if "session_action=created" not in first_plan:
179
+ raise AssertionError("first /se:plan should create a session")
180
+ first_session = read_json(workspace / ".super-engineer" / "current-session.json")
181
+ first_session_id = first_session["session_id"]
182
+
183
+ second_plan = run(
184
+ [
185
+ sys.executable,
186
+ str(RUN_WORKFLOW),
187
+ "route-se",
188
+ "--workspace",
189
+ str(workspace),
190
+ "--command-text",
191
+ "/se:plan",
192
+ ]
193
+ )
194
+ second_session = read_json(workspace / ".super-engineer" / "current-session.json")
195
+ if "session_action=reused" not in second_plan or second_session["session_id"] != first_session_id:
196
+ raise AssertionError("second /se:plan should reuse the existing planning session")
197
+ if len(list((workspace / ".super-engineer" / "sessions").iterdir())) != 1:
198
+ raise AssertionError("repeated /se:plan created an extra session")
199
+
200
+ apply_output = run(
201
+ [
202
+ sys.executable,
203
+ str(RUN_WORKFLOW),
204
+ "route-se",
205
+ "--workspace",
206
+ str(workspace),
207
+ "--command-text",
208
+ "/se:apply",
209
+ ]
210
+ )
211
+ if "apply_phase=implementing" not in apply_output:
212
+ raise AssertionError("/se:apply should enter implementing after planned session")
213
+ blocked_plan = run(
214
+ [
215
+ sys.executable,
216
+ str(RUN_WORKFLOW),
217
+ "route-se",
218
+ "--workspace",
219
+ str(workspace),
220
+ "--command-text",
221
+ "/se:plan",
222
+ ],
223
+ check=False,
224
+ )
225
+ final_session = read_json(workspace / ".super-engineer" / "current-session.json")
226
+ if blocked_plan.returncode == 0 or "不能重新执行 /se:plan" not in blocked_plan.output:
227
+ raise AssertionError("/se:plan during active delivery should be rejected")
228
+ if final_session["session_id"] != first_session_id or len(list((workspace / ".super-engineer" / "sessions").iterdir())) != 1:
229
+ raise AssertionError("rejected /se:plan should not create or switch sessions")
230
+
231
+
232
+ def test_todo_auto_session_and_verify_compaction(root: Path) -> None:
233
+ workspace = root / "todo-workspace"
234
+ code = root / "todo-code" / "demo-service"
235
+ demand_dir = workspace / "superengineer" / "9-demo"
236
+ demand_dir.mkdir(parents=True)
237
+ code.mkdir(parents=True)
238
+ (workspace / "docs").mkdir(parents=True)
239
+ (code / "verify.py").write_text(
240
+ "print('A' * 13000)\n"
241
+ "import sys\n"
242
+ "print('B' * 13000, file=sys.stderr)\n",
243
+ encoding="utf-8",
244
+ )
245
+ (code / "package.json").write_text(
246
+ json.dumps(
247
+ {
248
+ "name": "demo-service",
249
+ "version": "1.0.0",
250
+ "scripts": {"test": "node -e \"process.exit(0)\""},
251
+ },
252
+ ensure_ascii=False,
253
+ indent=2,
254
+ ),
255
+ encoding="utf-8",
256
+ )
257
+ verify_command = f'"{sys.executable}" verify.py'
258
+ (workspace / "workspace.yml").write_text(
259
+ "\n".join(
260
+ [
261
+ "version: 1",
262
+ "mode: auto",
263
+ "workflow_source: todo",
264
+ "vars:",
265
+ " demand_name: 9-demo",
266
+ "todo_file: superengineer/${demand_name}/todo.md",
267
+ "reference_files: []",
268
+ "code_path: ../todo-code/demo-service",
269
+ "output_dir: superengineer/${demand_name}/output",
270
+ "verify_commands:",
271
+ f" default: {verify_command}",
272
+ "",
273
+ ]
274
+ ),
275
+ encoding="utf-8",
276
+ )
277
+ (demand_dir / "todo.md").write_text(
278
+ "# 限制条件\n"
279
+ "- 修改的服务是 demo-service\n\n"
280
+ "# 待办事项\n\n"
281
+ "- [ ] 增加状态查询接口\n"
282
+ "1. 返回 ok\n\n"
283
+ "## 验收补充\n"
284
+ "- [ ] 执行验证命令\n",
285
+ encoding="utf-8",
286
+ )
287
+
288
+ apply_output = run(
289
+ [
290
+ sys.executable,
291
+ str(RUN_WORKFLOW),
292
+ "route-se",
293
+ "--workspace",
294
+ str(workspace),
295
+ "--command-text",
296
+ "/se:apply",
297
+ ]
298
+ )
299
+ if "apply_phase=implementing" not in apply_output:
300
+ raise AssertionError("/se:apply did not enter implementing phase")
301
+
302
+ run([sys.executable, str(RUN_WORKFLOW), "finish-implement", "--workspace", str(workspace)])
303
+ session = read_json(workspace / ".super-engineer" / "current-session.json")
304
+ data_dir = Path(session["data_dir"])
305
+ plan_summary = read_json(data_dir / "plan-summary.json")
306
+ verify = read_json(data_dir / "verify.json")
307
+ status = read_json(data_dir / "status.json")
308
+ notification = read_json(data_dir / "notification.json")
309
+
310
+ if not plan_summary.get("target_codebases"):
311
+ raise AssertionError("plan-summary.json missing target_codebases")
312
+ if verify.get("result") != "通过":
313
+ raise AssertionError("verify did not pass")
314
+ stdout = verify["sections"][0]["stdout"]
315
+ stderr = verify["sections"][0]["stderr"]
316
+ if "已省略" not in stdout or "已省略" not in stderr:
317
+ raise AssertionError("verify stdout/stderr were not compacted")
318
+ if len(stdout) > 12200 or len(stderr) > 12200:
319
+ raise AssertionError("verify stdout/stderr compaction exceeded expected size")
320
+ if status.get("phase") != "done":
321
+ raise AssertionError("todo auto session did not finish with done status")
322
+ if notification.get("status") != "skipped":
323
+ raise AssertionError("notification should be marked skipped when no provider is configured")
324
+
325
+
326
+ def run(command: list[str], check: bool = True, env: dict[str, str] | None = None):
327
+ result = subprocess.run(
328
+ command,
329
+ cwd=REPO_ROOT,
330
+ env=env,
331
+ text=True,
332
+ stdout=subprocess.PIPE,
333
+ stderr=subprocess.STDOUT,
334
+ check=False,
335
+ )
336
+ if check:
337
+ if result.returncode != 0:
338
+ print(result.stdout)
339
+ raise SystemExit(result.returncode)
340
+ return result.stdout
341
+ return CommandResult(result.returncode, result.stdout)
342
+
343
+
344
+ class CommandResult:
345
+ def __init__(self, returncode: int, output: str) -> None:
346
+ self.returncode = returncode
347
+ self.output = output
348
+
349
+
350
+ def read_text(path: Path) -> str:
351
+ if not path.is_file():
352
+ raise AssertionError(f"missing file: {path}")
353
+ return path.read_text(encoding="utf-8")
354
+
355
+
356
+ def read_json(path: Path) -> dict:
357
+ if not path.is_file():
358
+ raise AssertionError(f"missing file: {path}")
359
+ with path.open(encoding="utf-8") as handle:
360
+ data = json.load(handle)
361
+ if not isinstance(data, dict):
362
+ raise AssertionError(f"json root is not object: {path}")
363
+ return data
364
+
365
+
366
+ if __name__ == "__main__":
367
+ if shutil.which("node") is None:
368
+ raise SystemExit("node is required")
369
+ main()
@@ -319,6 +319,8 @@ AI 在 `/se:bridge` 完成后必须停止,只能提示用户审核 `todo.md`
319
319
  内部动作:
320
320
 
321
321
  - 执行 `python3 scripts/run-workflow.py plan`
322
+ - 如果当前 session 已有标准 `plan.json` 且仍停留在计划确认阶段,脚本必须复用当前 session
323
+ - 如果当前 session 已进入实现、自查、审查或验证阶段,脚本必须拒绝重复 `/se:plan`,不能创建第二个 session
322
324
 
323
325
  完成后汇报:
324
326
 
@@ -350,7 +352,8 @@ AI 在 `/se:bridge` 完成后必须停止,只能提示用户审核 `todo.md`
350
352
 
351
353
  内部动作:
352
354
 
353
- - 如果没有当前 session,先执行 `python3 scripts/run-workflow.py plan`
355
+ - 如果没有当前 session、当前 session 失效,或当前 session 缺少标准 `plan.json`,先执行 `python3 scripts/run-workflow.py plan`
356
+ - 如果当前 session 已有标准 `plan.json` 且未完成,必须复用当前 session
354
357
  - 执行 `python3 scripts/run-workflow.py start-implement`
355
358
  - 按当前 `plan.json` 实现代码
356
359
  - 执行 `python3 scripts/run-workflow.py finish-implement`
@@ -363,6 +366,7 @@ AI 在 `/se:bridge` 完成后必须停止,只能提示用户审核 `todo.md`
363
366
  - `todo` 模式下也必须通过当前 session 的 `status.json`、`todo-state.json` 和标准产物来源校验,不能复用旧需求 session
364
367
  - `todo` 模式的状态不能写入 OpenSpec 专用 `se-state.json`
365
368
  - `todo` 模式下如果 `current-session.json` 指向旧 `output_dir`,`/se:apply` 必须重新创建标准 session
369
+ - AI 不得在 `/se:apply` 链路中额外手工执行第二次 `/se:plan`;脚本检测到活跃交付 session 时必须拒绝重复计划
366
370
  - AI 只能在 `start-implement` 和 `finish-implement` 之间修改业务代码
367
371
  - AI 不得直接写 `.super-engineer` 下的状态 JSON
368
372
  - AI 不得直接写 output 下的标准 Markdown 报告
@@ -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":
@@ -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,
@@ -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)
@@ -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 模式适合首次接入、复杂需求或需要逐步确认的团队。