sdd-flow-kit 1.0.27 → 1.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.
- package/README.md +32 -9
- package/dist/cli.js +140 -18
- package/dist/core/gitDiffGuard.js +83 -0
- package/dist/core/openspecGate.js +83 -0
- package/dist/core/prdArtifacts.js +53 -0
- package/dist/core/sessionState.js +55 -0
- package/dist/core/writeNext.js +54 -0
- package/dist/index.js +13 -1
- package/dist/steps/resolveRun.js +61 -0
- package/dist/steps/runDeliver.js +82 -0
- package/dist/steps/runGate.js +220 -0
- package/dist/steps/runPhaseAdvance.js +195 -0
- package/dist/steps/runPropose.js +82 -0
- package/dist/steps/startFlow.js +10 -16
- package/package.json +1 -1
- package/src/templates/artifacts/06-agent-skill.template.md +49 -124
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +32 -33
package/README.md
CHANGED
|
@@ -73,14 +73,36 @@ npx sdd-flow-kit doctor --agent cursor
|
|
|
73
73
|
|
|
74
74
|
#### 使用说明
|
|
75
75
|
|
|
76
|
-
#####
|
|
76
|
+
##### 脚本门禁(v1.1+,**不依赖自然语言**)
|
|
77
|
+
|
|
78
|
+
`run` 会在 `openspec/PRD/<runId>/NEXT.md` 写入**必须按顺序执行的命令**。AI 与人都以 **exit code** 为准:失败则不得进入下一阶段、不得改 `src/`。
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
```bash
|
|
81
|
+
# 门禁(失败 exit 1)
|
|
82
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched
|
|
83
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed # 检测 git 是否误改 src
|
|
84
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect propose-ready --change <name>
|
|
85
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>
|
|
86
|
+
npx sdd-flow-kit gate --project-root . --run-id <runId> --expect shipped --change <name>
|
|
87
|
+
|
|
88
|
+
# 阶段推进(更新 .session-state.json + NEXT.md)
|
|
89
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd
|
|
90
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to docs-done
|
|
91
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to propose-done --change <name>
|
|
92
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to impl --change <name>
|
|
93
|
+
npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to shipped --change <name>
|
|
94
|
+
|
|
95
|
+
# 提案 / 交付
|
|
96
|
+
npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>
|
|
97
|
+
npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`docs-closed` 会在 git 仓库中扫描 `src/`、`frontend/src/` 等路径;文档阶段若已有实现层改动,**gate 失败**。
|
|
101
|
+
|
|
102
|
+
##### 需求澄清:用户无需背提示词
|
|
82
103
|
|
|
83
|
-
|
|
104
|
+
`install` 会写入 `.cursor/rules/sdd-flow-kit-gates.mdc` 与触发 Skill(已改为**脚本优先**)。
|
|
105
|
+
用户粘贴确认点后:先更新 01/02/03/04,再执行 `gate docs-closed` → `phase advance --to docs-done`;**禁止**跳过 gate 直接改代码。
|
|
84
106
|
|
|
85
107
|
##### `/opsx-apply` 与 TDD + Playwright 检查机制
|
|
86
108
|
|
|
@@ -105,9 +127,10 @@ npx sdd-flow-kit doctor --agent cursor --fix # 检查并尝试修复
|
|
|
105
127
|
|
|
106
128
|
#### 当前自动化边界
|
|
107
129
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
130
|
+
- 已支持:跨工具统一目录结构、提示词、报告模板、**NEXT.md 脚本清单**
|
|
131
|
+
- 已支持:**gate / phase / propose / deliver** 命令(exit code 机械门禁)
|
|
132
|
+
- 已支持:`ensure-opsx` + `openspec status` 校验 apply-ready
|
|
133
|
+
- 未支持:在 Cursor 内全自动跑完全流程(Cursor adapter 仍为 manual);可选 `SDD_FLOW_KIT_ENABLE_CLAUDE_AUTORUN=1` 给 Claude Code
|
|
111
134
|
|
|
112
135
|
#### npm 发包流程
|
|
113
136
|
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,10 @@ 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");
|
|
15
19
|
const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
|
|
16
20
|
function getArgValue(flag) {
|
|
17
21
|
const idx = process_1.default.argv.indexOf(flag);
|
|
@@ -45,7 +49,18 @@ function printHelp() {
|
|
|
45
49
|
"",
|
|
46
50
|
"6) Apply 前确保 OPSX+TDD+Playwright(/opsx-apply 前必须):",
|
|
47
51
|
" npx sdd-flow-kit ensure-opsx --agent cursor -y",
|
|
48
|
-
"
|
|
52
|
+
"",
|
|
53
|
+
"7) 脚本门禁(失败 exit 1,不依赖自然语言):",
|
|
54
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect prd-fetched",
|
|
55
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect docs-closed",
|
|
56
|
+
" npx sdd-flow-kit gate --project-root . --run-id <runId> --expect impl-allowed --change <name>",
|
|
57
|
+
"",
|
|
58
|
+
"8) 阶段推进(更新 session + NEXT.md):",
|
|
59
|
+
" npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to after-prd|docs-done|propose-done|impl|shipped",
|
|
60
|
+
"",
|
|
61
|
+
"9) 提案 / 交付:",
|
|
62
|
+
" npx sdd-flow-kit propose --project-root . --run-id <runId> --change <name>",
|
|
63
|
+
" npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
|
|
49
64
|
"",
|
|
50
65
|
"可选参数:",
|
|
51
66
|
" --projectRoot <path> 目标项目根目录(默认当前目录)",
|
|
@@ -61,32 +76,53 @@ function printHelp() {
|
|
|
61
76
|
].join("\n"));
|
|
62
77
|
}
|
|
63
78
|
async function main() {
|
|
64
|
-
var _a, _b, _c, _d;
|
|
79
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
65
80
|
const cmd = process_1.default.argv[2];
|
|
66
81
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
67
82
|
printHelp();
|
|
68
83
|
return;
|
|
69
84
|
}
|
|
85
|
+
const GATE_EXPECTS = [
|
|
86
|
+
"prd-fetched",
|
|
87
|
+
"questions-open",
|
|
88
|
+
"docs-closed",
|
|
89
|
+
"propose-ready",
|
|
90
|
+
"impl-allowed",
|
|
91
|
+
"shipped",
|
|
92
|
+
];
|
|
93
|
+
const PHASE_TARGETS = [
|
|
94
|
+
"after-prd",
|
|
95
|
+
"docs-done",
|
|
96
|
+
"propose-done",
|
|
97
|
+
"impl",
|
|
98
|
+
"shipped",
|
|
99
|
+
];
|
|
70
100
|
if (cmd !== "start" &&
|
|
71
101
|
cmd !== "run" &&
|
|
72
102
|
cmd !== "setup" &&
|
|
73
103
|
cmd !== "install" &&
|
|
74
104
|
cmd !== "doctor" &&
|
|
75
105
|
cmd !== "ensure-opsx" &&
|
|
76
|
-
cmd !== "invoke"
|
|
106
|
+
cmd !== "invoke" &&
|
|
107
|
+
cmd !== "gate" &&
|
|
108
|
+
cmd !== "phase" &&
|
|
109
|
+
cmd !== "propose" &&
|
|
110
|
+
cmd !== "deliver") {
|
|
77
111
|
printHelp();
|
|
78
112
|
process_1.default.exitCode = 1;
|
|
79
113
|
return;
|
|
80
114
|
}
|
|
81
115
|
const product = getArgValue("--product");
|
|
82
116
|
const version = getArgValue("--version");
|
|
83
|
-
const projectRoot = (_a = getArgValue("--
|
|
84
|
-
const
|
|
117
|
+
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());
|
|
118
|
+
const runId = (_d = (_c = getArgValue("--run-id")) !== null && _c !== void 0 ? _c : getArgValue("--runId")) !== null && _d !== void 0 ? _d : undefined;
|
|
119
|
+
const changeName = (_e = getArgValue("--change")) !== null && _e !== void 0 ? _e : undefined;
|
|
120
|
+
const branch = (_f = getArgValue("--branch")) !== null && _f !== void 0 ? _f : undefined;
|
|
85
121
|
const yes = process_1.default.argv.includes("-y");
|
|
86
122
|
const dryRun = process_1.default.argv.includes("--dry-run");
|
|
87
123
|
const planJson = process_1.default.argv.includes("--plan-json");
|
|
88
|
-
const agentValue = (
|
|
89
|
-
const projectKind = ((
|
|
124
|
+
const agentValue = (_g = getArgValue("--agent")) !== null && _g !== void 0 ? _g : "claude-code";
|
|
125
|
+
const projectKind = ((_h = getArgValue("--project")) !== null && _h !== void 0 ? _h : "OMS");
|
|
90
126
|
if (!ALLOWED_AGENTS.includes(agentValue)) {
|
|
91
127
|
// eslint-disable-next-line no-console
|
|
92
128
|
console.error(`不支持的 --agent: ${agentValue}`);
|
|
@@ -102,10 +138,96 @@ async function main() {
|
|
|
102
138
|
process_1.default.exitCode = 1;
|
|
103
139
|
return;
|
|
104
140
|
}
|
|
141
|
+
if (cmd === "gate") {
|
|
142
|
+
const expect = getArgValue("--expect");
|
|
143
|
+
if (!expect || !GATE_EXPECTS.includes(expect)) {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.error(`gate 需要 --expect ${GATE_EXPECTS.join("|")}`);
|
|
146
|
+
process_1.default.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const result = await (0, runGate_1.runGate)({
|
|
150
|
+
projectRoot,
|
|
151
|
+
runId,
|
|
152
|
+
expect,
|
|
153
|
+
change: changeName,
|
|
154
|
+
});
|
|
155
|
+
(0, runGate_1.printGateResult)(result);
|
|
156
|
+
if (!result.ok)
|
|
157
|
+
process_1.default.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (cmd === "phase") {
|
|
161
|
+
const sub = process_1.default.argv[3];
|
|
162
|
+
if (sub !== "advance") {
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.error("用法: sdd-flow-kit phase advance --to <target> ...");
|
|
165
|
+
process_1.default.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const to = getArgValue("--to");
|
|
169
|
+
if (!to || !PHASE_TARGETS.includes(to)) {
|
|
170
|
+
// eslint-disable-next-line no-console
|
|
171
|
+
console.error(`需要 --to ${PHASE_TARGETS.join("|")}`);
|
|
172
|
+
process_1.default.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const result = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
176
|
+
projectRoot,
|
|
177
|
+
runId,
|
|
178
|
+
to,
|
|
179
|
+
change: changeName,
|
|
180
|
+
dryRun,
|
|
181
|
+
});
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.log(result.ok ? `✅ ${result.message}` : `❌ ${result.message}`);
|
|
184
|
+
if (!result.ok)
|
|
185
|
+
process_1.default.exitCode = 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (cmd === "propose") {
|
|
189
|
+
if (!changeName) {
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.error("propose 需要 --change <kebab-name>");
|
|
192
|
+
process_1.default.exitCode = 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const result = await (0, runPropose_1.runPropose)({
|
|
196
|
+
projectRoot,
|
|
197
|
+
runId,
|
|
198
|
+
change: changeName,
|
|
199
|
+
dryRun,
|
|
200
|
+
});
|
|
201
|
+
// eslint-disable-next-line no-console
|
|
202
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
203
|
+
if (!result.ok)
|
|
204
|
+
process_1.default.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (cmd === "deliver") {
|
|
208
|
+
if (!changeName) {
|
|
209
|
+
// eslint-disable-next-line no-console
|
|
210
|
+
console.error("deliver 需要 --change <kebab-name>");
|
|
211
|
+
process_1.default.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const result = await (0, runDeliver_1.runDeliver)({
|
|
215
|
+
projectRoot,
|
|
216
|
+
runId,
|
|
217
|
+
change: changeName,
|
|
218
|
+
agent,
|
|
219
|
+
dryRun,
|
|
220
|
+
});
|
|
221
|
+
// eslint-disable-next-line no-console
|
|
222
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
223
|
+
if (!result.ok)
|
|
224
|
+
process_1.default.exitCode = 1;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
105
227
|
if (cmd === "doctor" || cmd === "ensure-opsx") {
|
|
106
228
|
const fix = cmd === "ensure-opsx" || process_1.default.argv.includes("--fix");
|
|
107
229
|
const doctor = await (0, doctor_1.runDoctor)({
|
|
108
|
-
projectRoot
|
|
230
|
+
projectRoot,
|
|
109
231
|
agent,
|
|
110
232
|
fix,
|
|
111
233
|
dryRun,
|
|
@@ -122,7 +244,7 @@ async function main() {
|
|
|
122
244
|
}
|
|
123
245
|
if (cmd === "ensure-opsx" && !dryRun) {
|
|
124
246
|
await (0, setupProject_1.setupProject)({
|
|
125
|
-
projectRoot
|
|
247
|
+
projectRoot,
|
|
126
248
|
agent,
|
|
127
249
|
yes: true,
|
|
128
250
|
light: true,
|
|
@@ -135,7 +257,7 @@ async function main() {
|
|
|
135
257
|
}
|
|
136
258
|
if (cmd === "setup" || cmd === "install") {
|
|
137
259
|
const result = await (0, setupProject_1.setupProject)({
|
|
138
|
-
projectRoot
|
|
260
|
+
projectRoot,
|
|
139
261
|
agent,
|
|
140
262
|
branch,
|
|
141
263
|
yes,
|
|
@@ -172,10 +294,9 @@ async function main() {
|
|
|
172
294
|
if (!parsed) {
|
|
173
295
|
throw new Error("invoke 解析失败,请使用:开发 ADI 2.3.2 需求");
|
|
174
296
|
}
|
|
175
|
-
const resolvedRoot = path_1.default.resolve(projectRoot);
|
|
176
297
|
const options = (0, invokeFlow_1.toRunOptions)({
|
|
177
298
|
parsed,
|
|
178
|
-
projectRoot
|
|
299
|
+
projectRoot,
|
|
179
300
|
branch,
|
|
180
301
|
yes,
|
|
181
302
|
dryRun,
|
|
@@ -194,22 +315,21 @@ async function main() {
|
|
|
194
315
|
process_1.default.exitCode = 1;
|
|
195
316
|
return;
|
|
196
317
|
}
|
|
197
|
-
const
|
|
198
|
-
const runId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
|
|
318
|
+
const scaffoldRunId = `${(0, utils_1.formatYYYYMMDD)(new Date())}-${product}-${version}`;
|
|
199
319
|
if (cmd === "start") {
|
|
200
320
|
await (0, startFlow_1.startFlowScaffold)({
|
|
201
321
|
product,
|
|
202
322
|
version,
|
|
203
|
-
projectRoot
|
|
323
|
+
projectRoot,
|
|
204
324
|
});
|
|
205
325
|
// eslint-disable-next-line no-console
|
|
206
|
-
console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${
|
|
326
|
+
console.log(`已生成 SDD 流程产物骨架:openspec/PRD/${scaffoldRunId}/`);
|
|
207
327
|
return;
|
|
208
328
|
}
|
|
209
329
|
const result = await (0, runFlow_1.runFlow)({
|
|
210
330
|
product,
|
|
211
331
|
version,
|
|
212
|
-
projectRoot
|
|
332
|
+
projectRoot,
|
|
213
333
|
agent,
|
|
214
334
|
branch,
|
|
215
335
|
yes,
|
|
@@ -222,6 +342,8 @@ async function main() {
|
|
|
222
342
|
// eslint-disable-next-line no-console
|
|
223
343
|
console.log(`流程已初始化:openspec/PRD/${result.runId}/`);
|
|
224
344
|
// eslint-disable-next-line no-console
|
|
345
|
+
console.log(`请打开 openspec/PRD/${result.runId}/NEXT.md,按脚本顺序执行 gate / phase / propose / deliver。`);
|
|
346
|
+
// eslint-disable-next-line no-console
|
|
225
347
|
console.log(`agent=${result.adapterName}, cli=${result.adapterAvailable ? "detected" : "missing"}, autoExecute=${result.executed ? "yes" : "manual"}`);
|
|
226
348
|
// eslint-disable-next-line no-console
|
|
227
349
|
console.log(`branch=${result.branch}, envSetup=${result.setupExecuted ? "installed" : "already-ready"}`);
|
|
@@ -233,7 +355,7 @@ async function main() {
|
|
|
233
355
|
// eslint-disable-next-line no-console
|
|
234
356
|
console.log(JSON.stringify({
|
|
235
357
|
command: "run",
|
|
236
|
-
projectRoot
|
|
358
|
+
projectRoot,
|
|
237
359
|
dryRun,
|
|
238
360
|
product,
|
|
239
361
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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.isPrdFetched = isPrdFetched;
|
|
7
|
+
exports.isQuestionsClosed = isQuestionsClosed;
|
|
8
|
+
exports.areAnalysisDocsReady = areAnalysisDocsReady;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = require("./fs");
|
|
11
|
+
const PRD_PLACEHOLDER_MARKERS = [
|
|
12
|
+
"请把《PRD》",
|
|
13
|
+
"占位",
|
|
14
|
+
"placeholder",
|
|
15
|
+
];
|
|
16
|
+
async function isPrdFetched(outputRoot) {
|
|
17
|
+
const prdPath = path_1.default.join(outputRoot, "source", "PRD.md");
|
|
18
|
+
const content = await (0, fs_1.readFileIfExists)(prdPath);
|
|
19
|
+
if (!content || content.trim().length < 80) {
|
|
20
|
+
return { ok: false, detail: "source/PRD.md 缺失或过短" };
|
|
21
|
+
}
|
|
22
|
+
if (PRD_PLACEHOLDER_MARKERS.some((m) => content.includes(m))) {
|
|
23
|
+
return { ok: false, detail: "source/PRD.md 仍为占位内容,请先执行 Step1 拉取 PRD" };
|
|
24
|
+
}
|
|
25
|
+
return { ok: true, detail: "PRD 已回填" };
|
|
26
|
+
}
|
|
27
|
+
async function isQuestionsClosed(outputRoot) {
|
|
28
|
+
const p = path_1.default.join(outputRoot, "03-待确认问题清单.md");
|
|
29
|
+
const content = await (0, fs_1.readFileIfExists)(p);
|
|
30
|
+
if (!content) {
|
|
31
|
+
return { ok: false, detail: "缺少 03-待确认问题清单.md" };
|
|
32
|
+
}
|
|
33
|
+
if (/矛盾点池状态[::]\s*[`「]*未闭合/.test(content)) {
|
|
34
|
+
return { ok: false, detail: "矛盾点池状态:未闭合" };
|
|
35
|
+
}
|
|
36
|
+
if (!/矛盾点池状态[::]\s*[`「]*已闭合/.test(content)) {
|
|
37
|
+
return { ok: false, detail: "03 未标注「矛盾点池状态:已闭合」" };
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, detail: "矛盾点池已闭合" };
|
|
40
|
+
}
|
|
41
|
+
async function areAnalysisDocsReady(outputRoot) {
|
|
42
|
+
const files = ["01-需求分析报告.md", "02-改动点清单.md", "04-技术文档草稿.md"];
|
|
43
|
+
for (const name of files) {
|
|
44
|
+
const content = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, name));
|
|
45
|
+
if (!content || content.trim().length < 120) {
|
|
46
|
+
return { ok: false, detail: `${name} 缺失或过短(阶段 B 未就绪)` };
|
|
47
|
+
}
|
|
48
|
+
if (name === "01-需求分析报告.md" && content.includes("(由 AI 工具生成")) {
|
|
49
|
+
return { ok: false, detail: `${name} 仍为模板占位` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, detail: "01/02/04 文档齐全" };
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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.readSession = readSession;
|
|
7
|
+
exports.writeSession = writeSession;
|
|
8
|
+
exports.defaultSession = defaultSession;
|
|
9
|
+
exports.normalizeSession = normalizeSession;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = require("./fs");
|
|
12
|
+
const SESSION_FILE = ".session-state.json";
|
|
13
|
+
async function readSession(outputRoot) {
|
|
14
|
+
const raw = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, SESSION_FILE));
|
|
15
|
+
if (!raw)
|
|
16
|
+
return null;
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
async function writeSession(outputRoot, state) {
|
|
20
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, SESSION_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
function defaultSession(args) {
|
|
23
|
+
return {
|
|
24
|
+
product: args.product,
|
|
25
|
+
version: args.version,
|
|
26
|
+
runId: args.runId,
|
|
27
|
+
outputRoot: args.outputRoot,
|
|
28
|
+
phase: "A",
|
|
29
|
+
status: "awaiting_user_confirmation",
|
|
30
|
+
docModeOnly: true,
|
|
31
|
+
autoChainOpsx: true,
|
|
32
|
+
activeChange: null,
|
|
33
|
+
createdAtISO: new Date().toISOString(),
|
|
34
|
+
implGitBaseline: null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** 将旧版 session(无 phase 字段)迁移为当前结构 */
|
|
38
|
+
function normalizeSession(raw, outputRoot) {
|
|
39
|
+
var _a, _b, _c, _d, _e;
|
|
40
|
+
const phase = raw.phase || "A";
|
|
41
|
+
const status = raw.status || "awaiting_user_confirmation";
|
|
42
|
+
return {
|
|
43
|
+
product: String((_a = raw.product) !== null && _a !== void 0 ? _a : ""),
|
|
44
|
+
version: String((_b = raw.version) !== null && _b !== void 0 ? _b : ""),
|
|
45
|
+
runId: String((_c = raw.runId) !== null && _c !== void 0 ? _c : ""),
|
|
46
|
+
outputRoot: String((_d = raw.outputRoot) !== null && _d !== void 0 ? _d : outputRoot),
|
|
47
|
+
phase,
|
|
48
|
+
status,
|
|
49
|
+
docModeOnly: raw.docModeOnly !== false,
|
|
50
|
+
autoChainOpsx: raw.autoChainOpsx !== false,
|
|
51
|
+
activeChange: raw.activeChange ? String(raw.activeChange) : null,
|
|
52
|
+
createdAtISO: String((_e = raw.createdAtISO) !== null && _e !== void 0 ? _e : new Date().toISOString()),
|
|
53
|
+
implGitBaseline: raw.implGitBaseline ? String(raw.implGitBaseline) : null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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.writeInitialNext = writeInitialNext;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = require("./fs");
|
|
9
|
+
/**
|
|
10
|
+
* run 初始化后写入 NEXT.md,强制 AI 按脚本推进而非自然语言跳步。
|
|
11
|
+
*/
|
|
12
|
+
async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
13
|
+
const pr = projectRoot.replace(/\\/g, "/");
|
|
14
|
+
const lines = [
|
|
15
|
+
"# NEXT(脚本门禁 — 必须按顺序执行)",
|
|
16
|
+
"",
|
|
17
|
+
"> **禁止**跳过下列命令。失败 (exit≠0) 时不得进入下一阶段或修改 `src/`。",
|
|
18
|
+
"",
|
|
19
|
+
"## 1. Step1 — 拉取 PRD",
|
|
20
|
+
"按 `01-获取需求文档指引.md` 执行 confluence 脚本,然后:",
|
|
21
|
+
"```bash",
|
|
22
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect prd-fetched`,
|
|
23
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to after-prd`,
|
|
24
|
+
"```",
|
|
25
|
+
"",
|
|
26
|
+
"## 2. Step2 — 需求分析(提问模式)",
|
|
27
|
+
"执行 `02-需求分析提示词.md`。未闭合时仅改 `03`,并确认:",
|
|
28
|
+
"```bash",
|
|
29
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect questions-open`,
|
|
30
|
+
"```",
|
|
31
|
+
"",
|
|
32
|
+
"## 3. 用户确认后 — 阶段 B(仅文档)",
|
|
33
|
+
"生成 01/02/04 后:",
|
|
34
|
+
"```bash",
|
|
35
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect docs-closed`,
|
|
36
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to docs-done`,
|
|
37
|
+
"```",
|
|
38
|
+
"",
|
|
39
|
+
"## 4. 阶段 C — propose",
|
|
40
|
+
"```bash",
|
|
41
|
+
`npx sdd-flow-kit propose --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
42
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to propose-done --change <kebab-name>`,
|
|
43
|
+
"```",
|
|
44
|
+
"",
|
|
45
|
+
"## 5. 阶段 D — 实现 + 交付",
|
|
46
|
+
"```bash",
|
|
47
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to impl --change <kebab-name>`,
|
|
48
|
+
`npx sdd-flow-kit deliver --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
49
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to shipped --change <kebab-name>`,
|
|
50
|
+
"```",
|
|
51
|
+
"",
|
|
52
|
+
];
|
|
53
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "NEXT.md"), lines.join("\n"));
|
|
54
|
+
}
|