sdd-flow-kit 1.0.27 → 1.1.1

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/README.md CHANGED
@@ -73,14 +73,55 @@ npx sdd-flow-kit doctor --agent cursor
73
73
 
74
74
  #### 使用说明
75
75
 
76
- ##### 需求澄清:用户无需背提示词
76
+ ##### 安装根:cd 到哪,装到哪(v1.2+)
77
+
78
+ 默认 **`installRoot = 你执行命令时的当前目录`**(`process.cwd()`),也可用 `--project-root` / `SDD_FLOW_KIT_INSTALL_ROOT` 指定。
79
+
80
+ | 你 cd 到 | 会安装到 |
81
+ |----------|----------|
82
+ | `adInsight-web/frontend` | `frontend/package.json`、`.cursor/skills`、`.opsx`、`pnpm run opsx:delivery-pipeline` |
83
+ | `adInsight-web`(仓库根) | 仓库根(需根目录也有 `package.json`,否则会失败) |
84
+
85
+ ```bash
86
+ cd /path/to/adInsight-web/frontend
87
+ npx sdd-flow-kit install --project ADI -y
88
+ npx sdd-flow-kit run --product ADI --version 2.3.2 -y
89
+ ```
90
+
91
+ **PRD / gate 的 `--project-root`**:与安装根相同;若 `openspec/` 在上级目录,工具会**向上查找**已有 `openspec/PRD`(不把安装写到上级)。
92
+
93
+ 旧行为(在仓库根执行时自动落到 `frontend/`):设置 `SDD_FLOW_KIT_LEGACY_MONOREPO=1`。
77
94
 
78
- `install` 会写入 `.cursor/rules/sdd-flow-kit-gates.mdc` 与触发 Skill。
79
- 澄清阶段你**直接粘贴确认点**(编号列表即可),AI 先进入文档模式更新 `openspec/PRD/**`。
80
- **默认**:03 闭环并生成 01/02/04 后,**同一回合自动** `/opsx-propose`(01+02+UI)→ `/opsx-apply`,无需再说「继续」。
81
- 说「只提案」或「先别写代码」可停在提案阶段;设 `autoChainOpsx: false` 可恢复人工分步。
95
+ ##### 脚本门禁(v1.1+,**不依赖自然语言**)
96
+
97
+ `run` 会在 `openspec/PRD/<runId>/NEXT.md` 写入**必须按顺序执行的命令**。AI 与人都以 **exit code** 为准:失败则不得进入下一阶段、不得改 `src/`。
98
+
99
+ ```bash
100
+ # 门禁(失败 exit 1)
101
+ npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched
102
+ npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed # 检测 git 是否误改 src
103
+ npx sdd-flow-kit gate --project-root . --run-id <runId> --expect propose-ready --change <name>
104
+ npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>
105
+ npx sdd-flow-kit gate --project-root . --run-id <runId> --expect shipped --change <name>
106
+
107
+ # 阶段推进(更新 .session-state.json + NEXT.md)
108
+ npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd
109
+ npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to docs-done
110
+ npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to propose-done --change <name>
111
+ npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to impl --change <name>
112
+ npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to shipped --change <name>
113
+
114
+ # 提案 / 交付
115
+ npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>
116
+ npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>
117
+ ```
118
+
119
+ `docs-closed` 会在 git 仓库中扫描 `src/`、`frontend/src/` 等路径;文档阶段若已有实现层改动,**gate 失败**。
120
+
121
+ ##### 需求澄清:用户无需背提示词
82
122
 
83
- 阶段口语(可选,用于断点续跑):`继续` / `做提案` / `开始 apply`。
123
+ `install` 会写入 `.cursor/rules/sdd-flow-kit-gates.mdc` 与触发 Skill(已改为**脚本优先**)。
124
+ 用户粘贴确认点后:先更新 01/02/03/04,再执行 `gate docs-closed` → `phase advance --to docs-done`;**禁止**跳过 gate 直接改代码。
84
125
 
85
126
  ##### `/opsx-apply` 与 TDD + Playwright 检查机制
86
127
 
@@ -105,9 +146,10 @@ npx sdd-flow-kit doctor --agent cursor --fix # 检查并尝试修复
105
146
 
106
147
  #### 当前自动化边界
107
148
 
108
- - 已支持:跨工具统一目录结构、提示词、报告模板、执行清单
109
- - 已支持:环境检测与按工具自动执行 `npx @lixin5257xxx/opsx-workflow setup ...`
110
- - 自动化增强:`claude-code` adapter 支持 `claude -p` 自动执行并记录 `AUTO-RUN-LOG.md`(失败自动降级人工)
149
+ - 已支持:跨工具统一目录结构、提示词、报告模板、**NEXT.md 脚本清单**
150
+ - 已支持:**gate / phase / propose / deliver** 命令(exit code 机械门禁)
151
+ - 已支持:`ensure-opsx` + `openspec status` 校验 apply-ready
152
+ - 未支持:在 Cursor 内全自动跑完全流程(Cursor adapter 仍为 manual);可选 `SDD_FLOW_KIT_ENABLE_CLAUDE_AUTORUN=1` 给 Claude Code
111
153
 
112
154
  #### npm 发包流程
113
155
 
package/dist/cli.js CHANGED
@@ -12,6 +12,11 @@ const utils_1 = require("./core/utils");
12
12
  const setupProject_1 = require("./steps/setupProject");
13
13
  const doctor_1 = require("./steps/doctor");
14
14
  const invokeFlow_1 = require("./steps/invokeFlow");
15
+ const runGate_1 = require("./steps/runGate");
16
+ const runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
17
+ const runPropose_1 = require("./steps/runPropose");
18
+ const runDeliver_1 = require("./steps/runDeliver");
19
+ const projectRoots_1 = require("./core/projectRoots");
15
20
  const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
16
21
  function getArgValue(flag) {
17
22
  const idx = process_1.default.argv.indexOf(flag);
@@ -45,10 +50,23 @@ function printHelp() {
45
50
  "",
46
51
  "6) Apply 前确保 OPSX+TDD+Playwright(/opsx-apply 前必须):",
47
52
  " npx sdd-flow-kit ensure-opsx --agent cursor -y",
48
- " (缺省时自动 npx @lixin5257xxx/opsx-workflow setup)",
53
+ "",
54
+ "7) 脚本门禁(失败 exit 1,不依赖自然语言):",
55
+ " npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched",
56
+ " npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed",
57
+ " npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>",
58
+ "",
59
+ "8) 阶段推进(更新 session + NEXT.md):",
60
+ " npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd|docs-done|propose-done|impl|shipped",
61
+ "",
62
+ "9) 提案 / 交付:",
63
+ " npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>",
64
+ " npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
49
65
  "",
50
66
  "可选参数:",
51
- " --projectRoot <path> 目标项目根目录(默认当前目录)",
67
+ " --projectRoot <path> 目标项目根目录(默认当前目录 = 安装根,cd 到哪装到哪)",
68
+ " 环境变量 SDD_FLOW_KIT_INSTALL_ROOT 可覆盖安装根(绝对路径)",
69
+ " 环境变量 SDD_FLOW_KIT_LEGACY_MONOREPO=1 恢复「仓库根无 package.json 时落到 frontend/」",
52
70
  " --agent <name> cursor | claude-code | codex | openclaw | custom(默认 claude-code)",
53
71
  " --branch <name> 开发分支;不存在会自动创建",
54
72
  " --project <name> OMS | ADI | 欢盟 | AD Tools(默认 OMS,用于生成 docs/*-doc-skill)",
@@ -61,32 +79,53 @@ function printHelp() {
61
79
  ].join("\n"));
62
80
  }
63
81
  async function main() {
64
- var _a, _b, _c, _d;
82
+ var _a, _b, _c, _d, _e, _f, _g, _h;
65
83
  const cmd = process_1.default.argv[2];
66
84
  if (!cmd || cmd === "--help" || cmd === "-h") {
67
85
  printHelp();
68
86
  return;
69
87
  }
88
+ const GATE_EXPECTS = [
89
+ "prd-fetched",
90
+ "questions-open",
91
+ "docs-closed",
92
+ "propose-ready",
93
+ "impl-allowed",
94
+ "shipped",
95
+ ];
96
+ const PHASE_TARGETS = [
97
+ "after-prd",
98
+ "docs-done",
99
+ "propose-done",
100
+ "impl",
101
+ "shipped",
102
+ ];
70
103
  if (cmd !== "start" &&
71
104
  cmd !== "run" &&
72
105
  cmd !== "setup" &&
73
106
  cmd !== "install" &&
74
107
  cmd !== "doctor" &&
75
108
  cmd !== "ensure-opsx" &&
76
- cmd !== "invoke") {
109
+ cmd !== "invoke" &&
110
+ cmd !== "gate" &&
111
+ cmd !== "phase" &&
112
+ cmd !== "propose" &&
113
+ cmd !== "deliver") {
77
114
  printHelp();
78
115
  process_1.default.exitCode = 1;
79
116
  return;
80
117
  }
81
118
  const product = getArgValue("--product");
82
119
  const version = getArgValue("--version");
83
- const projectRoot = (_a = getArgValue("--projectRoot")) !== null && _a !== void 0 ? _a : process_1.default.cwd();
84
- const branch = (_b = getArgValue("--branch")) !== null && _b !== void 0 ? _b : undefined;
120
+ const projectRoot = path_1.default.resolve((_b = (_a = getArgValue("--project-root")) !== null && _a !== void 0 ? _a : getArgValue("--projectRoot")) !== null && _b !== void 0 ? _b : process_1.default.cwd());
121
+ const runId = (_d = (_c = getArgValue("--run-id")) !== null && _c !== void 0 ? _c : getArgValue("--runId")) !== null && _d !== void 0 ? _d : undefined;
122
+ const changeName = (_e = getArgValue("--change")) !== null && _e !== void 0 ? _e : undefined;
123
+ const branch = (_f = getArgValue("--branch")) !== null && _f !== void 0 ? _f : undefined;
85
124
  const yes = process_1.default.argv.includes("-y");
86
125
  const dryRun = process_1.default.argv.includes("--dry-run");
87
126
  const planJson = process_1.default.argv.includes("--plan-json");
88
- const agentValue = (_c = getArgValue("--agent")) !== null && _c !== void 0 ? _c : "claude-code";
89
- const projectKind = ((_d = getArgValue("--project")) !== null && _d !== void 0 ? _d : "OMS");
127
+ const agentValue = (_g = getArgValue("--agent")) !== null && _g !== void 0 ? _g : "claude-code";
128
+ const projectKind = ((_h = getArgValue("--project")) !== null && _h !== void 0 ? _h : "OMS");
90
129
  if (!ALLOWED_AGENTS.includes(agentValue)) {
91
130
  // eslint-disable-next-line no-console
92
131
  console.error(`不支持的 --agent: ${agentValue}`);
@@ -102,10 +141,96 @@ async function main() {
102
141
  process_1.default.exitCode = 1;
103
142
  return;
104
143
  }
144
+ if (cmd === "gate") {
145
+ const expect = getArgValue("--expect");
146
+ if (!expect || !GATE_EXPECTS.includes(expect)) {
147
+ // eslint-disable-next-line no-console
148
+ console.error(`gate 需要 --expect ${GATE_EXPECTS.join("|")}`);
149
+ process_1.default.exitCode = 1;
150
+ return;
151
+ }
152
+ const result = await (0, runGate_1.runGate)({
153
+ projectRoot,
154
+ runId,
155
+ expect,
156
+ change: changeName,
157
+ });
158
+ (0, runGate_1.printGateResult)(result);
159
+ if (!result.ok)
160
+ process_1.default.exitCode = 1;
161
+ return;
162
+ }
163
+ if (cmd === "phase") {
164
+ const sub = process_1.default.argv[3];
165
+ if (sub !== "advance") {
166
+ // eslint-disable-next-line no-console
167
+ console.error("用法: sdd-flow-kit phase advance --to <target> ...");
168
+ process_1.default.exitCode = 1;
169
+ return;
170
+ }
171
+ const to = getArgValue("--to");
172
+ if (!to || !PHASE_TARGETS.includes(to)) {
173
+ // eslint-disable-next-line no-console
174
+ console.error(`需要 --to ${PHASE_TARGETS.join("|")}`);
175
+ process_1.default.exitCode = 1;
176
+ return;
177
+ }
178
+ const result = await (0, runPhaseAdvance_1.runPhaseAdvance)({
179
+ projectRoot,
180
+ runId,
181
+ to,
182
+ change: changeName,
183
+ dryRun,
184
+ });
185
+ // eslint-disable-next-line no-console
186
+ console.log(result.ok ? `✅ ${result.message}` : `❌ ${result.message}`);
187
+ if (!result.ok)
188
+ process_1.default.exitCode = 1;
189
+ return;
190
+ }
191
+ if (cmd === "propose") {
192
+ if (!changeName) {
193
+ // eslint-disable-next-line no-console
194
+ console.error("propose 需要 --change <kebab-name>");
195
+ process_1.default.exitCode = 1;
196
+ return;
197
+ }
198
+ const result = await (0, runPropose_1.runPropose)({
199
+ projectRoot,
200
+ runId,
201
+ change: changeName,
202
+ dryRun,
203
+ });
204
+ // eslint-disable-next-line no-console
205
+ console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
206
+ if (!result.ok)
207
+ process_1.default.exitCode = 1;
208
+ return;
209
+ }
210
+ if (cmd === "deliver") {
211
+ if (!changeName) {
212
+ // eslint-disable-next-line no-console
213
+ console.error("deliver 需要 --change <kebab-name>");
214
+ process_1.default.exitCode = 1;
215
+ return;
216
+ }
217
+ const result = await (0, runDeliver_1.runDeliver)({
218
+ projectRoot,
219
+ runId,
220
+ change: changeName,
221
+ agent,
222
+ dryRun,
223
+ });
224
+ // eslint-disable-next-line no-console
225
+ console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
226
+ if (!result.ok)
227
+ process_1.default.exitCode = 1;
228
+ return;
229
+ }
105
230
  if (cmd === "doctor" || cmd === "ensure-opsx") {
106
231
  const fix = cmd === "ensure-opsx" || process_1.default.argv.includes("--fix");
107
232
  const doctor = await (0, doctor_1.runDoctor)({
108
- projectRoot: path_1.default.resolve(projectRoot),
233
+ projectRoot,
109
234
  agent,
110
235
  fix,
111
236
  dryRun,
@@ -122,7 +247,7 @@ async function main() {
122
247
  }
123
248
  if (cmd === "ensure-opsx" && !dryRun) {
124
249
  await (0, setupProject_1.setupProject)({
125
- projectRoot: path_1.default.resolve(projectRoot),
250
+ projectRoot,
126
251
  agent,
127
252
  yes: true,
128
253
  light: true,
@@ -134,8 +259,10 @@ async function main() {
134
259
  return;
135
260
  }
136
261
  if (cmd === "setup" || cmd === "install") {
262
+ // eslint-disable-next-line no-console
263
+ console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
137
264
  const result = await (0, setupProject_1.setupProject)({
138
- projectRoot: path_1.default.resolve(projectRoot),
265
+ projectRoot,
139
266
  agent,
140
267
  branch,
141
268
  yes,
@@ -172,10 +299,9 @@ async function main() {
172
299
  if (!parsed) {
173
300
  throw new Error("invoke 解析失败,请使用:开发 ADI 2.3.2 需求");
174
301
  }
175
- const resolvedRoot = path_1.default.resolve(projectRoot);
176
302
  const options = (0, invokeFlow_1.toRunOptions)({
177
303
  parsed,
178
- projectRoot: resolvedRoot,
304
+ projectRoot,
179
305
  branch,
180
306
  yes,
181
307
  dryRun,
@@ -194,22 +320,23 @@ async function main() {
194
320
  process_1.default.exitCode = 1;
195
321
  return;
196
322
  }
197
- const resolvedRoot = path_1.default.resolve(projectRoot);
198
- const runId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
323
+ const scaffoldRunId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
199
324
  if (cmd === "start") {
200
325
  await (0, startFlow_1.startFlowScaffold)({
201
326
  product,
202
327
  version,
203
- projectRoot: resolvedRoot,
328
+ projectRoot,
204
329
  });
205
330
  // eslint-disable-next-line no-console
206
- console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${runId}/`);
331
+ console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${scaffoldRunId}/`);
207
332
  return;
208
333
  }
334
+ // eslint-disable-next-line no-console
335
+ console.log(`[sdd-flow-kit] 安装根 installRoot=${(0, projectRoots_1.resolveInstallRoot)(projectRoot)} (cwd=${process_1.default.cwd()})`);
209
336
  const result = await (0, runFlow_1.runFlow)({
210
337
  product,
211
338
  version,
212
- projectRoot: resolvedRoot,
339
+ projectRoot,
213
340
  agent,
214
341
  branch,
215
342
  yes,
@@ -222,6 +349,8 @@ async function main() {
222
349
  // eslint-disable-next-line no-console
223
350
  console.log(`流程已初始化:openspec/PRD/${result.runId}/`);
224
351
  // eslint-disable-next-line no-console
352
+ console.log(`请打开 openspec/PRD/${result.runId}/NEXT.md,按脚本顺序执行 gate / phase / propose / deliver。`);
353
+ // eslint-disable-next-line no-console
225
354
  console.log(`agent=${result.adapterName}, cli=${result.adapterAvailable ? "detected" : "missing"}, autoExecute=${result.executed ? "yes" : "manual"}`);
226
355
  // eslint-disable-next-line no-console
227
356
  console.log(`branch=${result.branch}, envSetup=${result.setupExecuted ? "installed" : "already-ready"}`);
@@ -233,7 +362,7 @@ async function main() {
233
362
  // eslint-disable-next-line no-console
234
363
  console.log(JSON.stringify({
235
364
  command: "run",
236
- projectRoot: resolvedRoot,
365
+ projectRoot,
237
366
  dryRun,
238
367
  product,
239
368
  version,
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.gitDiffNameOnly = gitDiffNameOnly;
7
+ exports.isImplPath = isImplPath;
8
+ exports.isAllowedDocPath = isAllowedDocPath;
9
+ exports.findImplChanges = findImplChanges;
10
+ exports.resolveGitRoot = resolveGitRoot;
11
+ const child_process_1 = require("child_process");
12
+ const path_1 = __importDefault(require("path"));
13
+ /**
14
+ * 在 projectRoot 执行 git 命令;非 git 仓库时 isRepo=false(不阻断文档阶段)。
15
+ */
16
+ function gitDiffNameOnly(projectRoot) {
17
+ const rev = (0, child_process_1.spawnSync)("git", ["rev-parse", "HEAD"], {
18
+ cwd: projectRoot,
19
+ encoding: "utf8",
20
+ });
21
+ if (rev.status !== 0) {
22
+ return { ok: true, isRepo: false, changedFiles: [], head: null };
23
+ }
24
+ const head = (rev.stdout || "").trim();
25
+ const diff = (0, child_process_1.spawnSync)("git", ["diff", "--name-only", "HEAD"], {
26
+ cwd: projectRoot,
27
+ encoding: "utf8",
28
+ });
29
+ const untracked = (0, child_process_1.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], {
30
+ cwd: projectRoot,
31
+ encoding: "utf8",
32
+ });
33
+ const changed = [
34
+ ...(diff.stdout || "").split("\n").filter(Boolean),
35
+ ...(untracked.stdout || "").split("\n").filter(Boolean),
36
+ ];
37
+ const unique = Array.from(new Set(changed));
38
+ return { ok: true, isRepo: true, changedFiles: unique, head };
39
+ }
40
+ /** 业务实现路径:命中则视为「实现层改动」 */
41
+ const IMPL_PATH_PATTERNS = [
42
+ /^src\//,
43
+ /^e2e\//,
44
+ /^frontend\/src\//,
45
+ /^frontend\/e2e\//,
46
+ /^app\//,
47
+ /^pages\//,
48
+ /^components\//,
49
+ ];
50
+ function isImplPath(filePath) {
51
+ const p = filePath.replace(/\\/g, "/");
52
+ return IMPL_PATH_PATTERNS.some((re) => re.test(p));
53
+ }
54
+ /** 仅允许 PRD / openspec 文档路径(阶段 B/C) */
55
+ const DOC_PATH_PATTERNS = [
56
+ /^openspec\/PRD\//,
57
+ /^openspec\/changes\//,
58
+ /^frontend\/openspec\/PRD\//,
59
+ /^frontend\/openspec\/changes\//,
60
+ /^\.opsx-active-change$/,
61
+ /^frontend\/\.opsx-active-change$/,
62
+ ];
63
+ function isAllowedDocPath(filePath) {
64
+ const p = filePath.replace(/\\/g, "/");
65
+ if (DOC_PATH_PATTERNS.some((re) => re.test(p)))
66
+ return true;
67
+ if (p.endsWith("docs/") || /^docs\//.test(p))
68
+ return true;
69
+ return false;
70
+ }
71
+ function findImplChanges(changedFiles) {
72
+ return changedFiles.filter((f) => isImplPath(f) && !isAllowedDocPath(f));
73
+ }
74
+ function resolveGitRoot(projectRoot) {
75
+ const top = (0, child_process_1.spawnSync)("git", ["rev-parse", "--show-toplevel"], {
76
+ cwd: projectRoot,
77
+ encoding: "utf8",
78
+ });
79
+ if (top.status === 0 && top.stdout) {
80
+ return top.stdout.trim();
81
+ }
82
+ return path_1.default.resolve(projectRoot);
83
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkOpenspecApplyReady = checkOpenspecApplyReady;
7
+ exports.checkTasksAllDone = checkTasksAllDone;
8
+ exports.changeDirExists = changeDirExists;
9
+ const child_process_1 = require("child_process");
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const fs_1 = require("./fs");
13
+ /**
14
+ * 解析 openspec status --json,判断 change 是否 apply-ready。
15
+ */
16
+ function checkOpenspecApplyReady(projectRoot, changeName) {
17
+ const ret = (0, child_process_1.spawnSync)("openspec", ["status", "--change", changeName, "--json"], {
18
+ cwd: projectRoot,
19
+ encoding: "utf8",
20
+ });
21
+ if (ret.status !== 0) {
22
+ return {
23
+ ok: false,
24
+ changeName,
25
+ applyReady: false,
26
+ detail: ret.stderr || ret.stdout || "openspec status 失败",
27
+ };
28
+ }
29
+ try {
30
+ const json = JSON.parse(ret.stdout || "{}");
31
+ const requires = json.applyRequires || [];
32
+ const artifacts = json.artifacts || [];
33
+ const pending = requires.filter((id) => {
34
+ const art = artifacts.find((a) => a.id === id);
35
+ return !art || art.status !== "done";
36
+ });
37
+ const applyReady = pending.length === 0 && requires.length > 0;
38
+ return {
39
+ ok: true,
40
+ changeName,
41
+ applyReady,
42
+ detail: applyReady
43
+ ? "apply-ready"
44
+ : `待完成 artifacts: ${pending.join(", ") || requires.join(", ")}`,
45
+ };
46
+ }
47
+ catch (e) {
48
+ return {
49
+ ok: false,
50
+ changeName,
51
+ applyReady: false,
52
+ detail: `无法解析 openspec status JSON: ${e instanceof Error ? e.message : String(e)}`,
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * 检查 tasks.md 是否全部勾选完成。
58
+ */
59
+ async function checkTasksAllDone(projectRoot, changeName) {
60
+ const tasksPath = path_1.default.join(projectRoot, "openspec", "changes", changeName, "tasks.md");
61
+ const content = await (0, fs_1.readFileIfExists)(tasksPath);
62
+ if (!content) {
63
+ return { ok: false, detail: `缺少 tasks.md: ${tasksPath}` };
64
+ }
65
+ const open = content.match(/^- \[ \]/gm);
66
+ if (open && open.length > 0) {
67
+ return { ok: false, detail: `tasks.md 仍有 ${open.length} 项未完成` };
68
+ }
69
+ const hasTask = /- \[[xX]\]/.test(content);
70
+ if (!hasTask) {
71
+ return { ok: false, detail: "tasks.md 无已勾选任务" };
72
+ }
73
+ return { ok: true, detail: "tasks.md 已全部勾选" };
74
+ }
75
+ async function changeDirExists(projectRoot, changeName) {
76
+ try {
77
+ await promises_1.default.access(path_1.default.join(projectRoot, "openspec", "changes", changeName));
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
@@ -15,6 +15,7 @@ const child_process_1 = require("child_process");
15
15
  const promises_1 = __importDefault(require("fs/promises"));
16
16
  const path_1 = __importDefault(require("path"));
17
17
  const fs_1 = require("./fs");
18
+ const projectRoots_1 = require("./projectRoots");
18
19
  const DELIVERY_SCRIPT_REL = ".cursor/skills/opsx-dev-delivery-pipeline/scripts/opsx-delivery-pipeline.sh";
19
20
  function deliveryPipelineNpmScripts() {
20
21
  const full = `bash ${DELIVERY_SCRIPT_REL}`;
@@ -92,13 +93,15 @@ function buildOpsxWorkflowSetupCommand(projectRoot, agent) {
92
93
  return { cmd: "npx", args: [...baseArgs, "--editor", "codex", "-y"] };
93
94
  }
94
95
  async function isOpsxApplyReady(projectRoot) {
95
- const checks = await collectDeliverySetupChecks(projectRoot);
96
+ const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(projectRoot));
97
+ const checks = await collectDeliverySetupChecks(installRoot);
96
98
  return checks.every((c) => c.ok);
97
99
  }
98
100
  /** 缺 openspec/TDD/Playwright 工作流时安装 @lixin5257xxx/opsx-workflow,并修复 delivery 脚本 */
99
101
  async function ensureOpsxWorkflow(options) {
102
+ const installRoot = await (0, projectRoots_1.resolvePackageRoot)((0, projectRoots_1.resolveInstallRoot)(options.projectRoot));
100
103
  const plannedActions = [];
101
- let checks = await collectDeliverySetupChecks(options.projectRoot);
104
+ let checks = await collectDeliverySetupChecks(installRoot);
102
105
  if (checks.every((c) => c.ok)) {
103
106
  return {
104
107
  ready: true,
@@ -108,8 +111,8 @@ async function ensureOpsxWorkflow(options) {
108
111
  plannedActions,
109
112
  };
110
113
  }
111
- const agent = await resolveOpsxAgent(options.projectRoot, options.agent);
112
- const { cmd, args } = buildOpsxWorkflowSetupCommand(options.projectRoot, agent);
114
+ const agent = await resolveOpsxAgent(installRoot, options.agent);
115
+ const { cmd, args } = buildOpsxWorkflowSetupCommand(installRoot, agent);
113
116
  plannedActions.push(`missing opsx apply stack → run: ${cmd} ${args.join(" ")}`);
114
117
  if (options.dryRun) {
115
118
  return {
@@ -121,18 +124,18 @@ async function ensureOpsxWorkflow(options) {
121
124
  };
122
125
  }
123
126
  const ret = (0, child_process_1.spawnSync)(cmd, args, {
124
- cwd: options.projectRoot,
127
+ cwd: installRoot,
125
128
  stdio: "inherit",
126
129
  env: process.env,
127
130
  });
128
131
  if (ret.status !== 0) {
129
132
  throw new Error(`opsx-workflow 安装失败:${cmd} ${args.join(" ")}`);
130
133
  }
131
- const repairedScripts = await repairOpsxDeliveryScripts(options.projectRoot);
134
+ const repairedScripts = await repairOpsxDeliveryScripts(installRoot);
132
135
  if (repairedScripts) {
133
136
  plannedActions.push("repaired package.json opsx:delivery-pipeline scripts");
134
137
  }
135
- checks = await collectDeliverySetupChecks(options.projectRoot);
138
+ checks = await collectDeliverySetupChecks(installRoot);
136
139
  return {
137
140
  ready: checks.every((c) => c.ok),
138
141
  workflowInstalled: true,
@@ -141,45 +144,52 @@ async function ensureOpsxWorkflow(options) {
141
144
  plannedActions,
142
145
  };
143
146
  }
144
- async function collectDeliverySetupChecks(projectRoot) {
147
+ async function collectDeliverySetupChecks(installRoot) {
145
148
  var _a, _b, _c, _d, _e, _f;
146
149
  const checks = [];
150
+ const root = (0, projectRoots_1.resolveInstallRoot)(installRoot);
151
+ const openspecRoot = await (0, projectRoots_1.resolveOpenspecRoot)(root);
152
+ checks.push({
153
+ name: "install-root",
154
+ ok: true,
155
+ detail: `安装根=${root}`,
156
+ });
147
157
  checks.push({
148
158
  name: "openspec-dir",
149
- ok: await pathExists(path_1.default.join(projectRoot, "openspec")),
150
- detail: "openspec/ 目录",
159
+ ok: await pathExists(path_1.default.join(openspecRoot, "openspec")),
160
+ detail: openspecRoot === root ? "openspec/ 目录(安装根)" : `openspec/ 目录(上级 ${openspecRoot})`,
151
161
  });
152
162
  checks.push({
153
163
  name: "openspec-cli",
154
164
  ok: hasCommand("openspec"),
155
165
  detail: "openspec CLI(npx openspec 或全局安装)",
156
166
  });
157
- const skillPath = path_1.default.join(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
167
+ const skillPath = path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md");
158
168
  checks.push({
159
169
  name: "opsx-dev-delivery-pipeline-skill",
160
170
  ok: await pathExists(skillPath),
161
171
  detail: skillPath,
162
172
  });
163
- const tddPath = path_1.default.join(projectRoot, ".cursor", "skills", "tdd-script", "SKILL.md");
173
+ const tddPath = path_1.default.join(root, ".cursor", "skills", "tdd-script", "SKILL.md");
164
174
  checks.push({
165
175
  name: "tdd-script-skill",
166
176
  ok: await pathExists(tddPath),
167
177
  detail: tddPath,
168
178
  });
169
- const applyPath = path_1.default.join(projectRoot, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
179
+ const applyPath = path_1.default.join(root, ".cursor", "skills", "openspec-apply-change", "SKILL.md");
170
180
  checks.push({
171
181
  name: "openspec-apply-change-skill",
172
182
  ok: await pathExists(applyPath),
173
183
  detail: applyPath,
174
184
  });
175
- const pipelineScript = path_1.default.join(projectRoot, DELIVERY_SCRIPT_REL);
185
+ const pipelineScript = path_1.default.join(root, DELIVERY_SCRIPT_REL);
176
186
  checks.push({
177
187
  name: "delivery-pipeline-script",
178
188
  ok: await pathExists(pipelineScript),
179
189
  detail: pipelineScript,
180
190
  });
181
191
  try {
182
- const pkgText = await promises_1.default.readFile(path_1.default.join(projectRoot, "package.json"), "utf8");
192
+ const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
183
193
  const pkg = JSON.parse(pkgText);
184
194
  const cmd = (_b = (_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a["opsx:delivery-pipeline"]) !== null && _b !== void 0 ? _b : "";
185
195
  const ok = cmd.includes("opsx-dev-delivery-pipeline") &&
@@ -199,10 +209,10 @@ async function collectDeliverySetupChecks(projectRoot) {
199
209
  });
200
210
  }
201
211
  try {
202
- const cfgText = await promises_1.default.readFile(path_1.default.join(projectRoot, ".opsx", "config.json"), "utf8");
212
+ const cfgText = await promises_1.default.readFile(path_1.default.join(root, ".opsx", "config.json"), "utf8");
203
213
  const cfg = JSON.parse(cfgText);
204
214
  const skillsDir = (_c = cfg.skillsDir) !== null && _c !== void 0 ? _c : "";
205
- const hasCursorDelivery = await pathExists(path_1.default.join(projectRoot, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
215
+ const hasCursorDelivery = await pathExists(path_1.default.join(root, ".cursor", "skills", "opsx-dev-delivery-pipeline", "SKILL.md"));
206
216
  const nameOk = ((_d = cfg.deliveryPipeline) === null || _d === void 0 ? void 0 : _d.skillName) === "opsx-dev-delivery-pipeline";
207
217
  const dirOk = (hasCursorDelivery && skillsDir.includes(".cursor")) ||
208
218
  (!hasCursorDelivery && (skillsDir.includes(".cursor") || skillsDir.includes(".claude")));
@@ -222,7 +232,7 @@ async function collectDeliverySetupChecks(projectRoot) {
222
232
  });
223
233
  }
224
234
  try {
225
- const pkgText = await promises_1.default.readFile(path_1.default.join(projectRoot, "package.json"), "utf8");
235
+ const pkgText = await promises_1.default.readFile(path_1.default.join(root, "package.json"), "utf8");
226
236
  const pkg = JSON.parse(pkgText);
227
237
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
228
238
  const hasPw = Boolean(deps["@playwright/test"] || deps.playwright);
@@ -239,7 +249,7 @@ async function collectDeliverySetupChecks(projectRoot) {
239
249
  detail: "无法读取 package.json 检查 Playwright",
240
250
  });
241
251
  }
242
- const agentsMd = path_1.default.join(projectRoot, "AGENTS.md");
252
+ const agentsMd = path_1.default.join(root, "AGENTS.md");
243
253
  checks.push({
244
254
  name: "agents-md",
245
255
  ok: await pathExists(agentsMd),