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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
11
+ ## 0.1.4
12
+
13
+ - Review 阶段优先读取 `plan-summary.json`,仅在缺失时回退到 `plan.json`。
14
+ - Self-check、verify、OpenSpec writeback 阶段优先读取轻量计划摘要,减少重复加载完整计划。
15
+ - Review 阶段默认压缩过长 diff 摘要,避免对话和报告带入大量无效上下文。
16
+ - Verify 阶段增加命令输出压缩工具,为长日志截断和摘要化提供统一入口。
17
+
18
+ ## 0.1.3
19
+
20
+ - 压缩 `SKILL.md` 为轻量入口,详细规则改为按需读取 references,降低每次 `/se:*` 固定上下文消耗。
21
+ - 快捷命令模板改为极简形式,减少 Claude / Codex slash command 触发时的重复提示词。
22
+ - `/se:propose` 不再把 `reference_files` 全文复制进 `propose-input.json` / `propose-input.md`,改为路径、sha256、标题和摘要片段。
23
+ - 大型 Markdown 参考文档自动摘要,保留按需读取全文能力。
24
+ - OpenSpec bridge context 增加 proposal/design 摘要片段,避免后续阶段重复读取全文。
25
+ - `plan` 阶段新增 `plan-summary.json`,供后续阶段优先读取轻量计划摘要。
26
+ - 收紧脚本最终回复约束文案,默认输出更 compact。
27
+
3
28
  ## 0.1.2
4
29
 
5
30
  - `se init` 在 `openspec` 模式下默认尝试执行 `openspec init . --tools codex,claude`。
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.2",
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()