sdd-flow-kit 1.3.11 → 1.3.13
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 +16 -1
- package/dist/adapters/index.js +6 -3
- package/dist/cli.js +73 -2
- package/dist/core/executionMode.js +78 -0
- package/dist/core/sessionState.js +4 -0
- package/dist/core/turnEndPolicy.js +49 -0
- package/dist/core/writeNext.js +9 -0
- package/dist/steps/runChain.js +212 -0
- package/dist/steps/runSessionMode.js +49 -0
- package/package.json +6 -2
- package/src/templates/artifacts/01-adi-doc-skill-/346/214/207/345/274/225.template.md +10 -6
- package/src/templates/artifacts/02-/351/234/200/346/261/202/345/210/206/346/236/220/346/217/220/347/244/272/350/257/215.template.md +1 -0
- package/src/templates/artifacts/06-agent-skill.template.md +30 -14
- package/src/templates/artifacts/07-opsx-auto-chain.template.md +33 -9
- package/src/templates/prompts/02-sdd-loop-prompt.md +7 -6
- package/src/templates/rules/sdd-no-implement-until-apply.mdc.template +13 -0
- package/src/templates/skills/confluence-doc.py.template +49 -13
- package/src/templates/skills/doc-skill.SKILL.md.template +9 -3
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ node ./node_modules/sdd-flow-kit/dist/postinstall.js
|
|
|
63
63
|
#### 当前自动化边界
|
|
64
64
|
|
|
65
65
|
- 已支持:跨工具统一目录结构、提示词、报告模板、**NEXT.md 脚本清单**
|
|
66
|
-
- 已支持:**gate / phase / propose / deliver** 命令(exit code 机械门禁)
|
|
66
|
+
- 已支持:**gate / phase / propose / deliver / chain / session mode** 命令(exit code 机械门禁)
|
|
67
67
|
- 已支持:`ensure-opsx` + `openspec status` 校验 apply-ready
|
|
68
68
|
- 未支持:在 Cursor 内全自动跑完全流程(Cursor adapter 仍为 manual);可选 `SDD_FLOW_KIT_ENABLE_CLAUDE_AUTORUN=1` 给 Claude Code
|
|
69
69
|
|
|
@@ -80,6 +80,13 @@ node ./node_modules/sdd-flow-kit/dist/postinstall.js
|
|
|
80
80
|
|
|
81
81
|
#### 变更记录
|
|
82
82
|
|
|
83
|
+
**1.3.13** — 执行模式拆分 + 回合结束约束 + chain 机械串联
|
|
84
|
+
|
|
85
|
+
- 新增 `executionMode`:`full-chain`(默认)与 `staged-confirm`(分阶段确认),正交于 `strictCommands`
|
|
86
|
+
- 兼容「严格SOP,不可替代」→ `staged-confirm` + `strictCommands`
|
|
87
|
+
- 模板写入回合结束条件,禁止待办清单式提前收尾
|
|
88
|
+
- 新增 `chain`、`session mode` CLI;`invoke` 自动解析执行模式
|
|
89
|
+
|
|
83
90
|
**1.3.11** — PRD 自动修复改为「每项最多 3 次」
|
|
84
91
|
|
|
85
92
|
- 每轮只修 `prd-diff` 仍失败的剩余项;8 项已修好则下轮只盯剩下 2 项
|
|
@@ -92,6 +99,14 @@ node ./node_modules/sdd-flow-kit/dist/postinstall.js
|
|
|
92
99
|
- Claude Code 安装(`.claude/skills`)时默认用 `claude -p` 执行修复,不限于 Cursor
|
|
93
100
|
- 可用 `--agent claude-code` 或 `SDD_FLOW_KIT_ENABLE_*_AUTORUN=0` 控制
|
|
94
101
|
|
|
102
|
+
**1.3.12** — Confluence 关键字搜索稳定性(dosearchsite 主路径)
|
|
103
|
+
|
|
104
|
+
- `confluence-doc.py`:每个关键词优先打开 `dosearchsite.action?queryString=...` 专用搜索页(不再依赖 `#all-updates` 顶栏搜索)
|
|
105
|
+
- 匹配放宽:**标题命中或 URL 路径命中** 均可得分(`score_link_match` 原已支持,增强前缀匹配与 ADI 加权)
|
|
106
|
+
- `run_search_dosearch`:处理 `ERR_ABORTED`、搜索结果页导航重试;顶栏搜索降为兜底
|
|
107
|
+
- 新增 `CONFLUENCE_DEBUG=1` 输出选择器解析日志
|
|
108
|
+
- 更新 `01-adi-doc-skill-指引` / `doc-skill.SKILL`:关键字搜索为主,pageId 直链为可选兜底
|
|
109
|
+
|
|
95
110
|
**1.3.9** — PRD 不一致自动修复循环(最多 3 次)
|
|
96
111
|
|
|
97
112
|
- 新增 `prd-remediate`:`prd-diff` 失败 → 按 `09` 生成修复指引 → 调 AI 工具 `/opsx-apply` 修代码 → 再 `prd-diff`(**每项**最多 3 次)
|
package/dist/adapters/index.js
CHANGED
|
@@ -43,8 +43,9 @@ async function runClaudeFlow(args) {
|
|
|
43
43
|
"1) 01-获取需求文档指引:回填 source/PRD.md(及接口文档,如有)。",
|
|
44
44
|
"2) 02-需求分析:若矛盾点未闭合,仅更新 03-待确认问题清单.md 并停止;用户确认后生成 05→04→01/02/闭环03。",
|
|
45
45
|
"3) 用户确认后从 source/PRD.md 填写 05-验收清单.md(定义状态:已定义),再补全 04-技术文档草稿.md(引用 AC-ID)。",
|
|
46
|
-
"4) 03
|
|
46
|
+
"4) 03 闭环后:读 .session-state.json 的 executionMode;full-chain(默认)同一回合 C→D→E 直至 deliver exit 0;staged-confirm 每阶段等人确认。",
|
|
47
47
|
"5) 仅当用户说「只提案」或 autoChainOpsx=false 时,在 propose 后暂停等待 apply。",
|
|
48
|
+
"6) full-chain 时禁止以待办清单结束;唯一出口 deliver exit 0。",
|
|
48
49
|
"完成后输出一句:SDD flow completed",
|
|
49
50
|
"",
|
|
50
51
|
"以下是第2步提示词:",
|
|
@@ -92,12 +93,14 @@ async function runCursorFlow(args) {
|
|
|
92
93
|
"强约束:",
|
|
93
94
|
"1) 在 docs 阶段(docModeOnly=true)只允许改 openspec/PRD/**,禁止改 src/。",
|
|
94
95
|
"2) 若矛盾点池未闭合:仅更新 03-待确认问题清单.md 并停止,等待用户确认点。",
|
|
95
|
-
"3) 若矛盾点池已闭合:同一回合生成 05→04→01/02
|
|
96
|
+
"3) 若矛盾点池已闭合:同一回合生成 05→04→01/02;executionMode=full-chain 时继续 C→D→E 直至 deliver exit 0。",
|
|
97
|
+
"4) executionMode=staged-confirm 时:每阶段输出门禁结果后暂停等人确认。",
|
|
98
|
+
"5) 禁止用「后续待办」「骨架已完成」结束回合(full-chain 时)。",
|
|
96
99
|
"",
|
|
97
100
|
"执行顺序:",
|
|
98
101
|
"A) 先按 01-获取需求文档指引.md 执行 confluence-doc.py,把 PRD 回填到 source/PRD.md。",
|
|
99
102
|
"B) 再执行 02-需求分析提示词.md(如下)生成/更新 03(必要时)或生成 05/04/01/02。",
|
|
100
|
-
"C)
|
|
103
|
+
"C) full-chain:按 07-opsx-自动串联指引.md 走 propose→ac-ready→apply→validate→deliver;可用 chain 命令机械串联 gate。",
|
|
101
104
|
"",
|
|
102
105
|
"完成判定:",
|
|
103
106
|
"- 若停在澄清:输出一句 `SDD_STATUS=AWAIT_CONFIRMATION`。",
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,8 @@ const runDeliver_1 = require("./steps/runDeliver");
|
|
|
20
20
|
const runPrdReview_1 = require("./steps/runPrdReview");
|
|
21
21
|
const runPrdDiff_1 = require("./steps/runPrdDiff");
|
|
22
22
|
const runPrdRemediate_1 = require("./steps/runPrdRemediate");
|
|
23
|
+
const runChain_1 = require("./steps/runChain");
|
|
24
|
+
const runSessionMode_1 = require("./steps/runSessionMode");
|
|
23
25
|
const bootstrap_1 = require("./core/bootstrap");
|
|
24
26
|
const projectRoots_1 = require("./core/projectRoots");
|
|
25
27
|
const ALLOWED_AGENTS = ["cursor", "claude-code", "codex", "openclaw", "custom"];
|
|
@@ -85,6 +87,14 @@ function printHelp() {
|
|
|
85
87
|
" npx sdd-flow-kit phase advance --project-root . --run-id <runId> --to validate-done --change <name>",
|
|
86
88
|
" npx sdd-flow-kit deliver --project-root . --run-id <runId> --change <name>",
|
|
87
89
|
"",
|
|
90
|
+
"11) 机械串联 gate→phase→validate→deliver(AI 步骤须先完成):",
|
|
91
|
+
" npx sdd-flow-kit chain --project-root . --run-id <runId> --change <name> --from docs-done",
|
|
92
|
+
" # --from: docs-done | propose-done | impl | validate-done",
|
|
93
|
+
"",
|
|
94
|
+
"12) 设置执行模式:",
|
|
95
|
+
" npx sdd-flow-kit session mode --run-id <runId> --execution-mode full-chain",
|
|
96
|
+
" npx sdd-flow-kit session mode --run-id <runId> --execution-mode staged-confirm --strict-commands",
|
|
97
|
+
"",
|
|
88
98
|
"可选参数:",
|
|
89
99
|
" --projectRoot <path> 目标项目根目录(默认当前目录 = 安装根,cd 到哪装到哪)",
|
|
90
100
|
" 环境变量 SDD_FLOW_KIT_INSTALL_ROOT 可覆盖安装根(绝对路径)",
|
|
@@ -101,7 +111,7 @@ function printHelp() {
|
|
|
101
111
|
].join("\n"));
|
|
102
112
|
}
|
|
103
113
|
async function main() {
|
|
104
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
114
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
105
115
|
const cmd = process_1.default.argv[2];
|
|
106
116
|
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
107
117
|
printHelp();
|
|
@@ -127,6 +137,7 @@ async function main() {
|
|
|
127
137
|
"validate-done",
|
|
128
138
|
"shipped",
|
|
129
139
|
];
|
|
140
|
+
const CHAIN_FROM = ["docs-done", "propose-done", "impl", "validate-done"];
|
|
130
141
|
if (cmd !== "start" &&
|
|
131
142
|
cmd !== "run" &&
|
|
132
143
|
cmd !== "setup" &&
|
|
@@ -141,7 +152,9 @@ async function main() {
|
|
|
141
152
|
cmd !== "prd-review" &&
|
|
142
153
|
cmd !== "prd-diff" &&
|
|
143
154
|
cmd !== "prd-remediate" &&
|
|
144
|
-
cmd !== "deliver"
|
|
155
|
+
cmd !== "deliver" &&
|
|
156
|
+
cmd !== "chain" &&
|
|
157
|
+
cmd !== "session") {
|
|
145
158
|
printHelp();
|
|
146
159
|
process_1.default.exitCode = 1;
|
|
147
160
|
return;
|
|
@@ -188,6 +201,8 @@ async function main() {
|
|
|
188
201
|
"prd-diff",
|
|
189
202
|
"prd-remediate",
|
|
190
203
|
"deliver",
|
|
204
|
+
"chain",
|
|
205
|
+
"session",
|
|
191
206
|
]);
|
|
192
207
|
if (!skipBootstrap && !dryRun && bootstrapCommands.has(cmd)) {
|
|
193
208
|
await (0, bootstrap_1.bootstrapProjectIfNeeded)({
|
|
@@ -354,6 +369,59 @@ async function main() {
|
|
|
354
369
|
process_1.default.exitCode = 1;
|
|
355
370
|
return;
|
|
356
371
|
}
|
|
372
|
+
if (cmd === "chain") {
|
|
373
|
+
if (!changeName) {
|
|
374
|
+
// eslint-disable-next-line no-console
|
|
375
|
+
console.error("chain 需要 --change <kebab-name>");
|
|
376
|
+
process_1.default.exitCode = 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const from = ((_j = getArgValue("--from")) !== null && _j !== void 0 ? _j : "docs-done");
|
|
380
|
+
if (!CHAIN_FROM.includes(from)) {
|
|
381
|
+
// eslint-disable-next-line no-console
|
|
382
|
+
console.error(`chain 需要 --from ${CHAIN_FROM.join("|")}`);
|
|
383
|
+
process_1.default.exitCode = 1;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const result = await (0, runChain_1.runChain)({
|
|
387
|
+
projectRoot,
|
|
388
|
+
runId,
|
|
389
|
+
change: changeName,
|
|
390
|
+
from,
|
|
391
|
+
agent,
|
|
392
|
+
dryRun,
|
|
393
|
+
});
|
|
394
|
+
// eslint-disable-next-line no-console
|
|
395
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
396
|
+
if (!result.ok)
|
|
397
|
+
process_1.default.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (cmd === "session") {
|
|
401
|
+
const sub = process_1.default.argv[3];
|
|
402
|
+
if (sub !== "mode") {
|
|
403
|
+
// eslint-disable-next-line no-console
|
|
404
|
+
console.error("用法: sdd-flow-kit session mode --execution-mode full-chain|staged-confirm [--strict-commands]");
|
|
405
|
+
process_1.default.exitCode = 1;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const modeArg = (_k = getArgValue("--execution-mode")) !== null && _k !== void 0 ? _k : getArgValue("--executionMode");
|
|
409
|
+
const textArg = getArgValue("--text");
|
|
410
|
+
const strictCommands = process_1.default.argv.includes("--strict-commands");
|
|
411
|
+
const result = await (0, runSessionMode_1.runSessionMode)({
|
|
412
|
+
projectRoot,
|
|
413
|
+
runId,
|
|
414
|
+
executionMode: modeArg === "full-chain" || modeArg === "staged-confirm" ? modeArg : null,
|
|
415
|
+
strictCommands: strictCommands || undefined,
|
|
416
|
+
text: textArg,
|
|
417
|
+
dryRun,
|
|
418
|
+
});
|
|
419
|
+
// eslint-disable-next-line no-console
|
|
420
|
+
console.log(result.ok ? `✅ ${result.detail}` : `❌ ${result.detail}`);
|
|
421
|
+
if (!result.ok)
|
|
422
|
+
process_1.default.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
357
425
|
if (cmd === "doctor" || cmd === "ensure-opsx") {
|
|
358
426
|
const fix = cmd === "ensure-opsx" || process_1.default.argv.includes("--fix");
|
|
359
427
|
const doctor = await (0, doctor_1.runDoctor)({
|
|
@@ -426,6 +494,9 @@ async function main() {
|
|
|
426
494
|
agent,
|
|
427
495
|
});
|
|
428
496
|
const result = await (0, runFlow_1.runFlow)(options);
|
|
497
|
+
if (!dryRun) {
|
|
498
|
+
await (0, runSessionMode_1.applyInvokeTextToSession)(projectRoot, result.runId, text);
|
|
499
|
+
}
|
|
429
500
|
// eslint-disable-next-line no-console
|
|
430
501
|
console.log(`invoke 成功:${parsed.product} ${parsed.version} -> runId=${result.runId}`);
|
|
431
502
|
if (planJson) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseExecutionModeFromText = parseExecutionModeFromText;
|
|
4
|
+
exports.applyExecutionModeToSession = applyExecutionModeToSession;
|
|
5
|
+
exports.shouldAutoChainAfterConfirm = shouldAutoChainAfterConfirm;
|
|
6
|
+
exports.shouldPauseBetweenPhases = shouldPauseBetweenPhases;
|
|
7
|
+
exports.requiresStrictCommands = requiresStrictCommands;
|
|
8
|
+
exports.normalizeExecutionModeFields = normalizeExecutionModeFields;
|
|
9
|
+
const DEFAULT_MODE = {
|
|
10
|
+
executionMode: "full-chain",
|
|
11
|
+
strictCommands: false,
|
|
12
|
+
legacyStrictSop: false,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 从用户自然语言解析执行模式(invoke / 确认点粘贴均可调用)。
|
|
16
|
+
*/
|
|
17
|
+
function parseExecutionModeFromText(text) {
|
|
18
|
+
const t = text.trim();
|
|
19
|
+
if (!t)
|
|
20
|
+
return { ...DEFAULT_MODE };
|
|
21
|
+
const legacyStrictSop = /严格\s*SOP|不可替代/.test(t);
|
|
22
|
+
let executionMode = "full-chain";
|
|
23
|
+
if (/分阶段确认|staged-confirm|阶段确认制/.test(t) ||
|
|
24
|
+
legacyStrictSop) {
|
|
25
|
+
executionMode = "staged-confirm";
|
|
26
|
+
}
|
|
27
|
+
if (/确认后全链路|full-chain|禁止中途停止|全链路/.test(t)) {
|
|
28
|
+
executionMode = "full-chain";
|
|
29
|
+
}
|
|
30
|
+
const strictCommands = legacyStrictSop ||
|
|
31
|
+
/严格命令|strict-commands|禁止.*等价/.test(t);
|
|
32
|
+
return { executionMode, strictCommands, legacyStrictSop };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 将解析结果写入 session;force=true 时无条件覆盖。
|
|
36
|
+
*/
|
|
37
|
+
function applyExecutionModeToSession(session, parsed, force = false) {
|
|
38
|
+
const isDefault = parsed.executionMode === "full-chain" &&
|
|
39
|
+
!parsed.strictCommands &&
|
|
40
|
+
!parsed.legacyStrictSop;
|
|
41
|
+
if (!force && isDefault)
|
|
42
|
+
return session;
|
|
43
|
+
return {
|
|
44
|
+
...session,
|
|
45
|
+
executionMode: parsed.executionMode,
|
|
46
|
+
strictCommands: parsed.strictCommands || session.strictCommands === true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** 矛盾点闭合后是否同一回合自动 propose→apply(不看 staged-confirm,那是阶段间暂停) */
|
|
50
|
+
function shouldAutoChainAfterConfirm(session) {
|
|
51
|
+
if (session.autoChainOpsx === false)
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** 完成一个阶段(C/D/E)后是否必须等待用户确认再进下一阶段 */
|
|
56
|
+
function shouldPauseBetweenPhases(session) {
|
|
57
|
+
if (session.executionMode === "staged-confirm")
|
|
58
|
+
return true;
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/** 是否禁止用「等价命令」替代 NEXT.md / gate 链路 */
|
|
62
|
+
function requiresStrictCommands(session) {
|
|
63
|
+
return session.strictCommands === true;
|
|
64
|
+
}
|
|
65
|
+
function normalizeExecutionModeFields(raw) {
|
|
66
|
+
const modeRaw = raw.executionMode;
|
|
67
|
+
let executionMode = "full-chain";
|
|
68
|
+
if (modeRaw === "staged-confirm" || modeRaw === "full-chain") {
|
|
69
|
+
executionMode = modeRaw;
|
|
70
|
+
}
|
|
71
|
+
else if (raw.legacyStrictSop === true || raw.strictSop === true) {
|
|
72
|
+
executionMode = "staged-confirm";
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
executionMode,
|
|
76
|
+
strictCommands: raw.strictCommands === true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -9,6 +9,7 @@ exports.defaultSession = defaultSession;
|
|
|
9
9
|
exports.normalizeSession = normalizeSession;
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const fs_1 = require("./fs");
|
|
12
|
+
const executionMode_1 = require("./executionMode");
|
|
12
13
|
const SESSION_FILE = ".session-state.json";
|
|
13
14
|
async function readSession(outputRoot) {
|
|
14
15
|
const raw = await (0, fs_1.readFileIfExists)(path_1.default.join(outputRoot, SESSION_FILE));
|
|
@@ -29,6 +30,8 @@ function defaultSession(args) {
|
|
|
29
30
|
status: "awaiting_user_confirmation",
|
|
30
31
|
docModeOnly: true,
|
|
31
32
|
autoChainOpsx: true,
|
|
33
|
+
executionMode: "full-chain",
|
|
34
|
+
strictCommands: false,
|
|
32
35
|
activeChange: null,
|
|
33
36
|
createdAtISO: new Date().toISOString(),
|
|
34
37
|
implGitBaseline: null,
|
|
@@ -50,6 +53,7 @@ function normalizeSession(raw, outputRoot) {
|
|
|
50
53
|
status,
|
|
51
54
|
docModeOnly: raw.docModeOnly !== false,
|
|
52
55
|
autoChainOpsx: raw.autoChainOpsx !== false,
|
|
56
|
+
...(0, executionMode_1.normalizeExecutionModeFields)(raw),
|
|
53
57
|
activeChange: raw.activeChange ? String(raw.activeChange) : null,
|
|
54
58
|
createdAtISO: String((_e = raw.createdAtISO) !== null && _e !== void 0 ? _e : new Date().toISOString()),
|
|
55
59
|
implGitBaseline: raw.implGitBaseline ? String(raw.implGitBaseline) : null,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTurnEndPolicyMarkdown = getTurnEndPolicyMarkdown;
|
|
4
|
+
exports.getExecutionModeGuideMarkdown = getExecutionModeGuideMarkdown;
|
|
5
|
+
/**
|
|
6
|
+
* 回合结束条件(写入 NEXT.md / 07 指引,约束 AI 不得「待办清单式」提前收尾)。
|
|
7
|
+
*/
|
|
8
|
+
function getTurnEndPolicyMarkdown() {
|
|
9
|
+
return [
|
|
10
|
+
"## 回合结束条件(唯一合法出口)",
|
|
11
|
+
"",
|
|
12
|
+
"- ✅ `gate shipped` 通过 **且** `deliver` exit 0",
|
|
13
|
+
"- ⛔ **禁止**在 `tasks.md` 未全 `- [x]`、delivery-pipeline 未 exit 0 时结束回合",
|
|
14
|
+
"- ⛔ **禁止**用「后续待办」「骨架已完成」「文档已更新」作为结束语",
|
|
15
|
+
"- ⛔ **禁止**输出仅含待办清单的收尾(`executionMode=full-chain` 时)",
|
|
16
|
+
"- 唯一允许暂停:",
|
|
17
|
+
" - `gate` / `validate` / `deliver` exit≠0",
|
|
18
|
+
" - `prd-remediate` 单项 3 次未果需人工裁定",
|
|
19
|
+
" - 矛盾点池未闭合(提问模式)",
|
|
20
|
+
" - `executionMode=staged-confirm` 且当前阶段已完成(输出门禁结果后等人确认)",
|
|
21
|
+
" - 用户明确说「只提案」「先别写代码」或 `autoChainOpsx=false`",
|
|
22
|
+
"",
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
function getExecutionModeGuideMarkdown() {
|
|
26
|
+
return [
|
|
27
|
+
"## 执行模式(`.session-state.json`)",
|
|
28
|
+
"",
|
|
29
|
+
"| 字段 | 值 | 含义 |",
|
|
30
|
+
"|------|-----|------|",
|
|
31
|
+
"| `executionMode` | `full-chain`(默认) | 矛盾点闭合后 C→D→E 同一回合跑到底 |",
|
|
32
|
+
"| `executionMode` | `staged-confirm` | 每阶段完成后暂停,等人确认再继续 |",
|
|
33
|
+
"| `strictCommands` | `true` | 禁止用等价命令替代 NEXT.md 中的 gate/phase 链路 |",
|
|
34
|
+
"| `autoChainOpsx` | `false` | 文档完成后停止,不自动 propose/apply |",
|
|
35
|
+
"",
|
|
36
|
+
"**用户口令示例**",
|
|
37
|
+
"- 确认后全链路:`执行模式:确认后全链路,禁止中途停止`",
|
|
38
|
+
"- 分阶段确认:`执行模式:分阶段确认`",
|
|
39
|
+
"- 严格命令(可与全链路组合):`执行模式:严格命令`",
|
|
40
|
+
"- 兼容旧版:`执行模式:严格SOP,不可替代` → `staged-confirm` + `strictCommands`",
|
|
41
|
+
"",
|
|
42
|
+
"设置模式:",
|
|
43
|
+
"```bash",
|
|
44
|
+
"npx sdd-flow-kit session mode --run-id <runId> --execution-mode full-chain",
|
|
45
|
+
"npx sdd-flow-kit session mode --run-id <runId> --execution-mode staged-confirm --strict-commands",
|
|
46
|
+
"```",
|
|
47
|
+
"",
|
|
48
|
+
];
|
|
49
|
+
}
|
package/dist/core/writeNext.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.writeInitialNext = writeInitialNext;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const fs_1 = require("./fs");
|
|
9
|
+
const turnEndPolicy_1 = require("./turnEndPolicy");
|
|
9
10
|
/**
|
|
10
11
|
* run 初始化后写入 NEXT.md,强制 AI 按脚本推进而非自然语言跳步。
|
|
11
12
|
*/
|
|
@@ -16,6 +17,14 @@ async function writeInitialNext(outputRoot, projectRoot, runId) {
|
|
|
16
17
|
"",
|
|
17
18
|
"> **禁止**跳过下列命令。失败 (exit≠0) 时不得进入下一阶段或修改 `src/`。",
|
|
18
19
|
"",
|
|
20
|
+
...(0, turnEndPolicy_1.getTurnEndPolicyMarkdown)(),
|
|
21
|
+
...(0, turnEndPolicy_1.getExecutionModeGuideMarkdown)(),
|
|
22
|
+
"## 机械串联(可选,AI 步骤完成后)",
|
|
23
|
+
"```bash",
|
|
24
|
+
`npx sdd-flow-kit chain --project-root "${pr}" --run-id ${runId} --change <kebab-name> --from docs-done`,
|
|
25
|
+
"# --from 可选:docs-done | propose-done | impl | validate-done",
|
|
26
|
+
"```",
|
|
27
|
+
"",
|
|
19
28
|
"## 文档职责(SSOT)",
|
|
20
29
|
"",
|
|
21
30
|
"| 文档 | 职责 |",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runChain = runChain;
|
|
4
|
+
const runGate_1 = require("./runGate");
|
|
5
|
+
const runPhaseAdvance_1 = require("./runPhaseAdvance");
|
|
6
|
+
const runPrdReview_1 = require("./runPrdReview");
|
|
7
|
+
const runPrdDiff_1 = require("./runPrdDiff");
|
|
8
|
+
const runValidate_1 = require("./runValidate");
|
|
9
|
+
const runDeliver_1 = require("./runDeliver");
|
|
10
|
+
const resolveRun_1 = require("./resolveRun");
|
|
11
|
+
/**
|
|
12
|
+
* 机械串联 gate / phase / validate / deliver;AI 步骤(propose、apply、填 08)须先完成,否则在对应 gate 失败即停。
|
|
13
|
+
*/
|
|
14
|
+
async function runChain(options) {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
17
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved)) {
|
|
18
|
+
return { ok: false, runId: null, detail: "未找到 PRD run" };
|
|
19
|
+
}
|
|
20
|
+
const runId = loaded.resolved.runId;
|
|
21
|
+
const pr = options.projectRoot;
|
|
22
|
+
const change = options.change;
|
|
23
|
+
const agent = (_a = options.agent) !== null && _a !== void 0 ? _a : "cursor";
|
|
24
|
+
const allSteps = [
|
|
25
|
+
{
|
|
26
|
+
label: "gate docs-closed",
|
|
27
|
+
run: async () => {
|
|
28
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "docs-closed" });
|
|
29
|
+
if (!g.ok)
|
|
30
|
+
(0, runGate_1.printGateResult)(g);
|
|
31
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "phase advance docs-done",
|
|
36
|
+
run: async () => {
|
|
37
|
+
const r = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
38
|
+
projectRoot: pr,
|
|
39
|
+
runId,
|
|
40
|
+
to: "docs-done",
|
|
41
|
+
dryRun: options.dryRun,
|
|
42
|
+
});
|
|
43
|
+
return { ok: r.ok, detail: r.message };
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: "gate propose-ready",
|
|
48
|
+
run: async () => {
|
|
49
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "propose-ready", change });
|
|
50
|
+
if (!g.ok)
|
|
51
|
+
(0, runGate_1.printGateResult)(g);
|
|
52
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: "gate ac-ready",
|
|
57
|
+
run: async () => {
|
|
58
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "ac-ready", change });
|
|
59
|
+
if (!g.ok)
|
|
60
|
+
(0, runGate_1.printGateResult)(g);
|
|
61
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "phase advance propose-done",
|
|
66
|
+
run: async () => {
|
|
67
|
+
const r = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
68
|
+
projectRoot: pr,
|
|
69
|
+
runId,
|
|
70
|
+
to: "propose-done",
|
|
71
|
+
change,
|
|
72
|
+
dryRun: options.dryRun,
|
|
73
|
+
});
|
|
74
|
+
return { ok: r.ok, detail: r.message };
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: "phase advance impl",
|
|
79
|
+
run: async () => {
|
|
80
|
+
const r = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
81
|
+
projectRoot: pr,
|
|
82
|
+
runId,
|
|
83
|
+
to: "impl",
|
|
84
|
+
change,
|
|
85
|
+
dryRun: options.dryRun,
|
|
86
|
+
});
|
|
87
|
+
return { ok: r.ok, detail: r.message };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
label: "gate impl-allowed",
|
|
92
|
+
run: async () => {
|
|
93
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "impl-allowed", change });
|
|
94
|
+
if (!g.ok)
|
|
95
|
+
(0, runGate_1.printGateResult)(g);
|
|
96
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: "prd-review",
|
|
101
|
+
run: async () => {
|
|
102
|
+
const r = await (0, runPrdReview_1.runPrdReview)({ projectRoot: pr, runId, dryRun: options.dryRun });
|
|
103
|
+
return { ok: r.ok, detail: r.detail };
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: "prd-diff",
|
|
108
|
+
run: async () => {
|
|
109
|
+
const r = await (0, runPrdDiff_1.runPrdDiff)({ projectRoot: pr, runId, dryRun: options.dryRun });
|
|
110
|
+
return { ok: r.ok, detail: r.detail };
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
label: "gate prd-coverage",
|
|
115
|
+
run: async () => {
|
|
116
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "prd-coverage", change });
|
|
117
|
+
if (!g.ok)
|
|
118
|
+
(0, runGate_1.printGateResult)(g);
|
|
119
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: "validate",
|
|
124
|
+
run: async () => {
|
|
125
|
+
const r = await (0, runValidate_1.runValidate)({
|
|
126
|
+
projectRoot: pr,
|
|
127
|
+
runId,
|
|
128
|
+
change,
|
|
129
|
+
agent,
|
|
130
|
+
dryRun: options.dryRun,
|
|
131
|
+
});
|
|
132
|
+
return { ok: r.ok, detail: r.detail };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "phase advance validate-done",
|
|
137
|
+
run: async () => {
|
|
138
|
+
const r = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
139
|
+
projectRoot: pr,
|
|
140
|
+
runId,
|
|
141
|
+
to: "validate-done",
|
|
142
|
+
change,
|
|
143
|
+
dryRun: options.dryRun,
|
|
144
|
+
});
|
|
145
|
+
return { ok: r.ok, detail: r.message };
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: "gate ac-signed",
|
|
150
|
+
run: async () => {
|
|
151
|
+
const g = await (0, runGate_1.runGate)({ projectRoot: pr, runId, expect: "ac-signed" });
|
|
152
|
+
if (!g.ok)
|
|
153
|
+
(0, runGate_1.printGateResult)(g);
|
|
154
|
+
return { ok: g.ok, detail: g.checks.map((c) => c.detail).join("; ") };
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
label: "deliver",
|
|
159
|
+
run: async () => {
|
|
160
|
+
const r = await (0, runDeliver_1.runDeliver)({
|
|
161
|
+
projectRoot: pr,
|
|
162
|
+
runId,
|
|
163
|
+
change,
|
|
164
|
+
agent,
|
|
165
|
+
dryRun: options.dryRun,
|
|
166
|
+
});
|
|
167
|
+
return { ok: r.ok, detail: r.detail };
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: "phase advance shipped",
|
|
172
|
+
run: async () => {
|
|
173
|
+
const r = await (0, runPhaseAdvance_1.runPhaseAdvance)({
|
|
174
|
+
projectRoot: pr,
|
|
175
|
+
runId,
|
|
176
|
+
to: "shipped",
|
|
177
|
+
change,
|
|
178
|
+
dryRun: options.dryRun,
|
|
179
|
+
});
|
|
180
|
+
return { ok: r.ok, detail: r.message };
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
const startIndex = {
|
|
185
|
+
"docs-done": 0,
|
|
186
|
+
"propose-done": 3,
|
|
187
|
+
impl: 5,
|
|
188
|
+
"validate-done": 11,
|
|
189
|
+
};
|
|
190
|
+
const steps = allSteps.slice(startIndex[options.from]);
|
|
191
|
+
if (options.dryRun) {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
runId,
|
|
195
|
+
detail: `dry-run: 将依次执行 ${steps.map((s) => s.label).join(" → ")}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
for (const step of steps) {
|
|
199
|
+
// eslint-disable-next-line no-console
|
|
200
|
+
console.log(`[chain] ${step.label} ...`);
|
|
201
|
+
const result = await step.run();
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
runId,
|
|
206
|
+
failedStep: step.label,
|
|
207
|
+
detail: `${step.label} 失败:${(_b = result.detail) !== null && _b !== void 0 ? _b : "exit≠0"}。请先完成 AI 步骤(propose/apply/填 08/签收 05)后重试 chain。`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { ok: true, runId, detail: "chain 完成:已执行至 shipped" };
|
|
212
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSessionMode = runSessionMode;
|
|
4
|
+
exports.applyInvokeTextToSession = applyInvokeTextToSession;
|
|
5
|
+
const sessionState_1 = require("../core/sessionState");
|
|
6
|
+
const executionMode_1 = require("../core/executionMode");
|
|
7
|
+
const resolveRun_1 = require("./resolveRun");
|
|
8
|
+
/**
|
|
9
|
+
* 更新 run 的 executionMode / strictCommands(用户口令或 CLI flag)。
|
|
10
|
+
*/
|
|
11
|
+
async function runSessionMode(options) {
|
|
12
|
+
var _a;
|
|
13
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(options.projectRoot, options.runId);
|
|
14
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved) || !loaded.session) {
|
|
15
|
+
return { ok: false, runId: null, detail: "未找到 run 或 .session-state.json" };
|
|
16
|
+
}
|
|
17
|
+
const { resolved, session } = loaded;
|
|
18
|
+
let parsed = (0, executionMode_1.parseExecutionModeFromText)((_a = options.text) !== null && _a !== void 0 ? _a : "");
|
|
19
|
+
if (options.executionMode) {
|
|
20
|
+
parsed = { ...parsed, executionMode: options.executionMode };
|
|
21
|
+
}
|
|
22
|
+
if (options.strictCommands === true) {
|
|
23
|
+
parsed = { ...parsed, strictCommands: true };
|
|
24
|
+
}
|
|
25
|
+
const next = (0, executionMode_1.applyExecutionModeToSession)(session, parsed, true);
|
|
26
|
+
if (!options.dryRun) {
|
|
27
|
+
await (0, sessionState_1.writeSession)(resolved.outputRoot, next);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
runId: resolved.runId,
|
|
32
|
+
detail: `executionMode=${next.executionMode}, strictCommands=${next.strictCommands === true}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** invoke 完成后根据用户口令写入 session 模式 */
|
|
36
|
+
async function applyInvokeTextToSession(projectRoot, runId, invokeText) {
|
|
37
|
+
const loaded = await (0, resolveRun_1.loadRunSession)(projectRoot, runId);
|
|
38
|
+
if (!(loaded === null || loaded === void 0 ? void 0 : loaded.resolved))
|
|
39
|
+
return;
|
|
40
|
+
const session = await (0, sessionState_1.readSession)(loaded.resolved.outputRoot);
|
|
41
|
+
if (!session)
|
|
42
|
+
return;
|
|
43
|
+
const parsed = (0, executionMode_1.parseExecutionModeFromText)(invokeText);
|
|
44
|
+
const next = (0, executionMode_1.applyExecutionModeToSession)(session, parsed, false);
|
|
45
|
+
if (next.executionMode !== session.executionMode ||
|
|
46
|
+
next.strictCommands !== session.strictCommands) {
|
|
47
|
+
await (0, sessionState_1.writeSession)(loaded.resolved.outputRoot, next);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdd-flow-kit",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Cross-agent SDD automated development workflow kit (ADI/ADI-like).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^20.14.10",
|
|
28
28
|
"typescript": "^5.6.3"
|
|
29
|
+
},
|
|
30
|
+
"pnpm": {
|
|
31
|
+
"onlyBuiltDependencies": [
|
|
32
|
+
"sdd-flow-kit"
|
|
33
|
+
]
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
|
|
@@ -12,14 +12,18 @@ python3 confluence-doc.py {{VERSION_EXAMPLE}}
|
|
|
12
12
|
|
|
13
13
|
版本号可传:`2.3.2`、`V2.3.2`、`ADI_V2.3.2`、`ADI-V2.3.2`(脚本会归一化)。
|
|
14
14
|
|
|
15
|
-
2.
|
|
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.
|
|
17
|
+
3. 匹配规则:**标题包含关键词或以关键词为前缀**,或 **URL 路径包含关键词**,任一满足即命中(Confluence 列表标题常被截断)。
|
|
18
18
|
|
|
19
|
-
4.
|
|
19
|
+
4. **禁止**在 Confluence 脚本未跑完或未确认失败前,用本地 Word/原型替代 `source/PRD.md`。
|
|
20
|
+
|
|
21
|
+
5. 成功后把生成的 `docs/*.md` **主文档**复制/回填到本 run 目录:
|
|
20
22
|
- `source/PRD.md`
|
|
21
23
|
|
|
22
|
-
##
|
|
24
|
+
## 直链兜底(可选,非默认)
|
|
25
|
+
|
|
26
|
+
仅在关键字搜索全部失败、且已知 pageId 时使用:
|
|
23
27
|
|
|
24
28
|
ADI V2.3.4 示例(`yyzcpt` 空间 pageId `109609740`):
|
|
25
29
|
|
|
@@ -44,8 +48,8 @@ python3 confluence-doc.py 2.3.4
|
|
|
44
48
|
|
|
45
49
|
## 失败排查
|
|
46
50
|
|
|
47
|
-
- 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V
|
|
48
|
-
-
|
|
51
|
+
- 输出 `❌ 未找到`:到 Confluence 确认页面标题(ADI 常为 `ADI_V2.3.4结算、开票…`,**下划线**而非 `ADI-V`);可尝试输入更长关键词如 `ADI_V2.3.4结算`
|
|
52
|
+
- 页面存在但搜不到:加 `CONFLUENCE_DEBUG=1` 查看选择器解析日志;或(可选)设置 `CONFLUENCE_PAGE_ID` / `CONFLUENCE_PAGE_URL` 直链下载
|
|
49
53
|
- Playwright 失败且设置 `CONFLUENCE_USE_REST=1` 时才回退 Token;登录最多重试 3 次,并用 `expect_navigation` 避免竞态
|
|
50
54
|
- 登录偶发失败:用 `CONFLUENCE_HEADED=1 CONFLUENCE_SLOW_MO=500` 有头观察;失败截图 `confluence-login-fail-*.png`
|
|
51
55
|
- `playwright` 未安装:`pip install playwright && playwright install chromium`
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- `source/PRD.md` 若仍是占位文本(没有真实 PRD 内容),**禁止**继续 Step2,先返回“Step1 未完成”并停止。
|
|
13
13
|
- 当处于“提问模式”时,**只允许更新** `03-待确认问题清单.md`;**禁止**产出最终版 `01-需求分析报告.md` / `02-改动点清单.md`。
|
|
14
14
|
- 只有当用户明确确认“矛盾点池已闭合”后,才可进入“交付模式”并输出最终版 `01` / `02`。
|
|
15
|
+
- 用户口令 `执行模式:确认后全链路` → `.session-state.json` 中 `executionMode: full-chain`(矛盾点闭合后禁止中途停止)。
|
|
15
16
|
|
|
16
17
|
## 输入注入(建议做法)
|
|
17
18
|
- 将 `source/PRD.md` 的内容作为 `{{PRD_MARKDOWN}}` 注入
|
|
@@ -46,27 +46,43 @@ npx sdd-flow-kit run --product {{DEFAULT_PRODUCT}} --version <版本号> -y --ag
|
|
|
46
46
|
- `.cursor/skills/opsx-superpowers-workflow/SKILL.md`
|
|
47
47
|
- `openspec-propose` → `openspec-apply-change` → `pnpm run opsx:delivery-pipeline -- <change>`
|
|
48
48
|
8. TDD/Playwright 遵循 `LAYERED-TESTING.md`(L2 交互契约、L3 每页 E2E)。
|
|
49
|
+
9. **`executionMode=full-chain`(默认)**:矛盾点闭合后 **禁止** 以待办清单结束回合;必须 C→D→E 直至 `deliver` exit 0。
|
|
49
50
|
|
|
50
|
-
##
|
|
51
|
+
## 执行模式(`.session-state.json`)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
| 口令 | `executionMode` | `strictCommands` | 行为 |
|
|
54
|
+
|------|-----------------|------------------|------|
|
|
55
|
+
| (默认) | `full-chain` | `false` | 确认后同一回合跑到底 |
|
|
56
|
+
| `执行模式:确认后全链路` | `full-chain` | — | 同上,禁止中途停止 |
|
|
57
|
+
| `执行模式:分阶段确认` | `staged-confirm` | — | 每阶段完成后等人确认 |
|
|
58
|
+
| `执行模式:严格命令` | — | `true` | 禁止等价命令替代 gate 链路 |
|
|
59
|
+
| `严格SOP,不可替代`(兼容) | `staged-confirm` | `true` | 分阶段确认 + 严格命令 |
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
```bash
|
|
62
|
+
npx sdd-flow-kit session mode --run-id <runId> --execution-mode full-chain
|
|
63
|
+
npx sdd-flow-kit chain --run-id <runId> --change <name> --from docs-done
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 回合结束条件
|
|
67
|
+
|
|
68
|
+
- ✅ 唯一合法出口:`gate shipped` + `deliver` exit 0
|
|
69
|
+
- ⛔ 禁止「骨架已完成」「后续待办」式收尾(`full-chain` 时)
|
|
70
|
+
- ⛔ 禁止在 `tasks.md` 未全 `[x]` 时宣称完成
|
|
71
|
+
|
|
72
|
+
## 严格命令模式(`strictCommands=true`)
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
与是否分阶段暂停**正交**;可与 `full-chain` 组合:
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
1. 必须严格按 `NEXT.md` 中的 gate/phase 链路执行,禁止“等价方式”替代。
|
|
77
|
+
2. Step1 须走 Confluence/脚本链路;除非用户明确允许,禁止手动回填代替。
|
|
78
|
+
3. 阶段 B 的 `04` 须可执行且逐条引用 AC-ID。
|
|
79
|
+
4. 阶段 C 须 `/opsx-propose`(输入 **05 + 04 + 02 + UI**),禁止仅用 `sdd-flow-kit propose` 代替 AI 提案生成。
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
## 分阶段确认模式(`executionMode=staged-confirm`)
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
1. 每阶段前先输出:`本次计划命令清单(逐条)`
|
|
84
|
+
2. 每阶段后输出:`门禁结果 + 证据文件路径 + 下一步命令`
|
|
85
|
+
3. **等待用户确认**后再进入下一阶段
|
|
70
86
|
|
|
71
87
|
## 阶段摘要
|
|
72
88
|
|
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
> **不要**在阶段 B 结束后停下来问用户「是否继续」或「是否 apply」。
|
|
4
4
|
> 除非用户明确说「只提案」「先别写代码」,或 `.session-state.json` 中 `autoChainOpsx: false`。
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
## 执行模式(读 `.session-state.json`)
|
|
7
|
+
|
|
8
|
+
| `executionMode` | 行为 |
|
|
9
|
+
|-----------------|------|
|
|
10
|
+
| `full-chain`(**默认**) | 矛盾点闭合后 **同一回合** C→D→E,直至 `deliver` exit 0 |
|
|
11
|
+
| `staged-confirm` | 每阶段完成后输出门禁结果,**等待用户确认**再进下一阶段 |
|
|
12
|
+
|
|
13
|
+
| `strictCommands` | 行为 |
|
|
14
|
+
|------------------|------|
|
|
15
|
+
| `true` | 禁止用等价命令替代 NEXT.md 中的 gate/phase 链路(可与 full-chain 组合) |
|
|
16
|
+
| `false`(默认) | 按 NEXT.md 执行即可 |
|
|
17
|
+
|
|
18
|
+
**用户口令**:`执行模式:确认后全链路` → `full-chain`;`执行模式:分阶段确认` → `staged-confirm`;`执行模式:严格命令` → `strictCommands: true`。
|
|
19
|
+
**兼容旧版**:`执行模式:严格SOP,不可替代` → `staged-confirm` + `strictCommands: true`。
|
|
20
|
+
|
|
21
|
+
## 回合结束条件(唯一合法出口)
|
|
22
|
+
|
|
23
|
+
- ✅ `gate shipped` 通过 **且** `deliver` exit 0
|
|
24
|
+
- ⛔ **禁止**在 `tasks.md` 未全 `- [x]`、pipeline 未 exit 0 时结束回合
|
|
25
|
+
- ⛔ **禁止**用「后续待办」「骨架已完成」作为结束语
|
|
26
|
+
- 唯一允许暂停:gate exit≠0、prd-remediate 3 次未果、矛盾点未闭合、`staged-confirm` 阶段间确认、`autoChainOpsx=false`
|
|
6
27
|
|
|
7
28
|
## 输入(本 runId)
|
|
8
29
|
|
|
@@ -33,18 +54,20 @@
|
|
|
33
54
|
```
|
|
34
55
|
5. 更新 `06-openspec-提案草稿.md` 与 `.session-state.json`:`status: propose_done`
|
|
35
56
|
6. **禁止**在本阶段写 `src/` 或跑 delivery-pipeline
|
|
36
|
-
7.
|
|
57
|
+
7. 阶段完成后输出:`门禁结果 + 证据文件路径 + 下一步命令`
|
|
58
|
+
8. **`executionMode=staged-confirm` 时**:等待用户确认后再进入阶段 D;**`full-chain` 时不得停止**
|
|
37
59
|
|
|
38
|
-
## 阶段 D — `/opsx-apply`(ac-ready
|
|
60
|
+
## 阶段 D — `/opsx-apply`(ac-ready 后执行)
|
|
39
61
|
|
|
40
|
-
>
|
|
62
|
+
> `full-chain`:**立即**执行,无需再等用户。`staged-confirm`:须先获得用户确认。
|
|
41
63
|
|
|
42
64
|
1. `npx sdd-flow-kit ensure-opsx --agent cursor -y`(仓库根)
|
|
43
|
-
2.
|
|
44
|
-
3.
|
|
45
|
-
4.
|
|
46
|
-
5.
|
|
47
|
-
6.
|
|
65
|
+
2. `npx sdd-flow-kit phase advance --to impl --change <name>`
|
|
66
|
+
3. 写 `.opsx-active-change` = change 名
|
|
67
|
+
4. **Apply 前必读**:`source/PRD.md` 对应章节 + `05-验收清单.md`
|
|
68
|
+
5. 读并执行:`openspec-apply-change` → `tdd-script`(分层测试策略)→ `openspec-superpowers-pipeline` → `opsx-dev-delivery-pipeline`
|
|
69
|
+
6. Red 断言须写死 PRD/05 原文(toast、tooltip、列序等)
|
|
70
|
+
7. `pnpm run opsx:delivery-pipeline -- <change-name>` 直至 **exit 0**
|
|
48
71
|
|
|
49
72
|
## 阶段 E — PRD 一致性复查 + 脚本语义 diff + 强制验收(交付前)
|
|
50
73
|
|
|
@@ -53,6 +76,7 @@
|
|
|
53
76
|
3. `npx sdd-flow-kit prd-diff --project-root <repo> --run-id <runId>`(脚本 PRD×代码字面量 diff → `09-PRD语义diff报告.md`)
|
|
54
77
|
4. 若 diff 失败:`prd-remediate --change <name>` 或 `validate` 内置循环(按 09 调 `/opsx-apply` 修,**每项最多 3 次**、每轮只修剩余失败项,重试附带上次错误代码摘要;仍失败 → 08 待人工确认)
|
|
55
78
|
5. `npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect prd-coverage`
|
|
79
|
+
6. 机械串联(AI 步骤完成后):`npx sdd-flow-kit chain --from validate-done --change <name>`
|
|
56
80
|
|
|
57
81
|
## 完成判定
|
|
58
82
|
|
|
@@ -98,20 +98,21 @@
|
|
|
98
98
|
#### 路径 B:交付模式 (矛盾点池已闭合)
|
|
99
99
|
> **触发条件**:用户已回复待确认点,或明确表示矛盾点池已闭合(不得在未获用户确认时自行进入)。
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
**任务(阶段 B 文档产出;`full-chain` 模式下不得在本路径结束回合)**:
|
|
102
102
|
1. 更新 `03-待确认问题清单.md`,标注“**矛盾点池状态:已闭合**”,写入用户确认结论。
|
|
103
103
|
2. **从 `source/PRD.md` 生成 `05-验收清单.md`**(标注「定义状态:已定义」),再生成 `04-技术文档草稿.md`(引用 AC-ID)。
|
|
104
104
|
3. 生成最终版 `01-需求分析报告.md` 和 `02-改动点清单.md`(清单中的路径是**计划**,不得在本回合修改这些文件)。
|
|
105
105
|
4. 确认 `02-改动点清单.md` 中仅包含代码文件改动,不涉及 `openspec/spec/` 内容。
|
|
106
106
|
5. **禁止** 在阶段 B 内修改 `src/` 等业务代码。
|
|
107
|
-
6. 更新 `.session-state.json`:`status: ready_for_propose
|
|
108
|
-
7.
|
|
109
|
-
8.
|
|
110
|
-
9.
|
|
107
|
+
6. 更新 `.session-state.json`:`status: ready_for_propose`;若用户口令含执行模式,同步 `executionMode` / `strictCommands`。
|
|
108
|
+
7. 执行 `gate docs-closed` → `phase advance --to docs-done`(exit 0 才继续)。
|
|
109
|
+
8. **若 `autoChainOpsx !== false` 且 `executionMode !== staged-confirm`(默认 full-chain)**:**不得结束回合**,同一回合继续读 `07-opsx-自动串联指引.md`,执行 C→D→E 直至 `deliver` exit 0;**不要**要求用户再说「继续」。
|
|
110
|
+
9. 仅当 `autoChainOpsx === false` 时输出:`05/01/02/03/04 已更新。回复「继续」进入提案。`
|
|
111
|
+
10. **`executionMode=staged-confirm` 时**(含兼容口令「严格SOP,不可替代」):
|
|
111
112
|
- 每阶段执行前先输出:`本次计划命令清单(逐条)`
|
|
112
113
|
- 每阶段执行后输出:`门禁结果 + 证据文件路径 + 下一步命令`
|
|
113
114
|
- 等待用户确认后再进入下一阶段
|
|
114
|
-
|
|
115
|
+
11. **`strictCommands=true` 时**:禁止用等价命令替代 NEXT.md 中的 gate/phase 链路(可与 full-chain 组合,不强制每阶段暂停)
|
|
115
116
|
|
|
116
117
|
---
|
|
117
118
|
|
|
@@ -24,6 +24,17 @@ alwaysApply: true
|
|
|
24
24
|
- `npx sdd-flow-kit phase advance --to impl` + `gate --expect impl-allowed`(阶段 D 前)
|
|
25
25
|
4. **禁止**在未执行 `gate prd-coverage`、`gate ac-signed` 与 `npx sdd-flow-kit deliver` 成功前宣称需求开发完成。
|
|
26
26
|
|
|
27
|
+
## 禁止过早结束(对称于「禁止过早写代码」)
|
|
28
|
+
|
|
29
|
+
当 `executionMode=full-chain`(默认)且 `autoChainOpsx !== false`:
|
|
30
|
+
|
|
31
|
+
- **禁止**在 propose/apply/validate/deliver 未完成前结束回合
|
|
32
|
+
- **禁止**输出仅含「后续待办」「骨架已完成」的收尾
|
|
33
|
+
- 矛盾点闭合后须同一回合继续 C→D→E,直至 `deliver` exit 0
|
|
34
|
+
- 唯一允许暂停:gate exit≠0、prd-remediate 需人工裁定、用户说「只提案」
|
|
35
|
+
|
|
36
|
+
`executionMode=staged-confirm` 时:每阶段完成后可暂停等人确认。
|
|
37
|
+
|
|
27
38
|
## docModeOnly
|
|
28
39
|
|
|
29
40
|
当 `openspec/PRD/*/.session-state.json` 中 `docModeOnly: true`:
|
|
@@ -68,6 +79,8 @@ npx sdd-flow-kit validate --project-root <repo> --run-id <runId> --change <name>
|
|
|
68
79
|
npx sdd-flow-kit gate --project-root <repo> --run-id <runId> --expect ac-signed
|
|
69
80
|
npx sdd-flow-kit deliver --project-root <repo> --run-id <runId> --change <name>
|
|
70
81
|
npx sdd-flow-kit phase advance --project-root <repo> --run-id <runId> --to shipped --change <name>
|
|
82
|
+
# 或机械串联(AI 步骤完成后):
|
|
83
|
+
npx sdd-flow-kit chain --project-root <repo> --run-id <runId> --change <name> --from docs-done
|
|
71
84
|
```
|
|
72
85
|
|
|
73
86
|
## 分层测试(L1–L4)
|
|
@@ -177,8 +177,10 @@ def href_path_matches_keyword(href: str, keyword: str) -> bool:
|
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
def text_matches_keyword(text: str, keyword: str) -> bool:
|
|
180
|
-
"""标题/链接文案匹配:忽略 -/_
|
|
181
|
-
|
|
180
|
+
"""标题/链接文案匹配:忽略 -/_ 差异;支持前缀命中(如 ADI_V2.3.4结算…)。"""
|
|
181
|
+
t_norm = normalize_match_token(text or "")
|
|
182
|
+
kw_norm = normalize_match_token(keyword)
|
|
183
|
+
return kw_norm in t_norm or t_norm.startswith(kw_norm)
|
|
182
184
|
|
|
183
185
|
|
|
184
186
|
def score_link_match(text: str, href: str, prefix: str, version: str, keyword: str) -> int:
|
|
@@ -212,22 +214,34 @@ def score_link_match(text: str, href: str, prefix: str, version: str, keyword: s
|
|
|
212
214
|
space = env("CONFLUENCE_SPACE", "").lower()
|
|
213
215
|
if space and f"/spaces/{space}/" in href.lower():
|
|
214
216
|
score += 5
|
|
217
|
+
# ADI/产品 PRD 标题或 URL 含版本号时加权,降低误命中其他空间页面
|
|
218
|
+
if re.search(r"adi[-_]?v", text, re.IGNORECASE) or re.search(
|
|
219
|
+
r"adi[-_]?v", unquote(href), re.IGNORECASE
|
|
220
|
+
):
|
|
221
|
+
score += 2
|
|
215
222
|
return score
|
|
216
223
|
|
|
217
224
|
|
|
218
225
|
def collect_search_candidates(
|
|
219
226
|
page, base_url: str, prefix: str, version: str, keyword: str
|
|
220
227
|
) -> list[tuple[str, str, int]]:
|
|
221
|
-
"""返回 [(url, title, score), ...]"""
|
|
228
|
+
"""返回 [(url, title, score), ...];标题命中或 URL 路径命中均可得分(>=2)。"""
|
|
222
229
|
seen: set[str] = set()
|
|
223
230
|
scored: list[tuple[int, str, str]] = []
|
|
231
|
+
debug = env_bool("CONFLUENCE_DEBUG")
|
|
224
232
|
|
|
225
233
|
selectors = [
|
|
226
234
|
".search-result .title a",
|
|
227
235
|
".search-result-title a",
|
|
228
236
|
"#search-results .gs-title a",
|
|
237
|
+
"ol.search-results-list a",
|
|
238
|
+
".search-results a",
|
|
239
|
+
".search-result a",
|
|
240
|
+
"#search-results a",
|
|
229
241
|
".search-results-panel a",
|
|
230
242
|
"a[data-linked-resource-id]",
|
|
243
|
+
f"a[href*='{keyword}']",
|
|
244
|
+
f"a:has-text('{keyword}')",
|
|
231
245
|
"a[href*='/pages/']",
|
|
232
246
|
"a[href*='/display/']",
|
|
233
247
|
]
|
|
@@ -235,6 +249,8 @@ def collect_search_candidates(
|
|
|
235
249
|
for selector in selectors:
|
|
236
250
|
links = page.locator(selector)
|
|
237
251
|
count = min(links.count(), 200)
|
|
252
|
+
if debug and count:
|
|
253
|
+
print(f" [debug] selector={selector!r} count={count}")
|
|
238
254
|
for i in range(count):
|
|
239
255
|
href = links.nth(i).get_attribute("href") or ""
|
|
240
256
|
raw_text = (links.nth(i).inner_text() or "").strip()
|
|
@@ -252,6 +268,9 @@ def collect_search_candidates(
|
|
|
252
268
|
if s >= 2:
|
|
253
269
|
scored.append((s, full_url, text[:120]))
|
|
254
270
|
|
|
271
|
+
if debug and not scored:
|
|
272
|
+
print(" [debug] 未解析到 score>=2 的 wiki 页面链接")
|
|
273
|
+
|
|
255
274
|
scored.sort(key=lambda x: x[0], reverse=True)
|
|
256
275
|
return [(u, t, s) for s, u, t in scored]
|
|
257
276
|
|
|
@@ -625,19 +644,36 @@ def try_rest_download(
|
|
|
625
644
|
|
|
626
645
|
|
|
627
646
|
def run_search_dosearch(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
|
|
647
|
+
"""打开 Confluence 全站专用搜索页(dosearchsite),比首页 #all-updates 顶栏搜索更稳定。"""
|
|
628
648
|
url = f"{base_url.rstrip('/')}/dosearchsite.action?queryString={quote(keyword)}"
|
|
629
|
-
print(f" →
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
649
|
+
print(f" → 专用搜索页: .../dosearchsite.action?queryString={quote(keyword)}")
|
|
650
|
+
for attempt in range(2):
|
|
651
|
+
try:
|
|
652
|
+
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
|
653
|
+
except Exception as exc:
|
|
654
|
+
# 搜索页常有重定向/长连接,networkidle 易误报 ERR_ABORTED
|
|
655
|
+
if "ERR_ABORTED" not in str(exc) and "Timeout" not in str(exc):
|
|
656
|
+
raise
|
|
657
|
+
if attempt == 0:
|
|
658
|
+
print(" 导航被中断,等待搜索结果页稳定...")
|
|
659
|
+
try:
|
|
660
|
+
page.goto(url, wait_until="load", timeout=60000)
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
page.wait_for_load_state("domcontentloaded", timeout=30000)
|
|
664
|
+
page.wait_for_timeout(2500)
|
|
665
|
+
if is_login_page(page):
|
|
666
|
+
return []
|
|
667
|
+
current = page.url.lower()
|
|
668
|
+
on_search = "dosearchsite" in current or "search" in current
|
|
669
|
+
if on_search or attempt == 1:
|
|
670
|
+
break
|
|
671
|
+
print(" 未停留在搜索结果页,重试一次...")
|
|
637
672
|
return collect_search_candidates(page, base_url, prefix, version, keyword)
|
|
638
673
|
|
|
639
674
|
|
|
640
675
|
def run_search_quick(page, base_url: str, prefix: str, version: str, keyword: str) -> list[tuple[str, str, int]]:
|
|
676
|
+
"""顶栏快速搜索(dosearchsite 无结果时的兜底,非主路径)。"""
|
|
641
677
|
search_selector = None
|
|
642
678
|
for selector in [
|
|
643
679
|
"#quick-search-query",
|
|
@@ -653,10 +689,10 @@ def run_search_quick(page, base_url: str, prefix: str, version: str, keyword: st
|
|
|
653
689
|
break
|
|
654
690
|
if not search_selector:
|
|
655
691
|
return []
|
|
656
|
-
print(f" →
|
|
692
|
+
print(f" → 顶栏搜索框(兜底): {search_selector} 关键词: {keyword}")
|
|
657
693
|
page.fill(search_selector, keyword, timeout=8000)
|
|
658
694
|
page.keyboard.press("Enter")
|
|
659
|
-
page.wait_for_load_state("
|
|
695
|
+
page.wait_for_load_state("domcontentloaded", timeout=30000)
|
|
660
696
|
page.wait_for_timeout(2500)
|
|
661
697
|
return collect_search_candidates(page, base_url, prefix, version, keyword)
|
|
662
698
|
|
|
@@ -60,9 +60,15 @@ 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
|
-
|
|
63
|
+
每个关键词优先走 **`dosearchsite.action?queryString=...` 专用搜索页**(比首页 `#all-updates` 顶栏搜索稳定);无结果时再兜底顶栏搜索。匹配时 **标题或 URL 路径** 任一命中即可,并忽略 `-`/`_` 差异;ADI 页按 `ADI_V*` 标题规则识别(面包屑可无 PRD 字样)。
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
调试搜索解析:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
CONFLUENCE_DEBUG=1 python3 confluence-doc.py ADI_V2.3.4
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 直链(可选,跳过搜索)
|
|
66
72
|
|
|
67
73
|
```bash
|
|
68
74
|
CONFLUENCE_PAGE_ID=109609740 python3 confluence-doc.py 2.3.4
|
|
@@ -88,6 +94,6 @@ CONFLUENCE_PAGE_URL="https://confluence.huan.tv/spaces/yyzcpt/pages/109609740/..
|
|
|
88
94
|
- 脚本:`docs/{{SKILL_DIR}}/confluence-doc.py`
|
|
89
95
|
- 脚本已内置默认账号密码;如需使用其他账号可用环境变量覆盖
|
|
90
96
|
- 默认 Confluence 地址为 `https://confluence.huan.tv`(可用 `CONFLUENCE_BASE_URL` 覆盖)
|
|
91
|
-
-
|
|
97
|
+
- 搜索主路径为 `dosearchsite.action?queryString=<关键词>`(全站专用搜索页)
|
|
92
98
|
- 脚本会自动尝试读取仓库根目录的 `.env.pre` / `.env` / `.env.local` / `.env.development` / `.env.test`(若其中存在 `CONFLUENCE_*` 配置可直接复用)
|
|
93
99
|
|