sillyspec 3.25.4 → 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.
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +26 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
- package/package.json +1 -1
- package/src/change-list.js +14 -0
- package/src/change-risk-profile.js +36 -1
- package/src/index.js +3 -0
- package/src/machine-interface.js +5 -1
- package/src/progress/stage-machine.js +17 -14
- package/src/run/command.js +1 -1
- package/src/run/complete-handlers.js +4 -2
- package/src/run/complete.js +110 -32
- package/src/run/prompt.js +20 -0
- package/src/stage-contract.js +33 -7
- package/src/stage-review.js +6 -1
- package/src/stages/archive.js +13 -9
- package/src/stages/brainstorm-auto.js +3 -2
- package/src/stages/brainstorm.js +4 -2
- package/src/stages/execute.js +4 -3
- package/src/stages/plan.js +7 -0
- package/src/stages/propose.js +3 -2
- package/src/stages/status.js +2 -2
- package/src/stages/verify.js +13 -5
- package/src/task-review.js +149 -3
- package/src/verify-postcheck.js +208 -38
- package/src/worktree-apply.js +101 -39
- package/src/worktree.js +106 -32
|
@@ -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
|
```
|
package/package.json
CHANGED
package/src/change-list.js
CHANGED
|
@@ -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
|
package/src/machine-interface.js
CHANGED
|
@@ -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(
|
|
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
|
-
//
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
package/src/run/command.js
CHANGED
|
@@ -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
|
|
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/${
|
|
162
|
+
console.log(`📦 已归档:${archiveChangeName} → archive/${destName}/`)
|
|
161
163
|
return destDir
|
|
162
164
|
}
|
|
163
165
|
/**
|
package/src/run/complete.js
CHANGED
|
@@ -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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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')
|