sillyspec 3.25.3 → 3.25.5

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.
@@ -75,6 +75,32 @@ brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
75
75
 
76
76
  > 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
77
77
 
78
+ ### Stage Review Gate(brainstorm 末尾的 design review.json)
79
+
80
+ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还需产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
81
+
82
+ - 路径:`.sillyspec/.runtime/stage-reviews/brainstorm-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-brainstorm-<变更名>`。
83
+ - 字段(`schemaVersion:1`,`reviewType=design` —— 区别于 execute 的 `acceptance`):
84
+
85
+ ```json
86
+ {
87
+ "schemaVersion": 1,
88
+ "reviewType": "design",
89
+ "reviewedFiles": ["changes/<变更名>/design.md"], // [0] 为主文档
90
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
91
+ "specVerdict": "pass", // pass | fail | cannot_verify
92
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
93
+ "checklist": [ // 扁平数组,逐条对照 design 章节核验
94
+ { "item": "背景与目标", "result": "pass", "note": "..." } // result: pass | gap | fail
95
+ ],
96
+ "requiredEvidence": [], // cannot_verify 时必填非空
97
+ "reviewerNotes": "..."
98
+ }
99
+ ```
100
+
101
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— brainstorm 主文档是 `design.md`(即 `reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 design.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审,不强制独立子代理。
102
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
103
+
78
104
  ## 铁律
79
105
 
80
106
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
@@ -60,6 +60,58 @@ sillyspec worktree doctor --fix --change <变更名>
60
60
 
61
61
  execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否则阻断完成。**例外**:task 在 `tasks/task-XX.md` frontmatter 声明 `low_risk: true`(type-only / 机械迁移等低逻辑风险)时,缺 review.json 只发 warning 不阻断。`cannot_verify` 的 task 会写入 `verify-required-evidence.json`,由 verify 阶段消费。
62
62
 
63
+ #### task 级 review.json 契约(CLI 硬校验)
64
+
65
+ - 路径:`.sillyspec/.runtime/execute-runs/<execute-run-id>/tasks/task-XX/review.json`(目录不存在需先建)。
66
+ - `execute-run-id` 取自**第一次** `--done` prompt 输出(形如 `exec-2026-07-28-112833`),也写在 marker 文件 `.runtime/current-execute-run-id-<变更名>`。多个 run 时 gate 读最新 marker。
67
+ - 字段(`schemaVersion:1`):
68
+
69
+ ```json
70
+ {
71
+ "schemaVersion": 1,
72
+ "task": "task-01",
73
+ "base": "<git-base-commit-hash>",
74
+ "head": "<git-head-commit-hash>",
75
+ "changedFiles": ["src/..."],
76
+ "specVerdict": "pass", // pass | fail | cannot_verify
77
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
78
+ "reviewerNotes": "...",
79
+ "requiredEvidence": [] // cannot_verify 时必须非空
80
+ }
81
+ ```
82
+
83
+ - `base` / `head` 必须是**真实 git commit**(`git rev-parse --verify`),否则判伪造并阻断。`changedFiles` 与实际 `git diff base..head` 完全不相交也判伪造。base..head 空 commit diff 但 working-tree 有未提交改动 → 视为有效改动(warning 不阻断)。
84
+ - 后端 router task 另需在 `.sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json` 写 API 端点清单(扫 `@router.get/post/...`)。
85
+
86
+ ### Stage Review Gate(execute 末尾的 acceptance review.json)
87
+
88
+ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json 外,整个 execute 阶段完成还需一个 stage 级 `review.json`(在 "完成确认"/acceptance 步骤产出)。CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
89
+
90
+ - 路径:`.sillyspec/.runtime/stage-reviews/execute-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-execute-<变更名>`。
91
+ - 字段(`schemaVersion:1`,`reviewType=acceptance` —— 区别于 brainstorm/plan/propose 的 `"design"`):
92
+
93
+ ```json
94
+ {
95
+ "schemaVersion": 1,
96
+ "reviewType": "acceptance",
97
+ "reviewedFiles": ["changes/<变更名>/design.md", "<可选追加 git diff 涉及的源码>"],
98
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
99
+ "specVerdict": "pass", // pass | fail | cannot_verify
100
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
101
+ "checklist": [ // 扁平数组(非按层嵌套!),逐条对照 design 章节 + FR/NFR/决策核验代码
102
+ { "item": "FR-01 排序", "result": "pass", "note": "..." } // result: pass | gap | fail
103
+ ],
104
+ "requiredEvidence": [], // cannot_verify 时必填非空
105
+ "reviewerNotes": "..."
106
+ }
107
+ ```
108
+
109
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— execute 主审查文档是 `design.md`,即 `reviewedFiles[0]`。CLI 会重算 sha256 比对,不符判伪造(fail-closed);找不到主文档也 fail。**改 design.md 后须重算 docHash 再写**,可用 `sillyspec run execute --done` 触发的 prompt 注入版契约(`{REVIEW_JSON_CONTRACT}`)逐字改值。
110
+ - `tier=independent` 时必须由独立 QA 子代理产出该 review.json(独立上下文,不共享实现者分析);`tier=self`(变更 ≤3 文件)降级为当前 agent 自审。
111
+ - 该 acceptance review 同时覆盖"代码审查"视角,后续代码审查步骤仅需轻量复审。
112
+
113
+ > 运行时 CLI 会把精确 schema 表 + 完整 JSON 示例 + docHash 算法通过 `{REVIEW_JSON_CONTRACT}` 占位符注入到该步 prompt(源码 `src/stage-review.js: renderReviewJsonContract()`)。本段为常驻摘要;以运行时注入的契约为权威逐字模板。
114
+
63
115
  ## worktree 子命令(execute 相关)
64
116
 
65
117
  ```bash
@@ -80,6 +132,10 @@ plan → execute → verify
80
132
 
81
133
  execute 完成后(所有 Wave/task 完成 + Task Review Gate 通过),运行 `sillyspec run verify --change <变更名>` 验证。
82
134
 
135
+ ### 批量完成(一次 --done 收尾)
136
+
137
+ 当 `plan.md` 所有 task checkbox 已勾(人工勾或基于各 task `review.json` pass 由 CLI 自动勾)且代码客观核验通过(`checkExecuteCodeEvidence` 非"零变更")时,任一 execute `--done` 会**一次性补完所有剩余 step** 直达阶段完成,不必逐次 +1 推进。日志会打印 `🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 N 个剩余 step`。条件不满足(plan 未全勾 / 代码零变更)时仍按单步推进,要求补 review 后重跑 `--done` 直至满足。
138
+
83
139
  ## 铁律
84
140
 
85
141
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
@@ -46,6 +46,32 @@ sillyspec run plan --done --answer "..." --output "..." # 一步完成 wait+d
46
46
 
47
47
  plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_plan`(审查计划,按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json,避免生成+自审同一次输出的偏差)→ CLI 从 `plan.md` 解析出 task 自动插入"任务蓝图协调器"步骤(per-task)。这是正常行为,不要手动添加。
48
48
 
49
+ ### Stage Review Gate(plan 的 design review.json)
50
+
51
+ `review_plan` 步骤在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
52
+
53
+ - 路径:`.sillyspec/.runtime/stage-reviews/plan-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-plan-<变更名>`。
54
+ - 字段(`schemaVersion:1`,`reviewType=plan` —— plan 阶段主审查文档是 `plan.md`;execute 阶段才是 `acceptance`):
55
+
56
+ ```json
57
+ {
58
+ "schemaVersion": 1,
59
+ "reviewType": "plan",
60
+ "reviewedFiles": ["changes/<变更名>/plan.md"], // [0] 为主文档
61
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
62
+ "specVerdict": "pass", // pass | fail | cannot_verify
63
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
64
+ "checklist": [ // 扁平数组,逐条对照 plan 章节/Wave 结构/task 完整性核验
65
+ { "item": "Wave 分级合理", "result": "pass", "note": "..." } // result: pass | gap | fail
66
+ ],
67
+ "requiredEvidence": [], // cannot_verify 时必填非空
68
+ "reviewerNotes": "..."
69
+ }
70
+ ```
71
+
72
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— plan 主文档是 `plan.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 plan.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
73
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
74
+
49
75
  ### 契约门控(阻断完成)
50
76
 
51
77
  - **plan 启动前**:CLI 校验 `design.md` 是否满足 plan 契约(缺文件变更清单/风险登记/自审章节会阻断)。若失败需先 `sillyspec run brainstorm --reopen --from-step N` 修订设计。
@@ -42,6 +42,32 @@ sillyspec run propose --reopen --from-step N # 重新打开已完成阶段修
42
42
 
43
43
  propose 含自检步骤(按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json),完成时校验四件套文档齐全 + 内容章节(如 proposal 的 Non-Goals、design 的文件变更清单/风险登记/自审)。缺失会阻断完成。
44
44
 
45
+ ### Stage Review Gate(propose 的 proposal review.json)
46
+
47
+ propose 在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
48
+
49
+ - 路径:`.sillyspec/.runtime/stage-reviews/propose-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-propose-<变更名>`。
50
+ - 字段(`schemaVersion:1`,`reviewType=proposal` —— propose 阶段主审查文档是 `proposal.md`):
51
+
52
+ ```json
53
+ {
54
+ "schemaVersion": 1,
55
+ "reviewType": "proposal",
56
+ "reviewedFiles": ["changes/<变更名>/proposal.md"], // [0] 为主文档
57
+ "docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
58
+ "specVerdict": "pass", // pass | fail | cannot_verify
59
+ "qualityVerdict": "pass", // pass | fail | cannot_verify
60
+ "checklist": [ // 扁平数组,逐条对照 proposal 章节/Non-Goals 核验
61
+ { "item": "Non-Goals 明确", "result": "pass", "note": "..." } // result: pass | gap | fail
62
+ ],
63
+ "requiredEvidence": [], // cannot_verify 时必填非空
64
+ "reviewerNotes": "..."
65
+ }
66
+ ```
67
+
68
+ - `docHash` = `sha256(主审查文档内容)`(hex)—— propose 主文档是 `proposal.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 proposal.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
69
+ - 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
70
+
45
71
  ## 阶段流转
46
72
 
47
73
  ```
@@ -87,7 +87,8 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
87
87
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
88
88
  - quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
89
89
  - 完成后立即 `--done`,不跳过
90
- - QUICKLOG 记录由 **CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),完成时 CLI 自动翻「已完成」并勾选 task。**你无需手写 QUICKLOG / tasks.md**,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
90
+ - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
91
+ - **QUICKLOG 正文精修(`--done` 后必做,不可跳过)**:CLI 落盘的是简版骨架(标题可能是 `(quick 任务)`、文件是单行逗号无括注),**不合格**。`--done` 成功后必须立即编辑本次 `<quicklog-id>` 条目,手动精修成丰富格式:① 标题改真实需求摘要(禁 `(quick 任务)`);② 文件改多行 bullet、每行带关键改动括注(禁 `(见实际改动)` 或无括注纯路径);③ 正文 `需求:`/`根因:`/`方案:`/`结果:` 四段逐项充实(禁只留一段「结果:」)。参照同文件早期丰富条目。一条 quick = 一条独立 ql,不追加到旧条目
91
92
  - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。前两个 step 的 `--output` 是中间摘要,不用此模板
92
93
  - **禁止**在没有运行 CLI 的情况下自行决定流程
93
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.25.3",
3
+ "version": "3.25.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -90,6 +90,18 @@ function isPathHeaderCell(c) {
90
90
  return /^(文件路径|文件名|文件|路径|filepath|filename|file\s*path|path)$/i.test(c)
91
91
  }
92
92
 
93
+ /**
94
+ * 路径合理性兜底:合法路径必含目录分隔 `/` 或扩展名点;
95
+ * 两者皆无时仅保留纯 ASCII 单词 token(Dockerfile / Makefile / LICENSE 这类无扩展名文件名),
96
+ * 丢弃含空格/中文的脏描述(如 design 表格第二列误写的「frontend 组件测试」「后端逻辑」)。
97
+ * 防止把自由文本当路径计入 fileCount,虚高 review-tier 分级(误推 independent)。
98
+ */
99
+ function looksLikePath(p) {
100
+ if (!p) return false
101
+ if (p.includes('/') || p.includes('.')) return true
102
+ return /^[\w@-]+$/.test(p)
103
+ }
104
+
93
105
  /**
94
106
  * 从 design.md 解析文件变更清单。兼容两种真实写法:
95
107
  * ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
@@ -147,6 +159,7 @@ export function parseFileChangeList(designMdPath) {
147
159
  // 列定位兜底:取到纯操作词(表头未命中且列顺序异常)→ 跳过,避免把「修改」当路径
148
160
  if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
149
161
  if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
162
+ if (!looksLikePath(filePath)) continue // 脏描述兜底:丢弃非路径自由文本(防虚高 fileCount)
150
163
  result.add(filePath)
151
164
  continue
152
165
  }
@@ -156,6 +169,7 @@ export function parseFileChangeList(designMdPath) {
156
169
  if (listItem) {
157
170
  const filePath = normalizePath(listItem[1])
158
171
  if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
172
+ if (!looksLikePath(filePath)) continue // 脏描述兜底
159
173
  if (listMode === 'exclude') result.delete(filePath)
160
174
  else result.add(filePath)
161
175
  }
@@ -290,14 +290,49 @@ export const VERIFICATION_NEEDS = {
290
290
  export const RISK_LEVEL_CAUSES = {
291
291
  'deployment-critical':
292
292
  'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
293
- '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design 中如实缩小范围,但不建议为绕门控而规避——危险链路该有真实启动证据。',
293
+ '注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design.md frontmatter 用 risk_level 显式声明真实等级(如 unit-sufficient)覆盖——不要靠删措辞绕门控,危险链路该有真实启动证据。',
294
294
  'integration-critical':
295
295
  'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。',
296
296
  'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
297
+ 'explicit': 'design.md frontmatter 的 risk_level 显式声明(覆盖关键词判级)。',
298
+ }
299
+
300
+ /** design.md frontmatter 可显式声明的合法 risk_level 值(与 detectChangeRisk 判级结果同集合) */
301
+ export const RISK_LEVELS = ['doc-only', 'unit-sufficient', 'contract-required', 'integration-critical', 'deployment-critical']
302
+
303
+ /**
304
+ * 从 design.md 顶部 frontmatter 提取显式 risk_level 声明。
305
+ * 只认文档最开头 `---\n...\n---` 块内的 `risk_level: <level>` 单行(与 plan.md 的 plan_level 同款解析),
306
+ * 不扫正文——避免正文里讨论 risk_level 措辞时被误当声明。返回合法 level 或 null。
307
+ */
308
+ export function extractExplicitRiskLevel(designContent = '') {
309
+ const fm = designContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
310
+ if (!fm) return null
311
+ const line = fm[1].split('\n').map(l => l.trim()).find(l => l.startsWith('risk_level:'))
312
+ if (!line) return null
313
+ const value = line.slice('risk_level:'.length).trim().replace(/^["']|["']$/g, '').toLowerCase()
314
+ return RISK_LEVELS.includes(value) ? value : null
297
315
  }
298
316
 
299
317
  export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
300
318
  const triggers = []
319
+
320
+ // ── 显式豁免优先:design.md frontmatter 声明 risk_level → 以声明为准,跳过关键词判级 ──
321
+ // 历史教训:detectChangeRisk 是机械字面匹配,不认否定语境——design 写「本次不改动 daemon/session」
322
+ // 仍命中关键词被误判 integration/deployment-critical,强制要全套集成证据,对无状态后端变更误伤。
323
+ // 与其在正则层做脆弱的否定识别,不如给一条显式、诚实、可审计(落在 design frontmatter + verify-result)
324
+ // 的覆盖通道。声明仍走门控(结论 FAIL 仍拦),但豁免级(unit-sufficient 等)不再强制集成证据。
325
+ const explicit = extractExplicitRiskLevel(designContent)
326
+ if (explicit) {
327
+ const requiredVerification =
328
+ explicit === 'deployment-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'real_startup_once'] :
329
+ explicit === 'integration-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion'] :
330
+ explicit === 'contract-required' ? ['unit_tests', 'contract_tests'] :
331
+ explicit === 'doc-only' ? ['static_check'] :
332
+ ['unit_tests']
333
+ return { level: explicit, triggers: ['risk_level (explicit)'], requiredVerification, explicit: true }
334
+ }
335
+
301
336
  const combined = [designContent, planContent].join('\n')
302
337
 
303
338
  for (const pattern of INTEGRATION_CRITICAL_PATTERNS) {
package/src/index.js CHANGED
@@ -1226,6 +1226,9 @@ SillySpec modules — 模块文档管理
1226
1226
  lines.push('# module_paths:');
1227
1227
  lines.push('# user-service: "user/"');
1228
1228
  lines.push('# order-service: "order/"');
1229
+ lines.push('# 已知预存失败(可选):verify 实测时这些失败被豁免、不阻断归档;fail-safe 仅豁免能检测到的失败行');
1230
+ lines.push('# known_failures:');
1231
+ lines.push('# - "tests/test_legacy.py::test_old"');
1229
1232
  const yamlText = lines.join('\n') + '\n';
1230
1233
 
1231
1234
  // 原子写(tmp + rename + Windows EPERM 重试),避免半截 local.yaml
@@ -225,7 +225,11 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
225
225
  const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
226
226
  const vtWarnings = [];
227
227
  if (vt.status === 'skipped') {
228
- vtWarnings.push('⚠️ verify-test SKIPPED — gate 未核验测试(local.yaml 未配置 commands.test 或显式无测试)。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL');
228
+ vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
229
+ }
230
+ // known_failures 豁免披露:本次 PASS 含豁免,driver 应提示人工复核清单是否过宽
231
+ if (vt.exemptedCount > 0) {
232
+ vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver 应提示人工复核豁免清单是否过宽(避免误豁免本变更引入的真实失败)`);
229
233
  }
230
234
  // 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
231
235
  // 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
@@ -287,22 +287,25 @@ export class StageMachine {
287
287
  }
288
288
  }
289
289
 
290
- // 找到第一个 pending 主流程阶段
290
+ // 找主流程里第一个「未完成且上游已就绪」的阶段,推荐它作为下一步。
291
+ // 修两个缺陷(曾导致 brainstorm 完成后误推 archive):
292
+ // ① 原 steps.length>0 要求:plan/execute/verify 首次 run 前惰性未初始化(steps 空)被跳过;
293
+ // ② 原 upstream 把 pending 当就绪:archive 的上游 plan/execute/verify 即便 pending 也算 ok。
294
+ // 叠加 → 循环跳过未初始化的中间阶段,漏到 archive(steps 非空)误推,违反流程顺序。
295
+ // 改为:不要求 steps(空=该开始了);upstream 必须全 completed(pending 不算就绪)——
296
+ // archive 只在 scan/brainstorm/plan/execute/verify 全完成后才可能被推荐。
291
297
  for (const s of STAGE_ORDER) {
292
298
  const sd = data.stages[s];
293
- if (sd && sd.status === 'pending' && sd.steps && sd.steps.length > 0) {
294
- // 检查上游是否都 completed
295
- const idx = STAGE_ORDER.indexOf(s);
296
- const upstream = STAGE_ORDER.slice(0, idx);
297
- const upstreamOk = upstream.every(us =>
298
- data.stages[us]?.status === 'completed' || !data.stages[us] || data.stages[us].status === 'pending'
299
- );
300
- if (upstreamOk) {
301
- return {
302
- text: `可以开始 ${STAGE_LABELS[s] || s}。`,
303
- command: `sillyspec run ${s}`,
304
- };
305
- }
299
+ if (!sd) continue;
300
+ if (sd.status === 'completed' || sd.status === 'skipped') continue;
301
+ const idx = STAGE_ORDER.indexOf(s);
302
+ const upstream = STAGE_ORDER.slice(0, idx);
303
+ const upstreamOk = upstream.every(us => data.stages[us]?.status === 'completed');
304
+ if (upstreamOk) {
305
+ return {
306
+ text: `可以开始 ${STAGE_LABELS[s] || s}。`,
307
+ command: `sillyspec run ${s}`,
308
+ };
306
309
  }
307
310
  }
308
311
 
@@ -884,7 +884,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
884
884
  // Auto 模式下 brainstorm 使用 artifact-first 步骤
885
885
  if (stage === 'brainstorm') {
886
886
  const existingSteps = progress.stages?.brainstorm?.steps
887
- const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '状态检查与上下文加载'
887
+ const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
888
888
  if (!isAutoModeSteps) {
889
889
  if (!progress.stages) progress.stages = {}
890
890
  progress.stages.brainstorm = {
@@ -29,6 +29,7 @@ import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
29
29
  import { printQuickAuditReview } from './quick-audit.js'
30
30
  import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
31
31
  import { getRule } from '../stage-contract-spec.js'
32
+ import { archiveDestDirName } from '../stage-contract.js'
32
33
 
33
34
  /**
34
35
  * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
@@ -101,7 +102,8 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
101
102
  const archiveDir = join(changesDir, 'archive')
102
103
  const srcDir = join(changesDir, archiveChangeName)
103
104
  const date = new Date().toISOString().slice(0, 10)
104
- const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
105
+ const destName = archiveDestDirName(date, archiveChangeName)
106
+ const destDir = join(archiveDir, destName)
105
107
 
106
108
  if (!existsSync(srcDir)) {
107
109
  console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
@@ -157,7 +159,7 @@ async function archiveChangeDirectory(pm, cwd, progress, specBase) {
157
159
  console.warn(`⚠️ 归档 worktree 清理失败(不阻断归档): ${e.message}`)
158
160
  }
159
161
 
160
- console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
162
+ console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
161
163
  return destDir
162
164
  }
163
165
  /**
@@ -211,6 +211,23 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
211
211
  // execute Wave artifact(W6 Step6c 抽至 complete-handlers.js handleExecuteWaveArtifact)
212
212
  await handleExecuteWaveArtifact({ stageName, steps, currentIdx, changeName, specBase, cwd })
213
213
 
214
+ // execute 批量完成检测:plan 全勾 + 代码客观核验通过 → 剩余 step 一次性标 completed,
215
+ // 使本次 --done 直接进入阶段完成分支(治"3 Wave 做完仍逐次 +1、需重走 7 次 --done")。
216
+ // 先按 review.json pass 自动勾 plan checkbox(复用 continueStep 同源逻辑),再判批量条件。
217
+ if (stageName === 'execute' && changeName) {
218
+ const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
219
+ if (_ac.autoChecked) {
220
+ console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
221
+ }
222
+ if (_ac.skippedCount > 0) {
223
+ console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ 批量完成条件不满足,仍按单步推进`)
224
+ }
225
+ const _bf = await detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps })
226
+ if (_bf.batched && _bf.aligned > 0) {
227
+ console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
228
+ }
229
+ }
230
+
214
231
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
215
232
 
216
233
  if (nextPendingIdx === -1) {
@@ -349,6 +366,92 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
349
366
  }
350
367
 
351
368
  // ── Step 调度兄弟函数(W6 Step7b-2 从 run.js 搬入:skip/wait/continue + formatWaitHistory)──
369
+ /**
370
+ * plan.md checkbox 自动勾选:execute 完成 + 各 task review.json 双 verdict 非 fail → 自动勾选。
371
+ * 治本于"plan 全靠手动勾、易遗漏"——以 review.json pass 为客观依据勾选。供 continueStep 完成段
372
+ * 与 completeStep 的 execute 批量完成检测复用(避免两处复制)。
373
+ *
374
+ * @returns {{autoChecked:boolean, checkedCount:number, skippedCount:number, planTotal:number, planChecked:number}}
375
+ * planTotal/planChecked 在调用方于本函数之后用 readPlanCheckboxStatus 重读(勾选已落盘)。
376
+ */
377
+ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
378
+ if (stageName !== 'execute' || !changeName) {
379
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
380
+ }
381
+ try {
382
+ const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
383
+ const changeDir = join(specBaseLc, 'changes', changeName)
384
+ const planPath = join(changeDir, 'plan.md')
385
+ const runtimeRoot = platformOpts?.runtimeRoot || join(specBaseLc, '.runtime')
386
+ const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
387
+ if (!existsSync(planPath) || !existsSync(runIdFile)) {
388
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
389
+ }
390
+ const executeRunId = readFileSync(runIdFile, 'utf8').trim()
391
+ const planContent = readFileSync(planPath, 'utf8')
392
+ const { readReview } = await import('../task-review.js')
393
+ let checkedCount = 0
394
+ let skippedCount = 0
395
+ const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
396
+ const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
397
+ const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
398
+ const r = readReview(reviewPath)
399
+ if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
400
+ checkedCount++
401
+ return `${p1}x${p2}` // 勾选
402
+ }
403
+ skippedCount++
404
+ return match // 不勾
405
+ })
406
+ if (checkedCount > 0) {
407
+ writeFileSync(planPath, updated)
408
+ }
409
+ return { autoChecked: checkedCount > 0, checkedCount, skippedCount }
410
+ } catch {
411
+ return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * execute 批量完成检测:plan.md 所有 task checkbox 已勾 + 代码客观核验非零变更 →
417
+ * 把剩余 pending/in-progress step 一次性标 completed,使 completeStep 本次 --done 即进入阶段完成
418
+ * 分支(而非逐次 +1)。治"3 Wave 全做完仍显示未开工、需重走 7 次 --done"。
419
+ * 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
420
+ * 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
421
+ *
422
+ * 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
423
+ * @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
424
+ */
425
+ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
426
+ if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
427
+ try {
428
+ const changeDir = join(specBase, 'changes', changeName)
429
+ const { total: planTotal, checked: planChecked } = pm.readPlanCheckboxStatus(changeDir)
430
+ if (planTotal === 0) return { batched: false, aligned: 0, reason: 'plan.md 无 task checkbox' }
431
+ if (planChecked < planTotal) {
432
+ return { batched: false, aligned: 0, reason: `plan 未全勾(${planChecked}/${planTotal})` }
433
+ }
434
+ // plan 全勾 → 代码客观核验(防 plan 被手动勾伪造导致空完成)
435
+ const { checkExecuteCodeEvidence } = await import('../stage-contract.js')
436
+ const evidence = checkExecuteCodeEvidence(cwd, changeName)
437
+ if (evidence.status === 'unchanged') {
438
+ return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
439
+ }
440
+ const now = new Date().toLocaleString('zh-CN', { hour12: false })
441
+ let aligned = 0
442
+ for (const step of steps) {
443
+ if (step.status === 'pending' || step.status === 'in-progress') {
444
+ step.status = 'completed'
445
+ step.completedAt = now
446
+ aligned++
447
+ }
448
+ }
449
+ return { batched: true, aligned }
450
+ } catch {
451
+ return { batched: false, aligned: 0 }
452
+ }
453
+ }
454
+
352
455
  function formatWaitHistory(step) {
353
456
  const answers = Array.isArray(step.waitAnswers) ? step.waitAnswers : []
354
457
  if (answers.length === 0) return null
@@ -621,38 +724,13 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
621
724
  console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
622
725
  console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
623
726
  // plan.md checkbox auto-check:execute 完成 + review.json pass → 自动勾选(治本,比警告可靠)
624
- try {
625
- const specBaseLc = platformOpts.specRoot || join(cwd, '.sillyspec')
626
- const changeDir = join(specBaseLc, 'changes', changeName)
627
- const planPath = join(changeDir, 'plan.md')
628
- const runtimeRoot = platformOpts.runtimeRoot || join(specBaseLc, '.runtime')
629
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
630
- if (existsSync(planPath) && existsSync(runIdFile)) {
631
- const executeRunId = readFileSync(runIdFile, 'utf8').trim()
632
- const planContent = readFileSync(planPath, 'utf8')
633
- const { readReview } = await import('../task-review.js')
634
- let checkedCount = 0
635
- let skippedCount = 0
636
- const updated = planContent.replace(/^(\s*[-*]\s*\[)\s(\]\s*task-\d+)/gim, (match, p1, p2) => {
637
- const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
638
- const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
639
- const r = readReview(reviewPath)
640
- if (r.ok && r.review?.specVerdict !== 'fail' && r.review?.qualityVerdict !== 'fail') {
641
- checkedCount++
642
- return `${p1}x${p2}` // 勾选
643
- }
644
- skippedCount++
645
- return match // 不勾
646
- })
647
- if (checkedCount > 0) {
648
- writeFileSync(planPath, updated)
649
- console.log(` ✅ 自动勾选 ${checkedCount} 个 task checkbox(基于 review.json pass)`)
650
- }
651
- if (skippedCount > 0) {
652
- console.warn(` ⚠️ ${skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
653
- }
654
- }
655
- } catch {}
727
+ const _ac = await autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts })
728
+ if (_ac.autoChecked) {
729
+ console.log(` ✅ 自动勾选 ${_ac.checkedCount} 个 task checkbox(基于 review.json pass)`)
730
+ }
731
+ if (_ac.skippedCount > 0) {
732
+ console.warn(` ⚠️ ${_ac.skippedCount} 个 task 未勾(review.json 缺失/fail)→ archive 会拦。补 review 后重跑 execute --done 触发自动勾`)
733
+ }
656
734
  }
657
735
  }
658
736
  return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
package/src/run/prompt.js CHANGED
@@ -517,6 +517,26 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
517
517
  }
518
518
  }
519
519
 
520
+ // archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
521
+ // 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
522
+ // review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
523
+ // summarizeTaskCompletion 内部已 fail-safe 降级(无 plan / 无 runId → checkbox 统计 + 标注 source),
524
+ // 此处仅兜底注入异常,绝不阻断 archive。
525
+ if (stageName === 'archive' && promptText.includes('{TASK_COMPLETION_REPORT}')) {
526
+ try {
527
+ const { summarizeTaskCompletion } = await import('../task-review.js')
528
+ const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
529
+ const tcrRuntimeRoot = platformOpts?.runtimeRoot || join(tcrSpecBase, '.runtime')
530
+ const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
531
+ const summary = tcrChangeDir
532
+ ? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
533
+ : null
534
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join(summary ? summary.report : '(无法计算完成度:变更目录缺失)')
535
+ } catch (e) {
536
+ promptText = promptText.split('{TASK_COMPLETION_REPORT}').join('(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
537
+ }
538
+ }
539
+
520
540
  // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
521
541
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
522
542
  const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')