super-engineer-workflow 0.1.0

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 (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/CONTRIBUTING.md +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +300 -0
  5. package/SECURITY.md +21 -0
  6. package/bin/super-engineer.js +19 -0
  7. package/docs/se/345/221/275/344/273/244/345/215/217/350/256/256.md +335 -0
  8. package/docs//344/270/255/346/226/207/344/275/277/347/224/250/346/211/213/345/206/214.md +707 -0
  9. package/docs//345/205/254/345/274/200/345/217/221/345/270/203/346/243/200/346/237/245/346/270/205/345/215/225.md +43 -0
  10. 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 +419 -0
  11. package/docs//351/241/271/347/233/256/346/236/266/346/236/204/344/270/216/350/256/276/350/256/241/350/257/264/346/230/216.md +657 -0
  12. package/package.json +55 -0
  13. package/scripts/se-cli.py +301 -0
  14. package/scripts/se-setup.py +331 -0
  15. package/scripts/se-smoke-test.py +86 -0
  16. package/super-engineer-workflow/SKILL.md +439 -0
  17. package/super-engineer-workflow/adapters/go.yml +8 -0
  18. package/super-engineer-workflow/adapters/java-gradle.yml +8 -0
  19. package/super-engineer-workflow/adapters/java-maven.yml +8 -0
  20. package/super-engineer-workflow/adapters/node-react.yml +8 -0
  21. package/super-engineer-workflow/adapters/node-vue.yml +8 -0
  22. package/super-engineer-workflow/adapters/python.yml +8 -0
  23. package/super-engineer-workflow/agents/openai.yaml +4 -0
  24. package/super-engineer-workflow/assets/config-schema.json +100 -0
  25. package/super-engineer-workflow/assets/config.example.yml +12 -0
  26. package/super-engineer-workflow/assets/plan-schema.json +362 -0
  27. package/super-engineer-workflow/assets/status-schema.json +83 -0
  28. package/super-engineer-workflow/assets/workspace.example.yml +25 -0
  29. package/super-engineer-workflow/config.example.yml +12 -0
  30. package/super-engineer-workflow/references/contracts.md +39 -0
  31. package/super-engineer-workflow/references/execution-modes.md +38 -0
  32. package/super-engineer-workflow/references/java.md +21 -0
  33. package/super-engineer-workflow/references/planning.md +45 -0
  34. package/super-engineer-workflow/references/platform-openclaw.md +10 -0
  35. package/super-engineer-workflow/references/project-docs.md +7 -0
  36. package/super-engineer-workflow/references/review-checklist.md +26 -0
  37. package/super-engineer-workflow/references/se-commands.md +582 -0
  38. package/super-engineer-workflow/references/verify-checklist.md +45 -0
  39. package/super-engineer-workflow/references/workflow.md +208 -0
  40. package/super-engineer-workflow/scripts/archive-openspec.py +110 -0
  41. package/super-engineer-workflow/scripts/bootstrap-openspec.py +42 -0
  42. package/super-engineer-workflow/scripts/common.py +3285 -0
  43. package/super-engineer-workflow/scripts/generate-discovery.py +185 -0
  44. package/super-engineer-workflow/scripts/generate-review-report.py +296 -0
  45. package/super-engineer-workflow/scripts/generate-self-check.py +185 -0
  46. package/super-engineer-workflow/scripts/generate-smart-plan.py +429 -0
  47. package/super-engineer-workflow/scripts/init-workspace.py +68 -0
  48. package/super-engineer-workflow/scripts/prepare-archive-openspec.py +186 -0
  49. package/super-engineer-workflow/scripts/propose-openspec.py +170 -0
  50. package/super-engineer-workflow/scripts/run-verify-and-report.py +399 -0
  51. package/super-engineer-workflow/scripts/run-workflow.py +506 -0
  52. package/super-engineer-workflow/scripts/update-status.py +43 -0
  53. package/super-engineer-workflow/scripts/writeback-openspec.py +311 -0
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import importlib.util
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ REPO_ROOT = Path(__file__).resolve().parents[1]
15
+ SKILL_DIR = REPO_ROOT / "super-engineer-workflow"
16
+ PACKAGE_JSON = REPO_ROOT / "package.json"
17
+
18
+
19
+ def main() -> None:
20
+ if len(sys.argv) == 1:
21
+ run_setup([])
22
+ return
23
+ if sys.argv[1] in ("init", "setup"):
24
+ run_setup(sys.argv[2:])
25
+ return
26
+
27
+ parser = argparse.ArgumentParser(
28
+ prog="se",
29
+ description="Super Engineer workflow CLI.",
30
+ )
31
+ parser.add_argument("--version", action="store_true", help="显示版本号。")
32
+ subparsers = parser.add_subparsers(dest="command")
33
+
34
+ subparsers.add_parser("init", help="交互式安装 skill 并初始化工作区。")
35
+ subparsers.add_parser("setup", help="init 的别名。")
36
+
37
+ install_parser = subparsers.add_parser("install", help="安装 skill 到 Codex / Claude。")
38
+ install_parser.add_argument("--target", choices=["codex", "claude", "both"], default="both")
39
+ install_parser.add_argument("--force", action="store_true", help="安装前删除旧 skill 目录。")
40
+
41
+ sync_parser = subparsers.add_parser("sync", help="重新同步 skill 到 Codex / Claude。")
42
+ sync_parser.add_argument("--target", choices=["codex", "claude", "both"], default="both")
43
+
44
+ doctor_parser = subparsers.add_parser("doctor", help="检查本机环境和工作区配置。")
45
+ doctor_parser.add_argument("--workspace", default=".", help="工作区目录,默认当前目录。")
46
+ doctor_parser.add_argument("--json", action="store_true", help="输出 JSON。")
47
+
48
+ migrate_parser = subparsers.add_parser("migrate", help="补齐旧工作区缺失的 workspace.yml 配置项。")
49
+ migrate_parser.add_argument("--workspace", default=".", help="工作区目录,默认当前目录。")
50
+ migrate_parser.add_argument("--dry-run", action="store_true", help="只展示计划,不写入文件。")
51
+
52
+ subparsers.add_parser("version", help="显示版本号。")
53
+
54
+ args = parser.parse_args()
55
+ if args.version or args.command == "version":
56
+ print(package_version())
57
+ return
58
+ if args.command == "install":
59
+ install_targets(args.target, force=args.force)
60
+ return
61
+ if args.command == "sync":
62
+ install_targets(args.target, force=True)
63
+ return
64
+ if args.command == "doctor":
65
+ exit_code = doctor(Path(args.workspace).expanduser().resolve(), output_json=args.json)
66
+ raise SystemExit(exit_code)
67
+ if args.command == "migrate":
68
+ exit_code = migrate(Path(args.workspace).expanduser().resolve(), dry_run=args.dry_run)
69
+ raise SystemExit(exit_code)
70
+
71
+ parser.print_help()
72
+
73
+
74
+ def package_version() -> str:
75
+ if not PACKAGE_JSON.exists():
76
+ return "0.0.0"
77
+ data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
78
+ return str(data.get("version", "0.0.0"))
79
+
80
+
81
+ def run_setup(args: list[str]) -> None:
82
+ script = REPO_ROOT / "scripts" / "se-setup.py"
83
+ result = subprocess.run([sys.executable, str(script), *args], check=False)
84
+ raise SystemExit(result.returncode)
85
+
86
+
87
+ def install_targets(target: str, force: bool) -> None:
88
+ targets = []
89
+ if target in ("codex", "both"):
90
+ targets.append(skill_target("codex"))
91
+ if target in ("claude", "both"):
92
+ targets.append(skill_target("claude"))
93
+ for item in targets:
94
+ install_skill(item, force=force)
95
+
96
+
97
+ def skill_target(kind: str) -> Path:
98
+ if kind == "codex":
99
+ base = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
100
+ elif kind == "claude":
101
+ base = Path(os.environ.get("CLAUDE_HOME", "~/.claude")).expanduser()
102
+ else:
103
+ raise ValueError(kind)
104
+ return base / "skills" / "super-engineer-workflow"
105
+
106
+
107
+ def install_skill(target: Path, force: bool) -> None:
108
+ if not SKILL_DIR.exists():
109
+ raise SystemExit(f"skill 目录不存在:{SKILL_DIR}")
110
+ if target.exists() and force:
111
+ shutil.rmtree(target)
112
+ target.parent.mkdir(parents=True, exist_ok=True)
113
+ shutil.copytree(
114
+ SKILL_DIR,
115
+ target,
116
+ dirs_exist_ok=True,
117
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
118
+ )
119
+ print(f"✓ 已同步 skill: {target}")
120
+
121
+
122
+ def doctor(workspace: Path, output_json: bool) -> int:
123
+ checks: list[dict[str, str]] = []
124
+ add_check(checks, "python", "ok", sys.version.split()[0])
125
+ add_check(checks, "skill_source", "ok" if SKILL_DIR.exists() else "fail", str(SKILL_DIR))
126
+ add_check(checks, "codex_skill", "ok" if skill_target("codex").exists() else "warn", str(skill_target("codex")))
127
+ add_check(checks, "claude_skill", "ok" if skill_target("claude").exists() else "warn", str(skill_target("claude")))
128
+ add_check(checks, "openspec_cli", "ok" if shutil.which("openspec") else "warn", shutil.which("openspec") or "未安装")
129
+ add_check(checks, "workspace", "ok" if workspace.exists() else "fail", str(workspace))
130
+
131
+ workspace_yml = workspace / "workspace.yml"
132
+ add_check(checks, "workspace_yml", "ok" if workspace_yml.exists() else "fail", str(workspace_yml))
133
+ if workspace_yml.exists():
134
+ try:
135
+ config = read_workspace_yaml(workspace_yml)
136
+ except ValueError as exc:
137
+ add_check(checks, "workspace_yml.parse", "fail", str(exc))
138
+ else:
139
+ add_check(checks, "workspace_yml.parse", "ok", "解析成功")
140
+ validate_workspace(checks, workspace, config)
141
+
142
+ if output_json:
143
+ print(json.dumps({"checks": checks}, ensure_ascii=False, indent=2))
144
+ else:
145
+ print("Super Engineer doctor")
146
+ for check in checks:
147
+ mark = {"ok": "✓", "warn": "!", "fail": "✗"}[check["status"]]
148
+ print(f"{mark} {check['name']}: {check['message']}")
149
+
150
+ return 1 if any(item["status"] == "fail" for item in checks) else 0
151
+
152
+
153
+ def add_check(checks: list[dict[str, str]], name: str, status: str, message: str) -> None:
154
+ checks.append({"name": name, "status": status, "message": message})
155
+
156
+
157
+ def read_workspace_yaml(path: Path) -> dict[str, object]:
158
+ text = path.read_text(encoding="utf-8")
159
+ try:
160
+ import yaml # type: ignore
161
+ except Exception:
162
+ yaml = None
163
+ if yaml is not None:
164
+ loaded = yaml.safe_load(text) or {}
165
+ if not isinstance(loaded, dict):
166
+ raise ValueError("workspace.yml 顶层必须是对象")
167
+ return loaded
168
+
169
+ common_path = SKILL_DIR / "scripts" / "common.py"
170
+ spec = importlib.util.spec_from_file_location("se_common_for_cli", common_path)
171
+ if spec is None or spec.loader is None:
172
+ raise ValueError(f"无法加载 YAML 解析器:{common_path}")
173
+ module = importlib.util.module_from_spec(spec)
174
+ spec.loader.exec_module(module)
175
+ loaded = module.parse_simple_yaml(text)
176
+ if not isinstance(loaded, dict):
177
+ raise ValueError("workspace.yml 顶层必须是对象")
178
+ return loaded
179
+
180
+
181
+ def config_get(config: dict[str, object], key: str, default: object = None) -> object:
182
+ current: object = config
183
+ for part in key.split("."):
184
+ if not isinstance(current, dict) or part not in current:
185
+ return default
186
+ current = current[part]
187
+ return current
188
+
189
+
190
+ def config_str(config: dict[str, object], key: str, default: str = "") -> str:
191
+ value = config_get(config, key, default)
192
+ if value is None:
193
+ return default
194
+ return str(value)
195
+
196
+
197
+ def validate_workspace(checks: list[dict[str, str]], workspace: Path, config: dict[str, object]) -> None:
198
+ for key in ("workflow_source", "mode", "code_path", "output_dir"):
199
+ value = config_str(config, key)
200
+ add_check(checks, f"config.{key}", "ok" if value else "fail", value or "缺失")
201
+
202
+ version = config_get(config, "version")
203
+ add_check(checks, "config.version", "ok" if version == 1 else "fail", str(version or "缺失"))
204
+
205
+ references = config_get(config, "reference_files")
206
+ add_check(checks, "config.reference_files", "ok" if isinstance(references, list) else "fail", "list" if isinstance(references, list) else "缺失或非数组")
207
+
208
+ source = config_str(config, "workflow_source")
209
+ if source not in ("openspec", "todo"):
210
+ add_check(checks, "config.workflow_source.value", "fail", source or "缺失")
211
+
212
+ mode = config_str(config, "mode")
213
+ if mode not in ("auto", "manual"):
214
+ add_check(checks, "config.mode.value", "fail", mode or "缺失")
215
+
216
+ code_path = config_str(config, "code_path")
217
+ if code_path:
218
+ resolved = resolve_workspace_path(workspace, code_path)
219
+ add_check(checks, "code_path.exists", "ok" if resolved.exists() else "warn", str(resolved))
220
+
221
+ demand_file = config_str(config, "demand_file")
222
+ if source == "openspec":
223
+ add_check(checks, "config.demand_file", "ok" if demand_file else "fail", demand_file or "缺失")
224
+ if demand_file:
225
+ resolved = resolve_workspace_path(workspace, expand_vars(demand_file, config))
226
+ add_check(checks, "demand_file.exists", "ok" if resolved.exists() else "warn", str(resolved))
227
+ changes_dir = config_str(config, "openspec.changes_dir", "openspec/changes")
228
+ resolved_changes = resolve_workspace_path(workspace, changes_dir)
229
+ add_check(checks, "openspec.changes_dir", "ok" if resolved_changes.exists() else "warn", str(resolved_changes))
230
+
231
+ todo_file = config_str(config, "todo_file")
232
+ add_check(checks, "config.todo_file", "ok" if todo_file else "fail", todo_file or "缺失")
233
+ if todo_file:
234
+ resolved = resolve_workspace_path(workspace, expand_vars(todo_file, config))
235
+ status = "ok" if resolved.exists() else ("warn" if source == "openspec" else "fail")
236
+ add_check(checks, "todo_file.exists", status, str(resolved))
237
+
238
+
239
+ def expand_vars(value: str, config: dict[str, object]) -> str:
240
+ demand_name = config_str(config, "vars.demand_name")
241
+ return value.replace("${demand_name}", demand_name)
242
+
243
+
244
+ def resolve_workspace_path(workspace: Path, value: str) -> Path:
245
+ path = Path(value).expanduser()
246
+ if path.is_absolute():
247
+ return path
248
+ return (workspace / path).resolve()
249
+
250
+
251
+ def migrate(workspace: Path, dry_run: bool) -> int:
252
+ workspace_yml = workspace / "workspace.yml"
253
+ if not workspace_yml.exists():
254
+ print(f"✗ workspace.yml 不存在:{workspace_yml}")
255
+ return 1
256
+
257
+ try:
258
+ config = read_workspace_yaml(workspace_yml)
259
+ except ValueError as exc:
260
+ print(f"✗ workspace.yml 无法解析:{exc}")
261
+ return 1
262
+
263
+ additions = default_missing_lines(config)
264
+ if not additions:
265
+ print("✓ workspace.yml 已是当前版本,无需迁移。")
266
+ return 0
267
+
268
+ print("将补齐以下配置:")
269
+ for line in additions:
270
+ print(f" {line}")
271
+
272
+ if dry_run:
273
+ return 0
274
+
275
+ with workspace_yml.open("a", encoding="utf-8") as handle:
276
+ handle.write("\n# Added by super-engineer migrate\n")
277
+ for line in additions:
278
+ handle.write(f"{line}\n")
279
+ print(f"✓ 已迁移:{workspace_yml}")
280
+ return 0
281
+
282
+
283
+ def default_missing_lines(config: dict[str, object]) -> list[str]:
284
+ additions: list[str] = []
285
+ if config_get(config, "version") is None:
286
+ additions.append("version: 1")
287
+ if not config_str(config, "mode"):
288
+ additions.append("mode: manual")
289
+ if not config_str(config, "workflow_source"):
290
+ additions.append("workflow_source: todo")
291
+ if config_get(config, "reference_files") is None:
292
+ additions.extend(["reference_files:", " - docs/项目介绍.md"])
293
+ if not config_str(config, "code_path"):
294
+ additions.append("code_path: ../your-project")
295
+ if not config_str(config, "output_dir"):
296
+ additions.append("output_dir: output")
297
+ return additions
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ REPO_ROOT = Path(__file__).resolve().parents[1]
13
+ SKILL_DIR = REPO_ROOT / "super-engineer-workflow"
14
+
15
+
16
+ def main() -> None:
17
+ parser = argparse.ArgumentParser(
18
+ description="安装 super-engineer skill,并初始化业务工作区。"
19
+ )
20
+ parser.add_argument("--workspace", help="业务工作区目录,默认使用当前目录。")
21
+ parser.add_argument("--code-path", help="代码目录,写入 workspace.yml。默认:../code")
22
+ parser.add_argument("--demand-name", help="需求目录名。默认:1-your-demand")
23
+ parser.add_argument("--source", choices=["todo", "openspec"], help="输入模式。默认:openspec")
24
+ parser.add_argument("--mode", choices=["manual", "auto"], help="执行模式。默认:auto")
25
+ parser.add_argument(
26
+ "--install",
27
+ choices=["none", "codex", "claude", "both"],
28
+ help="安装 skill 到本机目录。默认:none",
29
+ )
30
+ parser.add_argument("--force-skill", action="store_true", help="安装 skill 时先删除已有目录。")
31
+ parser.add_argument("--openspec-init", action="store_true", help="如果安装了 openspec CLI,尝试执行 openspec init。")
32
+ parser.add_argument("--yes", action="store_true", help="非交互模式,全部使用参数或默认值。")
33
+ args = parser.parse_args()
34
+
35
+ if should_prompt(args):
36
+ args = prompt_args(args)
37
+
38
+ install = args.install or "none"
39
+ source = args.source or "openspec"
40
+ mode = args.mode or "auto"
41
+ demand_name = args.demand_name or "1-your-demand"
42
+ code_path = args.code_path or "../code"
43
+ workspace = Path(args.workspace).expanduser().resolve() if args.workspace else Path.cwd().resolve()
44
+ workspace.mkdir(parents=True, exist_ok=True)
45
+
46
+ if install in ("codex", "both"):
47
+ install_skill(skill_target("codex"), args.force_skill)
48
+ if install in ("claude", "both"):
49
+ install_skill(skill_target("claude"), args.force_skill)
50
+
51
+ ensure_global_skill_config()
52
+ init_workspace(
53
+ workspace=workspace,
54
+ code_path=code_path,
55
+ demand_name=demand_name,
56
+ source=source,
57
+ mode=mode,
58
+ openspec_init=args.openspec_init,
59
+ )
60
+ print_summary(workspace, demand_name, source, mode)
61
+
62
+
63
+ def should_prompt(args: argparse.Namespace) -> bool:
64
+ if args.yes:
65
+ return False
66
+ provided = any(
67
+ [
68
+ args.workspace,
69
+ args.code_path,
70
+ args.demand_name,
71
+ args.source,
72
+ args.mode,
73
+ args.install,
74
+ args.force_skill,
75
+ args.openspec_init,
76
+ ]
77
+ )
78
+ return not provided and sys.stdin.isatty()
79
+
80
+
81
+ def prompt_args(args: argparse.Namespace) -> argparse.Namespace:
82
+ print("Super Engineer 初始化向导")
83
+ print("该向导会逐步完成:环境检查、skill 安装、工作区结构初始化、workspace.yml 生成。")
84
+ print("按 Enter 使用默认值。")
85
+ print("")
86
+ print("Step 1/7 环境检查")
87
+ print(f" Python: {sys.executable}")
88
+ print(f" Skill: {SKILL_DIR}")
89
+ print(f" OpenSpec CLI: {'已安装' if shutil.which('openspec') else '未检测到'}")
90
+ print("")
91
+ print("Step 2/7 选择本机 skill 安装目标")
92
+ args.install = prompt_choice(
93
+ "安装 skill 到哪里",
94
+ choices=["both", "codex", "claude", "none"],
95
+ default="both",
96
+ labels={
97
+ "both": "Codex 和 Claude",
98
+ "codex": "仅 Codex",
99
+ "claude": "仅 Claude",
100
+ "none": "暂不安装",
101
+ },
102
+ )
103
+ args.force_skill = prompt_bool("如果 skill 已存在,是否覆盖安装", default=False)
104
+ print("")
105
+ print("Step 3/7 选择业务工作区")
106
+ args.workspace = prompt_text("业务工作区目录", default=str(Path.cwd()))
107
+ print("")
108
+ print("Step 4/7 配置代码目录和当前需求")
109
+ args.code_path = prompt_text("代码目录 code_path", default="../code")
110
+ args.demand_name = prompt_text("当前需求目录名", default="1-your-demand")
111
+ print("")
112
+ print("Step 5/7 选择工作流模式")
113
+ args.source = prompt_choice(
114
+ "输入模式",
115
+ choices=["openspec", "todo"],
116
+ default="openspec",
117
+ labels={
118
+ "openspec": "OpenSpec + todo 桥接",
119
+ "todo": "直接读取 todo.md",
120
+ },
121
+ )
122
+ args.mode = prompt_choice(
123
+ "执行模式",
124
+ choices=["auto", "manual"],
125
+ default="auto",
126
+ labels={
127
+ "auto": "自动推进到验证",
128
+ "manual": "关键阶段等待人工确认",
129
+ },
130
+ )
131
+ print("")
132
+ print("Step 6/7 OpenSpec 初始化")
133
+ args.openspec_init = prompt_bool("是否尝试执行 openspec init", default=False)
134
+ print("")
135
+ print("Step 7/7 执行前确认")
136
+ print(f" install: {args.install}")
137
+ print(f" force_skill: {args.force_skill}")
138
+ print(f" workspace: {Path(args.workspace).expanduser()}")
139
+ print(f" code_path: {args.code_path}")
140
+ print(f" demand_name: {args.demand_name}")
141
+ print(f" source: {args.source}")
142
+ print(f" mode: {args.mode}")
143
+ print(f" openspec_init: {args.openspec_init}")
144
+ if not prompt_bool("确认开始初始化", default=True):
145
+ raise SystemExit("已取消初始化。")
146
+ print("")
147
+ return args
148
+
149
+
150
+ def prompt_text(title: str, default: str) -> str:
151
+ value = input(f"{title} [{default}]: ").strip()
152
+ return value or default
153
+
154
+
155
+ def prompt_bool(title: str, default: bool) -> bool:
156
+ suffix = "Y/n" if default else "y/N"
157
+ value = input(f"{title} [{suffix}]: ").strip().lower()
158
+ if not value:
159
+ return default
160
+ return value in ("y", "yes", "true", "1", "是")
161
+
162
+
163
+ def prompt_choice(title: str, choices: list[str], default: str, labels: dict[str, str]) -> str:
164
+ print(title)
165
+ for index, choice in enumerate(choices, start=1):
166
+ marker = " 默认" if choice == default else ""
167
+ print(f" {index}. {labels.get(choice, choice)} ({choice}){marker}")
168
+ while True:
169
+ value = input(f"请选择 [默认 {default}]: ").strip()
170
+ if not value:
171
+ return default
172
+ if value in choices:
173
+ return value
174
+ if value.isdigit() and 1 <= int(value) <= len(choices):
175
+ return choices[int(value) - 1]
176
+ print("输入无效,请重新选择。")
177
+
178
+
179
+ def skill_target(kind: str) -> Path:
180
+ if kind == "codex":
181
+ base = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
182
+ elif kind == "claude":
183
+ base = Path(os.environ.get("CLAUDE_HOME", "~/.claude")).expanduser()
184
+ else:
185
+ raise ValueError(kind)
186
+ return base / "skills" / "super-engineer-workflow"
187
+
188
+
189
+ def install_skill(target: Path, force: bool) -> None:
190
+ if not SKILL_DIR.exists():
191
+ raise SystemExit(f"skill 目录不存在:{SKILL_DIR}")
192
+ if target.exists() and force:
193
+ shutil.rmtree(target)
194
+ target.parent.mkdir(parents=True, exist_ok=True)
195
+ shutil.copytree(SKILL_DIR, target, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
196
+ print(f"installed_skill={target}")
197
+
198
+
199
+ def ensure_global_skill_config() -> None:
200
+ config_dir = Path("~/.super-engineer").expanduser()
201
+ config_file = config_dir / "skill-config.yml"
202
+ if config_file.exists():
203
+ print(f"skill_config=exists:{config_file}")
204
+ return
205
+ config_dir.mkdir(parents=True, exist_ok=True)
206
+ source = SKILL_DIR / "assets" / "config.example.yml"
207
+ if source.exists():
208
+ shutil.copyfile(source, config_file)
209
+ else:
210
+ config_file.write_text("version: 1\nnotification: {}\n", encoding="utf-8")
211
+ print(f"skill_config=created:{config_file}")
212
+
213
+
214
+ def init_workspace(
215
+ workspace: Path,
216
+ code_path: str,
217
+ demand_name: str,
218
+ source: str,
219
+ mode: str,
220
+ openspec_init: bool,
221
+ ) -> None:
222
+ (workspace / "docs").mkdir(parents=True, exist_ok=True)
223
+ (workspace / "openspec" / "changes").mkdir(parents=True, exist_ok=True)
224
+ (workspace / "openspec" / "specs").mkdir(parents=True, exist_ok=True)
225
+ demand_dir = workspace / "superengineer" / demand_name
226
+ demand_dir.mkdir(parents=True, exist_ok=True)
227
+
228
+ demand_file = demand_dir / "需求.md"
229
+ if not demand_file.exists():
230
+ demand_file.write_text(
231
+ "# 需求说明\n\n"
232
+ "## 背景\n\n"
233
+ "请在这里描述业务背景。\n\n"
234
+ "## 目标\n\n"
235
+ "请在这里描述本次需求目标。\n\n"
236
+ "## 验收标准\n\n"
237
+ "- [ ] 补充验收标准。\n",
238
+ encoding="utf-8",
239
+ )
240
+
241
+ workspace_yml = workspace / "workspace.yml"
242
+ if not workspace_yml.exists():
243
+ workspace_yml.write_text(
244
+ build_workspace_yml(
245
+ code_path=code_path,
246
+ demand_name=demand_name,
247
+ source=source,
248
+ mode=mode,
249
+ ),
250
+ encoding="utf-8",
251
+ )
252
+ print(f"workspace_yml=created:{workspace_yml}")
253
+ else:
254
+ print(f"workspace_yml=exists:{workspace_yml}")
255
+
256
+ if openspec_init:
257
+ try_init_openspec(workspace)
258
+
259
+
260
+ def build_workspace_yml(code_path: str, demand_name: str, source: str, mode: str) -> str:
261
+ if source == "todo":
262
+ return (
263
+ "version: 1\n"
264
+ f"mode: {mode}\n"
265
+ "workflow_source: todo\n"
266
+ "vars:\n"
267
+ f" demand_name: {demand_name}\n"
268
+ "todo_file: superengineer/${demand_name}/todo.md\n"
269
+ "reference_files:\n"
270
+ " - docs/需求分析与实现指南.md\n"
271
+ f"code_path: {code_path}\n"
272
+ "output_dir: superengineer/${demand_name}/output\n"
273
+ )
274
+ return (
275
+ "version: 1\n"
276
+ f"mode: {mode}\n"
277
+ "workflow_source: openspec\n"
278
+ "vars:\n"
279
+ f" demand_name: {demand_name}\n"
280
+ "demand_file: superengineer/${demand_name}/需求.md\n"
281
+ "todo_file: superengineer/${demand_name}/todo.md\n"
282
+ "reference_files:\n"
283
+ " - docs/需求分析与实现指南.md\n"
284
+ f"code_path: {code_path}\n"
285
+ "output_dir: superengineer/${demand_name}/output\n"
286
+ "openspec:\n"
287
+ " changes_dir: openspec/changes\n"
288
+ )
289
+
290
+
291
+ def try_init_openspec(workspace: Path) -> None:
292
+ if not shutil.which("openspec"):
293
+ print("openspec_init=skipped:openspec CLI not found")
294
+ return
295
+ config_file = workspace / "openspec" / "config.yaml"
296
+ if config_file.exists():
297
+ print("openspec_init=skipped:already initialized")
298
+ return
299
+ result = subprocess.run(
300
+ ["openspec", "init", ".", "--tools", "codex,claude"],
301
+ cwd=workspace,
302
+ text=True,
303
+ stdout=subprocess.PIPE,
304
+ stderr=subprocess.STDOUT,
305
+ check=False,
306
+ )
307
+ print(result.stdout.strip())
308
+ print(f"openspec_init={'ok' if result.returncode == 0 else 'failed'}")
309
+
310
+
311
+ def print_summary(workspace: Path, demand_name: str, source: str, mode: str) -> None:
312
+ print("")
313
+ print("初始化完成。")
314
+ print(f"workspace={workspace}")
315
+ print(f"demand_dir={workspace / 'superengineer' / demand_name}")
316
+ print(f"workflow_source={source}")
317
+ print(f"mode={mode}")
318
+ print("")
319
+ if source == "openspec":
320
+ print("下一步提示词:")
321
+ print(f"/se:propose <change-name>")
322
+ print("请根据当前 workspace 的 demand_file 生成或完善 OpenSpec change。")
323
+ print("不要改代码。过程中请使用中文。")
324
+ else:
325
+ print("下一步提示词:")
326
+ print("/se:apply")
327
+ print("请根据当前 workspace 的 todo_file 推进交付工作流。过程中请使用中文。")
328
+
329
+
330
+ if __name__ == "__main__":
331
+ main()