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 +51 -9
- package/dist/cli.js +148 -19
- package/dist/core/gitDiffGuard.js +83 -0
- package/dist/core/openspecGate.js +83 -0
- package/dist/core/opsxDelivery.js +29 -19
- package/dist/core/prdArtifacts.js +53 -0
- package/dist/core/projectRoots.js +94 -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 +60 -0
- package/dist/steps/runDeliver.js +77 -0
- package/dist/steps/runGate.js +280 -0
- package/dist/steps/runPhaseAdvance.js +194 -0
- package/dist/steps/runPropose.js +76 -0
- package/dist/steps/setupProject.js +19 -16
- package/dist/steps/startFlow.js +13 -17
- 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
|
@@ -0,0 +1,280 @@
|
|
|
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.runGate = runGate;
|
|
7
|
+
exports.printGateResult = printGateResult;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const prdArtifacts_1 = require("../core/prdArtifacts");
|
|
10
|
+
const gitDiffGuard_1 = require("../core/gitDiffGuard");
|
|
11
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
12
|
+
const openspecGate_1 = require("../core/openspecGate");
|
|
13
|
+
const resolveRun_1 = require("./resolveRun");
|
|
14
|
+
const fs_1 = require("../core/fs");
|
|
15
|
+
function fail(checks, expect, runId) {
|
|
16
|
+
return { ok: false, expect, runId, checks };
|
|
17
|
+
}
|
|
18
|
+
function pass(checks, expect, runId) {
|
|
19
|
+
return { ok: checks.every((c) => c.ok), expect, runId, checks };
|
|
20
|
+
}
|
|
21
|
+
async function readActiveChange(projectRoot, session, override) {
|
|
22
|
+
if (override)
|
|
23
|
+
return override;
|
|
24
|
+
if (session === null || session === void 0 ? void 0 : session.activeChange)
|
|
25
|
+
return session.activeChange;
|
|
26
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(projectRoot);
|
|
27
|
+
const prdRoots = await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot);
|
|
28
|
+
const dirs = [installRoot, ...prdRoots.map((p) => path_1.default.dirname(path_1.default.dirname(p)))];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const root of dirs) {
|
|
31
|
+
const key = path_1.default.resolve(root);
|
|
32
|
+
if (seen.has(key))
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
const text = await (0, fs_1.readFileIfExists)(path_1.default.join(key, ".opsx-active-change"));
|
|
36
|
+
if (text) {
|
|
37
|
+
const line = text
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map((l) => l.trim())
|
|
40
|
+
.find(Boolean);
|
|
41
|
+
if (line)
|
|
42
|
+
return line;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if ((0, projectRoots_1.isLegacyMonorepoMode)()) {
|
|
46
|
+
const fe = path_1.default.join(installRoot, "frontend", ".opsx-active-change");
|
|
47
|
+
const text = await (0, fs_1.readFileIfExists)(fe);
|
|
48
|
+
if (text) {
|
|
49
|
+
const line = text
|
|
50
|
+
.split("\n")
|
|
51
|
+
.map((l) => l.trim())
|
|
52
|
+
.find(Boolean);
|
|
53
|
+
if (line)
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
async function resolveImplRoots(projectRoot) {
|
|
60
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(projectRoot);
|
|
61
|
+
const roots = [installRoot];
|
|
62
|
+
if ((0, projectRoots_1.isLegacyMonorepoMode)()) {
|
|
63
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
64
|
+
if (pkgRoot !== installRoot && !roots.includes(pkgRoot)) {
|
|
65
|
+
roots.push(pkgRoot);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return roots;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 脚本门禁:失败时 process.exitCode=1,成功 exit 0。
|
|
72
|
+
*/
|
|
73
|
+
async function runGate(options) {
|
|
74
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
75
|
+
const checks = [];
|
|
76
|
+
if (!loaded || !loaded.resolved) {
|
|
77
|
+
checks.push({ name: "run-resolved", ok: false, detail: "未找到 openspec/PRD/<runId> 目录" });
|
|
78
|
+
return fail(checks, options.expect, null);
|
|
79
|
+
}
|
|
80
|
+
const { resolved, session } = loaded;
|
|
81
|
+
const outputRoot = resolved.outputRoot;
|
|
82
|
+
const runId = resolved.runId;
|
|
83
|
+
const implRoots = await resolveImplRoots(options.projectRoot);
|
|
84
|
+
if (options.expect === "prd-fetched") {
|
|
85
|
+
const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
|
|
86
|
+
checks.push({ name: "prd-fetched", ...prd });
|
|
87
|
+
return pass(checks, options.expect, runId);
|
|
88
|
+
}
|
|
89
|
+
if (options.expect === "questions-open") {
|
|
90
|
+
const closed = await (0, prdArtifacts_1.isQuestionsClosed)(outputRoot);
|
|
91
|
+
checks.push({
|
|
92
|
+
name: "questions-open",
|
|
93
|
+
ok: !closed.ok,
|
|
94
|
+
detail: closed.ok ? "矛盾点已闭合,不应处于提问模式" : "提问模式有效(未闭合)",
|
|
95
|
+
});
|
|
96
|
+
return pass(checks, options.expect, runId);
|
|
97
|
+
}
|
|
98
|
+
if (options.expect === "docs-closed") {
|
|
99
|
+
const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
|
|
100
|
+
checks.push({ name: "prd-fetched", ...prd });
|
|
101
|
+
const closed = await (0, prdArtifacts_1.isQuestionsClosed)(outputRoot);
|
|
102
|
+
checks.push({ name: "questions-closed", ...closed });
|
|
103
|
+
const docs = await (0, prdArtifacts_1.areAnalysisDocsReady)(outputRoot);
|
|
104
|
+
checks.push({ name: "analysis-docs", ...docs });
|
|
105
|
+
if (session && session.docModeOnly === false) {
|
|
106
|
+
checks.push({
|
|
107
|
+
name: "doc-mode",
|
|
108
|
+
ok: false,
|
|
109
|
+
detail: "session.docModeOnly=false,请先处于文档阶段(勿提前 phase advance --to impl)",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (session) {
|
|
113
|
+
checks.push({ name: "doc-mode", ok: true, detail: "docModeOnly=true" });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
checks.push({ name: "doc-mode", ok: true, detail: "无 session,跳过 docModeOnly 校验" });
|
|
117
|
+
}
|
|
118
|
+
for (const root of implRoots) {
|
|
119
|
+
const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(root);
|
|
120
|
+
const diff = (0, gitDiffGuard_1.gitDiffNameOnly)(gitRoot);
|
|
121
|
+
if (!diff.isRepo) {
|
|
122
|
+
checks.push({ name: "git-impl-diff", ok: true, detail: "非 git 仓库,跳过 src 改动检测" });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const impl = (0, gitDiffGuard_1.findImplChanges)(diff.changedFiles);
|
|
126
|
+
checks.push({
|
|
127
|
+
name: "git-impl-diff",
|
|
128
|
+
ok: impl.length === 0,
|
|
129
|
+
detail: impl.length === 0
|
|
130
|
+
? "工作区无业务实现路径改动"
|
|
131
|
+
: `禁止在文档阶段修改实现代码: ${impl.slice(0, 8).join(", ")}${impl.length > 8 ? "..." : ""}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return pass(checks, options.expect, runId);
|
|
135
|
+
}
|
|
136
|
+
if (options.expect === "propose-ready") {
|
|
137
|
+
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
138
|
+
if (!changeName) {
|
|
139
|
+
checks.push({ name: "active-change", ok: false, detail: "未指定 change(--change 或 .opsx-active-change)" });
|
|
140
|
+
return fail(checks, options.expect, runId);
|
|
141
|
+
}
|
|
142
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
143
|
+
const openspecRoots = [
|
|
144
|
+
installRoot,
|
|
145
|
+
...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
|
|
146
|
+
];
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
let found = false;
|
|
149
|
+
for (const root of openspecRoots) {
|
|
150
|
+
const key = path_1.default.resolve(root);
|
|
151
|
+
if (seen.has(key))
|
|
152
|
+
continue;
|
|
153
|
+
seen.add(key);
|
|
154
|
+
if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
|
|
155
|
+
found = true;
|
|
156
|
+
const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
|
|
157
|
+
checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!found) {
|
|
162
|
+
checks.push({
|
|
163
|
+
name: "change-dir",
|
|
164
|
+
ok: false,
|
|
165
|
+
detail: `openspec/changes/${changeName} 不存在`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return pass(checks, options.expect, runId);
|
|
169
|
+
}
|
|
170
|
+
if (options.expect === "impl-allowed") {
|
|
171
|
+
if (!session) {
|
|
172
|
+
checks.push({ name: "session", ok: false, detail: "缺少 .session-state.json" });
|
|
173
|
+
return fail(checks, options.expect, runId);
|
|
174
|
+
}
|
|
175
|
+
checks.push({
|
|
176
|
+
name: "doc-mode-off",
|
|
177
|
+
ok: session.docModeOnly === false,
|
|
178
|
+
detail: session.docModeOnly === false
|
|
179
|
+
? "已进入实现模式 docModeOnly=false"
|
|
180
|
+
: "仍处文档模式,请执行: sdd-flow-kit phase advance --to impl",
|
|
181
|
+
});
|
|
182
|
+
checks.push({
|
|
183
|
+
name: "phase-d",
|
|
184
|
+
ok: session.phase === "D" || session.status === "apply_in_progress",
|
|
185
|
+
detail: `当前 phase=${session.phase}, status=${session.status}`,
|
|
186
|
+
});
|
|
187
|
+
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
188
|
+
if (!changeName) {
|
|
189
|
+
checks.push({ name: "active-change", ok: false, detail: "未绑定 activeChange" });
|
|
190
|
+
return fail(checks, options.expect, runId);
|
|
191
|
+
}
|
|
192
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
193
|
+
const openspecRoots = [
|
|
194
|
+
installRoot,
|
|
195
|
+
...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
|
|
196
|
+
];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
let found = false;
|
|
199
|
+
for (const root of openspecRoots) {
|
|
200
|
+
const key = path_1.default.resolve(root);
|
|
201
|
+
if (seen.has(key))
|
|
202
|
+
continue;
|
|
203
|
+
seen.add(key);
|
|
204
|
+
if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
|
|
205
|
+
found = true;
|
|
206
|
+
const st = (0, openspecGate_1.checkOpenspecApplyReady)(key, changeName);
|
|
207
|
+
checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!found) {
|
|
212
|
+
checks.push({
|
|
213
|
+
name: "change-dir",
|
|
214
|
+
ok: false,
|
|
215
|
+
detail: `openspec/changes/${changeName} 不存在`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return pass(checks, options.expect, runId);
|
|
219
|
+
}
|
|
220
|
+
if (options.expect === "shipped") {
|
|
221
|
+
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
222
|
+
if (!changeName) {
|
|
223
|
+
checks.push({ name: "active-change", ok: false, detail: "未指定 change" });
|
|
224
|
+
return fail(checks, options.expect, runId);
|
|
225
|
+
}
|
|
226
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
227
|
+
const openspecRoots = [
|
|
228
|
+
installRoot,
|
|
229
|
+
...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
|
|
230
|
+
];
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
for (const root of openspecRoots) {
|
|
233
|
+
const key = path_1.default.resolve(root);
|
|
234
|
+
if (seen.has(key))
|
|
235
|
+
continue;
|
|
236
|
+
seen.add(key);
|
|
237
|
+
if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
|
|
238
|
+
const tasks = await (0, openspecGate_1.checkTasksAllDone)(key, changeName);
|
|
239
|
+
checks.push({ name: "tasks-done", ...tasks });
|
|
240
|
+
const marker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".delivery-pass.json"));
|
|
241
|
+
if (marker) {
|
|
242
|
+
try {
|
|
243
|
+
const j = JSON.parse(marker);
|
|
244
|
+
const ok = j.exitCode === 0 && (!j.change || j.change === changeName);
|
|
245
|
+
checks.push({
|
|
246
|
+
name: "delivery-pass",
|
|
247
|
+
ok,
|
|
248
|
+
detail: ok ? "delivery-pass.json 有效" : "delivery-pass.json 与 change 不匹配或失败",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
checks.push({ name: "delivery-pass", ok: false, detail: "delivery-pass.json 解析失败" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
checks.push({
|
|
257
|
+
name: "delivery-pass",
|
|
258
|
+
ok: false,
|
|
259
|
+
detail: "缺少 .delivery-pass.json,请先执行: sdd-flow-kit deliver --change <name>",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return pass(checks, options.expect, runId);
|
|
266
|
+
}
|
|
267
|
+
checks.push({ name: "expect", ok: false, detail: `未知 --expect: ${options.expect}` });
|
|
268
|
+
return fail(checks, options.expect, runId);
|
|
269
|
+
}
|
|
270
|
+
function printGateResult(result) {
|
|
271
|
+
var _a;
|
|
272
|
+
// eslint-disable-next-line no-console
|
|
273
|
+
console.log(`\n[sdd-flow-kit gate] expect=${result.expect} runId=${(_a = result.runId) !== null && _a !== void 0 ? _a : "?"}`);
|
|
274
|
+
for (const c of result.checks) {
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.log(` [${c.ok ? "OK" : "FAIL"}] ${c.name}: ${c.detail}`);
|
|
277
|
+
}
|
|
278
|
+
// eslint-disable-next-line no-console
|
|
279
|
+
console.log(result.ok ? "\n✅ GATE PASS\n" : "\n❌ GATE FAIL\n");
|
|
280
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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.runPhaseAdvance = runPhaseAdvance;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const runGate_1 = require("./runGate");
|
|
9
|
+
const resolveRun_1 = require("./resolveRun");
|
|
10
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
11
|
+
const sessionState_1 = require("../core/sessionState");
|
|
12
|
+
const fs_1 = require("../core/fs");
|
|
13
|
+
const gitDiffGuard_1 = require("../core/gitDiffGuard");
|
|
14
|
+
async function writeNextMd(outputRoot, lines) {
|
|
15
|
+
const body = ["# NEXT(脚本门禁 — 请按顺序执行)", "", ...lines, ""].join("\n");
|
|
16
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "NEXT.md"), body);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 阶段推进:先跑 gate,通过后更新 .session-state.json 与 NEXT.md。
|
|
20
|
+
*/
|
|
21
|
+
async function runPhaseAdvance(options) {
|
|
22
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
23
|
+
if (!loaded || !loaded.resolved || !loaded.session) {
|
|
24
|
+
return { ok: false, runId: null, message: "未找到 run 或 .session-state.json" };
|
|
25
|
+
}
|
|
26
|
+
const { resolved, session } = loaded;
|
|
27
|
+
const outputRoot = resolved.outputRoot;
|
|
28
|
+
const runId = resolved.runId;
|
|
29
|
+
let gate = await (0, runGate_1.runGate)({
|
|
30
|
+
projectRoot: options.projectRoot,
|
|
31
|
+
runId,
|
|
32
|
+
expect: "prd-fetched",
|
|
33
|
+
change: options.change,
|
|
34
|
+
});
|
|
35
|
+
if (options.to === "after-prd") {
|
|
36
|
+
gate = await (0, runGate_1.runGate)({ projectRoot: options.projectRoot, runId, expect: "prd-fetched" });
|
|
37
|
+
if (!gate.ok) {
|
|
38
|
+
(0, runGate_1.printGateResult)(gate);
|
|
39
|
+
return { ok: false, runId, message: "gate prd-fetched 未通过" };
|
|
40
|
+
}
|
|
41
|
+
const next = {
|
|
42
|
+
...session,
|
|
43
|
+
phase: "A",
|
|
44
|
+
status: "awaiting_user_confirmation",
|
|
45
|
+
docModeOnly: true,
|
|
46
|
+
};
|
|
47
|
+
if (!options.dryRun) {
|
|
48
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
49
|
+
await writeNextMd(outputRoot, [
|
|
50
|
+
"## 当前:阶段 A(需求分析)",
|
|
51
|
+
"1. 执行 `02-需求分析提示词.md`",
|
|
52
|
+
"2. 若未闭合:仅更新 `03-待确认问题清单.md`,然后执行:",
|
|
53
|
+
"```bash",
|
|
54
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect questions-open`,
|
|
55
|
+
"```",
|
|
56
|
+
"3. 用户确认后生成 01/02/04,再执行:",
|
|
57
|
+
"```bash",
|
|
58
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to docs-done`,
|
|
59
|
+
"```",
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, runId, message: "已进入阶段 A(PRD 已就绪)" };
|
|
63
|
+
}
|
|
64
|
+
if (options.to === "docs-done") {
|
|
65
|
+
gate = await (0, runGate_1.runGate)({ projectRoot: options.projectRoot, runId, expect: "docs-closed" });
|
|
66
|
+
if (!gate.ok) {
|
|
67
|
+
(0, runGate_1.printGateResult)(gate);
|
|
68
|
+
return { ok: false, runId, message: "gate docs-closed 未通过(禁止带 src 改动)" };
|
|
69
|
+
}
|
|
70
|
+
const next = {
|
|
71
|
+
...session,
|
|
72
|
+
phase: "B",
|
|
73
|
+
status: "ready_for_propose",
|
|
74
|
+
docModeOnly: true,
|
|
75
|
+
};
|
|
76
|
+
if (!options.dryRun) {
|
|
77
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
78
|
+
await writeNextMd(outputRoot, [
|
|
79
|
+
"## 当前:阶段 B 完成 → 进入 C(提案)",
|
|
80
|
+
"1. 执行 openspec propose(或):",
|
|
81
|
+
"```bash",
|
|
82
|
+
`npx sdd-flow-kit propose --project-root ${options.projectRoot} --run-id ${runId} --change <kebab-name>`,
|
|
83
|
+
"```",
|
|
84
|
+
"2. 提案 apply-ready 后:",
|
|
85
|
+
"```bash",
|
|
86
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to propose-done --change <name>`,
|
|
87
|
+
"```",
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, runId, message: "阶段 B 完成,可进入 propose" };
|
|
91
|
+
}
|
|
92
|
+
if (options.to === "propose-done") {
|
|
93
|
+
const changeName = options.change || session.activeChange;
|
|
94
|
+
if (!changeName) {
|
|
95
|
+
return { ok: false, runId, message: "需要 --change <name>" };
|
|
96
|
+
}
|
|
97
|
+
gate = await (0, runGate_1.runGate)({
|
|
98
|
+
projectRoot: options.projectRoot,
|
|
99
|
+
runId,
|
|
100
|
+
expect: "propose-ready",
|
|
101
|
+
change: changeName,
|
|
102
|
+
});
|
|
103
|
+
if (!gate.ok) {
|
|
104
|
+
(0, runGate_1.printGateResult)(gate);
|
|
105
|
+
return { ok: false, runId, message: "gate propose-ready 未通过" };
|
|
106
|
+
}
|
|
107
|
+
const next = {
|
|
108
|
+
...session,
|
|
109
|
+
phase: "C",
|
|
110
|
+
status: "propose_done",
|
|
111
|
+
docModeOnly: true,
|
|
112
|
+
activeChange: changeName,
|
|
113
|
+
};
|
|
114
|
+
if (!options.dryRun) {
|
|
115
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
116
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
117
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(installRoot, ".opsx-active-change"), `${changeName}\n`);
|
|
118
|
+
await writeNextMd(outputRoot, [
|
|
119
|
+
"## 当前:阶段 C 完成 → 进入 D(实现)",
|
|
120
|
+
"```bash",
|
|
121
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to impl --change ${changeName}`,
|
|
122
|
+
"```",
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, runId, message: `提案就绪: ${changeName}` };
|
|
126
|
+
}
|
|
127
|
+
if (options.to === "impl") {
|
|
128
|
+
const changeName = options.change || session.activeChange;
|
|
129
|
+
if (!changeName) {
|
|
130
|
+
return { ok: false, runId, message: "需要 --change <name>" };
|
|
131
|
+
}
|
|
132
|
+
gate = await (0, runGate_1.runGate)({
|
|
133
|
+
projectRoot: options.projectRoot,
|
|
134
|
+
runId,
|
|
135
|
+
expect: "propose-ready",
|
|
136
|
+
change: changeName,
|
|
137
|
+
});
|
|
138
|
+
if (!gate.ok) {
|
|
139
|
+
(0, runGate_1.printGateResult)(gate);
|
|
140
|
+
return { ok: false, runId, message: "propose 未就绪,禁止进入实现" };
|
|
141
|
+
}
|
|
142
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
143
|
+
const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(installRoot);
|
|
144
|
+
const head = (0, gitDiffGuard_1.gitDiffNameOnly)(gitRoot).head;
|
|
145
|
+
const next = {
|
|
146
|
+
...session,
|
|
147
|
+
phase: "D",
|
|
148
|
+
status: "apply_in_progress",
|
|
149
|
+
docModeOnly: false,
|
|
150
|
+
activeChange: changeName,
|
|
151
|
+
implGitBaseline: head,
|
|
152
|
+
};
|
|
153
|
+
if (!options.dryRun) {
|
|
154
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
155
|
+
await writeNextMd(outputRoot, [
|
|
156
|
+
"## 当前:阶段 D(允许修改 src/)",
|
|
157
|
+
"实现前门禁:",
|
|
158
|
+
"```bash",
|
|
159
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect impl-allowed --change ${changeName}`,
|
|
160
|
+
"```",
|
|
161
|
+
"完成后交付:",
|
|
162
|
+
"```bash",
|
|
163
|
+
`npx sdd-flow-kit deliver --project-root ${options.projectRoot} --run-id ${runId} --change ${changeName}`,
|
|
164
|
+
"```",
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
return { ok: true, runId, message: "已进入实现阶段(docModeOnly=false)" };
|
|
168
|
+
}
|
|
169
|
+
if (options.to === "shipped") {
|
|
170
|
+
const changeName = options.change || session.activeChange;
|
|
171
|
+
gate = await (0, runGate_1.runGate)({
|
|
172
|
+
projectRoot: options.projectRoot,
|
|
173
|
+
runId,
|
|
174
|
+
expect: "shipped",
|
|
175
|
+
change: changeName,
|
|
176
|
+
});
|
|
177
|
+
if (!gate.ok) {
|
|
178
|
+
(0, runGate_1.printGateResult)(gate);
|
|
179
|
+
return { ok: false, runId, message: "gate shipped 未通过" };
|
|
180
|
+
}
|
|
181
|
+
const next = {
|
|
182
|
+
...session,
|
|
183
|
+
phase: "done",
|
|
184
|
+
status: "complete",
|
|
185
|
+
docModeOnly: false,
|
|
186
|
+
};
|
|
187
|
+
if (!options.dryRun) {
|
|
188
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
189
|
+
await writeNextMd(outputRoot, ["## ✅ SDD 流程已完成(delivery-pass)"]);
|
|
190
|
+
}
|
|
191
|
+
return { ok: true, runId, message: "流程完成" };
|
|
192
|
+
}
|
|
193
|
+
return { ok: false, runId, message: `未知 --to: ${options.to}` };
|
|
194
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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.runPropose = runPropose;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const resolveRun_1 = require("./resolveRun");
|
|
10
|
+
const runGate_1 = require("./runGate");
|
|
11
|
+
const fs_1 = require("../core/fs");
|
|
12
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
13
|
+
/**
|
|
14
|
+
* 创建/校验 openspec change,并检查 apply-ready(不代替 AI 写 proposal 正文,但强制 gate)。
|
|
15
|
+
*/
|
|
16
|
+
async function runPropose(options) {
|
|
17
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
18
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
19
|
+
return { ok: false, detail: "未找到 PRD run" };
|
|
20
|
+
}
|
|
21
|
+
const docsGate = await (0, runGate_1.runGate)({
|
|
22
|
+
projectRoot: options.projectRoot,
|
|
23
|
+
runId: loaded.resolved.runId,
|
|
24
|
+
expect: "docs-closed",
|
|
25
|
+
});
|
|
26
|
+
if (!docsGate.ok) {
|
|
27
|
+
(0, runGate_1.printGateResult)(docsGate);
|
|
28
|
+
return { ok: false, detail: "docs-closed gate 未通过,禁止 propose" };
|
|
29
|
+
}
|
|
30
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
31
|
+
const cwd = await (0, projectRoots_1.resolveOpenspecRoot)(installRoot);
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
return { ok: true, detail: `dry-run: would openspec new change ${options.change}` };
|
|
34
|
+
}
|
|
35
|
+
const exists = (0, child_process_1.spawnSync)("openspec", ["status", "--change", options.change, "--json"], {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
});
|
|
39
|
+
if (exists.status !== 0) {
|
|
40
|
+
const created = (0, child_process_1.spawnSync)("openspec", ["new", "change", options.change], {
|
|
41
|
+
cwd,
|
|
42
|
+
stdio: "inherit",
|
|
43
|
+
});
|
|
44
|
+
if (created.status !== 0) {
|
|
45
|
+
return { ok: false, detail: `openspec new change 失败: ${options.change}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(cwd, ".opsx-active-change"), `${options.change}\n`);
|
|
49
|
+
const proposeNote = [
|
|
50
|
+
"# Propose 脚本检查点",
|
|
51
|
+
"",
|
|
52
|
+
`change: \`${options.change}\``,
|
|
53
|
+
"",
|
|
54
|
+
"AI 须按 openspec-propose skill 生成 proposal/design/specs/tasks,完成后执行:",
|
|
55
|
+
"```bash",
|
|
56
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --expect propose-ready --change ${options.change}`,
|
|
57
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --to propose-done --change ${options.change}`,
|
|
58
|
+
"```",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n");
|
|
61
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, "05-openspec-提案草稿.md"), proposeNote);
|
|
62
|
+
const ready = await (0, runGate_1.runGate)({
|
|
63
|
+
projectRoot: options.projectRoot,
|
|
64
|
+
runId: loaded.resolved.runId,
|
|
65
|
+
expect: "propose-ready",
|
|
66
|
+
change: options.change,
|
|
67
|
+
});
|
|
68
|
+
(0, runGate_1.printGateResult)(ready);
|
|
69
|
+
if (!ready.ok) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
detail: "change 已创建,但尚未 apply-ready;请生成 artifacts 后重跑 gate propose-ready",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, detail: `change ${options.change} apply-ready` };
|
|
76
|
+
}
|
|
@@ -11,6 +11,7 @@ const fs_1 = require("../core/fs");
|
|
|
11
11
|
const readlinePrompt_1 = require("../core/readlinePrompt");
|
|
12
12
|
const utils_1 = require("../core/utils");
|
|
13
13
|
const opsxDelivery_1 = require("../core/opsxDelivery");
|
|
14
|
+
const projectRoots_1 = require("../core/projectRoots");
|
|
14
15
|
const templates_1 = require("../core/templates");
|
|
15
16
|
const EDITOR_CHOICES = ["claude-code", "cursor", "codex", "openclaw"];
|
|
16
17
|
const PROJECT_CHOICES = ["OMS", "ADI", "欢盟", "AD Tools"];
|
|
@@ -219,14 +220,15 @@ async function installOpsxConfig(projectRoot, agent) {
|
|
|
219
220
|
}
|
|
220
221
|
async function setupProject(options) {
|
|
221
222
|
var _a, _b, _c;
|
|
223
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
222
224
|
const defaultAgent = (_a = options.agent) !== null && _a !== void 0 ? _a : "claude-code";
|
|
223
225
|
const selectedAgent = options.yes ? defaultAgent : await askAgent(defaultAgent);
|
|
224
226
|
const defaultKind = (_b = options.projectKind) !== null && _b !== void 0 ? _b : "OMS";
|
|
225
227
|
const selectedKind = options.yes ? defaultKind : await askProjectKind(defaultKind);
|
|
226
|
-
const defaultBranch = (_c = options.branch) !== null && _c !== void 0 ? _c : pickDefaultBranch(
|
|
228
|
+
const defaultBranch = (_c = options.branch) !== null && _c !== void 0 ? _c : pickDefaultBranch(installRoot);
|
|
227
229
|
const selectedBranch = options.yes ? defaultBranch : await askBranch(defaultBranch);
|
|
228
|
-
const plannedActions = [];
|
|
229
|
-
const branches = listLocalBranches(
|
|
230
|
+
const plannedActions = [`installRoot=${installRoot}`];
|
|
231
|
+
const branches = listLocalBranches(installRoot);
|
|
230
232
|
const branchExists = branches.includes(selectedBranch);
|
|
231
233
|
let branchResult;
|
|
232
234
|
if (options.light) {
|
|
@@ -240,9 +242,9 @@ async function setupProject(options) {
|
|
|
240
242
|
: `would create and checkout branch: ${selectedBranch}`);
|
|
241
243
|
}
|
|
242
244
|
else {
|
|
243
|
-
branchResult = ensureBranch(
|
|
245
|
+
branchResult = ensureBranch(installRoot, selectedBranch);
|
|
244
246
|
}
|
|
245
|
-
let environmentReady = await (0, opsxDelivery_1.isOpsxApplyReady)(
|
|
247
|
+
let environmentReady = await (0, opsxDelivery_1.isOpsxApplyReady)(installRoot);
|
|
246
248
|
let setupExecuted = false;
|
|
247
249
|
let skillInstalled = false;
|
|
248
250
|
let docsSkillInstalled = false;
|
|
@@ -255,7 +257,7 @@ async function setupProject(options) {
|
|
|
255
257
|
}
|
|
256
258
|
else if (!environmentReady) {
|
|
257
259
|
const ensured = await (0, opsxDelivery_1.ensureOpsxWorkflow)({
|
|
258
|
-
projectRoot:
|
|
260
|
+
projectRoot: installRoot,
|
|
259
261
|
agent: selectedAgent,
|
|
260
262
|
dryRun: options.dryRun,
|
|
261
263
|
});
|
|
@@ -267,23 +269,24 @@ async function setupProject(options) {
|
|
|
267
269
|
plannedActions.push("opsx apply stack already ready");
|
|
268
270
|
}
|
|
269
271
|
if (options.dryRun) {
|
|
270
|
-
for (const skillPath of allSkillInstallPaths(
|
|
272
|
+
for (const skillPath of allSkillInstallPaths(installRoot)) {
|
|
271
273
|
plannedActions.push(`would install skill: ${skillPath}`);
|
|
272
274
|
}
|
|
273
275
|
const { dir } = docSkillDirName(selectedKind);
|
|
274
|
-
plannedActions.push(`would install docs skill: ${path_1.default.join(
|
|
275
|
-
plannedActions.push(`would install opsx config: ${path_1.default.join(
|
|
276
|
-
plannedActions.push(`would install cursor rule: ${path_1.default.join(
|
|
276
|
+
plannedActions.push(`would install docs skill: ${path_1.default.join(installRoot, "docs", dir)}`);
|
|
277
|
+
plannedActions.push(`would install opsx config: ${path_1.default.join(installRoot, ".opsx", "config.json")}`);
|
|
278
|
+
plannedActions.push(`would install cursor rule: ${path_1.default.join(installRoot, ".cursor", "rules", "sdd-flow-kit-gates.mdc")}`);
|
|
277
279
|
}
|
|
278
280
|
else {
|
|
279
|
-
await installAgentSkill(
|
|
280
|
-
await installSddCursorRule(
|
|
281
|
+
await installAgentSkill(installRoot);
|
|
282
|
+
await installSddCursorRule(installRoot);
|
|
281
283
|
skillInstalled = true;
|
|
282
|
-
await installDocsSkill(
|
|
284
|
+
await installDocsSkill(installRoot, selectedKind);
|
|
283
285
|
docsSkillInstalled = true;
|
|
284
|
-
await installOpsxConfig(
|
|
286
|
+
await installOpsxConfig(installRoot, selectedAgent);
|
|
285
287
|
opsxConfigInstalled = true;
|
|
286
|
-
const
|
|
288
|
+
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
|
289
|
+
const repaired = await (0, opsxDelivery_1.repairOpsxDeliveryScripts)(pkgRoot);
|
|
287
290
|
if (repaired) {
|
|
288
291
|
plannedActions.push("repaired package.json opsx:delivery-pipeline scripts (poquan → opsx)");
|
|
289
292
|
}
|
|
@@ -307,7 +310,7 @@ async function setupProject(options) {
|
|
|
307
310
|
...plannedActions.map((x) => `- ${x}`),
|
|
308
311
|
"",
|
|
309
312
|
].join("\n");
|
|
310
|
-
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(
|
|
313
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(installRoot, ".sdd-flow-kit", "setup-summary.md"), summary);
|
|
311
314
|
return {
|
|
312
315
|
agent: selectedAgent,
|
|
313
316
|
projectKind: selectedKind,
|