sillyspec 3.25.0 → 3.25.2
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-doctor/SKILL.md +9 -0
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
- package/CLAUDE.md +18 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +60 -12
- package/src/check-primitives.js +65 -0
- package/src/db.js +28 -8
- package/src/hooks/worktree-guard.js +9 -2
- package/src/index.js +140 -11
- package/src/machine-interface.js +13 -7
- package/src/progress/change-registry.js +217 -0
- package/src/progress/consistency-doctor.js +259 -0
- package/src/progress/shared.js +33 -0
- package/src/progress/stage-machine.js +535 -0
- package/src/progress/step-store.js +256 -0
- package/src/progress.js +75 -1191
- package/src/quicklog.js +3 -1
- package/src/run/command.js +1088 -0
- package/src/run/complete-handlers.js +874 -0
- package/src/run/complete.js +725 -0
- package/src/run/gates.js +326 -0
- package/src/run/prompt.js +619 -0
- package/src/run/quick-audit.js +107 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +418 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4379
- package/src/stage-contract-engine.js +150 -0
- package/src/stage-contract-spec.js +517 -0
- package/src/stage-contract.js +87 -150
- package/src/stage-review.js +63 -0
- package/src/stages/brainstorm.js +3 -1
- package/src/stages/execute.js +5 -2
- package/src/stages/plan-postcheck.js +38 -27
- package/src/stages/plan.js +12 -49
- package/src/stages/propose.js +2 -1
- package/src/stages/scan.js +3 -1
- package/src/workflow.js +9 -17
- package/src/worktree-apply.js +5 -2
- package/src/worktree-deps.js +114 -32
- package/src/worktree.js +23 -11
- package/src/brainstorm-postcheck.js +0 -158
package/src/run/gates.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run/gates.js(W6 Step4 从 run.js 抽出)。
|
|
3
|
+
*
|
|
4
|
+
* 阶段完成校验 gate 级联 + execute deps 硬门 + 完成回滚(自洽叶子模块,仅被 completeStep 调用):
|
|
5
|
+
* - runStageCompletionGates:runValidators → verify-test 对账 → Plan→Execute contract →
|
|
6
|
+
* Stage Review Gate → Execute Task Review Gate,任一失败走 rollbackCompletionAndReturn
|
|
7
|
+
* - enforceDepsGate:execute depsStatus 不达标且非 wave 级 opt-out 时阻断 --done(exit 1)
|
|
8
|
+
* - rollbackCompletionAndReturn / rollbackStageCompletion:统一回滚 stage/step 状态 + 落盘 + sync
|
|
9
|
+
* - isCurrentWaveAllNoDepsVerify:enforceDepsGate 的私有 helper(wave 级 no_deps_verify opt-out 判定)
|
|
10
|
+
*
|
|
11
|
+
* 安全锚:run.js 始终 barrel。enforceDepsGate + runStageCompletionGates 由 run.js import 回来;
|
|
12
|
+
* 无 test 直接 import(与 parsePorcelainPath/applyRootPlaceholders 不同),无需 barrel re-export。
|
|
13
|
+
* completeStep(Step7 搬)将把这行 import 一起带走。
|
|
14
|
+
*
|
|
15
|
+
* 路径修正(相对 src/run/):
|
|
16
|
+
* - triggerSync/resolveChangeDir 从 './shared.js';runValidators 从 '../stage-contract.js'
|
|
17
|
+
* - 动态 import ./worktree.js / ./verify-postcheck.js / ./stages/execute.js / ./review-tier.js /
|
|
18
|
+
* ./stage-review.js / ./task-review.js → '../X.js'(src/ 下,退一层;真环依赖保留动态)
|
|
19
|
+
*
|
|
20
|
+
* 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
|
|
21
|
+
*/
|
|
22
|
+
import { basename, join } from 'node:path'
|
|
23
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { triggerSync, resolveChangeDir } from './shared.js'
|
|
25
|
+
import { runValidators } from '../stage-contract.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
|
|
29
|
+
* 仅 wave 执行步骤(名如 "Wave N 执行")可 opt-out;非 wave 步骤恒返回 false(保守过门)。
|
|
30
|
+
*/
|
|
31
|
+
function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
32
|
+
if (!stepName) return false
|
|
33
|
+
const m = String(stepName).match(/^Wave\s+(\d+)/i)
|
|
34
|
+
if (!m) return false
|
|
35
|
+
const waveN = parseInt(m[1], 10)
|
|
36
|
+
if (!changeDir) return false
|
|
37
|
+
const planPath = join(changeDir, 'plan.md')
|
|
38
|
+
if (!existsSync(planPath)) return false
|
|
39
|
+
const plan = readFileSync(planPath, 'utf8')
|
|
40
|
+
const waveRe = new RegExp(`^##\\s*Wave\\s+${waveN}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n#\\s|$)`, 'm')
|
|
41
|
+
const waveMatch = plan.match(waveRe)
|
|
42
|
+
if (!waveMatch) return false
|
|
43
|
+
const taskIds = [...waveMatch[1].matchAll(/^[-*]\s*\[[ x]\]\s*(task-\d+)/gim)].map(x => x[1])
|
|
44
|
+
if (taskIds.length === 0) return false
|
|
45
|
+
for (const id of taskIds) {
|
|
46
|
+
const cardPath = join(changeDir, 'tasks', `${id}.md`)
|
|
47
|
+
if (!existsSync(cardPath)) return false // 卡片缺失 → 保守不跳
|
|
48
|
+
const card = readFileSync(cardPath, 'utf8')
|
|
49
|
+
const fm = card.match(/^---\n([\s\S]*?)\n---/)
|
|
50
|
+
if (!fm) return false
|
|
51
|
+
if (!/no_deps_verify:\s*true/i.test(fm[1])) return false
|
|
52
|
+
}
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
58
|
+
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
59
|
+
* 放行返回 true;阻断时 process.exit(1) 不返回。
|
|
60
|
+
*/
|
|
61
|
+
export async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
62
|
+
if (stageName !== 'execute') return true
|
|
63
|
+
let meta = null
|
|
64
|
+
let wm = null
|
|
65
|
+
try {
|
|
66
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
67
|
+
wm = new WorktreeManager({ cwd })
|
|
68
|
+
meta = wm.getMeta(changeName)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// WorktreeManager 构造/读取失败(git 不可用 / worktree 元数据损坏)不阻断 deps gate;
|
|
71
|
+
// 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
|
|
72
|
+
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
73
|
+
}
|
|
74
|
+
const depsStatus = meta?.depsStatus
|
|
75
|
+
if (['linked', 'installed', 'n/a'].includes(depsStatus)) return true
|
|
76
|
+
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
77
|
+
if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
|
|
78
|
+
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
79
|
+
// ── 诊断分支(Phase 2,G2/R3 修正:判定基于物理目录而非 !meta)──
|
|
80
|
+
// getMeta 对"目录不存在"与"meta 损坏"都返回 null,后者会误判终态 → 用物理目录存在性判定。
|
|
81
|
+
let worktreeGone = true
|
|
82
|
+
try {
|
|
83
|
+
worktreeGone = !!(wm && changeName) && !existsSync(wm.getWorktreePath(changeName))
|
|
84
|
+
} catch {}
|
|
85
|
+
// ── fail-loud 块(Phase 3,D-005@v1:仅改拒绝侧 stderr)──
|
|
86
|
+
console.error('❌ ── deps 门控阻断(本次 --done 未完成,进度未推进)──')
|
|
87
|
+
if (worktreeGone) {
|
|
88
|
+
console.error(' worktree 不可用(已 cleanup 或目录不存在)。')
|
|
89
|
+
console.error(` 修复:sillyspec doctor --align-execute-progress${changeName ? ` --change ${changeName}` : ''} 按 plan.md 对齐进度`)
|
|
90
|
+
console.error(` 或: sillyspec worktree create ${changeName || '<change>'} 重建 worktree 继续跑`)
|
|
91
|
+
} else {
|
|
92
|
+
console.error(` 原因:依赖未就绪(depsStatus=${depsStatus || 'unknown'}),不得在无构建/测试能力时声称完成。`)
|
|
93
|
+
console.error(` 修复:sillyspec worktree doctor --fix${changeName ? ` --change ${changeName}` : ''}`)
|
|
94
|
+
console.error(' 或在 worktree 内手动安装依赖后重试。')
|
|
95
|
+
if (meta?.depsError) console.error(` 上次供给错误:${meta.depsError}`)
|
|
96
|
+
}
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 阶段完成校验失败时回滚状态。
|
|
101
|
+
*
|
|
102
|
+
* completeStep 在跑 validator 之前就把 stageData.status 写成 'completed',
|
|
103
|
+
* 若校验失败不回滚,DB 会与真实产物不一致(hook/doctor/下游阶段全部误判),
|
|
104
|
+
* 且所有步骤都是 completed 时 agent 无法重新 --done("没有待完成的步骤")。
|
|
105
|
+
* 此处将 stage 回滚为 in-progress,最后一步重置为 pending,供修复产物后重做。
|
|
106
|
+
*/
|
|
107
|
+
function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
108
|
+
// 辅助阶段在 validator 前已被重置为 pending(steps 也换成了新数组),不要覆盖
|
|
109
|
+
if (stageData.status === 'completed') {
|
|
110
|
+
stageData.status = 'in-progress'
|
|
111
|
+
stageData.completedAt = null
|
|
112
|
+
}
|
|
113
|
+
if (steps[currentIdx] && steps[currentIdx].status === 'completed') {
|
|
114
|
+
steps[currentIdx].status = 'pending'
|
|
115
|
+
steps[currentIdx].completedAt = null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 阶段完成校验失败后的统一收尾:回滚 stage/step 状态 + 落盘 + sync + 返回「未完成」。
|
|
121
|
+
*
|
|
122
|
+
* completeStep 的各 gate(runValidators / verify-test 对账 / plan→execute contract /
|
|
123
|
+
* Stage Review / Execute Task Review)失败时都走这套动作;原先每个失败分支手写重复
|
|
124
|
+
* ~7 次(含 lastActive 落盘 + triggerSync + return 结构),统一进来消坑,避免某分支
|
|
125
|
+
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
126
|
+
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
127
|
+
*/
|
|
128
|
+
async function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
129
|
+
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
130
|
+
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
131
|
+
await pm._write(cwd, progress, changeName)
|
|
132
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
133
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 阶段完成校验 gate 级联(从 completeStep 抽出,行为保持)。仅当所有步骤确实标记为 completed 时
|
|
137
|
+
* 由 completeStep 调用。顺序:runValidators → verify-test 对账 → Plan→Execute contract →
|
|
138
|
+
* Stage Review Gate → Execute Task Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
|
|
139
|
+
* (统一回滚 + 返回 early-return 对象);全部通过 → 返回 null(completeStep 继续收尾)。
|
|
140
|
+
*
|
|
141
|
+
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
142
|
+
*/
|
|
143
|
+
export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx }) {
|
|
144
|
+
const projectName = progress.project || basename(cwd)
|
|
145
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
146
|
+
if (contractResult.errors.length > 0) {
|
|
147
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
148
|
+
for (const err of contractResult.errors) {
|
|
149
|
+
console.error(` - ${err}`)
|
|
150
|
+
}
|
|
151
|
+
// 出路提示常放在 warnings(如「写明不改理由即可豁免」),此处即将阻断 return,
|
|
152
|
+
// 必须一并打出,否则 agent 只看到症状看不到怎么过。
|
|
153
|
+
for (const w of (contractResult.warnings || [])) {
|
|
154
|
+
console.error(` · ${w}`)
|
|
155
|
+
}
|
|
156
|
+
console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
|
|
157
|
+
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
158
|
+
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
159
|
+
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
160
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
161
|
+
}
|
|
162
|
+
if (contractResult.warnings.length > 0) {
|
|
163
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
164
|
+
for (const w of contractResult.warnings) {
|
|
165
|
+
console.warn(` - ${w}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
170
|
+
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
171
|
+
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
172
|
+
if (stageName === 'verify') {
|
|
173
|
+
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
174
|
+
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName })
|
|
175
|
+
printVerifyTestCheck(testCheck)
|
|
176
|
+
if (testCheck.status === 'failed') {
|
|
177
|
+
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
178
|
+
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
179
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
180
|
+
}
|
|
181
|
+
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
182
|
+
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
183
|
+
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
184
|
+
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: platformOpts?.runtimeRoot })
|
|
185
|
+
printVerifyParityCheck(parityCheck)
|
|
186
|
+
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
190
|
+
if (stageName === 'plan') {
|
|
191
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
192
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
193
|
+
if (planPath && existsSync(planPath)) {
|
|
194
|
+
const { validatePlanForExecute } = await import('../stages/execute.js')
|
|
195
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
196
|
+
const planValidation = validatePlanForExecute(planContent)
|
|
197
|
+
if (!planValidation.ok) {
|
|
198
|
+
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
199
|
+
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
200
|
+
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
201
|
+
// 阻断 completed
|
|
202
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
203
|
+
}
|
|
204
|
+
if (planValidation.warnings.length > 0) {
|
|
205
|
+
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
206
|
+
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
207
|
+
}
|
|
208
|
+
if (planValidation.ok) {
|
|
209
|
+
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Stage Review Gate:brainstorm/plan/propose/execute 独立审查(按 tier 分级)──
|
|
215
|
+
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
216
|
+
if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName)) {
|
|
217
|
+
try {
|
|
218
|
+
const { classifyReviewTier } = await import('../review-tier.js')
|
|
219
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult } = await import('../stage-review.js')
|
|
220
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
221
|
+
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
222
|
+
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
223
|
+
let planLevel = null
|
|
224
|
+
if (reviewChangeDir) {
|
|
225
|
+
const planPath = join(reviewChangeDir, 'plan.md')
|
|
226
|
+
if (existsSync(planPath)) {
|
|
227
|
+
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
228
|
+
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const tier = classifyReviewTier({ planLevel, designPath })
|
|
232
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
233
|
+
|
|
234
|
+
if (tier.tier === 'self') {
|
|
235
|
+
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
236
|
+
} else {
|
|
237
|
+
const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName)
|
|
238
|
+
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
239
|
+
: stageName === 'plan' ? 'plan'
|
|
240
|
+
: stageName === 'propose' ? 'proposal'
|
|
241
|
+
: 'acceptance'
|
|
242
|
+
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
243
|
+
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
244
|
+
printStageReviewResult(reviewResult, { stage: stageName })
|
|
245
|
+
if (!reviewResult.ok) {
|
|
246
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
251
|
+
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
252
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
257
|
+
if (stageName === 'execute') {
|
|
258
|
+
try {
|
|
259
|
+
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('../task-review.js')
|
|
260
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
261
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
262
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
263
|
+
|
|
264
|
+
if (planPath && existsSync(planPath)) {
|
|
265
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
266
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
|
|
267
|
+
|
|
268
|
+
// execute run id:从变更专属标记文件读取
|
|
269
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
270
|
+
let executeRunId = ''
|
|
271
|
+
try {
|
|
272
|
+
if (existsSync(runIdFile)) {
|
|
273
|
+
executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
if (!executeRunId) {
|
|
277
|
+
const { generateExecuteRunId } = await import('../task-review.js')
|
|
278
|
+
executeRunId = generateExecuteRunId()
|
|
279
|
+
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
280
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库
|
|
284
|
+
let reviewGitDir = cwd
|
|
285
|
+
try {
|
|
286
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
287
|
+
const wm = new WorktreeManager({ cwd })
|
|
288
|
+
const meta = wm.getMeta(changeName)
|
|
289
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
290
|
+
reviewGitDir = meta.worktreePath
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
|
|
295
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
296
|
+
|
|
297
|
+
if (!reviewResult.ok) {
|
|
298
|
+
// Task review 校验失败,阻断 execute 完成
|
|
299
|
+
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
300
|
+
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
301
|
+
if (uncheckedTasks.length > 0) {
|
|
302
|
+
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
303
|
+
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
304
|
+
}
|
|
305
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
309
|
+
if (reviewResult.requiredEvidence.length > 0) {
|
|
310
|
+
const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
|
|
311
|
+
if (evidencePath) {
|
|
312
|
+
console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
|
|
313
|
+
console.log(' verify 阶段必须满足这些证据要求。')
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
319
|
+
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
320
|
+
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
321
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return null
|
|
325
|
+
}
|
|
326
|
+
|