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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toRunOptions = exports.parseInvokeText = exports.runDoctor = exports.setupProject = exports.runFlow = exports.startFlowScaffold = void 0;
3
+ exports.loadRunSession = exports.resolveRunDir = exports.runDeliver = exports.runPropose = exports.runPhaseAdvance = exports.printGateResult = exports.runGate = exports.toRunOptions = exports.parseInvokeText = exports.runDoctor = exports.setupProject = exports.runFlow = exports.startFlowScaffold = void 0;
4
4
  var startFlow_1 = require("./steps/startFlow");
5
5
  Object.defineProperty(exports, "startFlowScaffold", { enumerable: true, get: function () { return startFlow_1.startFlowScaffold; } });
6
6
  var runFlow_1 = require("./steps/runFlow");
@@ -12,3 +12,15 @@ Object.defineProperty(exports, "runDoctor", { enumerable: true, get: function ()
12
12
  var invokeFlow_1 = require("./steps/invokeFlow");
13
13
  Object.defineProperty(exports, "parseInvokeText", { enumerable: true, get: function () { return invokeFlow_1.parseInvokeText; } });
14
14
  Object.defineProperty(exports, "toRunOptions", { enumerable: true, get: function () { return invokeFlow_1.toRunOptions; } });
15
+ var runGate_1 = require("./steps/runGate");
16
+ Object.defineProperty(exports, "runGate", { enumerable: true, get: function () { return runGate_1.runGate; } });
17
+ Object.defineProperty(exports, "printGateResult", { enumerable: true, get: function () { return runGate_1.printGateResult; } });
18
+ var runPhaseAdvance_1 = require("./steps/runPhaseAdvance");
19
+ Object.defineProperty(exports, "runPhaseAdvance", { enumerable: true, get: function () { return runPhaseAdvance_1.runPhaseAdvance; } });
20
+ var runPropose_1 = require("./steps/runPropose");
21
+ Object.defineProperty(exports, "runPropose", { enumerable: true, get: function () { return runPropose_1.runPropose; } });
22
+ var runDeliver_1 = require("./steps/runDeliver");
23
+ Object.defineProperty(exports, "runDeliver", { enumerable: true, get: function () { return runDeliver_1.runDeliver; } });
24
+ var resolveRun_1 = require("./steps/resolveRun");
25
+ Object.defineProperty(exports, "resolveRunDir", { enumerable: true, get: function () { return resolveRun_1.resolveRunDir; } });
26
+ Object.defineProperty(exports, "loadRunSession", { enumerable: true, get: function () { return resolveRun_1.loadRunSession; } });
@@ -0,0 +1,61 @@
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.resolveRunDir = resolveRunDir;
7
+ exports.loadRunSession = loadRunSession;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const sessionState_1 = require("../core/sessionState");
11
+ const fs_1 = require("../core/fs");
12
+ /**
13
+ * 解析 openspec/PRD 目录:优先 --run-id,否则取最近修改的 run 目录。
14
+ */
15
+ async function resolveRunDir(projectRoot, runId) {
16
+ const candidates = [
17
+ path_1.default.join(projectRoot, "openspec", "PRD"),
18
+ path_1.default.join(projectRoot, "frontend", "openspec", "PRD"),
19
+ ];
20
+ for (const prdRoot of candidates) {
21
+ try {
22
+ const entries = await promises_1.default.readdir(prdRoot, { withFileTypes: true });
23
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
24
+ if (dirs.length === 0)
25
+ continue;
26
+ if (runId) {
27
+ if (!dirs.includes(runId))
28
+ continue;
29
+ const outputRoot = path_1.default.join(prdRoot, runId);
30
+ return { runId, outputRoot, prdRoot };
31
+ }
32
+ let latest = null;
33
+ for (const name of dirs) {
34
+ const full = path_1.default.join(prdRoot, name);
35
+ const st = await promises_1.default.stat(full);
36
+ if (!latest || st.mtimeMs > latest.mtime) {
37
+ latest = { name, mtime: st.mtimeMs };
38
+ }
39
+ }
40
+ if (!latest)
41
+ continue;
42
+ const outputRoot = path_1.default.join(prdRoot, latest.name);
43
+ return { runId: latest.name, outputRoot, prdRoot };
44
+ }
45
+ catch {
46
+ // 目录不存在,尝试下一个候选
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ async function loadRunSession(projectRoot, runId) {
52
+ const resolved = await resolveRunDir(projectRoot, runId);
53
+ if (!resolved)
54
+ return null;
55
+ const raw = await (0, fs_1.readFileIfExists)(path_1.default.join(resolved.outputRoot, ".session-state.json"));
56
+ if (!raw)
57
+ return { resolved, session: null };
58
+ const parsed = JSON.parse(raw);
59
+ const session = (0, sessionState_1.normalizeSession)(parsed, resolved.outputRoot);
60
+ return { resolved, session };
61
+ }
@@ -0,0 +1,82 @@
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.runDeliver = runDeliver;
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 opsxDelivery_1 = require("../core/opsxDelivery");
12
+ const fs_1 = require("../core/fs");
13
+ async function resolvePackageRoot(projectRoot) {
14
+ const fe = path_1.default.join(projectRoot, "frontend", "package.json");
15
+ if (await (0, opsxDelivery_1.pathExists)(fe)) {
16
+ return path_1.default.join(projectRoot, "frontend");
17
+ }
18
+ return projectRoot;
19
+ }
20
+ /**
21
+ * 实现阶段交付:先 gate impl-allowed,再 ensure-opsx,再跑 opsx:delivery-pipeline,写入 .delivery-pass.json。
22
+ */
23
+ async function runDeliver(options) {
24
+ var _a;
25
+ const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
26
+ if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
27
+ return { ok: false, detail: "未找到 PRD run" };
28
+ }
29
+ const implGate = await (0, runGate_1.runGate)({
30
+ projectRoot: options.projectRoot,
31
+ runId: loaded.resolved.runId,
32
+ expect: "impl-allowed",
33
+ change: options.change,
34
+ });
35
+ if (!implGate.ok) {
36
+ (0, runGate_1.printGateResult)(implGate);
37
+ return { ok: false, detail: "impl-allowed gate 未通过" };
38
+ }
39
+ const pkgRoot = await resolvePackageRoot(options.projectRoot);
40
+ const agent = options.agent || "cursor";
41
+ if (options.dryRun) {
42
+ return { ok: true, detail: `dry-run: would deliver in ${pkgRoot}` };
43
+ }
44
+ const ensured = await (0, opsxDelivery_1.ensureOpsxWorkflow)({
45
+ projectRoot: pkgRoot,
46
+ agent,
47
+ dryRun: false,
48
+ });
49
+ if (!ensured.ready) {
50
+ return { ok: false, detail: "ensure-opsx 未就绪" };
51
+ }
52
+ let exitCode = 0;
53
+ if (!options.skipPipeline) {
54
+ const ret = (0, child_process_1.spawnSync)("pnpm", ["run", "opsx:delivery-pipeline", "--", options.change], {
55
+ cwd: pkgRoot,
56
+ stdio: "inherit",
57
+ shell: process.platform === "win32",
58
+ });
59
+ exitCode = (_a = ret.status) !== null && _a !== void 0 ? _a : 1;
60
+ }
61
+ const passPayload = {
62
+ change: options.change,
63
+ exitCode,
64
+ at: new Date().toISOString(),
65
+ projectRoot: pkgRoot,
66
+ };
67
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, ".delivery-pass.json"), `${JSON.stringify(passPayload, null, 2)}\n`);
68
+ if (exitCode !== 0) {
69
+ return { ok: false, detail: `delivery-pipeline 退出码 ${exitCode}` };
70
+ }
71
+ const shipped = await (0, runGate_1.runGate)({
72
+ projectRoot: options.projectRoot,
73
+ runId: loaded.resolved.runId,
74
+ expect: "shipped",
75
+ change: options.change,
76
+ });
77
+ (0, runGate_1.printGateResult)(shipped);
78
+ if (!shipped.ok) {
79
+ return { ok: false, detail: "pipeline 已跑但 shipped gate 未通过(检查 tasks.md)" };
80
+ }
81
+ return { ok: true, detail: "交付完成" };
82
+ }
@@ -0,0 +1,220 @@
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 openspecGate_1 = require("../core/openspecGate");
12
+ const resolveRun_1 = require("./resolveRun");
13
+ const fs_1 = require("../core/fs");
14
+ function fail(checks, expect, runId) {
15
+ return { ok: false, expect, runId, checks };
16
+ }
17
+ function pass(checks, expect, runId) {
18
+ return { ok: checks.every((c) => c.ok), expect, runId, checks };
19
+ }
20
+ async function readActiveChange(projectRoot, session, override) {
21
+ if (override)
22
+ return override;
23
+ if (session === null || session === void 0 ? void 0 : session.activeChange)
24
+ return session.activeChange;
25
+ const rootCandidates = [projectRoot, path_1.default.join(projectRoot, "frontend")];
26
+ for (const root of rootCandidates) {
27
+ const text = await (0, fs_1.readFileIfExists)(path_1.default.join(root, ".opsx-active-change"));
28
+ if (text) {
29
+ const line = text
30
+ .split("\n")
31
+ .map((l) => l.trim())
32
+ .find(Boolean);
33
+ if (line)
34
+ return line;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function resolveImplRoots(projectRoot) {
40
+ const roots = [path_1.default.resolve(projectRoot)];
41
+ const fe = path_1.default.join(projectRoot, "frontend");
42
+ if (fe !== roots[0])
43
+ roots.push(path_1.default.resolve(fe));
44
+ return roots;
45
+ }
46
+ /**
47
+ * 脚本门禁:失败时 process.exitCode=1,成功 exit 0。
48
+ */
49
+ async function runGate(options) {
50
+ const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
51
+ const checks = [];
52
+ if (!loaded || !loaded.resolved) {
53
+ checks.push({ name: "run-resolved", ok: false, detail: "未找到 openspec/PRD/<runId> 目录" });
54
+ return fail(checks, options.expect, null);
55
+ }
56
+ const { resolved, session } = loaded;
57
+ const outputRoot = resolved.outputRoot;
58
+ const runId = resolved.runId;
59
+ const implRoots = resolveImplRoots(options.projectRoot);
60
+ if (options.expect === "prd-fetched") {
61
+ const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
62
+ checks.push({ name: "prd-fetched", ...prd });
63
+ return pass(checks, options.expect, runId);
64
+ }
65
+ if (options.expect === "questions-open") {
66
+ const closed = await (0, prdArtifacts_1.isQuestionsClosed)(outputRoot);
67
+ checks.push({
68
+ name: "questions-open",
69
+ ok: !closed.ok,
70
+ detail: closed.ok ? "矛盾点已闭合,不应处于提问模式" : "提问模式有效(未闭合)",
71
+ });
72
+ return pass(checks, options.expect, runId);
73
+ }
74
+ if (options.expect === "docs-closed") {
75
+ const prd = await (0, prdArtifacts_1.isPrdFetched)(outputRoot);
76
+ checks.push({ name: "prd-fetched", ...prd });
77
+ const closed = await (0, prdArtifacts_1.isQuestionsClosed)(outputRoot);
78
+ checks.push({ name: "questions-closed", ...closed });
79
+ const docs = await (0, prdArtifacts_1.areAnalysisDocsReady)(outputRoot);
80
+ checks.push({ name: "analysis-docs", ...docs });
81
+ if (session && session.docModeOnly === false) {
82
+ checks.push({
83
+ name: "doc-mode",
84
+ ok: false,
85
+ detail: "session.docModeOnly=false,请先处于文档阶段(勿提前 phase advance --to impl)",
86
+ });
87
+ }
88
+ else if (session) {
89
+ checks.push({ name: "doc-mode", ok: true, detail: "docModeOnly=true" });
90
+ }
91
+ else {
92
+ checks.push({ name: "doc-mode", ok: true, detail: "无 session,跳过 docModeOnly 校验" });
93
+ }
94
+ for (const root of implRoots) {
95
+ const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(root);
96
+ const diff = (0, gitDiffGuard_1.gitDiffNameOnly)(gitRoot);
97
+ if (!diff.isRepo) {
98
+ checks.push({ name: "git-impl-diff", ok: true, detail: "非 git 仓库,跳过 src 改动检测" });
99
+ continue;
100
+ }
101
+ const impl = (0, gitDiffGuard_1.findImplChanges)(diff.changedFiles);
102
+ checks.push({
103
+ name: "git-impl-diff",
104
+ ok: impl.length === 0,
105
+ detail: impl.length === 0
106
+ ? "工作区无业务实现路径改动"
107
+ : `禁止在文档阶段修改实现代码: ${impl.slice(0, 8).join(", ")}${impl.length > 8 ? "..." : ""}`,
108
+ });
109
+ }
110
+ return pass(checks, options.expect, runId);
111
+ }
112
+ if (options.expect === "propose-ready") {
113
+ const changeName = await readActiveChange(options.projectRoot, session, options.change);
114
+ if (!changeName) {
115
+ checks.push({ name: "active-change", ok: false, detail: "未指定 change(--change 或 .opsx-active-change)" });
116
+ return fail(checks, options.expect, runId);
117
+ }
118
+ const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
119
+ let found = false;
120
+ for (const root of roots) {
121
+ if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
122
+ found = true;
123
+ const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
124
+ checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
125
+ break;
126
+ }
127
+ }
128
+ if (!found) {
129
+ checks.push({
130
+ name: "change-dir",
131
+ ok: false,
132
+ detail: `openspec/changes/${changeName} 不存在`,
133
+ });
134
+ }
135
+ return pass(checks, options.expect, runId);
136
+ }
137
+ if (options.expect === "impl-allowed") {
138
+ if (!session) {
139
+ checks.push({ name: "session", ok: false, detail: "缺少 .session-state.json" });
140
+ return fail(checks, options.expect, runId);
141
+ }
142
+ checks.push({
143
+ name: "doc-mode-off",
144
+ ok: session.docModeOnly === false,
145
+ detail: session.docModeOnly === false
146
+ ? "已进入实现模式 docModeOnly=false"
147
+ : "仍处文档模式,请执行: sdd-flow-kit phase advance --to impl",
148
+ });
149
+ checks.push({
150
+ name: "phase-d",
151
+ ok: session.phase === "D" || session.status === "apply_in_progress",
152
+ detail: `当前 phase=${session.phase}, status=${session.status}`,
153
+ });
154
+ const changeName = await readActiveChange(options.projectRoot, session, options.change);
155
+ if (!changeName) {
156
+ checks.push({ name: "active-change", ok: false, detail: "未绑定 activeChange" });
157
+ return fail(checks, options.expect, runId);
158
+ }
159
+ const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
160
+ for (const root of roots) {
161
+ if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
162
+ const st = (0, openspecGate_1.checkOpenspecApplyReady)(root, changeName);
163
+ checks.push({ name: "openspec-apply-ready", ok: st.applyReady, detail: st.detail });
164
+ break;
165
+ }
166
+ }
167
+ return pass(checks, options.expect, runId);
168
+ }
169
+ if (options.expect === "shipped") {
170
+ const changeName = await readActiveChange(options.projectRoot, session, options.change);
171
+ if (!changeName) {
172
+ checks.push({ name: "active-change", ok: false, detail: "未指定 change" });
173
+ return fail(checks, options.expect, runId);
174
+ }
175
+ const roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
176
+ for (const root of roots) {
177
+ if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
178
+ const tasks = await (0, openspecGate_1.checkTasksAllDone)(root, changeName);
179
+ checks.push({ name: "tasks-done", ...tasks });
180
+ const marker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".delivery-pass.json"));
181
+ if (marker) {
182
+ try {
183
+ const j = JSON.parse(marker);
184
+ const ok = j.exitCode === 0 && (!j.change || j.change === changeName);
185
+ checks.push({
186
+ name: "delivery-pass",
187
+ ok,
188
+ detail: ok ? "delivery-pass.json 有效" : "delivery-pass.json 与 change 不匹配或失败",
189
+ });
190
+ }
191
+ catch {
192
+ checks.push({ name: "delivery-pass", ok: false, detail: "delivery-pass.json 解析失败" });
193
+ }
194
+ }
195
+ else {
196
+ checks.push({
197
+ name: "delivery-pass",
198
+ ok: false,
199
+ detail: "缺少 .delivery-pass.json,请先执行: sdd-flow-kit deliver --change <name>",
200
+ });
201
+ }
202
+ break;
203
+ }
204
+ }
205
+ return pass(checks, options.expect, runId);
206
+ }
207
+ checks.push({ name: "expect", ok: false, detail: `未知 --expect: ${options.expect}` });
208
+ return fail(checks, options.expect, runId);
209
+ }
210
+ function printGateResult(result) {
211
+ var _a;
212
+ // eslint-disable-next-line no-console
213
+ console.log(`\n[sdd-flow-kit gate] expect=${result.expect} runId=${(_a = result.runId) !== null && _a !== void 0 ? _a : "?"}`);
214
+ for (const c of result.checks) {
215
+ // eslint-disable-next-line no-console
216
+ console.log(` [${c.ok ? "OK" : "FAIL"}] ${c.name}: ${c.detail}`);
217
+ }
218
+ // eslint-disable-next-line no-console
219
+ console.log(result.ok ? "\n✅ GATE PASS\n" : "\n❌ GATE FAIL\n");
220
+ }
@@ -0,0 +1,195 @@
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 sessionState_1 = require("../core/sessionState");
11
+ const fs_1 = require("../core/fs");
12
+ const gitDiffGuard_1 = require("../core/gitDiffGuard");
13
+ async function writeNextMd(outputRoot, lines) {
14
+ const body = ["# NEXT(脚本门禁 — 请按顺序执行)", "", ...lines, ""].join("\n");
15
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(outputRoot, "NEXT.md"), body);
16
+ }
17
+ /**
18
+ * 阶段推进:先跑 gate,通过后更新 .session-state.json 与 NEXT.md。
19
+ */
20
+ async function runPhaseAdvance(options) {
21
+ const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
22
+ if (!loaded || !loaded.resolved || !loaded.session) {
23
+ return { ok: false, runId: null, message: "未找到 run 或 .session-state.json" };
24
+ }
25
+ const { resolved, session } = loaded;
26
+ const outputRoot = resolved.outputRoot;
27
+ const runId = resolved.runId;
28
+ let gate = await (0, runGate_1.runGate)({
29
+ projectRoot: options.projectRoot,
30
+ runId,
31
+ expect: "prd-fetched",
32
+ change: options.change,
33
+ });
34
+ if (options.to === "after-prd") {
35
+ gate = await (0, runGate_1.runGate)({ projectRoot: options.projectRoot, runId, expect: "prd-fetched" });
36
+ if (!gate.ok) {
37
+ (0, runGate_1.printGateResult)(gate);
38
+ return { ok: false, runId, message: "gate prd-fetched 未通过" };
39
+ }
40
+ const next = {
41
+ ...session,
42
+ phase: "A",
43
+ status: "awaiting_user_confirmation",
44
+ docModeOnly: true,
45
+ };
46
+ if (!options.dryRun) {
47
+ await (0, sessionState_1.writeSession)(outputRoot, next);
48
+ await writeNextMd(outputRoot, [
49
+ "## 当前:阶段 A(需求分析)",
50
+ "1. 执行 `02-需求分析提示词.md`",
51
+ "2. 若未闭合:仅更新 `03-待确认问题清单.md`,然后执行:",
52
+ "```bash",
53
+ `npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect questions-open`,
54
+ "```",
55
+ "3. 用户确认后生成 01/02/04,再执行:",
56
+ "```bash",
57
+ `npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to docs-done`,
58
+ "```",
59
+ ]);
60
+ }
61
+ return { ok: true, runId, message: "已进入阶段 A(PRD 已就绪)" };
62
+ }
63
+ if (options.to === "docs-done") {
64
+ gate = await (0, runGate_1.runGate)({ projectRoot: options.projectRoot, runId, expect: "docs-closed" });
65
+ if (!gate.ok) {
66
+ (0, runGate_1.printGateResult)(gate);
67
+ return { ok: false, runId, message: "gate docs-closed 未通过(禁止带 src 改动)" };
68
+ }
69
+ const next = {
70
+ ...session,
71
+ phase: "B",
72
+ status: "ready_for_propose",
73
+ docModeOnly: true,
74
+ };
75
+ if (!options.dryRun) {
76
+ await (0, sessionState_1.writeSession)(outputRoot, next);
77
+ await writeNextMd(outputRoot, [
78
+ "## 当前:阶段 B 完成 → 进入 C(提案)",
79
+ "1. 执行 openspec propose(或):",
80
+ "```bash",
81
+ `npx sdd-flow-kit propose --project-root ${options.projectRoot} --run-id ${runId} --change <kebab-name>`,
82
+ "```",
83
+ "2. 提案 apply-ready 后:",
84
+ "```bash",
85
+ `npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to propose-done --change <name>`,
86
+ "```",
87
+ ]);
88
+ }
89
+ return { ok: true, runId, message: "阶段 B 完成,可进入 propose" };
90
+ }
91
+ if (options.to === "propose-done") {
92
+ const changeName = options.change || session.activeChange;
93
+ if (!changeName) {
94
+ return { ok: false, runId, message: "需要 --change <name>" };
95
+ }
96
+ gate = await (0, runGate_1.runGate)({
97
+ projectRoot: options.projectRoot,
98
+ runId,
99
+ expect: "propose-ready",
100
+ change: changeName,
101
+ });
102
+ if (!gate.ok) {
103
+ (0, runGate_1.printGateResult)(gate);
104
+ return { ok: false, runId, message: "gate propose-ready 未通过" };
105
+ }
106
+ const next = {
107
+ ...session,
108
+ phase: "C",
109
+ status: "propose_done",
110
+ docModeOnly: true,
111
+ activeChange: changeName,
112
+ };
113
+ if (!options.dryRun) {
114
+ await (0, sessionState_1.writeSession)(outputRoot, next);
115
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(options.projectRoot, ".opsx-active-change"), `${changeName}\n`);
116
+ const fe = path_1.default.join(options.projectRoot, "frontend", ".opsx-active-change");
117
+ await (0, fs_1.writeTextFileEnsuredDir)(fe, `${changeName}\n`).catch(() => undefined);
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 gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(path_1.default.join(options.projectRoot, "frontend") === path_1.default.join(options.projectRoot)
143
+ ? options.projectRoot
144
+ : path_1.default.join(options.projectRoot, "frontend"));
145
+ const head = (0, gitDiffGuard_1.gitDiffNameOnly)(gitRoot).head;
146
+ const next = {
147
+ ...session,
148
+ phase: "D",
149
+ status: "apply_in_progress",
150
+ docModeOnly: false,
151
+ activeChange: changeName,
152
+ implGitBaseline: head,
153
+ };
154
+ if (!options.dryRun) {
155
+ await (0, sessionState_1.writeSession)(outputRoot, next);
156
+ await writeNextMd(outputRoot, [
157
+ "## 当前:阶段 D(允许修改 src/)",
158
+ "实现前门禁:",
159
+ "```bash",
160
+ `npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect impl-allowed --change ${changeName}`,
161
+ "```",
162
+ "完成后交付:",
163
+ "```bash",
164
+ `npx sdd-flow-kit deliver --project-root ${options.projectRoot} --run-id ${runId} --change ${changeName}`,
165
+ "```",
166
+ ]);
167
+ }
168
+ return { ok: true, runId, message: "已进入实现阶段(docModeOnly=false)" };
169
+ }
170
+ if (options.to === "shipped") {
171
+ const changeName = options.change || session.activeChange;
172
+ gate = await (0, runGate_1.runGate)({
173
+ projectRoot: options.projectRoot,
174
+ runId,
175
+ expect: "shipped",
176
+ change: changeName,
177
+ });
178
+ if (!gate.ok) {
179
+ (0, runGate_1.printGateResult)(gate);
180
+ return { ok: false, runId, message: "gate shipped 未通过" };
181
+ }
182
+ const next = {
183
+ ...session,
184
+ phase: "done",
185
+ status: "complete",
186
+ docModeOnly: false,
187
+ };
188
+ if (!options.dryRun) {
189
+ await (0, sessionState_1.writeSession)(outputRoot, next);
190
+ await writeNextMd(outputRoot, ["## ✅ SDD 流程已完成(delivery-pass)"]);
191
+ }
192
+ return { ok: true, runId, message: "流程完成" };
193
+ }
194
+ return { ok: false, runId, message: `未知 --to: ${options.to}` };
195
+ }
@@ -0,0 +1,82 @@
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 opsxDelivery_1 = require("../core/opsxDelivery");
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 roots = [options.projectRoot, path_1.default.join(options.projectRoot, "frontend")];
31
+ let cwd = options.projectRoot;
32
+ for (const r of roots) {
33
+ if (await (0, opsxDelivery_1.pathExists)(path_1.default.join(r, "openspec"))) {
34
+ cwd = r;
35
+ break;
36
+ }
37
+ }
38
+ if (options.dryRun) {
39
+ return { ok: true, detail: `dry-run: would openspec new change ${options.change}` };
40
+ }
41
+ const exists = (0, child_process_1.spawnSync)("openspec", ["status", "--change", options.change, "--json"], {
42
+ cwd,
43
+ encoding: "utf8",
44
+ });
45
+ if (exists.status !== 0) {
46
+ const created = (0, child_process_1.spawnSync)("openspec", ["new", "change", options.change], {
47
+ cwd,
48
+ stdio: "inherit",
49
+ });
50
+ if (created.status !== 0) {
51
+ return { ok: false, detail: `openspec new change 失败: ${options.change}` };
52
+ }
53
+ }
54
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(cwd, ".opsx-active-change"), `${options.change}\n`);
55
+ const proposeNote = [
56
+ "# Propose 脚本检查点",
57
+ "",
58
+ `change: \`${options.change}\``,
59
+ "",
60
+ "AI 须按 openspec-propose skill 生成 proposal/design/specs/tasks,完成后执行:",
61
+ "```bash",
62
+ `npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --expect propose-ready --change ${options.change}`,
63
+ `npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --to propose-done --change ${options.change}`,
64
+ "```",
65
+ "",
66
+ ].join("\n");
67
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, "05-openspec-提案草稿.md"), proposeNote);
68
+ const ready = await (0, runGate_1.runGate)({
69
+ projectRoot: options.projectRoot,
70
+ runId: loaded.resolved.runId,
71
+ expect: "propose-ready",
72
+ change: options.change,
73
+ });
74
+ (0, runGate_1.printGateResult)(ready);
75
+ if (!ready.ok) {
76
+ return {
77
+ ok: false,
78
+ detail: "change 已创建,但尚未 apply-ready;请生成 artifacts 后重跑 gate propose-ready",
79
+ };
80
+ }
81
+ return { ok: true, detail: `change ${options.change} apply-ready` };
82
+ }