sdd-flow-kit 1.3.13 → 1.3.15

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/cli.js CHANGED
@@ -22,8 +22,10 @@ const runPrdDiff_1 = require("./steps/runPrdDiff");
22
22
  const runPrdRemediate_1 = require("./steps/runPrdRemediate");
23
23
  const runChain_1 = require("./steps/runChain");
24
24
  const runSessionMode_1 = require("./steps/runSessionMode");
25
+ const runGuard_1 = require("./steps/runGuard");
25
26
  const bootstrap_1 = require("./core/bootstrap");
26
27
  const projectRoots_1 = require("./core/projectRoots");
28
+ const versionIntent_1 = require("./core/versionIntent");
27
29
  const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
28
30
  function getArgValue(flag) {
29
31
  const idx = process_1.default.argv.indexOf(flag);
@@ -34,6 +36,42 @@ function getArgValue(flag) {
34
36
  return null;
35
37
  return v;
36
38
  }
39
+ function getFreeTextArgs(startIndex) {
40
+ const flagsWithValue = new Set([
41
+ "--agent",
42
+ "--project",
43
+ "--product",
44
+ "--version",
45
+ "--project-root",
46
+ "--projectRoot",
47
+ "--run-id",
48
+ "--runId",
49
+ "--change",
50
+ "--branch",
51
+ "--from",
52
+ "--to",
53
+ "--expect",
54
+ "--execution-mode",
55
+ "--executionMode",
56
+ "--text",
57
+ ]);
58
+ const parts = [];
59
+ const tokens = process_1.default.argv.slice(startIndex);
60
+ for (let i = 0; i < tokens.length; i += 1) {
61
+ const token = tokens[i];
62
+ if (token === "-y" || token === "--dry-run" || token === "--plan-json" || token === "--strict-commands") {
63
+ continue;
64
+ }
65
+ if (flagsWithValue.has(token)) {
66
+ i += 1;
67
+ continue;
68
+ }
69
+ if (token.startsWith("--"))
70
+ continue;
71
+ parts.push(token);
72
+ }
73
+ return parts.join(" ").trim();
74
+ }
37
75
  function printHelp() {
38
76
  // eslint-disable-next-line no-console
39
77
  console.log([
@@ -51,7 +89,8 @@ function printHelp() {
51
89
  "3) 生成骨架并进入可执行模式(推荐):",
52
90
  " npx sdd-flow-kit run --product ADI --version 2.3.2 --agent claude-code",
53
91
  "",
54
- "4) 一句话触发(可选能力):",
92
+ "4) 一句话触发(版本开发强制入口):",
93
+ " npx sdd-flow-kit guard \"开发 ADI_V2.3.4版本\" -y --agent cursor",
55
94
  " npx sdd-flow-kit invoke \"开发 XXX 1.2.3 需求\"",
56
95
  "",
57
96
  "5) 环境健检:",
@@ -111,7 +150,7 @@ function printHelp() {
111
150
  ].join("\n"));
112
151
  }
113
152
  async function main() {
114
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
153
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
115
154
  const cmd = process_1.default.argv[2];
116
155
  if (!cmd || cmd === "--help" || cmd === "-h") {
117
156
  printHelp();
@@ -144,6 +183,7 @@ async function main() {
144
183
  cmd !== "install" &&
145
184
  cmd !== "doctor" &&
146
185
  cmd !== "ensure-opsx" &&
186
+ cmd !== "guard" &&
147
187
  cmd !== "invoke" &&
148
188
  cmd !== "gate" &&
149
189
  cmd !== "phase" &&
@@ -169,7 +209,11 @@ async function main() {
169
209
  const dryRun = process_1.default.argv.includes("--dry-run");
170
210
  const planJson = process_1.default.argv.includes("--plan-json");
171
211
  const agentValue = (_g = getArgValue("--agent")) !== null && _g !== void 0 ? _g : "claude-code";
172
- const projectKind = ((_h = getArgValue("--project")) !== null && _h !== void 0 ? _h : "OMS");
212
+ const rawGuardText = cmd === "guard"
213
+ ? ((_h = getArgValue("--text")) !== null && _h !== void 0 ? _h : getFreeTextArgs(3)).trim()
214
+ : "";
215
+ const guardIntent = rawGuardText ? (0, versionIntent_1.parseVersionIntent)(rawGuardText) : null;
216
+ const projectKind = ((_k = (_j = getArgValue("--project")) !== null && _j !== void 0 ? _j : guardIntent === null || guardIntent === void 0 ? void 0 : guardIntent.product) !== null && _k !== void 0 ? _k : "OMS");
173
217
  if (!ALLOWED_AGENTS.includes(agentValue)) {
174
218
  // eslint-disable-next-line no-console
175
219
  console.error(`不支持的 --agent: ${agentValue}`);
@@ -189,6 +233,7 @@ async function main() {
189
233
  const bootstrapCommands = new Set([
190
234
  "doctor",
191
235
  "ensure-opsx",
236
+ "guard",
192
237
  "run",
193
238
  "invoke",
194
239
  "install",
@@ -211,6 +256,36 @@ async function main() {
211
256
  projectKind: projectKind,
212
257
  });
213
258
  }
259
+ if (cmd === "guard") {
260
+ const text = rawGuardText;
261
+ if (!text) {
262
+ // eslint-disable-next-line no-console
263
+ console.error("guard 需要自然语言文本,例如:npx sdd-flow-kit guard \"开发 ADI_V2.3.4版本\" -y --agent cursor");
264
+ process_1.default.exitCode = 1;
265
+ return;
266
+ }
267
+ const result = await (0, runGuard_1.runGuard)({
268
+ text,
269
+ projectRoot,
270
+ branch,
271
+ yes,
272
+ dryRun,
273
+ agent,
274
+ });
275
+ // eslint-disable-next-line no-console
276
+ console.log(result.ok ? `✅ ${result.detail}` : `ℹ️ ${result.detail}`);
277
+ if (result.ok && result.nextPath) {
278
+ // eslint-disable-next-line no-console
279
+ console.log(`下一步:读取 ${result.nextPath} 与同目录 .session-state.json`);
280
+ }
281
+ if (planJson) {
282
+ // eslint-disable-next-line no-console
283
+ console.log(JSON.stringify({ command: "guard", projectRoot, dryRun, result }, null, 2));
284
+ }
285
+ if (!result.ok)
286
+ process_1.default.exitCode = 2;
287
+ return;
288
+ }
214
289
  if (cmd === "gate") {
215
290
  const expect = getArgValue("--expect");
216
291
  if (!expect || !GATE_EXPECTS.includes(expect)) {
@@ -376,7 +451,7 @@ async function main() {
376
451
  process_1.default.exitCode = 1;
377
452
  return;
378
453
  }
379
- const from = ((_j = getArgValue("--from")) !== null && _j !== void 0 ? _j : "docs-done");
454
+ const from = ((_l = getArgValue("--from")) !== null && _l !== void 0 ? _l : "docs-done");
380
455
  if (!CHAIN_FROM.includes(from)) {
381
456
  // eslint-disable-next-line no-console
382
457
  console.error(`chain 需要 --from ${CHAIN_FROM.join("|")}`);
@@ -405,7 +480,7 @@ async function main() {
405
480
  process_1.default.exitCode = 1;
406
481
  return;
407
482
  }
408
- const modeArg = (_k = getArgValue("--execution-mode")) !== null && _k !== void 0 ? _k : getArgValue("--executionMode");
483
+ const modeArg = (_m = getArgValue("--execution-mode")) !== null && _m !== void 0 ? _m : getArgValue("--executionMode");
409
484
  const textArg = getArgValue("--text");
410
485
  const strictCommands = process_1.default.argv.includes("--strict-commands");
411
486
  const result = await (0, runSessionMode_1.runSessionMode)({
@@ -474,11 +549,7 @@ async function main() {
474
549
  }
475
550
  if (!product || !version) {
476
551
  if (cmd === "invoke") {
477
- const raw = process_1.default.argv
478
- .slice(3)
479
- .filter((token) => !token.startsWith("--") && token !== "-y")
480
- .join(" ")
481
- .trim();
552
+ const raw = getFreeTextArgs(3);
482
553
  const fromFlag = getArgValue("--text");
483
554
  const text = (fromFlag !== null && fromFlag !== void 0 ? fromFlag : raw).trim();
484
555
  const parsed = (0, invokeFlow_1.parseInvokeText)(text);
@@ -16,6 +16,31 @@ const PRD_PLACEHOLDER_MARKERS = [
16
16
  "占位",
17
17
  "placeholder",
18
18
  ];
19
+ const PRD_TECHNICAL_TITLE_MARKERS = [
20
+ "技术文档",
21
+ "技术方案",
22
+ "详细设计",
23
+ "接口文档",
24
+ "后端",
25
+ "数据库",
26
+ "实现文档",
27
+ "设计文档",
28
+ "开发文档",
29
+ ];
30
+ /**
31
+ * 判断 PRD 标题是否属于技术/后端/设计类文档,避免错误进入需求分析链路。
32
+ */
33
+ function hasForbiddenPrdTitle(title) {
34
+ const upper = (title || "").toUpperCase();
35
+ return PRD_TECHNICAL_TITLE_MARKERS.some((marker) => upper.includes(marker.toUpperCase()));
36
+ }
37
+ /**
38
+ * 从 Markdown 主标题读取 Confluence 页面标题,作为 prd-fetched 的类型校验入口。
39
+ */
40
+ function extractPrdTitle(content) {
41
+ var _a, _b;
42
+ return (_b = (_a = content.split("\n").find((line) => line.trim().startsWith("# "))) === null || _a === void 0 ? void 0 : _a.replace(/^#\s*/, "").trim()) !== null && _b !== void 0 ? _b : "";
43
+ }
19
44
  async function isPrdFetched(outputRoot) {
20
45
  const prdPath = path_1.default.join(outputRoot, "source", "PRD.md");
21
46
  const content = await (0, fs_1.readFileIfExists)(prdPath);
@@ -25,6 +50,10 @@ async function isPrdFetched(outputRoot) {
25
50
  if (PRD_PLACEHOLDER_MARKERS.some((m) => content.includes(m))) {
26
51
  return { ok: false, detail: "source/PRD.md 仍为占位内容,请先执行 Step1 拉取 PRD" };
27
52
  }
53
+ const title = extractPrdTitle(content);
54
+ if (title && hasForbiddenPrdTitle(title)) {
55
+ return { ok: false, detail: `source/PRD.md 命中技术文档标题: ${title}` };
56
+ }
28
57
  return { ok: true, detail: "PRD 已回填" };
29
58
  }
30
59
  async function isQuestionsClosed(outputRoot) {
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVersionIntent = parseVersionIntent;
4
+ exports.normalizeProductName = normalizeProductName;
5
+ exports.isVersionDevelopmentIntent = isVersionDevelopmentIntent;
6
+ exports.getVersionIntentInvokeCommand = getVersionIntentInvokeCommand;
7
+ /**
8
+ * 解析“开发某产品某版本”的自然语言意图。
9
+ * 该解析器只负责提取产品与版本,不承担后续流程执行。
10
+ */
11
+ function parseVersionIntent(text) {
12
+ const normalized = text.trim();
13
+ if (!normalized)
14
+ return null;
15
+ const normalizedText = normalized
16
+ .replace(/[::,,。;;!!??]/g, " ")
17
+ .replace(/[_-]/g, " ")
18
+ .replace(/\s+/g, " ")
19
+ .trim();
20
+ const patterns = [
21
+ /(?:开发|进行|实现|处理|做)\s*([\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z\s]*?)\s*[Vv]?\s*([0-9]+(?:\.[0-9]+){1,3})\s*(?:版本|需求|PRD)?/i,
22
+ /([\u4e00-\u9fa5A-Za-z][\u4e00-\u9fa5A-Za-z\s]*?)\s*[Vv]?\s*([0-9]+(?:\.[0-9]+){1,3})\s*(?:版本|需求)?\s*(?:开发|进行|实现|处理|做)?/i,
23
+ ];
24
+ for (const re of patterns) {
25
+ const match = normalizedText.match(re);
26
+ if (!match)
27
+ continue;
28
+ const product = normalizeProductName(match[1]);
29
+ const version = match[2];
30
+ if (!product || !version)
31
+ continue;
32
+ return { product, version };
33
+ }
34
+ return null;
35
+ }
36
+ function normalizeProductName(raw) {
37
+ const value = raw
38
+ .replace(/\b(PRD|需求|版本|开发|进行|实现|处理|做)\b/gi, " ")
39
+ .replace(/需求|版本|开发|进行|实现|处理|做/g, " ")
40
+ .replace(/\s+/g, " ")
41
+ .trim();
42
+ if (!value)
43
+ return "";
44
+ const upper = value.toUpperCase();
45
+ if (upper === "ADI" || /ADINSIGHT/i.test(value))
46
+ return "ADI";
47
+ if (upper === "OMS" || /OPERATION/i.test(value))
48
+ return "OMS";
49
+ if (/BREAK\s*X/i.test(value) || /BREAKX/i.test(value))
50
+ return "BreakX";
51
+ if (/欢盟|HUANMENG|HM/i.test(value))
52
+ return "欢盟";
53
+ if (/AD\s*TOOLS/i.test(value) || /ADTOOLS/i.test(value))
54
+ return "AD Tools";
55
+ return value;
56
+ }
57
+ function isVersionDevelopmentIntent(text) {
58
+ return parseVersionIntent(text) !== null;
59
+ }
60
+ function getVersionIntentInvokeCommand(options) {
61
+ const parsed = parseVersionIntent(options.rawText);
62
+ if (!parsed)
63
+ return null;
64
+ return `npx sdd-flow-kit invoke "开发 ${parsed.product} ${parsed.version} 需求" -y --agent ${options.agent}`;
65
+ }
@@ -35,7 +35,7 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
35
35
  "| `tasks.md` | AC 的可执行映射(`[AC-XX]`) |",
36
36
  "",
37
37
  "## 1. Step1 — 拉取 PRD",
38
- "按 `01-获取需求文档指引.md` 执行 confluence 脚本,然后:",
38
+ "按 `01-获取需求文档指引.md` 执行 confluence 脚本;脚本和 gate 会拒绝 `技术文档`/`后端`/`详细设计` 等非 PRD 页面,然后:",
39
39
  "```bash",
40
40
  `npx sdd-flow-kit gate --project-root "${pr}" --run-id ${runId} --expect prd-fetched`,
41
41
  `npx sdd-flow-kit phase advance --project-root "${pr}" --run-id ${runId} --to after-prd`,
@@ -2,26 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseInvokeText = parseInvokeText;
4
4
  exports.toRunOptions = toRunOptions;
5
+ const versionIntent_1 = require("../core/versionIntent");
5
6
  function parseInvokeText(text) {
6
- const normalized = text.trim();
7
- const patterns = [
8
- /(?:开发|进行)\s*(.+?)(?:[-_\s]*)V?([0-9]+(?:\.[0-9]+){1,3})\s*(?:版本)?\s*(?:需求|PRD)?/i,
9
- /([A-Za-z\u4e00-\u9fa5]+)[_\s-]?V?([0-9]+(?:\.[0-9]+){1,3})\s*版本/i,
10
- /([A-Za-z]+)[_\s-]([0-9]+(?:\.[0-9]+){2,3})(?:\s|$)/i,
11
- ];
12
- for (const re of patterns) {
13
- const match = normalized.match(re);
14
- if (!match)
15
- continue;
16
- let product = match[1].replace(/[-_\s]+$/g, "").trim();
17
- product = product.replace(/^进行/, "").trim();
18
- if (/^adi$/i.test(product) || product.toUpperCase() === "ADI")
19
- product = "ADI";
20
- if (/^oms$/i.test(product))
21
- product = "OMS";
22
- return { product: product.toUpperCase(), version: match[2] };
23
- }
24
- return null;
7
+ const parsed = (0, versionIntent_1.parseVersionIntent)(text);
8
+ if (!parsed)
9
+ return null;
10
+ return { product: parsed.product, version: parsed.version };
25
11
  }
26
12
  function toRunOptions(args) {
27
13
  var _a, _b;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runGuard = runGuard;
4
+ const runFlow_1 = require("./runFlow");
5
+ const invokeFlow_1 = require("./invokeFlow");
6
+ const runSessionMode_1 = require("./runSessionMode");
7
+ /**
8
+ * 识别“开发产品版本”的自然语言入口,并在命中时直接启动 SDD 流程。
9
+ * 该守卫用于压过普通 OpenSpec/OPSX 入口,避免版本需求误走自由提案流程。
10
+ */
11
+ async function runGuard(options) {
12
+ const text = options.text.trim();
13
+ const parsed = (0, invokeFlow_1.parseInvokeText)(text);
14
+ if (!parsed) {
15
+ return {
16
+ ok: false,
17
+ action: "noop",
18
+ input: text,
19
+ detail: "未识别到版本开发意图,无需启动 sdd-flow-kit invoke。",
20
+ };
21
+ }
22
+ const runOptions = (0, invokeFlow_1.toRunOptions)({
23
+ parsed,
24
+ projectRoot: options.projectRoot,
25
+ branch: options.branch,
26
+ yes: options.yes,
27
+ dryRun: options.dryRun,
28
+ agent: options.agent,
29
+ });
30
+ const result = await (0, runFlow_1.runFlow)(runOptions);
31
+ if (!options.dryRun) {
32
+ await (0, runSessionMode_1.applyInvokeTextToSession)(options.projectRoot, result.runId, text);
33
+ }
34
+ return {
35
+ ok: true,
36
+ action: "invoke",
37
+ input: text,
38
+ product: parsed.product,
39
+ version: parsed.version,
40
+ runId: result.runId,
41
+ nextPath: `openspec/PRD/${result.runId}/NEXT.md`,
42
+ detail: `已识别版本开发意图并启动 SDD 流程:${parsed.product} ${parsed.version} -> runId=${result.runId}`,
43
+ };
44
+ }
@@ -152,6 +152,28 @@ async function installAgentSkill(projectRoot, projectKind) {
152
152
  await (0, fs_1.writeTextFileEnsuredDir)(layeredPath, layered);
153
153
  }
154
154
  }
155
+ async function installSddVersionTriggerRule(projectRoot, agent) {
156
+ const template = await (0, templates_1.loadTemplateText)("rules/sdd-version-trigger.mdc.template");
157
+ const content = renderVars(template, {
158
+ AGENT_NAME: agent,
159
+ });
160
+ if (agent === "cursor" || agent === "custom") {
161
+ const rulePath = path_1.default.join(projectRoot, ".cursor", "rules", "00-sdd-flow-kit-version-trigger.mdc");
162
+ await (0, fs_1.writeTextFileEnsuredDir)(rulePath, content);
163
+ }
164
+ }
165
+ async function installSddVersionTriggerSkills(projectRoot) {
166
+ const template = await (0, templates_1.loadTemplateText)("skills/version-trigger.SKILL.md.template");
167
+ const targets = [
168
+ { agent: "cursor", relPath: ".cursor/skills/00-sdd-flow-kit-version-trigger/SKILL.md" },
169
+ { agent: "claude-code", relPath: ".claude/skills/00-sdd-flow-kit-version-trigger/SKILL.md" },
170
+ { agent: "codex", relPath: ".codex/skills/00-sdd-flow-kit-version-trigger/SKILL.md" },
171
+ { agent: "openclaw", relPath: ".openclaw/skills/00-sdd-flow-kit-version-trigger/SKILL.md" },
172
+ ];
173
+ for (const target of targets) {
174
+ await (0, fs_1.writeTextFileEnsuredDir)(path_1.default.join(projectRoot, target.relPath), renderVars(template, { AGENT_NAME: target.agent }));
175
+ }
176
+ }
155
177
  async function installSddCursorRule(projectRoot) {
156
178
  const rulePath = path_1.default.join(projectRoot, ".cursor", "rules", "sdd-flow-kit-gates.mdc");
157
179
  const template = await (0, templates_1.loadTemplateText)("rules/sdd-no-implement-until-apply.mdc.template");
@@ -343,6 +365,8 @@ async function setupProject(options) {
343
365
  const { dir } = docSkillDirName(selectedKind);
344
366
  plannedActions.push(`would install docs skill: ${path_1.default.join(installRoot, "docs", dir)}`);
345
367
  plannedActions.push(`would install opsx config: ${path_1.default.join(installRoot, ".opsx", "config.json")}`);
368
+ plannedActions.push(`would install highest-priority version trigger rule: ${path_1.default.join(installRoot, ".cursor", "rules", "00-sdd-flow-kit-version-trigger.mdc")}`);
369
+ plannedActions.push("would install cross-agent version trigger skills: cursor/claude-code/codex/openclaw");
346
370
  plannedActions.push(`would install cursor rule: ${path_1.default.join(installRoot, ".cursor", "rules", "sdd-flow-kit-gates.mdc")}`);
347
371
  }
348
372
  else {
@@ -352,6 +376,10 @@ async function setupProject(options) {
352
376
  plannedActions.push("added pnpm.onlyBuiltDependencies: sdd-flow-kit");
353
377
  }
354
378
  await installAgentSkill(installRoot, selectedKind);
379
+ await installSddVersionTriggerRule(installRoot, selectedAgent);
380
+ await installSddVersionTriggerSkills(installRoot);
381
+ plannedActions.push("installed highest-priority version trigger rule (.cursor/rules/00-sdd-flow-kit-version-trigger.mdc)");
382
+ plannedActions.push("installed cross-agent version trigger skills (cursor/claude-code/codex/openclaw)");
355
383
  await installSddCursorRule(installRoot);
356
384
  skillInstalled = true;
357
385
  await installDocsSkill(installRoot, selectedKind);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-flow-kit",
3
- "version": "1.3.13",
3
+ "version": "1.3.15",
4
4
  "private": false,
5
5
  "description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
6
6
  "license": "MIT",
@@ -14,11 +14,17 @@ python3 confluence-doc.py {{VERSION_EXAMPLE}}
14
14
 
15
15
  2. 脚本会依次用关键词打开 **Confluence 专用搜索页** `dosearchsite.action?queryString=<关键词>`(非首页顶栏搜索),并依次尝试:`ADI_V*` → `ADI-V*` → `ADI_*` → `ADI-*` → `ADI_v*` → `ADI-v*` → `ADI V*` → `ADI_V*结算` → `V*` 等。
16
16
 
17
- 3. 匹配规则:**标题包含关键词或以关键词为前缀**,或 **URL 路径包含关键词**,任一满足即命中(Confluence 列表标题常被截断)。
17
+ 3. 匹配规则:先收集 **标题包含关键词或以关键词为前缀**、或 **URL 路径包含关键词** 的候选页,再执行 PRD 类型校验。
18
18
 
19
- 4. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
19
+ 4. PRD 类型校验(强制黑名单):候选页必须通过脚本内 `is_prd_page()` `has_forbidden_prd_title()` 双重校验。以下标题特征会被直接拒绝:
20
+ - `技术文档`、`技术方案`、`后端`、`详细设计`、`数据库`、`接口文档`
21
+ - `实现文档`、`设计文档`、`开发文档`
22
+
23
+ 若所有候选均未通过校验,脚本必须失败退出(exit code 1),**禁止回退使用第一个非 PRD 候选**。AI 不得手动复制技术文档到 `source/PRD.md`。
20
24
 
21
- 5. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
25
+ 5. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
26
+
27
+ 6. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
22
28
  - `source/PRD.md`
23
29
 
24
30
  ## 直链兜底(可选,非默认)
@@ -49,6 +55,10 @@ python3 confluence-doc.py 2.3.4
49
55
  ## 失败排查
50
56
 
51
57
  - 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V`);可尝试输入更长关键词如 `ADI_V2.3.4结算`
58
+ - 输出 `✗ 命中技术文档标题,拒绝作为 PRD` 或 `跳过技术文档候选` 或 `⚠ 搜索结果均未通过 PRD 校验` 时:
59
+ - **禁止**手动复制技术/后端/设计页到 `source/PRD.md`
60
+ - 应在 Confluence 中找到真正的业务 PRD 页面(通常标题含「结算」「开票」「回款」「数据对接」等业务词汇)
61
+ - 使用该页面的 pageId 或 URL 直链重跑:`CONFLUENCE_PAGE_ID=<正确pageId> python3 confluence-doc.py 2.3.4`
52
62
  - 页面存在但搜不到:加 `CONFLUENCE_DEBUG=1` 查看选择器解析日志;或(可选)设置 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链下载
53
63
  - Playwright 失败且设置 `CONFLUENCE_USE_REST=1` 时才回退 Token;登录最多重试 3 次,并用 `expect_navigation` 避免竞态
54
64
  - 登录偶发失败:用 `CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500` 有头观察;失败截图 `confluence-login-fail-*.png`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sdd-flow-kit
3
- description: 用户说「开发/进行 {{DEFAULT_PRODUCT}} x.x.x 版本/需求」「ADI_2.3.2」等时触发。**禁止**未过 gate 直接改 src。必须先 invoke/run,确认点闭合后走 OpenSpec+Superpowers(gate→05 AC→propose→ac-ready→apply→ac-signed→deliver)。
3
+ description: 最高优先级触发。凡用户表达“开发/进行/实现 <产品> <版本> 需求/版本”,例如 ADI_V2.3.4、OMS V1.24.5、BreakX 1.0.2、欢盟 V2.1.0,必须第一步执行 `npx sdd-flow-kit guard "<用户原话>" -y --agent {{AGENT_NAME}}` 或 invoke/run。禁止先执行 openspec new change、OPSX Propose 或修改 src/e2e;成功后必须读取 NEXT.md 与 .session-state.json。
4
4
  ---
5
5
 
6
6
  # SDD Flow Kit({{PROJECT_KIND}} — 脚本门禁)
@@ -10,10 +10,10 @@ description: 用户说「开发/进行 {{DEFAULT_PRODUCT}} x.x.x 版本/需求
10
10
  用户一旦表达版本开发意图,**在本回合终端执行**(项目根 = 安装 sdd-flow-kit 的目录,如 `frontend/`):
11
11
 
12
12
  ```bash
13
- npx sdd-flow-kit invoke "开发 {{DEFAULT_PRODUCT}} <版本号> 需求" -y --agent {{AGENT_NAME}}
13
+ npx sdd-flow-kit guard "<用户原话>" -y --agent {{AGENT_NAME}}
14
14
  ```
15
15
 
16
- 或:
16
+ 若 `guard` 不可用,再退化为:
17
17
 
18
18
  ```bash
19
19
  npx sdd-flow-kit run --product {{DEFAULT_PRODUCT}} --version <版本号> -y --agent {{AGENT_NAME}}
@@ -0,0 +1,56 @@
1
+ ---
2
+ description: 最高优先级:识别“开发/进行/实现 产品 版本”时必须先执行 sdd-flow-kit guard/invoke,禁止先走 OpenSpec/OPSX 或修改业务代码
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # SDD Flow Kit 版本开发强制入口(最高优先级)
7
+
8
+ 当用户输入匹配以下任一意图时:
9
+
10
+ - `开发 <产品> <版本>`
11
+ - `进行 <产品> <版本> 需求`
12
+ - `实现 <产品> <版本> 版本`
13
+ - `<产品>_V<版本>` / `<产品>-V<版本>`
14
+ - 示例:`开发 ADI_V2.3.4版本`、`开发 OMS V1.24.5`、`进行 BreakX 1.0.2 需求`、`欢盟 V2.1.0 版本开发`
15
+
16
+ ## 第一动作(必须)
17
+
18
+ 必须先执行:
19
+
20
+ ```bash
21
+ npx sdd-flow-kit guard "<用户原话>" -y --agent {{AGENT_NAME}}
22
+ ```
23
+
24
+ 若无法使用 `guard`,退化为:
25
+
26
+ ```bash
27
+ npx sdd-flow-kit invoke "开发 <产品> <版本> 需求" -y --agent {{AGENT_NAME}}
28
+ ```
29
+
30
+ ## 严格禁止
31
+
32
+ 在 `guard/invoke` 成功前,禁止:
33
+
34
+ - 创建 `openspec/changes/**`
35
+ - 执行 `openspec new change`
36
+ - 执行 OPSX / OpenSpec Propose
37
+ - 修改 `src/**`、`frontend/src/**`、`e2e/**`
38
+ - 用自然语言计划替代脚本入口
39
+
40
+ ## 成功后继续
41
+
42
+ `guard/invoke` 成功后,必须读取并遵循:
43
+
44
+ - `openspec/PRD/<runId>/NEXT.md`
45
+ - `openspec/PRD/<runId>/.session-state.json`
46
+
47
+ 若 `.session-state.json` 中 `docModeOnly=true`,仍然禁止修改业务代码,直到 `gate docs-closed`、`gate ac-ready`、`phase impl` 与 `gate impl-allowed` 全部通过。
48
+
49
+ ## 优先级
50
+
51
+ 本规则优先于普通 OpenSpec/OPSX 自然语言需求入口:
52
+
53
+ ```text
54
+ 版本开发意图 → sdd-flow-kit guard/invoke → NEXT.md
55
+ 普通代码需求 → OPSX/OpenSpec
56
+ ```
@@ -292,10 +292,36 @@ def read_breadcrumb_text(page) -> str:
292
292
  return " > ".join(parts)
293
293
 
294
294
 
295
+ PRD_TECHNICAL_TITLE_MARKERS = (
296
+ "技术文档",
297
+ "技术方案",
298
+ "详细设计",
299
+ "接口文档",
300
+ "后端",
301
+ "数据库",
302
+ "实现文档",
303
+ "设计文档",
304
+ "开发文档",
305
+ "API文档",
306
+ "数据结构",
307
+ "表设计",
308
+ )
309
+
310
+
311
+ def has_forbidden_prd_title(title: str) -> bool:
312
+ """技术文档/后端设计页必须在 PRD 拉取阶段直接拦截。"""
313
+ title_upper = (title or "").upper()
314
+ return any(marker.upper() in title_upper for marker in PRD_TECHNICAL_TITLE_MARKERS)
315
+
316
+
295
317
  def is_adi_requirement_page(title: str, crumbs: str, page_url: str) -> bool:
296
318
  """ADI 需求页常无 PRD 字样,标题形如 ADI_V2.3.4结算、开票、回款数据对接欢聚。"""
297
319
  title_upper = (title or "").upper()
298
320
  crumbs_upper = (crumbs or "").upper()
321
+
322
+ if has_forbidden_prd_title(title):
323
+ print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
324
+ return False
299
325
  if re.search(r"ADI[_-]?V\d+\.\d+\.\d+", title_upper):
300
326
  print(f" ✓ ADI 版本标题命中: {title[:80]}")
301
327
  return True
@@ -326,6 +352,10 @@ def is_prd_page(page, page_url: str) -> bool:
326
352
  except Exception:
327
353
  title = ""
328
354
 
355
+ if has_forbidden_prd_title(title):
356
+ print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
357
+ return False
358
+
329
359
  # ADI:标题常无「PRD/需求」字样,单独识别
330
360
  if prefix == "ADI" and is_adi_requirement_page(title, crumbs, page_url):
331
361
  return True
@@ -445,7 +475,9 @@ def rest_search_candidates(
445
475
  for item in data.get("results", []):
446
476
  title = (item.get("title") or "").strip()
447
477
  page_id = str(item.get("id", ""))
448
- if not page_id:
478
+ if not page_id or has_forbidden_prd_title(title):
479
+ if title and has_forbidden_prd_title(title):
480
+ print(f" 跳过技术文档候选: {title[:80]}")
449
481
  continue
450
482
  href = f"/spaces/{(item.get('space') or {}).get('key', '')}/pages/{page_id}/"
451
483
  score = score_link_match(title, href, prefix, version, keyword)
@@ -462,10 +494,13 @@ def rest_resolve_target(
462
494
  direct_id = resolve_direct_page_id()
463
495
  if direct_id:
464
496
  page = rest_fetch_page(base_url, token, direct_id)
497
+ if has_forbidden_prd_title(page["title"]):
498
+ print(f"❌ 直链页面不是业务 PRD,命中技术文档黑名单: {page['title']}")
499
+ print(" 请确认提供的 pageId 是业务需求文档,而非技术文档/后端设计文档")
500
+ raise RuntimeError(f"直链页面不是 PRD,命中技术文档标题: {page['title']}")
465
501
  kw = keywords[0] if keywords else f"{prefix}_V{version}"
466
502
  return page["id"], page["title"], kw, page["url"]
467
503
 
468
- fallback: tuple[str, str, str, str] | None = None
469
504
  for kw in keywords:
470
505
  print(f"\n🔍 [REST] 搜索: {kw}")
471
506
  candidates = rest_search_candidates(base_url, token, prefix, version, kw)
@@ -475,17 +510,25 @@ def rest_resolve_target(
475
510
  for score, page_id, title in candidates:
476
511
  print(f" 候选 (score={score}): {title[:80]}")
477
512
  page = rest_fetch_page(base_url, token, page_id)
478
- if prefix.upper() == "ADI" and is_adi_title_match(title, version):
513
+ # 双重校验:标题黑名单 + ADI 特殊规则
514
+ if has_forbidden_prd_title(page["title"]):
515
+ print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {page['title'][:80]}")
516
+ continue
517
+ if prefix.upper() == "ADI" and is_adi_title_match(page["title"], version):
518
+ print(f" ✓ ADI 业务 PRD 命中")
479
519
  return page_id, page["title"], kw, page["url"]
480
520
  if score >= 4:
481
521
  return page_id, page["title"], kw, page["url"]
482
- if fallback is None:
483
- fallback = (page_id, page["title"], kw, page["url"])
484
- return fallback
522
+ print(" ⚠ REST 搜索结果均未通过 PRD 校验")
523
+ print(" 所有候选页面要么是技术/后端/设计文档,要么评分不足")
524
+ print(" → 不回退到非 PRD 候选;请确认业务需求文档的 pageId")
525
+ return None
485
526
 
486
527
 
487
528
  def is_adi_title_match(title: str, version: str) -> bool:
488
529
  """ADI 长标题页识别(面包屑可无 PRD 字样)。"""
530
+ if has_forbidden_prd_title(title):
531
+ return False
489
532
  title_upper = (title or "").upper()
490
533
  v_compact = version.replace(".", "")
491
534
  if re.search(rf"ADI[_-]?V{re.escape(version)}", title_upper):
@@ -702,7 +745,6 @@ def resolve_prd_target(
702
745
  ) -> tuple[str | None, str | None, str]:
703
746
  """依次用每个关键词搜索,直到找到父级为 PRD 的页面。"""
704
747
  seen_urls: set[str] = set()
705
- fallback: tuple[str, str, str] | None = None # url, title, keyword
706
748
 
707
749
  for kw in keywords:
708
750
  print(f"\n🔍 搜索关键词: {kw}")
@@ -720,15 +762,16 @@ def resolve_prd_target(
720
762
  continue
721
763
  seen_urls.add(url)
722
764
  print(f" · 候选: {title[:70]}")
765
+ # 先执行标题级黑名单快速拒绝
766
+ if has_forbidden_prd_title(title):
767
+ print(f" ✗ 命中技术文档标题,拒绝作为 PRD: {title[:80]}")
768
+ continue
723
769
  if is_prd_page(page, url):
724
770
  return url, kw, title
725
- if fallback is None:
726
- fallback = (url, kw, title)
727
771
 
728
- if fallback:
729
- url, kw, title = fallback
730
- print(f" 未找到 PRD 父级,回退使用: {title[:70]}")
731
- return url, kw, title
772
+ print(" ⚠ 搜索结果均未通过 PRD 校验")
773
+ print(" → 所有候选页面要么是技术/后端/设计文档,要么面包屑不含 PRD 父级")
774
+ print(" 不回退到非 PRD 候选;请确认业务需求文档的 Confluence 标题与 pageId")
732
775
  return None, None, ""
733
776
 
734
777
 
@@ -919,8 +962,12 @@ def main():
919
962
  return
920
963
 
921
964
  print("\n❌ PRD 拉取失败:Playwright 与 REST 均未成功。")
922
- print(" 排查:CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500 有头模式观察登录")
923
- print(" 或直链:CONFLUENCE_PAGE_ID=<pageId> python3 confluence-doc.py <版本>")
965
+ print(" 排查:")
966
+ print(" 1. CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500 有头模式观察登录与搜索结果")
967
+ print(" 2. 到 Confluence 确认业务 PRD 页面的准确标题与 pageId")
968
+ print(" 3. 若搜索命中了技术/后端/设计文档,这些已被黑名单拒绝")
969
+ print(" 4. 使用业务 PRD 的直链重跑:CONFLUENCE_PAGE_ID=<pageId> python3 confluence-doc.py <版本>")
970
+ print(" 5. 禁止手动复制非 PRD 页面到 source/PRD.md")
924
971
  sys.exit(1)
925
972
 
926
973
 
@@ -976,23 +1023,42 @@ def _download_via_playwright(
976
1023
  print(f" 已保存调试截图: {debug_path}")
977
1024
  except Exception:
978
1025
  pass
979
- print(f"❌ 未找到 {prefix} {version} 的需求文档。")
1026
+ print(f"❌ 未找到 {prefix} {version} 的业务需求文档(PRD)。")
980
1027
  print(
981
1028
  " 排查:1) 页面标题是否含 ADI_V2.3.4(下划线,非 ADI-V)及中文后缀"
982
1029
  )
983
1030
  print(
984
1031
  " 2) 依次尝试 ADI_V2.3.4 / ADI-V2.3.4 / ADI_V2.3.4结算 / V2.3.4"
985
1032
  )
986
- print(" 3) 直链:CONFLUENCE_PAGE_ID=109609740 或 CONFLUENCE_PAGE_URL=<完整URL>")
987
- print(" 4) ADI 页面包屑可能无 PRD 字样,脚本已按 ADI_V* 标题规则识别")
1033
+ print(" 3) 若搜索命中了「技术文档」「后端」「详细设计」等页面,这些已被黑名单自动拒绝")
1034
+ print(" 4) 直链解决:CONFLUENCE_PAGE_ID=<正确pageId> CONFLUENCE_PAGE_URL=<完整URL>")
1035
+ print(" 5) ADI 页面包屑可能无 PRD 字样,脚本已按 ADI_V* 标题规则识别")
1036
+ print(" 6) 禁止手动复制技术文档到 source/PRD.md,必须找到真正的业务 PRD")
988
1037
  return False
989
1038
 
990
1039
  print(f"✅ 命中关键词: {matched_keyword}")
991
1040
  if matched_title:
992
1041
  print(f" 页面: {matched_title[:100]}")
993
1042
  print(f" URL: {target_url[:120]}...")
1043
+
1044
+ # 直链模式下也需检查标题黑名单
1045
+ if direct_url:
1046
+ page.goto(target_url, wait_until="domcontentloaded", timeout=60000)
1047
+ try:
1048
+ direct_title = page.locator("#title-text").text_content().strip()
1049
+ if has_forbidden_prd_title(direct_title):
1050
+ print(f"❌ 直链页面不是业务 PRD,命中技术文档黑名单: {direct_title}")
1051
+ print(" 请确认提供的 pageId/URL 是业务需求文档,而非技术文档/后端设计文档")
1052
+ raise RuntimeError("直链页面命中技术文档标题,拒绝作为 PRD")
1053
+ except Exception as e:
1054
+ if "技术文档" in str(e) or "黑名单" in str(e):
1055
+ raise
1056
+
994
1057
  page.goto(target_url, wait_until="networkidle", timeout=60000)
995
1058
 
1059
+ if not is_prd_page(page, target_url):
1060
+ raise RuntimeError("命中的页面未通过 PRD 校验,疑似技术文档或设计文档")
1061
+
996
1062
  try:
997
1063
  title = page.locator("#title-text").text_content().strip()
998
1064
  except Exception:
@@ -2,7 +2,7 @@
2
2
  name: {{SKILL_NAME}}
3
3
  description: |
4
4
  当用户说「获取 {{PRODUCT}} Vx.y.z 需求文档」「下载 {{PRODUCT}}-Vx.y.z PRD」「查询 {{PRODUCT}} x.y.z 版本需求」时触发。
5
- 自动从 Confluence 搜索对应版本 PRD 页面,下载图片并生成 Markdown 到本仓库 `docs/`。
5
+ 自动从 Confluence 搜索对应版本 PRD 页面;`技术文档`、`后端`、`详细设计` 类页面会被拒绝,避免误下载为 PRD;下载图片并生成 Markdown 到本仓库 `docs/`。
6
6
  ---
7
7
 
8
8
  # {{PRODUCT}} 需求文档获取(docs/{{SKILL_DIR}})
@@ -60,7 +60,9 @@ python3 confluence-doc.py {{PRODUCT_PREFIX}}_V1.2.3
60
60
  4. `{{PRODUCT_PREFIX}}-<版本>`(如 ADI-2.3.2)
61
61
  5. `ADI_V<版本>结算`(ADI 长标题页,如 V2.3.4 结算对接欢聚)
62
62
 
63
- 每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates` 顶栏搜索稳定);无结果时再兜底顶栏搜索。匹配时 **标题或 URL 路径** 任一命中即可,并忽略 `-`/`_` 差异;ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
63
+ 每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates` 顶栏搜索稳定);无结果时再兜底顶栏搜索。匹配时先收集 **标题或 URL 路径** 命中的候选,再执行 PRD 类型校验;`技术文档`、`技术方案`、`后端`、`详细设计`、`数据库`、`接口文档` 等页面会被直接拒绝。ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
64
+
65
+ 若所有候选均未通过 PRD 校验,脚本必须失败退出,**禁止回退使用第一个非 PRD 候选**;此时应改用正确 PRD 的 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链。
64
66
 
65
67
  调试搜索解析:
66
68
 
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: 00-sdd-flow-kit-version-trigger
3
+ description: 最高优先级触发。凡用户表达“开发/进行/实现 <产品> <版本> 需求/版本”,例如 ADI_V2.3.4、OMS V1.24.5、BreakX 1.0.2、欢盟 V2.1.0,必须第一步执行 `npx sdd-flow-kit guard "<用户原话>" -y --agent {{AGENT_NAME}}`。禁止先执行 openspec new change、OPSX Propose 或修改 src/e2e。
4
+ ---
5
+
6
+ # SDD Flow Kit 版本开发强制入口
7
+
8
+ ## 适用范围
9
+
10
+ 当用户输入匹配以下任一意图时,本 skill 优先于普通 OpenSpec / OPSX 流程:
11
+
12
+ - `开发 <产品> <版本>`
13
+ - `进行 <产品> <版本> 需求`
14
+ - `实现 <产品> <版本> 版本`
15
+ - `<产品>_V<版本>` / `<产品>-V<版本>`
16
+ - 示例:`开发 ADI_V2.3.4版本`、`开发 OMS V1.24.5`、`进行 BreakX 1.0.2 需求`、`欢盟 V2.1.0 版本开发`
17
+
18
+ ## 第一动作(必须)
19
+
20
+ 在项目根目录立即执行:
21
+
22
+ ```bash
23
+ npx sdd-flow-kit guard "<用户原话>" -y --agent {{AGENT_NAME}}
24
+ ```
25
+
26
+ 若 `guard` 不可用,退化为:
27
+
28
+ ```bash
29
+ npx sdd-flow-kit invoke "开发 <产品> <版本> 需求" -y --agent {{AGENT_NAME}}
30
+ ```
31
+
32
+ ## 严格禁止
33
+
34
+ 在 `guard/invoke` 成功前,禁止:
35
+
36
+ - 创建 `openspec/changes/**`
37
+ - 执行 `openspec new change`
38
+ - 执行 OPSX / OpenSpec Propose
39
+ - 修改 `src/**`、`frontend/src/**`、`e2e/**`
40
+ - 用自然语言计划替代脚本入口
41
+
42
+ ## 成功后继续
43
+
44
+ `guard/invoke` 成功后,必须读取并遵循:
45
+
46
+ - `openspec/PRD/<runId>/NEXT.md`
47
+ - `openspec/PRD/<runId>/.session-state.json`
48
+
49
+ 若 `.session-state.json` 中 `docModeOnly=true`,仍然禁止修改业务代码,直到 `gate docs-closed`、`gate ac-ready`、`phase impl` 与 `gate impl-allowed` 全部通过。
50
+
51
+ ## 优先级
52
+
53
+ ```text
54
+ 版本开发意图 → sdd-flow-kit guard/invoke → NEXT.md
55
+ 普通代码需求 → OPSX/OpenSpec
56
+ ```