sdd-flow-kit 1.2.3 → 1.3.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 +71 -45
- package/dist/adapters/index.js +6 -6
- package/dist/cli.js +45 -9
- package/dist/core/acMatrix.js +116 -0
- package/dist/core/bootstrap.js +14 -6
- package/dist/core/inferProject.js +9 -2
- package/dist/core/openspecGate.js +45 -0
- package/dist/core/prdArtifacts.js +26 -0
- package/dist/core/projectRoots.js +79 -2
- package/dist/core/sessionState.js +2 -0
- package/dist/core/writeNext.js +19 -2
- package/dist/postinstall.js +22 -8
- package/dist/steps/runDeliver.js +18 -6
- package/dist/steps/runGate.js +97 -0
- package/dist/steps/runPhaseAdvance.js +53 -6
- package/dist/steps/runPropose.js +3 -1
- package/dist/steps/runValidate.js +204 -0
- package/dist/steps/setupProject.js +65 -2
- package/dist/steps/startFlow.js +14 -11
- package/package.json +1 -1
- package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +4 -2
- package/src/templates/artifacts/01-/351/234/200/346/261/202/345/210/206/346/236/220/346/212/245/345/221/212.template.md +6 -1
- package/src/templates/artifacts/04-/346/212/200/346/234/257/346/226/207/346/241/243.template.md +272 -11
- package/src/templates/artifacts/05-/351/252/214/346/224/266/346/270/205/345/215/225.template.md +38 -0
- package/src/templates/artifacts/06-agent-skill.template.md +23 -9
- package/src/templates/artifacts/{05-openspec- → 06-openspec-}/346/217/220/346/241/210/350/215/211/347/250/277.template.md +16 -4
- package/src/templates/artifacts/{06-opsx-auto-chain.template.md → 07-opsx-auto-chain.template.md} +23 -9
- package/src/templates/prompts/02-sdd-loop-prompt.md +26 -9
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +27 -8
- package/src/templates/skills/confluence-doc.py.template +65 -11
- package/src/templates/skills/doc-skill.SKILL.md.template +2 -0
- package/src/templates/skills/layered-testing-strategy.md +35 -0
|
@@ -7,9 +7,12 @@ exports.resolveInstallRoot = resolveInstallRoot;
|
|
|
7
7
|
exports.isLegacyMonorepoMode = isLegacyMonorepoMode;
|
|
8
8
|
exports.resolveOpenspecRoot = resolveOpenspecRoot;
|
|
9
9
|
exports.resolvePackageRoot = resolvePackageRoot;
|
|
10
|
+
exports.isSddKitSkillInstalled = isSddKitSkillInstalled;
|
|
11
|
+
exports.resolvePostinstallProjectRoot = resolvePostinstallProjectRoot;
|
|
10
12
|
exports.listOpenspecPrdRoots = listOpenspecPrdRoots;
|
|
11
13
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const process_1 = __importDefault(require("process"));
|
|
13
16
|
async function pathExists(filePath) {
|
|
14
17
|
try {
|
|
15
18
|
await promises_1.default.access(filePath);
|
|
@@ -26,14 +29,14 @@ async function pathExists(filePath) {
|
|
|
26
29
|
*/
|
|
27
30
|
function resolveInstallRoot(projectRoot) {
|
|
28
31
|
var _a;
|
|
29
|
-
const fromEnv = (_a =
|
|
32
|
+
const fromEnv = (_a = process_1.default.env.SDD_FLOW_KIT_INSTALL_ROOT) === null || _a === void 0 ? void 0 : _a.trim();
|
|
30
33
|
if (fromEnv)
|
|
31
34
|
return path_1.default.resolve(fromEnv);
|
|
32
35
|
return path_1.default.resolve(projectRoot);
|
|
33
36
|
}
|
|
34
37
|
/** 是否启用旧版 monorepo 行为(在仓库根执行时自动落到 frontend/) */
|
|
35
38
|
function isLegacyMonorepoMode() {
|
|
36
|
-
return
|
|
39
|
+
return process_1.default.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "1" || process_1.default.env.SDD_FLOW_KIT_LEGACY_MONOREPO === "true";
|
|
37
40
|
}
|
|
38
41
|
/**
|
|
39
42
|
* 从 installRoot 向上查找含 openspec/ 的目录(用于解析 PRD run、openspec CLI)。
|
|
@@ -74,6 +77,80 @@ async function resolvePackageRoot(installRoot) {
|
|
|
74
77
|
/**
|
|
75
78
|
* 收集自 installRoot 向上所有存在的 openspec/PRD 目录(用于 resolveRun)。
|
|
76
79
|
*/
|
|
80
|
+
/** SDD 主 skill 是否已写入(任一 agent 目录有 SKILL.md 即视为已安装) */
|
|
81
|
+
async function isSddKitSkillInstalled(installRoot) {
|
|
82
|
+
const root = resolveInstallRoot(installRoot);
|
|
83
|
+
const candidates = [
|
|
84
|
+
path_1.default.join(root, ".cursor", "skills", "sdd-flow-kit", "SKILL.md"),
|
|
85
|
+
path_1.default.join(root, ".claude", "skills", "sdd-flow-kit", "SKILL.md"),
|
|
86
|
+
path_1.default.join(root, ".codex", "skills", "sdd-flow-kit", "SKILL.md"),
|
|
87
|
+
path_1.default.join(root, ".openclaw", "skills", "sdd-flow-kit", "SKILL.md"),
|
|
88
|
+
];
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (await pathExists(candidate))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function packageJsonListsSddFlowKit(pkgPath, text) {
|
|
96
|
+
try {
|
|
97
|
+
const pkg = JSON.parse(text);
|
|
98
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
99
|
+
return Boolean(deps["sdd-flow-kit"]);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 解析 postinstall 应写入的目标项目根。
|
|
107
|
+
* 优先级:INIT_CWD → 从 node_modules/sdd-flow-kit 向上找消费方 package.json → process.cwd()。
|
|
108
|
+
*/
|
|
109
|
+
async function resolvePostinstallProjectRoot(pkgDir) {
|
|
110
|
+
var _a;
|
|
111
|
+
const resolvedPkgDir = path_1.default.resolve(pkgDir);
|
|
112
|
+
const initCwd = (_a = process_1.default.env.INIT_CWD) === null || _a === void 0 ? void 0 : _a.trim();
|
|
113
|
+
if (initCwd) {
|
|
114
|
+
const root = resolveInstallRoot(initCwd);
|
|
115
|
+
if (root !== resolvedPkgDir) {
|
|
116
|
+
return { root, source: "INIT_CWD" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let cur = path_1.default.dirname(resolvedPkgDir);
|
|
120
|
+
for (let i = 0; i < 12; i += 1) {
|
|
121
|
+
cur = path_1.default.dirname(cur);
|
|
122
|
+
const pkgPath = path_1.default.join(cur, "package.json");
|
|
123
|
+
try {
|
|
124
|
+
const text = await promises_1.default.readFile(pkgPath, "utf8");
|
|
125
|
+
if (packageJsonListsSddFlowKit(pkgPath, text)) {
|
|
126
|
+
const root = resolveInstallRoot(cur);
|
|
127
|
+
if (root !== resolvedPkgDir) {
|
|
128
|
+
return { root, source: "node_modules-ancestor" };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// continue walking up
|
|
134
|
+
}
|
|
135
|
+
const parent = path_1.default.dirname(cur);
|
|
136
|
+
if (parent === cur)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const cwdRoot = resolveInstallRoot(process_1.default.cwd());
|
|
140
|
+
if (cwdRoot !== resolvedPkgDir) {
|
|
141
|
+
try {
|
|
142
|
+
const pkgPath = path_1.default.join(cwdRoot, "package.json");
|
|
143
|
+
const text = await promises_1.default.readFile(pkgPath, "utf8");
|
|
144
|
+
if (packageJsonListsSddFlowKit(pkgPath, text)) {
|
|
145
|
+
return { root: cwdRoot, source: "process.cwd()" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// fall through
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
77
154
|
async function listOpenspecPrdRoots(startDir) {
|
|
78
155
|
const seen = new Set();
|
|
79
156
|
const out = [];
|
|
@@ -32,6 +32,7 @@ function defaultSession(args) {
|
|
|
32
32
|
activeChange: null,
|
|
33
33
|
createdAtISO: new Date().toISOString(),
|
|
34
34
|
implGitBaseline: null,
|
|
35
|
+
validateRetryCount: 0,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
/** 将旧版 session(无 phase 字段)迁移为当前结构 */
|
|
@@ -51,5 +52,6 @@ function normalizeSession(raw, outputRoot) {
|
|
|
51
52
|
activeChange: raw.activeChange ? String(raw.activeChange) : null,
|
|
52
53
|
createdAtISO: String((_e = raw.createdAtISO) !== null && _e !== void 0 ? _e : new Date().toISOString()),
|
|
53
54
|
implGitBaseline: raw.implGitBaseline ? String(raw.implGitBaseline) : null,
|
|
55
|
+
validateRetryCount: typeof raw.validateRetryCount === "number" ? raw.validateRetryCount : 0,
|
|
54
56
|
};
|
|
55
57
|
}
|
package/dist/core/writeNext.js
CHANGED
|
@@ -16,6 +16,15 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
16
16
|
"",
|
|
17
17
|
"> **禁止**跳过下列命令。失败 (exit≠0) 时不得进入下一阶段或修改 `src/`。",
|
|
18
18
|
"",
|
|
19
|
+
"## 文档职责(SSOT)",
|
|
20
|
+
"",
|
|
21
|
+
"| 文档 | 职责 |",
|
|
22
|
+
"|------|------|",
|
|
23
|
+
"| `01` | 业务概述、范围、风险(**非**完整验收) |",
|
|
24
|
+
"| `05` | **唯一验收 SSOT**(AC Matrix) |",
|
|
25
|
+
"| `04` | 技术方案(须引用 AC-ID) |",
|
|
26
|
+
"| `tasks.md` | AC 的可执行映射(`[AC-XX]`) |",
|
|
27
|
+
"",
|
|
19
28
|
"## 1. Step1 — 拉取 PRD",
|
|
20
29
|
"按 `01-获取需求文档指引.md` 执行 confluence 脚本,然后:",
|
|
21
30
|
"```bash",
|
|
@@ -30,7 +39,7 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
30
39
|
"```",
|
|
31
40
|
"",
|
|
32
41
|
"## 3. 用户确认后 — 阶段 B(仅文档)",
|
|
33
|
-
"生成 01
|
|
42
|
+
"顺序:**03 闭合 → 从 `source/PRD.md` 生成 `05-验收清单.md` → `04`(引用 AC-ID)→ `01`/`02`**",
|
|
34
43
|
"```bash",
|
|
35
44
|
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect docs-closed`,
|
|
36
45
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to docs-done`,
|
|
@@ -39,17 +48,25 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
39
48
|
"## 4. 阶段 C — propose",
|
|
40
49
|
"```bash",
|
|
41
50
|
`npx sdd-flow-kit propose --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
51
|
+
"# AI 生成 openspec artifacts 后,tasks.md 每条须含 [AC-XX]",
|
|
52
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect propose-ready --change <kebab-name>`,
|
|
53
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect ac-ready --change <kebab-name>`,
|
|
42
54
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to propose-done --change <kebab-name>`,
|
|
43
55
|
"```",
|
|
44
56
|
"",
|
|
45
57
|
"## 5. 阶段 D — /opsx-apply 实现 tasks(必做)",
|
|
58
|
+
"Apply 前必读:`source/PRD.md` + `05-验收清单.md`",
|
|
46
59
|
"```bash",
|
|
47
60
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to impl --change <kebab-name>`,
|
|
48
61
|
"# 然后在 AI 工具中执行 /opsx-apply,按 tasks.md 逐条实现并勾选为 [x]",
|
|
49
62
|
"```",
|
|
50
63
|
"",
|
|
51
|
-
"## 6. 阶段 E —
|
|
64
|
+
"## 6. 阶段 E — 验收签收与交付",
|
|
52
65
|
"```bash",
|
|
66
|
+
`npx sdd-flow-kit validate --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
67
|
+
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to validate-done --change <kebab-name>`,
|
|
68
|
+
"# 在 05 中将 P0 标为已验收,签收状态改为已签收",
|
|
69
|
+
`npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect ac-signed`,
|
|
53
70
|
`npx sdd-flow-kit deliver --project-root "${pr}" --run-id ${runId} --change <kebab-name>`,
|
|
54
71
|
`npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to shipped --change <kebab-name>`,
|
|
55
72
|
"```",
|
package/dist/postinstall.js
CHANGED
|
@@ -68,26 +68,36 @@ async function inferAgent(projectRoot) {
|
|
|
68
68
|
return "openclaw";
|
|
69
69
|
return "cursor";
|
|
70
70
|
}
|
|
71
|
+
/** postinstall 默认 light;设 SDD_FLOW_KIT_POSTINSTALL_FULL=1 跑全量(含 opsx-workflow) */
|
|
72
|
+
function isPostinstallLightMode() {
|
|
73
|
+
if (process_1.default.env.SDD_FLOW_KIT_POSTINSTALL_FULL === "1")
|
|
74
|
+
return false;
|
|
75
|
+
if (process_1.default.env.SDD_FLOW_KIT_POSTINSTALL_LIGHT === "0")
|
|
76
|
+
return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
71
79
|
async function main() {
|
|
72
|
-
var _a;
|
|
73
80
|
if (process_1.default.env.SDD_FLOW_KIT_SKIP_POSTINSTALL === "1") {
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
|
-
const initCwd = (_a = process_1.default.env.INIT_CWD) === null || _a === void 0 ? void 0 : _a.trim();
|
|
77
83
|
const pkgDir = path_1.default.resolve(__dirname, "..");
|
|
78
|
-
|
|
84
|
+
const resolved = await (0, projectRoots_1.resolvePostinstallProjectRoot)(pkgDir);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
const fallbackKind = await (0, inferProject_1.inferProjectKind)(process_1.default.cwd());
|
|
79
87
|
// eslint-disable-next-line no-console
|
|
80
|
-
console.warn("[sdd-flow-kit] postinstall:
|
|
88
|
+
console.warn("[sdd-flow-kit] postinstall: 无法定位消费方项目根(INIT_CWD / node_modules 向上查找 / cwd 均失败)。" +
|
|
89
|
+
`请执行: npx sdd-flow-kit install --project ${fallbackKind} -y`);
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
83
|
-
const projectRoot =
|
|
84
|
-
if (projectRoot === pkgDir)
|
|
92
|
+
const { root: projectRoot, source } = resolved;
|
|
93
|
+
if (projectRoot === pkgDir) {
|
|
85
94
|
return;
|
|
95
|
+
}
|
|
86
96
|
const projectKind = await (0, inferProject_1.inferProjectKind)(projectRoot);
|
|
87
97
|
const agent = await inferAgent(projectRoot);
|
|
88
|
-
const light =
|
|
98
|
+
const light = isPostinstallLightMode();
|
|
89
99
|
// eslint-disable-next-line no-console
|
|
90
|
-
console.log(`[sdd-flow-kit] postinstall (${light ? "light" : "full"}) at: ${projectRoot}`);
|
|
100
|
+
console.log(`[sdd-flow-kit] postinstall (${light ? "light" : "full"}) at: ${projectRoot} (via ${source})`);
|
|
91
101
|
// eslint-disable-next-line no-console
|
|
92
102
|
console.log(`[sdd-flow-kit] agent=${agent}, project=${projectKind}`);
|
|
93
103
|
const result = await (0, setupProject_1.setupProject)({
|
|
@@ -106,6 +116,10 @@ async function main() {
|
|
|
106
116
|
`${projectKind} -y` +
|
|
107
117
|
(process_1.default.env.SDD_OPSX_WORKFLOW_PACKAGE ? "" : "(或设 SDD_OPSX_WORKFLOW_PACKAGE 指向本地 opsx-workflow)"));
|
|
108
118
|
}
|
|
119
|
+
else if (light) {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.log("[sdd-flow-kit] 轻量安装完成:SDD skill / 规则 / docs-skill / .opsx 已写入");
|
|
122
|
+
}
|
|
109
123
|
else {
|
|
110
124
|
// eslint-disable-next-line no-console
|
|
111
125
|
console.log("[sdd-flow-kit] 安装完成:SDD + OPSX + Superpowers 已就绪");
|
package/dist/steps/runDeliver.js
CHANGED
|
@@ -12,7 +12,7 @@ const opsxDelivery_1 = require("../core/opsxDelivery");
|
|
|
12
12
|
const projectRoots_1 = require("../core/projectRoots");
|
|
13
13
|
const fs_1 = require("../core/fs");
|
|
14
14
|
/**
|
|
15
|
-
* 实现阶段交付:先 gate
|
|
15
|
+
* 实现阶段交付:先 gate validate-pass(确保验收通过),再 ensure-opsx,再跑 opsx:delivery-pipeline,写入 .delivery-pass.json。
|
|
16
16
|
*/
|
|
17
17
|
async function runDeliver(options) {
|
|
18
18
|
var _a;
|
|
@@ -20,15 +20,27 @@ async function runDeliver(options) {
|
|
|
20
20
|
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
21
21
|
return { ok: false, detail: "未找到 PRD run" };
|
|
22
22
|
}
|
|
23
|
-
const
|
|
23
|
+
const validateGate = await (0, runGate_1.runGate)({
|
|
24
24
|
projectRoot: options.projectRoot,
|
|
25
25
|
runId: loaded.resolved.runId,
|
|
26
|
-
expect: "
|
|
26
|
+
expect: "validate-pass",
|
|
27
27
|
change: options.change,
|
|
28
28
|
});
|
|
29
|
-
if (!
|
|
30
|
-
(0, runGate_1.printGateResult)(
|
|
31
|
-
return { ok: false, detail: "
|
|
29
|
+
if (!validateGate.ok) {
|
|
30
|
+
(0, runGate_1.printGateResult)(validateGate);
|
|
31
|
+
return { ok: false, detail: "validate-pass gate 未通过,禁止交付(请先执行 sdd-flow-kit validate --change <name>)" };
|
|
32
|
+
}
|
|
33
|
+
const acSignedGate = await (0, runGate_1.runGate)({
|
|
34
|
+
projectRoot: options.projectRoot,
|
|
35
|
+
runId: loaded.resolved.runId,
|
|
36
|
+
expect: "ac-signed",
|
|
37
|
+
});
|
|
38
|
+
if (!acSignedGate.ok) {
|
|
39
|
+
(0, runGate_1.printGateResult)(acSignedGate);
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
detail: "ac-signed gate 未通过,禁止交付(请在 05-验收清单.md 完成 P0 签收)",
|
|
43
|
+
};
|
|
32
44
|
}
|
|
33
45
|
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
34
46
|
const pkgRoot = await (0, projectRoots_1.resolvePackageRoot)(installRoot);
|
package/dist/steps/runGate.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.printGateResult = printGateResult;
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const prdArtifacts_1 = require("../core/prdArtifacts");
|
|
10
10
|
const gitDiffGuard_1 = require("../core/gitDiffGuard");
|
|
11
|
+
const acMatrix_1 = require("../core/acMatrix");
|
|
11
12
|
const projectRoots_1 = require("../core/projectRoots");
|
|
12
13
|
const openspecGate_1 = require("../core/openspecGate");
|
|
13
14
|
const resolveRun_1 = require("./resolveRun");
|
|
@@ -71,6 +72,7 @@ async function resolveImplRoots(projectRoot) {
|
|
|
71
72
|
* 脚本门禁:失败时 process.exitCode=1,成功 exit 0。
|
|
72
73
|
*/
|
|
73
74
|
async function runGate(options) {
|
|
75
|
+
var _a, _b, _c;
|
|
74
76
|
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
75
77
|
const checks = [];
|
|
76
78
|
if (!loaded || !loaded.resolved) {
|
|
@@ -102,6 +104,8 @@ async function runGate(options) {
|
|
|
102
104
|
checks.push({ name: "questions-closed", ...closed });
|
|
103
105
|
const docs = await (0, prdArtifacts_1.areAnalysisDocsReady)(outputRoot);
|
|
104
106
|
checks.push({ name: "analysis-docs", ...docs });
|
|
107
|
+
const acDefined = await (0, prdArtifacts_1.isAcceptanceChecklistDefined)(outputRoot);
|
|
108
|
+
checks.push({ name: "acceptance-checklist-defined", ...acDefined });
|
|
105
109
|
if (session && session.docModeOnly === false) {
|
|
106
110
|
checks.push({
|
|
107
111
|
name: "doc-mode",
|
|
@@ -133,6 +137,48 @@ async function runGate(options) {
|
|
|
133
137
|
}
|
|
134
138
|
return pass(checks, options.expect, runId);
|
|
135
139
|
}
|
|
140
|
+
if (options.expect === "ac-ready") {
|
|
141
|
+
const acDefined = await (0, prdArtifacts_1.isAcceptanceChecklistDefined)(outputRoot);
|
|
142
|
+
checks.push({ name: "acceptance-checklist-defined", ...acDefined });
|
|
143
|
+
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
144
|
+
if (!changeName) {
|
|
145
|
+
checks.push({ name: "active-change", ok: false, detail: "未指定 change(--change 或 .opsx-active-change)" });
|
|
146
|
+
return fail(checks, options.expect, runId);
|
|
147
|
+
}
|
|
148
|
+
const parsed = await (0, acMatrix_1.parseAcMatrix)(outputRoot);
|
|
149
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
150
|
+
const openspecRoots = [
|
|
151
|
+
installRoot,
|
|
152
|
+
...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
|
|
153
|
+
];
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
let mapped = false;
|
|
156
|
+
for (const root of openspecRoots) {
|
|
157
|
+
const key = path_1.default.resolve(root);
|
|
158
|
+
if (seen.has(key))
|
|
159
|
+
continue;
|
|
160
|
+
seen.add(key);
|
|
161
|
+
if (await (0, openspecGate_1.changeDirExists)(root, changeName)) {
|
|
162
|
+
const map = await (0, openspecGate_1.checkP0AcMappedInChange)(root, changeName, parsed.p0Ids);
|
|
163
|
+
checks.push({ name: "p0-ac-mapped", ...map });
|
|
164
|
+
mapped = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!mapped) {
|
|
169
|
+
checks.push({
|
|
170
|
+
name: "change-dir",
|
|
171
|
+
ok: false,
|
|
172
|
+
detail: `openspec/changes/${changeName} 不存在,无法校验 AC 映射`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return pass(checks, options.expect, runId);
|
|
176
|
+
}
|
|
177
|
+
if (options.expect === "ac-signed") {
|
|
178
|
+
const signed = await (0, prdArtifacts_1.isAcceptanceChecklistSigned)(outputRoot);
|
|
179
|
+
checks.push({ name: "acceptance-checklist-signed", ...signed });
|
|
180
|
+
return pass(checks, options.expect, runId);
|
|
181
|
+
}
|
|
136
182
|
if (options.expect === "propose-ready") {
|
|
137
183
|
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
138
184
|
if (!changeName) {
|
|
@@ -217,6 +263,57 @@ async function runGate(options) {
|
|
|
217
263
|
}
|
|
218
264
|
return pass(checks, options.expect, runId);
|
|
219
265
|
}
|
|
266
|
+
if (options.expect === "validate-pass") {
|
|
267
|
+
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
268
|
+
if (!changeName) {
|
|
269
|
+
checks.push({ name: "active-change", ok: false, detail: "未指定 change" });
|
|
270
|
+
return fail(checks, options.expect, runId);
|
|
271
|
+
}
|
|
272
|
+
// 1) tasks.md 全完成
|
|
273
|
+
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
274
|
+
const openspecRoots = [
|
|
275
|
+
installRoot,
|
|
276
|
+
...(await (0, projectRoots_1.listOpenspecPrdRoots)(installRoot)).map((p) => path_1.default.dirname(path_1.default.dirname(p))),
|
|
277
|
+
];
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
for (const root of openspecRoots) {
|
|
280
|
+
const key = path_1.default.resolve(root);
|
|
281
|
+
if (seen.has(key))
|
|
282
|
+
continue;
|
|
283
|
+
seen.add(key);
|
|
284
|
+
if (await (0, openspecGate_1.changeDirExists)(key, changeName)) {
|
|
285
|
+
const tasks = await (0, openspecGate_1.checkTasksAllDone)(key, changeName);
|
|
286
|
+
checks.push({ name: "tasks-done", ...tasks });
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// 2) 验收报告签收文件
|
|
291
|
+
const marker = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, ".validate-pass.json"));
|
|
292
|
+
if (marker) {
|
|
293
|
+
try {
|
|
294
|
+
const j = JSON.parse(marker);
|
|
295
|
+
const ok = j.verdict === "pass" && (!j.change || j.change === changeName);
|
|
296
|
+
checks.push({
|
|
297
|
+
name: "validate-report",
|
|
298
|
+
ok,
|
|
299
|
+
detail: ok
|
|
300
|
+
? `验收通过 (pipeline=${(_a = j.pipelineShortExitCode) !== null && _a !== void 0 ? _a : "?"}, e2e=${(_b = j.e2eExitCode) !== null && _b !== void 0 ? _b : "?"})`
|
|
301
|
+
: `验收结果: ${(_c = j.verdict) !== null && _c !== void 0 ? _c : "unknown"}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
checks.push({ name: "validate-report", ok: false, detail: ".validate-pass.json 解析失败" });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
checks.push({
|
|
310
|
+
name: "validate-report",
|
|
311
|
+
ok: false,
|
|
312
|
+
detail: "缺少 .validate-pass.json,请先执行: sdd-flow-kit validate --change <name>",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return pass(checks, options.expect, runId);
|
|
316
|
+
}
|
|
220
317
|
if (options.expect === "shipped") {
|
|
221
318
|
const changeName = await readActiveChange(options.projectRoot, session, options.change);
|
|
222
319
|
if (!changeName) {
|
|
@@ -53,7 +53,7 @@ async function runPhaseAdvance(options) {
|
|
|
53
53
|
"```bash",
|
|
54
54
|
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect questions-open`,
|
|
55
55
|
"```",
|
|
56
|
-
"3. 用户确认后生成 01/02
|
|
56
|
+
"3. 用户确认后生成 **05 → 04 → 01/02**,再执行:",
|
|
57
57
|
"```bash",
|
|
58
58
|
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to docs-done`,
|
|
59
59
|
"```",
|
|
@@ -104,6 +104,16 @@ async function runPhaseAdvance(options) {
|
|
|
104
104
|
(0, runGate_1.printGateResult)(gate);
|
|
105
105
|
return { ok: false, runId, message: "gate propose-ready 未通过" };
|
|
106
106
|
}
|
|
107
|
+
gate = await (0, runGate_1.runGate)({
|
|
108
|
+
projectRoot: options.projectRoot,
|
|
109
|
+
runId,
|
|
110
|
+
expect: "ac-ready",
|
|
111
|
+
change: changeName,
|
|
112
|
+
});
|
|
113
|
+
if (!gate.ok) {
|
|
114
|
+
(0, runGate_1.printGateResult)(gate);
|
|
115
|
+
return { ok: false, runId, message: "gate ac-ready 未通过(P0 未映射到 spec/tasks)" };
|
|
116
|
+
}
|
|
107
117
|
const next = {
|
|
108
118
|
...session,
|
|
109
119
|
phase: "C",
|
|
@@ -117,7 +127,7 @@ async function runPhaseAdvance(options) {
|
|
|
117
127
|
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(installRoot, ".opsx-active-change"), `${changeName}\n`);
|
|
118
128
|
await writeNextMd(outputRoot, [
|
|
119
129
|
"## 当前:阶段 C 完成 → 进入 D(实现)",
|
|
120
|
-
"说明:先进入 impl,再在 AI 工具执行 `/opsx-apply
|
|
130
|
+
"说明:先进入 impl,再在 AI 工具执行 `/opsx-apply`;Apply 前必读 `source/PRD.md` + `05-验收清单.md`。",
|
|
121
131
|
"```bash",
|
|
122
132
|
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to impl --change ${changeName}`,
|
|
123
133
|
"```",
|
|
@@ -133,12 +143,12 @@ async function runPhaseAdvance(options) {
|
|
|
133
143
|
gate = await (0, runGate_1.runGate)({
|
|
134
144
|
projectRoot: options.projectRoot,
|
|
135
145
|
runId,
|
|
136
|
-
expect: "
|
|
146
|
+
expect: "ac-ready",
|
|
137
147
|
change: changeName,
|
|
138
148
|
});
|
|
139
149
|
if (!gate.ok) {
|
|
140
150
|
(0, runGate_1.printGateResult)(gate);
|
|
141
|
-
return { ok: false, runId, message: "
|
|
151
|
+
return { ok: false, runId, message: "ac-ready 未通过,禁止进入实现" };
|
|
142
152
|
}
|
|
143
153
|
const installRoot = (0, projectRoots_1.resolveInstallRoot)(options.projectRoot);
|
|
144
154
|
const gitRoot = (0, gitDiffGuard_1.resolveGitRoot)(installRoot);
|
|
@@ -160,14 +170,51 @@ async function runPhaseAdvance(options) {
|
|
|
160
170
|
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect impl-allowed --change ${changeName}`,
|
|
161
171
|
"```",
|
|
162
172
|
"在 AI 工具中执行:`/opsx-apply`(必须先完成 tasks.md 全部 [x])",
|
|
163
|
-
"
|
|
173
|
+
"完成后验收:",
|
|
164
174
|
"```bash",
|
|
165
|
-
`npx sdd-flow-kit
|
|
175
|
+
`npx sdd-flow-kit validate --project-root ${options.projectRoot} --run-id ${runId} --change ${changeName}`,
|
|
176
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to validate-done --change ${changeName}`,
|
|
166
177
|
"```",
|
|
178
|
+
"验收不通过 → 修复后重新执行 validate;超过 3 次需用户确认。",
|
|
167
179
|
]);
|
|
168
180
|
}
|
|
169
181
|
return { ok: true, runId, message: "已进入实现阶段(docModeOnly=false)" };
|
|
170
182
|
}
|
|
183
|
+
if (options.to === "validate-done") {
|
|
184
|
+
const changeName = options.change || session.activeChange;
|
|
185
|
+
if (!changeName) {
|
|
186
|
+
return { ok: false, runId, message: "需要 --change <name>" };
|
|
187
|
+
}
|
|
188
|
+
gate = await (0, runGate_1.runGate)({
|
|
189
|
+
projectRoot: options.projectRoot,
|
|
190
|
+
runId,
|
|
191
|
+
expect: "validate-pass",
|
|
192
|
+
change: changeName,
|
|
193
|
+
});
|
|
194
|
+
if (!gate.ok) {
|
|
195
|
+
(0, runGate_1.printGateResult)(gate);
|
|
196
|
+
return { ok: false, runId, message: "validate-pass gate 未通过" };
|
|
197
|
+
}
|
|
198
|
+
const next = {
|
|
199
|
+
...session,
|
|
200
|
+
phase: "D",
|
|
201
|
+
status: "validate_done",
|
|
202
|
+
docModeOnly: false,
|
|
203
|
+
};
|
|
204
|
+
if (!options.dryRun) {
|
|
205
|
+
await (0, sessionState_1.writeSession)(outputRoot, next);
|
|
206
|
+
await writeNextMd(outputRoot, [
|
|
207
|
+
"## 当前:验收通过 → 可交付",
|
|
208
|
+
"1. 在 `05-验收清单.md` 将 P0 标为「已验收」,签收状态改为「已签收」",
|
|
209
|
+
"```bash",
|
|
210
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${runId} --expect ac-signed`,
|
|
211
|
+
`npx sdd-flow-kit deliver --project-root ${options.projectRoot} --run-id ${runId} --change ${changeName}`,
|
|
212
|
+
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${runId} --to shipped --change ${changeName}`,
|
|
213
|
+
"```",
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
return { ok: true, runId, message: "验收通过,可进入交付" };
|
|
217
|
+
}
|
|
171
218
|
if (options.to === "shipped") {
|
|
172
219
|
const changeName = options.change || session.activeChange;
|
|
173
220
|
gate = await (0, runGate_1.runGate)({
|
package/dist/steps/runPropose.js
CHANGED
|
@@ -52,15 +52,17 @@ async function runPropose(options) {
|
|
|
52
52
|
`change: \`${options.change}\``,
|
|
53
53
|
"",
|
|
54
54
|
"AI 须按 openspec-propose skill 生成 proposal/design/specs/tasks。",
|
|
55
|
+
"tasks.md 每条任务标题须含 `[AC-XX]`,与 `05-验收清单.md` 中 P0 一一映射。",
|
|
55
56
|
"注意:propose 结束后不能直接 deliver,必须先 `/opsx-apply` 执行 tasks。",
|
|
56
57
|
"完成提案后执行:",
|
|
57
58
|
"```bash",
|
|
58
59
|
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --expect propose-ready --change ${options.change}`,
|
|
60
|
+
`npx sdd-flow-kit gate --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --expect ac-ready --change ${options.change}`,
|
|
59
61
|
`npx sdd-flow-kit phase advance --project-root ${options.projectRoot} --run-id ${loaded.resolved.runId} --to propose-done --change ${options.change}`,
|
|
60
62
|
"```",
|
|
61
63
|
"",
|
|
62
64
|
].join("\n");
|
|
63
|
-
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, "
|
|
65
|
+
await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(loaded.resolved.outputRoot, "06-openspec-提案草稿.md"), proposeNote);
|
|
64
66
|
const ready = await (0, runGate_1.runGate)({
|
|
65
67
|
projectRoot: options.projectRoot,
|
|
66
68
|
runId: loaded.resolved.runId,
|