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.
- package/CHANGELOG.md +25 -0
- package/README.md +11 -0
- 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
- package/docs//346/250/241/346/235/277/344/275/277/347/224/250/346/214/207/345/215/227.md +55 -0
- package/package.json +4 -2
- package/scripts/se-cli.py +77 -0
- package/scripts/se-e2e-test.py +369 -0
- package/scripts/se-setup.py +12 -83
- package/super-engineer-workflow/SKILL.md +50 -405
- package/super-engineer-workflow/references/se-commands.md +5 -1
- package/super-engineer-workflow/references/workflow.md +3 -1
- package/super-engineer-workflow/scripts/common.py +103 -1
- package/super-engineer-workflow/scripts/generate-review-report.py +18 -2
- package/super-engineer-workflow/scripts/generate-self-check.py +1 -1
- package/super-engineer-workflow/scripts/generate-smart-plan.py +24 -0
- package/super-engineer-workflow/scripts/propose-openspec.py +20 -11
- package/super-engineer-workflow/scripts/run-verify-and-report.py +28 -5
- package/super-engineer-workflow/scripts/run-workflow.py +18 -4
- package/super-engineer-workflow/scripts/writeback-openspec.py +9 -1
- package/templates/workspaces/frontend.yml +18 -0
- package/templates/workspaces/java-microservice.yml +19 -0
- package/templates/workspaces/multi-repo.yml +21 -0
- package/templates/workspaces/openspec-auto.yml +18 -0
- package/templates/workspaces/openspec-manual.yml +16 -0
- package/templates/workspaces/todo-auto.yml +13 -0
- 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.
|
|
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()
|