sillyspec 3.26.10 → 3.26.11
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-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/.npmrc-publish +2 -0
- package/README.md +201 -201
- package/SKILL.md +90 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +601 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +310 -310
- package/src/contract-matrix.js +379 -379
- package/src/db.js +350 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +210 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +131 -4
- package/src/doctor-diagnostics.js +711 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +79 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +848 -848
- package/src/index.js +90 -8
- package/src/init.js +623 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +120 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +29 -1
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1131 -1127
- package/src/quicklog.js +693 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1385 -1364
- package/src/run/complete-handlers.js +1443 -1399
- package/src/run/complete.js +225 -24
- package/src/run/gates.js +755 -755
- package/src/run/prompt.js +761 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1268 -1215
- package/src/run/stage.js +487 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +416 -416
- package/src/sillyhub-mcp/client.js +529 -529
- package/src/spec-sync.js +323 -319
- package/src/stage-contract.js +897 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +155 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +596 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +371 -371
- package/src/stages/execute.js +1003 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1302 -1302
- package/src/stages/plan.js +625 -623
- package/src/stages/quick.js +151 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1223 -1135
- package/src/task-review.js +1019 -1019
- package/src/verify-postcheck.js +1186 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1213 -1136
- package/src/worktree-deps.js +353 -353
- package/src/worktree.js +1481 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/run/gates.js
CHANGED
|
@@ -1,755 +1,755 @@
|
|
|
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, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
-
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
25
|
-
import { runValidators } from '../stage-contract.js'
|
|
26
|
-
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
27
|
-
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
28
|
-
import { stageRegistry } from '../stages/index.js'
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 从 plan.md 提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
32
|
-
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js parseWavesFromPlan 的
|
|
33
|
-
* 收容口径一致:### 标题式 task / 非 task 区 checkbox 不算),light plan 的 `## Tasks` 段同样收。
|
|
34
|
-
*/
|
|
35
|
-
function extractTaskIdsFromPlan(planContent) {
|
|
36
|
-
if (!planContent) return []
|
|
37
|
-
return [...String(planContent).matchAll(/^[-*]\s*\[[ x]\]\s*(?:.*\b)?task-(\d+)\b/gim)]
|
|
38
|
-
.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 符号影响面报告结构校验(纯函数):execute「加载上下文」步的产出落盘核验。
|
|
43
|
-
*
|
|
44
|
-
* 治 persuasion-only 失效现场(ql-20260816-005-3d7f):execute 前缀 4 步无任何 gate,agent 可
|
|
45
|
-
* <1s 连发 4 次 --done 盖章跳过,「符号影响面扫描」(execute.js 操作 11)被一句「上下文在会话内」
|
|
46
|
-
* 带过(输出契约只写「上下文摘要」,操作与输出脱节)。修法 = 报告落盘
|
|
47
|
-
* {SPEC_ROOT}/changes/<change>/symbol-impact.md + 本函数校验:plan.md 每个 task-XX 在报告中出现
|
|
48
|
-
* (逐 task 结论,「无签名级变更」也要显式写,治整体一句话带过)。语义质量(调用点找没找全)
|
|
49
|
-
* 仍归 agent,CLI 只核结构覆盖度——符合债单「persuasion-only → 补最小硬门」原则。
|
|
50
|
-
*
|
|
51
|
-
* @param {{reportContent: string|null, planContent: string|null}} params
|
|
52
|
-
* @returns {{ok: boolean, errors: string[]}}
|
|
53
|
-
*/
|
|
54
|
-
export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
55
|
-
const errors = []
|
|
56
|
-
if (!reportContent || !String(reportContent).trim()) {
|
|
57
|
-
return { ok: false, errors: ['symbol-impact.md 不存在或内容为空——「加载上下文」步的符号影响面报告必须落盘(逐 task 一行结论,「无签名级变更」也要显式写)'] }
|
|
58
|
-
}
|
|
59
|
-
const taskIds = extractTaskIdsFromPlan(planContent)
|
|
60
|
-
if (taskIds.length === 0) return { ok: true, errors } // plan 无 task 可列(默认兜底形态)→ 无可校验对象
|
|
61
|
-
const missing = taskIds.filter(id => !String(reportContent).includes(id))
|
|
62
|
-
for (const id of missing) {
|
|
63
|
-
errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
|
|
64
|
-
}
|
|
65
|
-
return { ok: errors.length === 0, errors }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 符号影响面报告硬门:execute「加载上下文」步 --done 时校验 symbol-impact.md 落盘 + task 覆盖度。
|
|
70
|
-
* fail-closed:不通过 → 当前步标 blocked + exit 1(进度不推进)。非目标步骤/阶段直接放行。
|
|
71
|
-
*/
|
|
72
|
-
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
73
|
-
if (stageName !== 'execute') return
|
|
74
|
-
if (!currentStepName || !currentStepName.includes('加载上下文')) return
|
|
75
|
-
const planPath = join(specBase, 'changes', changeName || '', 'plan.md')
|
|
76
|
-
const reportPath = join(specBase, 'changes', changeName || '', 'symbol-impact.md')
|
|
77
|
-
if (!existsSync(planPath)) return // plan.md 缺失场景已有 plan 阶段 gate 把关,此处不重复拦
|
|
78
|
-
let planContent = ''
|
|
79
|
-
try { planContent = readFileSync(planPath, 'utf8') } catch { return }
|
|
80
|
-
let reportContent = null
|
|
81
|
-
try { reportContent = readFileSync(reportPath, 'utf8') } catch { /* 缺失由 validate 报 */ }
|
|
82
|
-
const result = validateSymbolImpactReport({ reportContent, planContent })
|
|
83
|
-
if (result.ok) return
|
|
84
|
-
console.error('❌ ── 符号影响面报告校验阻断(本次 --done 未完成,进度未推进)──')
|
|
85
|
-
console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
|
|
86
|
-
console.error(` 期望路径:${reportPath}`)
|
|
87
|
-
for (const e of result.errors) console.error(` • ${e}`)
|
|
88
|
-
console.error(' 修复:按「加载上下文」步操作 11 完成符号影响面扫描,逐 task 写结论(无签名级变更也要显式写「无」),落盘后重跑 execute --done。')
|
|
89
|
-
process.exit(1)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
|
|
94
|
-
* 仅 wave 执行步骤(名如 "Wave N 执行")可 opt-out;非 wave 步骤恒返回 false(保守过门)。
|
|
95
|
-
*/
|
|
96
|
-
function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
97
|
-
if (!stepName) return false
|
|
98
|
-
const m = String(stepName).match(/^Wave\s+(\d+)/i)
|
|
99
|
-
if (!m) return false
|
|
100
|
-
const waveN = parseInt(m[1], 10)
|
|
101
|
-
if (!changeDir) return false
|
|
102
|
-
const planPath = join(changeDir, 'plan.md')
|
|
103
|
-
if (!existsSync(planPath)) return false
|
|
104
|
-
const plan = readFileSync(planPath, 'utf8')
|
|
105
|
-
const waveRe = new RegExp(`^##\\s*Wave\\s+${waveN}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n#\\s|$)`, 'm')
|
|
106
|
-
const waveMatch = plan.match(waveRe)
|
|
107
|
-
if (!waveMatch) return false
|
|
108
|
-
const taskIds = [...waveMatch[1].matchAll(/^[-*]\s*\[[ x]\]\s*(task-\d+)/gim)].map(x => x[1])
|
|
109
|
-
if (taskIds.length === 0) return false
|
|
110
|
-
for (const id of taskIds) {
|
|
111
|
-
const cardPath = join(changeDir, 'tasks', `${id}.md`)
|
|
112
|
-
if (!existsSync(cardPath)) return false // 卡片缺失 → 保守不跳
|
|
113
|
-
const card = readFileSync(cardPath, 'utf8')
|
|
114
|
-
const fm = card.match(/^---\n([\s\S]*?)\n---/)
|
|
115
|
-
if (!fm) return false
|
|
116
|
-
if (!/no_deps_verify:\s*true/i.test(fm[1])) return false
|
|
117
|
-
}
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
123
|
-
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
124
|
-
* 放行返回 true;阻断时 process.exit(1) 不返回。
|
|
125
|
-
*/
|
|
126
|
-
export async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
127
|
-
if (stageName !== 'execute') return true
|
|
128
|
-
let meta = null
|
|
129
|
-
let wm = null
|
|
130
|
-
try {
|
|
131
|
-
const { WorktreeManager } = await import('../worktree.js')
|
|
132
|
-
wm = new WorktreeManager({ cwd })
|
|
133
|
-
meta = wm.getMeta(changeName)
|
|
134
|
-
} catch (e) {
|
|
135
|
-
// WorktreeManager 构造/读取失败(git 不可用 / worktree 元数据损坏)不阻断 deps gate;
|
|
136
|
-
// 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
|
|
137
|
-
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
138
|
-
}
|
|
139
|
-
const depsStatus = meta?.depsStatus
|
|
140
|
-
if (['linked', 'installed', 'n/a'].includes(depsStatus)) return true
|
|
141
|
-
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
142
|
-
if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
|
|
143
|
-
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
144
|
-
// ── 诊断分支(Phase 2,G2/R3 修正:判定基于物理目录而非 !meta)──
|
|
145
|
-
// getMeta 对"目录不存在"与"meta 损坏"都返回 null,后者会误判终态 → 用物理目录存在性判定。
|
|
146
|
-
let worktreeGone = true
|
|
147
|
-
try {
|
|
148
|
-
worktreeGone = !!(wm && changeName) && !existsSync(wm.getWorktreePath(changeName))
|
|
149
|
-
} catch {}
|
|
150
|
-
// ── fail-loud 块(Phase 3,D-005@v1:仅改拒绝侧 stderr)──
|
|
151
|
-
console.error('❌ ── deps 门控阻断(本次 --done 未完成,进度未推进)──')
|
|
152
|
-
if (worktreeGone) {
|
|
153
|
-
console.error(' worktree 不可用(已 cleanup 或目录不存在)。')
|
|
154
|
-
console.error(` 修复:sillyspec doctor --align-execute-progress${changeName ? ` --change ${changeName}` : ''} 按 plan.md 对齐进度`)
|
|
155
|
-
console.error(` 或: sillyspec worktree create ${changeName || '<change>'} 重建 worktree 继续跑`)
|
|
156
|
-
} else {
|
|
157
|
-
console.error(` 原因:依赖未就绪(depsStatus=${depsStatus || 'unknown'}),不得在无构建/测试能力时声称完成。`)
|
|
158
|
-
console.error(` 修复:sillyspec worktree doctor --fix${changeName ? ` --change ${changeName}` : ''}`)
|
|
159
|
-
console.error(' 或在 worktree 内手动安装依赖后重试。')
|
|
160
|
-
if (meta?.depsError) console.error(` 上次供给错误:${meta.depsError}`)
|
|
161
|
-
}
|
|
162
|
-
process.exit(1)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
|
|
167
|
-
*
|
|
168
|
-
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
169
|
-
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
170
|
-
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
171
|
-
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式。
|
|
172
|
-
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
173
|
-
*/
|
|
174
|
-
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
175
|
-
if (stageName !== 'execute' || !changeName) return true
|
|
176
|
-
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
177
|
-
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
178
|
-
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
179
|
-
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
180
|
-
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
181
|
-
let executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
182
|
-
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
|
|
183
|
-
if (executeRunId && !isValidExecuteRunId(executeRunId)) {
|
|
184
|
-
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
|
|
185
|
-
executeRunId = ''
|
|
186
|
-
}
|
|
187
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
188
|
-
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
189
|
-
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
190
|
-
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
191
|
-
// resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
|
|
192
|
-
if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
|
|
193
|
-
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
194
|
-
if (relocated && relocated !== executeRunId) {
|
|
195
|
-
console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
|
|
196
|
-
executeRunId = relocated
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
200
|
-
if (result.ok) return true
|
|
201
|
-
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
202
|
-
console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
|
|
203
|
-
console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
|
|
204
|
-
for (const f of result.failures) {
|
|
205
|
-
const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
|
|
206
|
-
console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
|
|
207
|
-
for (const e of f.errors) console.error(` - ${e}`)
|
|
208
|
-
}
|
|
209
|
-
console.error(' 修复:补全上述 review.json 字段后重跑 execute --done。')
|
|
210
|
-
process.exit(1)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* 阶段完成校验失败时回滚状态。
|
|
215
|
-
*
|
|
216
|
-
* completeStep 在跑 validator 之前就把 stageData.status 写成 'completed',
|
|
217
|
-
* 若校验失败不回滚,DB 会与真实产物不一致(hook/doctor/下游阶段全部误判),
|
|
218
|
-
* 且所有步骤都是 completed 时 agent 无法重新 --done("没有待完成的步骤")。
|
|
219
|
-
* 此处将 stage 回滚为 in-progress,最后一步重置为 pending,供修复产物后重做。
|
|
220
|
-
*/
|
|
221
|
-
function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
222
|
-
// 辅助阶段在 validator 前已被重置为 pending(steps 也换成了新数组),不要覆盖
|
|
223
|
-
if (stageData.status === 'completed') {
|
|
224
|
-
stageData.status = 'in-progress'
|
|
225
|
-
stageData.completedAt = null
|
|
226
|
-
}
|
|
227
|
-
if (steps[currentIdx] && steps[currentIdx].status === 'completed') {
|
|
228
|
-
steps[currentIdx].status = 'pending'
|
|
229
|
-
steps[currentIdx].completedAt = null
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 阶段完成校验失败后的统一收尾:回滚 stage/step 状态 + 落盘 + sync + 返回「未完成」。
|
|
235
|
-
*
|
|
236
|
-
* completeStep 的各 gate(runValidators / verify-test 对账 / plan→execute contract /
|
|
237
|
-
* Stage Review / Execute Task Review)失败时都走这套动作;原先每个失败分支手写重复
|
|
238
|
-
* ~7 次(含 lastActive 落盘 + triggerSync + return 结构),统一进来消坑,避免某分支
|
|
239
|
-
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
240
|
-
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
241
|
-
*/
|
|
242
|
-
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
243
|
-
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
244
|
-
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
245
|
-
pm._write(cwd, progress, changeName)
|
|
246
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
247
|
-
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* 阶段完成校验 gate 级联(从 completeStep 抽出,行为保持)。仅当所有步骤确实标记为 completed 时
|
|
251
|
-
* 由 completeStep 调用。顺序:runValidators → verify-test 对账 → Plan→Execute contract →
|
|
252
|
-
* Stage Review Gate → Execute Task Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
|
|
253
|
-
* (统一回滚 + 返回 early-return 对象);全部通过 → 返回 null(completeStep 继续收尾)。
|
|
254
|
-
*
|
|
255
|
-
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
256
|
-
*/
|
|
257
|
-
export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx = null }) {
|
|
258
|
-
const projectName = progress.project || basename(cwd)
|
|
259
|
-
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
260
|
-
if (contractResult.errors.length > 0) {
|
|
261
|
-
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
262
|
-
for (const err of contractResult.errors) {
|
|
263
|
-
console.error(` - ${err}`)
|
|
264
|
-
}
|
|
265
|
-
// 出路提示常放在 warnings(如「写明不改理由即可豁免」),此处即将阻断 return,
|
|
266
|
-
// 必须一并打出,否则 agent 只看到症状看不到怎么过。
|
|
267
|
-
for (const w of (contractResult.warnings || [])) {
|
|
268
|
-
console.error(` · ${w}`)
|
|
269
|
-
}
|
|
270
|
-
console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
|
|
271
|
-
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
272
|
-
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
273
|
-
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
274
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
275
|
-
}
|
|
276
|
-
if (contractResult.warnings.length > 0) {
|
|
277
|
-
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
278
|
-
for (const w of contractResult.warnings) {
|
|
279
|
-
console.warn(` - ${w}`)
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
284
|
-
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
285
|
-
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
286
|
-
if (stageName === 'verify') {
|
|
287
|
-
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
288
|
-
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
289
|
-
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
290
|
-
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
|
|
291
|
-
printVerifyTestCheck(testCheck)
|
|
292
|
-
if (testCheck.status === 'failed') {
|
|
293
|
-
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
294
|
-
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
295
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
296
|
-
}
|
|
297
|
-
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
298
|
-
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
299
|
-
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
300
|
-
const parityRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
301
|
-
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: parityRuntimeRoot })
|
|
302
|
-
printVerifyParityCheck(parityCheck)
|
|
303
|
-
// ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
|
|
304
|
-
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
305
|
-
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
306
|
-
printVerifyDeletionCheck(deletionCheck)
|
|
307
|
-
// ── required-evidence 对账(advisory,不阻断):闭合 execute→verify evidence 死链 ──
|
|
308
|
-
// execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json,
|
|
309
|
-
// 本探针查每个 cannot_verify 任务是否在 verify-result.md 体现(CLI 只查提及,满足度 agent 自报告)。
|
|
310
|
-
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
311
|
-
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
|
|
312
|
-
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
313
|
-
// ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
|
|
314
|
-
// 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
|
|
315
|
-
// agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
|
|
316
|
-
// 类「verify PASS → archive 才发现 pending」的时序漏洞)。
|
|
317
|
-
const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
318
|
-
if (verifyChangeDir) {
|
|
319
|
-
const impactPath = join(verifyChangeDir, 'module-impact.md')
|
|
320
|
-
if (existsSync(impactPath)) {
|
|
321
|
-
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
322
|
-
const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
|
|
323
|
-
if (pendingRows.length > 0) {
|
|
324
|
-
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
325
|
-
for (const row of pendingRows) console.error(` - ${row}`)
|
|
326
|
-
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
327
|
-
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
335
|
-
if (stageName === 'plan') {
|
|
336
|
-
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
337
|
-
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
338
|
-
if (planPath && existsSync(planPath)) {
|
|
339
|
-
const { validatePlanForExecute } = await import('../stages/execute.js')
|
|
340
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
341
|
-
const planValidation = validatePlanForExecute(planContent)
|
|
342
|
-
if (!planValidation.ok) {
|
|
343
|
-
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
344
|
-
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
345
|
-
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
346
|
-
// 阻断 completed
|
|
347
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
348
|
-
}
|
|
349
|
-
if (planValidation.warnings.length > 0) {
|
|
350
|
-
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
351
|
-
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
352
|
-
}
|
|
353
|
-
if (planValidation.ok) {
|
|
354
|
-
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ── Stage Review Gate:brainstorm/plan/execute 独立审查(按 tier 分级)──
|
|
360
|
-
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
361
|
-
if (['brainstorm', 'plan', 'execute'].includes(stageName)) {
|
|
362
|
-
try {
|
|
363
|
-
const { classifyReviewTier } = await import('../review-tier.js')
|
|
364
|
-
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult, generateStageReviewRunId, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
365
|
-
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
366
|
-
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
367
|
-
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
368
|
-
let planLevel = null
|
|
369
|
-
if (reviewChangeDir) {
|
|
370
|
-
const planPath = join(reviewChangeDir, 'plan.md')
|
|
371
|
-
if (existsSync(planPath)) {
|
|
372
|
-
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
373
|
-
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const tier = classifyReviewTier({ planLevel, designPath })
|
|
377
|
-
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
378
|
-
|
|
379
|
-
if (tier.tier === 'self') {
|
|
380
|
-
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
381
|
-
} else {
|
|
382
|
-
let reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
|
|
383
|
-
if (!reviewRunId) {
|
|
384
|
-
// marker 缺失(execute 批量完成跳过 prompt 渲染等场景)→ 自生 + 写盘
|
|
385
|
-
// 让 gate 读到确定 ID,错误从 execute-null 变 execute-review-<id>(可执行)
|
|
386
|
-
reviewRunId = generateStageReviewRunId()
|
|
387
|
-
try {
|
|
388
|
-
mkdirSync(runtimeRoot, { recursive: true })
|
|
389
|
-
writeFileSync(stageReviewMarkerPath(runtimeRoot, stageName, changeName), reviewRunId + '\n')
|
|
390
|
-
} catch {}
|
|
391
|
-
}
|
|
392
|
-
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
393
|
-
: stageName === 'plan' ? 'plan'
|
|
394
|
-
: 'acceptance'
|
|
395
|
-
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
396
|
-
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
397
|
-
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot })
|
|
398
|
-
if (!reviewResult.ok) {
|
|
399
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} catch (e) {
|
|
403
|
-
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
404
|
-
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
405
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
410
|
-
if (stageName === 'execute') {
|
|
411
|
-
try {
|
|
412
|
-
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('../task-review.js')
|
|
413
|
-
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
414
|
-
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
415
|
-
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
416
|
-
|
|
417
|
-
if (planPath && existsSync(planPath)) {
|
|
418
|
-
const planContent = readFileSync(planPath, 'utf8')
|
|
419
|
-
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
420
|
-
|
|
421
|
-
// execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
|
|
422
|
-
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
423
|
-
let executeRunId = ''
|
|
424
|
-
try {
|
|
425
|
-
if (existsSync(runIdFile)) {
|
|
426
|
-
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
427
|
-
const { isValidExecuteRunId } = await import('../task-review.js')
|
|
428
|
-
if (c && !isValidExecuteRunId(c)) {
|
|
429
|
-
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),视为缺失回退扫描`)
|
|
430
|
-
} else {
|
|
431
|
-
executeRunId = c
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
} catch {}
|
|
435
|
-
if (!executeRunId) {
|
|
436
|
-
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
437
|
-
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
438
|
-
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
439
|
-
const { generateExecuteRunId, resolveLatestExecuteRunId } = await import('../task-review.js')
|
|
440
|
-
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
441
|
-
if (!executeRunId) {
|
|
442
|
-
executeRunId = generateExecuteRunId()
|
|
443
|
-
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
444
|
-
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
|
|
445
|
-
// marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
|
|
446
|
-
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
447
|
-
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
448
|
-
writeFileSync(runIdFile, executeRunId + '\n')
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库。
|
|
453
|
-
// 跨仓支持(task-07 / design §6 gates 行 + D-013):
|
|
454
|
-
// - 有 ctx(execute 启动入口 task-09 构造的 MultiRepoContext 实例)→ 用
|
|
455
|
-
// ctx.resolve('main').gitDir 作为主仓 task 的校验 cwd。ctx 的 main entry 已在
|
|
456
|
-
// _buildMainEntry 内统一编码 worktreePath/cwd + in-place-fallback 兜底(与 task-review.js:724
|
|
457
|
-
// generateTaskReviewDrafts 的 in-place 逻辑同源),避免两处漂移。Task Review Gate 内部循环
|
|
458
|
-
// 再按 review.repo ?? 'main' 从 ctx.resolve(repo).gitDir 切跨仓 task 的 gitDir(task-04 实现)。
|
|
459
|
-
// - 无 ctx(ctx=null 缺省,单仓 / 旧调用链)→ 走原逻辑(meta.worktreePath 或 cwd),零回归。
|
|
460
|
-
let reviewGitDir = cwd
|
|
461
|
-
if (ctx) {
|
|
462
|
-
const mainEntry = ctx.resolve('main')
|
|
463
|
-
if (mainEntry?.gitDir) reviewGitDir = mainEntry.gitDir
|
|
464
|
-
} else {
|
|
465
|
-
try {
|
|
466
|
-
const { WorktreeManager } = await import('../worktree.js')
|
|
467
|
-
const wm = new WorktreeManager({ cwd })
|
|
468
|
-
const meta = wm.getMeta(changeName)
|
|
469
|
-
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
470
|
-
reviewGitDir = meta.worktreePath
|
|
471
|
-
}
|
|
472
|
-
} catch {}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir, ctx })
|
|
476
|
-
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
477
|
-
|
|
478
|
-
if (!reviewResult.ok) {
|
|
479
|
-
// Task review 校验失败,阻断 execute 完成
|
|
480
|
-
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
481
|
-
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
482
|
-
if (uncheckedTasks.length > 0) {
|
|
483
|
-
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
484
|
-
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
485
|
-
}
|
|
486
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
490
|
-
if (reviewResult.requiredEvidence.length > 0) {
|
|
491
|
-
const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
|
|
492
|
-
if (evidencePath) {
|
|
493
|
-
console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
|
|
494
|
-
console.log(' verify 阶段必须满足这些证据要求。')
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} catch (e) {
|
|
499
|
-
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
500
|
-
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
501
|
-
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
502
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return null
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
|
|
510
|
-
* completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
|
|
511
|
-
* 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
|
|
512
|
-
* 从 complete.js 迁入(completeStageGates 共享收尾管线 + completeStep brainstorm 提示消费)。
|
|
513
|
-
*/
|
|
514
|
-
export function readDesignScale(specBase, changeName) {
|
|
515
|
-
if (!changeName) return null
|
|
516
|
-
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
517
|
-
if (!existsSync(designPath)) return null
|
|
518
|
-
const text = readFileSync(designPath, 'utf8')
|
|
519
|
-
const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
520
|
-
if (!fm) return null
|
|
521
|
-
const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
522
|
-
return m ? m[1] : null
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* 校验变更目录下近 10 分钟新增的 .md/.yaml/.yml 文件含 author/created_at 元数据(advisory 打印)。
|
|
527
|
-
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
528
|
-
*/
|
|
529
|
-
export function validateMetadata(cwd, stageName, specBase) {
|
|
530
|
-
const changesDir = join(specBase, 'changes')
|
|
531
|
-
if (!existsSync(changesDir)) return
|
|
532
|
-
|
|
533
|
-
const cutoff = Date.now() - 10 * 60 * 1000
|
|
534
|
-
const missing = []
|
|
535
|
-
|
|
536
|
-
function walk(dir) {
|
|
537
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
538
|
-
const full = join(dir, entry.name)
|
|
539
|
-
try {
|
|
540
|
-
if (entry.isDirectory()) { walk(full); continue }
|
|
541
|
-
if (!/\.(md|yaml|yml)$/.test(entry.name)) continue
|
|
542
|
-
const mtime = statSync(full).mtimeMs
|
|
543
|
-
if (mtime < cutoff) continue
|
|
544
|
-
const content = readFileSync(full, 'utf-8')
|
|
545
|
-
if (!content.includes('author:') && !content.includes('author:')) missing.push(full)
|
|
546
|
-
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.push(full)
|
|
547
|
-
} catch (e) { /* skip unreadable files */ }
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
walk(changesDir)
|
|
552
|
-
const unique = [...new Set(missing)]
|
|
553
|
-
if (unique.length > 0) {
|
|
554
|
-
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
555
|
-
unique.forEach(f => console.log(` - ${f.replace(cwd + '/', '')}`))
|
|
556
|
-
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* 验证关键文件是否存在于正确的变更目录下(advisory 打印,防 AI 写错路径)。
|
|
562
|
-
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
563
|
-
*/
|
|
564
|
-
export function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
565
|
-
const effectiveChange = changeName || progress.currentChange
|
|
566
|
-
if (!effectiveChange) return
|
|
567
|
-
|
|
568
|
-
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
569
|
-
if (!existsSync(changeDir)) return
|
|
570
|
-
|
|
571
|
-
// 每个阶段完成后预期存在的文件
|
|
572
|
-
// brainstorm:scale=small(小变更)只必产 design.md;large/未标 scale → 四件套全。
|
|
573
|
-
// 与 validateBrainstormOutputs 的 BRAINSTORM_RULES condition(scale≠small)同源,避免对合法 small 变更
|
|
574
|
-
// 误报"⬜ proposal/requirements/tasks 未找到"(本检查仅 advisory 打印不 gate,但误导输出仍要消除)。
|
|
575
|
-
const brainstormExpected = readDesignScale(specBase, effectiveChange) === 'small'
|
|
576
|
-
? ['design.md']
|
|
577
|
-
: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
578
|
-
const expectedFiles = {
|
|
579
|
-
brainstorm: brainstormExpected,
|
|
580
|
-
plan: ['plan.md'],
|
|
581
|
-
archive: ['module-impact.md'],
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const expected = expectedFiles[stageName]
|
|
585
|
-
if (!expected) return
|
|
586
|
-
|
|
587
|
-
const missing = []
|
|
588
|
-
for (const file of expected) {
|
|
589
|
-
if (!existsSync(join(changeDir, file))) {
|
|
590
|
-
missing.push(file)
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (missing.length > 0) {
|
|
595
|
-
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
596
|
-
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
597
|
-
for (const f of missing) {
|
|
598
|
-
// 检查是否写到了错误的位置
|
|
599
|
-
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
600
|
-
if (existsSync(wrongPath)) {
|
|
601
|
-
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
602
|
-
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
603
|
-
} else {
|
|
604
|
-
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
} else {
|
|
608
|
-
console.log(`\n✅ 文件位置验证:所有 ${expected.length} 个预期文件均在变更目录中`)
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* 读本变更 design.md §6「文件变更清单」表格,提取声明交付的文件路径(concurrent 预检 ownFiles 源,D-002)。
|
|
614
|
-
*
|
|
615
|
-
* 用途:execute --done 并发预检在 in-place-fallback 模式下,需把本变更交付文件从 foreignFiles 排除
|
|
616
|
-
* (否则自己产出的脏文件会被当他者)。worktree 模式下交付文件不在主仓 git status,ownFiles 用空数组即可。
|
|
617
|
-
*
|
|
618
|
-
* 解析:定位 `## 6.` 段(到下一个 `## N.` 前),逐行扫表格行第 2 列的反引号文件路径,跳过表头/分隔行。
|
|
619
|
-
* 设计容错(design §9 / R-02):design.md 缺失 / 无 §6 / 解析空 → 返回 [],foreignFiles 退化为保守噪音,
|
|
620
|
-
* otherActiveChanges 仍可靠。
|
|
621
|
-
*
|
|
622
|
-
* @param {string} specBase 规范根(.sillyspec/ 所在)
|
|
623
|
-
* @param {string} changeName 当前变更名
|
|
624
|
-
* @returns {string[]} 本变更声明交付的文件路径(git 路径,正斜杠)
|
|
625
|
-
*/
|
|
626
|
-
function readDesignOwnFiles(specBase, changeName) {
|
|
627
|
-
if (!specBase || !changeName) return []
|
|
628
|
-
let content
|
|
629
|
-
try {
|
|
630
|
-
content = readFileSync(join(specBase, 'changes', changeName, 'design.md'), 'utf8')
|
|
631
|
-
} catch {
|
|
632
|
-
return []
|
|
633
|
-
}
|
|
634
|
-
const files = []
|
|
635
|
-
let inSection6 = false
|
|
636
|
-
for (const line of content.split('\n')) {
|
|
637
|
-
const heading = line.match(/^##\s+(\d+)\./)
|
|
638
|
-
if (heading) {
|
|
639
|
-
inSection6 = heading[1] === '6'
|
|
640
|
-
continue
|
|
641
|
-
}
|
|
642
|
-
if (!inSection6 || !line.startsWith('|')) continue
|
|
643
|
-
// 跳过表头分隔行(|---|---|)
|
|
644
|
-
if (/^\|[\s:|-]+\|?$/.test(line.trim())) continue
|
|
645
|
-
// 表格第 2 列反引号文件路径:`| 新增 | \`src/.../x.js\` | 说明 |`
|
|
646
|
-
const m = line.match(/^\|[^|]+\|\s*`([^`]+)`/)
|
|
647
|
-
if (m) files.push(m[1].trim())
|
|
648
|
-
}
|
|
649
|
-
return files
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* 阶段完成收尾共享管线(从 completeStep 抽出,消除 noAI 末步 / continueStep 完成分支绕过 gate 的
|
|
654
|
-
* S1/S2/S3 三处不对称)。调用方已自行标记 stageData.status='completed' 并 pm._write 落盘后调用本函数。
|
|
655
|
-
*
|
|
656
|
-
* 序列:handleScanStageCompleted → validateMetadata → validateFileLocations[completed‖skipped 守卫] →
|
|
657
|
-
* auxiliary 重置 → runStageCompletionGates[同守卫] → handleExecuteWorktreeCleanup。
|
|
658
|
-
*
|
|
659
|
-
* 关键陷阱(design §5.4):auxiliary 重置把 stageData.steps 换成 freshSteps(全 pending);下方
|
|
660
|
-
* runStageCompletionGates 守卫与 rollbackStageCompletion 必须用入参 steps(pre-reset 原数组),
|
|
661
|
-
* 不得重读 stageData.steps(否则计数恒 0 → gate 永跳过)。本函数全程用入参 steps,未重读。
|
|
662
|
-
*
|
|
663
|
-
* @returns {Promise<{stageCompleted:false,currentIdx,nextPendingIdx:number}|null>}
|
|
664
|
-
* null = 全部通过,调用方继续自管收尾(如下一步提示);
|
|
665
|
-
* 非 null = gate/handler 失败已 rollback,调用方直接 return。
|
|
666
|
-
*/
|
|
667
|
-
export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx = null }) {
|
|
668
|
-
// 整体 try/catch(task-04 / review-2026-08-09 #2):收尾段(execute 预检 + scan handler + validate* +
|
|
669
|
-
// auxiliary 重置 + runStageCompletionGates)任一段抛非结构化异常 → rollbackCompletionAndReturn
|
|
670
|
-
// (回滚 in-progress + 落盘 + 返回未完成对象),不冒顶 exit 1。正常的 _scanResult/_gateEarlyReturn
|
|
671
|
-
// early-return 是 return 非 throw,不被 catch 拦;下方 handleExecuteWorktreeCleanup 在 try 外(副作用独立,失败不 rollback stage 状态)。
|
|
672
|
-
try {
|
|
673
|
-
// ── execute --done 并发他者改动预检(FR-06/FR-07,非阻断 advisory)──
|
|
674
|
-
// 仅 execute 触发,不影响 scan/plan/verify/archive 等 stage 的 completeStageGates。
|
|
675
|
-
// 纯副作用:console.warn 后照常推进,不改 stageData / 不阻断级联(FR-07)。
|
|
676
|
-
// 整个钩子 try/catch 兜底——任何异常吞掉,绝不影响 gate 级联(FR-07 不阻断铁律)。
|
|
677
|
-
if (stageName === 'execute') {
|
|
678
|
-
try {
|
|
679
|
-
// ownFiles 源(D-002 + B-002,钉死):动态 import WorktreeManager 取 meta.mode(复用上方
|
|
680
|
-
// Task Review Gate 的 ../worktree.js 动态 import 先例)。WorktreeManager 无 appliedFiles
|
|
681
|
-
// 字段(B-002 已证),不用模糊的「worktree applied 文件」。
|
|
682
|
-
// - worktree 模式(meta.mode 非 in-place-fallback):主仓 git status 看不见本变更交付文件
|
|
683
|
-
// → ownFiles=[](无害,dogfood 当前实际模式)
|
|
684
|
-
// - in-place-fallback 模式:交付文件就在主仓 dirty → ownFiles 读 design.md §6 文件清单排除
|
|
685
|
-
let ownFiles = []
|
|
686
|
-
// linkedChanges:execute 通常无(quick 管线概念,存 quick guard 文件);stageData 取不到则 []。
|
|
687
|
-
const linkedChanges = Array.isArray(stageData?.linkedChanges) ? stageData.linkedChanges : []
|
|
688
|
-
try {
|
|
689
|
-
const { WorktreeManager } = await import('../worktree.js')
|
|
690
|
-
const wm = new WorktreeManager({ cwd })
|
|
691
|
-
const meta = wm.getMeta(changeName)
|
|
692
|
-
if (meta?.mode === 'in-place-fallback') {
|
|
693
|
-
ownFiles = readDesignOwnFiles(specBase, changeName)
|
|
694
|
-
}
|
|
695
|
-
} catch {
|
|
696
|
-
// getMeta 抛(meta.json 缺 / worktree 损坏)→ ownFiles=[] 兜底,不崩(FR-07)。
|
|
697
|
-
ownFiles = []
|
|
698
|
-
}
|
|
699
|
-
const detected = detectConcurrentChanges(cwd, { changeName, linkedChanges, ownFiles })
|
|
700
|
-
const warn = formatConcurrentWarning(detected)
|
|
701
|
-
if (warn) console.warn(warn)
|
|
702
|
-
} catch {
|
|
703
|
-
// FR-07 不阻断:钩子任何意外异常静默吞掉,gate 级联照常推进。
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// scan 平台 manifest + post-check(S1 平台受害者:noAI scanPostcheck 末步 / continueStep 现也走这里)
|
|
708
|
-
const _scanResult = await handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts })
|
|
709
|
-
if (_scanResult) return _scanResult
|
|
710
|
-
|
|
711
|
-
// 守卫计数:completed‖skipped === total(修 S3);用入参 steps(pre-reset 原数组,design §5.4)
|
|
712
|
-
const settledCount = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
713
|
-
const total = steps.length
|
|
714
|
-
|
|
715
|
-
validateMetadata(cwd, stageName, specBase)
|
|
716
|
-
|
|
717
|
-
// 验证关键文件位置(仅当所有步骤已结案 completed‖skipped 时才校验)
|
|
718
|
-
if (settledCount === total && total > 0) {
|
|
719
|
-
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)
|
|
723
|
-
const stageDef = stageRegistry[stageName]
|
|
724
|
-
if (stageDef?.auxiliary) {
|
|
725
|
-
const freshSteps = (stageDef.steps || []).map(s => ({
|
|
726
|
-
name: s.name,
|
|
727
|
-
status: 'pending',
|
|
728
|
-
output: null,
|
|
729
|
-
completedAt: null
|
|
730
|
-
}))
|
|
731
|
-
stageData.steps = freshSteps
|
|
732
|
-
stageData.status = 'pending'
|
|
733
|
-
stageData.completedAt = null
|
|
734
|
-
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
735
|
-
pm._write(cwd, progress, changeName)
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review → Task Review)
|
|
739
|
-
if (settledCount === total && total > 0) {
|
|
740
|
-
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx })
|
|
741
|
-
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
742
|
-
} else if (settledCount < total) {
|
|
743
|
-
console.log(`\n⚠️ 阶段校验跳过:${total} 步中仅 ${settledCount} 步标记为已结案(completed‖skipped),可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
744
|
-
}
|
|
745
|
-
} catch (e) {
|
|
746
|
-
console.error(`\n❌ ${stageName} 阶段完成收尾异常(已 rollback 为 in-progress,请修复后重新 --done):${e?.message || e}`)
|
|
747
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// execute worktree cleanup(completeStep / continueStep 完成分支统一调用,避免双清理)
|
|
751
|
-
await handleExecuteWorktreeCleanup({ stageName, changeName, cwd })
|
|
752
|
-
|
|
753
|
-
return null
|
|
754
|
-
}
|
|
755
|
-
|
|
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, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
24
|
+
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
25
|
+
import { runValidators } from '../stage-contract.js'
|
|
26
|
+
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
27
|
+
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
28
|
+
import { stageRegistry } from '../stages/index.js'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从 plan.md 提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
32
|
+
* 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js parseWavesFromPlan 的
|
|
33
|
+
* 收容口径一致:### 标题式 task / 非 task 区 checkbox 不算),light plan 的 `## Tasks` 段同样收。
|
|
34
|
+
*/
|
|
35
|
+
function extractTaskIdsFromPlan(planContent) {
|
|
36
|
+
if (!planContent) return []
|
|
37
|
+
return [...String(planContent).matchAll(/^[-*]\s*\[[ x]\]\s*(?:.*\b)?task-(\d+)\b/gim)]
|
|
38
|
+
.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 符号影响面报告结构校验(纯函数):execute「加载上下文」步的产出落盘核验。
|
|
43
|
+
*
|
|
44
|
+
* 治 persuasion-only 失效现场(ql-20260816-005-3d7f):execute 前缀 4 步无任何 gate,agent 可
|
|
45
|
+
* <1s 连发 4 次 --done 盖章跳过,「符号影响面扫描」(execute.js 操作 11)被一句「上下文在会话内」
|
|
46
|
+
* 带过(输出契约只写「上下文摘要」,操作与输出脱节)。修法 = 报告落盘
|
|
47
|
+
* {SPEC_ROOT}/changes/<change>/symbol-impact.md + 本函数校验:plan.md 每个 task-XX 在报告中出现
|
|
48
|
+
* (逐 task 结论,「无签名级变更」也要显式写,治整体一句话带过)。语义质量(调用点找没找全)
|
|
49
|
+
* 仍归 agent,CLI 只核结构覆盖度——符合债单「persuasion-only → 补最小硬门」原则。
|
|
50
|
+
*
|
|
51
|
+
* @param {{reportContent: string|null, planContent: string|null}} params
|
|
52
|
+
* @returns {{ok: boolean, errors: string[]}}
|
|
53
|
+
*/
|
|
54
|
+
export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
55
|
+
const errors = []
|
|
56
|
+
if (!reportContent || !String(reportContent).trim()) {
|
|
57
|
+
return { ok: false, errors: ['symbol-impact.md 不存在或内容为空——「加载上下文」步的符号影响面报告必须落盘(逐 task 一行结论,「无签名级变更」也要显式写)'] }
|
|
58
|
+
}
|
|
59
|
+
const taskIds = extractTaskIdsFromPlan(planContent)
|
|
60
|
+
if (taskIds.length === 0) return { ok: true, errors } // plan 无 task 可列(默认兜底形态)→ 无可校验对象
|
|
61
|
+
const missing = taskIds.filter(id => !String(reportContent).includes(id))
|
|
62
|
+
for (const id of missing) {
|
|
63
|
+
errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
|
|
64
|
+
}
|
|
65
|
+
return { ok: errors.length === 0, errors }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 符号影响面报告硬门:execute「加载上下文」步 --done 时校验 symbol-impact.md 落盘 + task 覆盖度。
|
|
70
|
+
* fail-closed:不通过 → 当前步标 blocked + exit 1(进度不推进)。非目标步骤/阶段直接放行。
|
|
71
|
+
*/
|
|
72
|
+
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
73
|
+
if (stageName !== 'execute') return
|
|
74
|
+
if (!currentStepName || !currentStepName.includes('加载上下文')) return
|
|
75
|
+
const planPath = join(specBase, 'changes', changeName || '', 'plan.md')
|
|
76
|
+
const reportPath = join(specBase, 'changes', changeName || '', 'symbol-impact.md')
|
|
77
|
+
if (!existsSync(planPath)) return // plan.md 缺失场景已有 plan 阶段 gate 把关,此处不重复拦
|
|
78
|
+
let planContent = ''
|
|
79
|
+
try { planContent = readFileSync(planPath, 'utf8') } catch { return }
|
|
80
|
+
let reportContent = null
|
|
81
|
+
try { reportContent = readFileSync(reportPath, 'utf8') } catch { /* 缺失由 validate 报 */ }
|
|
82
|
+
const result = validateSymbolImpactReport({ reportContent, planContent })
|
|
83
|
+
if (result.ok) return
|
|
84
|
+
console.error('❌ ── 符号影响面报告校验阻断(本次 --done 未完成,进度未推进)──')
|
|
85
|
+
console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
|
|
86
|
+
console.error(` 期望路径:${reportPath}`)
|
|
87
|
+
for (const e of result.errors) console.error(` • ${e}`)
|
|
88
|
+
console.error(' 修复:按「加载上下文」步操作 11 完成符号影响面扫描,逐 task 写结论(无签名级变更也要显式写「无」),落盘后重跑 execute --done。')
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
|
|
94
|
+
* 仅 wave 执行步骤(名如 "Wave N 执行")可 opt-out;非 wave 步骤恒返回 false(保守过门)。
|
|
95
|
+
*/
|
|
96
|
+
function isCurrentWaveAllNoDepsVerify(stepName, changeDir) {
|
|
97
|
+
if (!stepName) return false
|
|
98
|
+
const m = String(stepName).match(/^Wave\s+(\d+)/i)
|
|
99
|
+
if (!m) return false
|
|
100
|
+
const waveN = parseInt(m[1], 10)
|
|
101
|
+
if (!changeDir) return false
|
|
102
|
+
const planPath = join(changeDir, 'plan.md')
|
|
103
|
+
if (!existsSync(planPath)) return false
|
|
104
|
+
const plan = readFileSync(planPath, 'utf8')
|
|
105
|
+
const waveRe = new RegExp(`^##\\s*Wave\\s+${waveN}\\b[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|\\n#\\s|$)`, 'm')
|
|
106
|
+
const waveMatch = plan.match(waveRe)
|
|
107
|
+
if (!waveMatch) return false
|
|
108
|
+
const taskIds = [...waveMatch[1].matchAll(/^[-*]\s*\[[ x]\]\s*(task-\d+)/gim)].map(x => x[1])
|
|
109
|
+
if (taskIds.length === 0) return false
|
|
110
|
+
for (const id of taskIds) {
|
|
111
|
+
const cardPath = join(changeDir, 'tasks', `${id}.md`)
|
|
112
|
+
if (!existsSync(cardPath)) return false // 卡片缺失 → 保守不跳
|
|
113
|
+
const card = readFileSync(cardPath, 'utf8')
|
|
114
|
+
const fm = card.match(/^---\n([\s\S]*?)\n---/)
|
|
115
|
+
if (!fm) return false
|
|
116
|
+
if (!/no_deps_verify:\s*true/i.test(fm[1])) return false
|
|
117
|
+
}
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
123
|
+
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
124
|
+
* 放行返回 true;阻断时 process.exit(1) 不返回。
|
|
125
|
+
*/
|
|
126
|
+
export async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
127
|
+
if (stageName !== 'execute') return true
|
|
128
|
+
let meta = null
|
|
129
|
+
let wm = null
|
|
130
|
+
try {
|
|
131
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
132
|
+
wm = new WorktreeManager({ cwd })
|
|
133
|
+
meta = wm.getMeta(changeName)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// WorktreeManager 构造/读取失败(git 不可用 / worktree 元数据损坏)不阻断 deps gate;
|
|
136
|
+
// 下游已有物理目录存在性判定兜底(G2/R3),这里 warn 留下根因线索,避免静默误诊。
|
|
137
|
+
console.warn(`⚠️ worktree meta 读取失败(deps gate 将走物理目录判定): ${e.message}`)
|
|
138
|
+
}
|
|
139
|
+
const depsStatus = meta?.depsStatus
|
|
140
|
+
if (['linked', 'installed', 'n/a'].includes(depsStatus)) return true
|
|
141
|
+
const changeDir = changeName ? join(specBase, 'changes', changeName) : null
|
|
142
|
+
if (isCurrentWaveAllNoDepsVerify(step?.name, changeDir)) return true
|
|
143
|
+
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
144
|
+
// ── 诊断分支(Phase 2,G2/R3 修正:判定基于物理目录而非 !meta)──
|
|
145
|
+
// getMeta 对"目录不存在"与"meta 损坏"都返回 null,后者会误判终态 → 用物理目录存在性判定。
|
|
146
|
+
let worktreeGone = true
|
|
147
|
+
try {
|
|
148
|
+
worktreeGone = !!(wm && changeName) && !existsSync(wm.getWorktreePath(changeName))
|
|
149
|
+
} catch {}
|
|
150
|
+
// ── fail-loud 块(Phase 3,D-005@v1:仅改拒绝侧 stderr)──
|
|
151
|
+
console.error('❌ ── deps 门控阻断(本次 --done 未完成,进度未推进)──')
|
|
152
|
+
if (worktreeGone) {
|
|
153
|
+
console.error(' worktree 不可用(已 cleanup 或目录不存在)。')
|
|
154
|
+
console.error(` 修复:sillyspec doctor --align-execute-progress${changeName ? ` --change ${changeName}` : ''} 按 plan.md 对齐进度`)
|
|
155
|
+
console.error(` 或: sillyspec worktree create ${changeName || '<change>'} 重建 worktree 继续跑`)
|
|
156
|
+
} else {
|
|
157
|
+
console.error(` 原因:依赖未就绪(depsStatus=${depsStatus || 'unknown'}),不得在无构建/测试能力时声称完成。`)
|
|
158
|
+
console.error(` 修复:sillyspec worktree doctor --fix${changeName ? ` --change ${changeName}` : ''}`)
|
|
159
|
+
console.error(' 或在 worktree 内手动安装依赖后重试。')
|
|
160
|
+
if (meta?.depsError) console.error(` 上次供给错误:${meta.depsError}`)
|
|
161
|
+
}
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
|
|
167
|
+
*
|
|
168
|
+
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
169
|
+
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
170
|
+
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
171
|
+
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式。
|
|
172
|
+
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
173
|
+
*/
|
|
174
|
+
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
175
|
+
if (stageName !== 'execute' || !changeName) return true
|
|
176
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
177
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
178
|
+
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
179
|
+
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
180
|
+
const { validateCheckedTaskReviews, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('../task-review.js')
|
|
181
|
+
let executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
182
|
+
// marker 是 agent 可写内容:格式校验防注入/穿越,非法视为缺失走漂移兜底重定位
|
|
183
|
+
if (executeRunId && !isValidExecuteRunId(executeRunId)) {
|
|
184
|
+
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(executeRunId.slice(0, 60))}),改扫真实 run 目录`)
|
|
185
|
+
executeRunId = ''
|
|
186
|
+
}
|
|
187
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
188
|
+
// marker 漂移兜底(gate-atom-a 正确修法):marker 指向的 run 缺 tasks/(generateExecuteRunId 只写
|
|
189
|
+
// marker 不建目录,漂移后新 run 不继承旧 review)时,无视 marker 改扫 execute-runs/ 取 mtime 最新
|
|
190
|
+
// 且真正含 tasks/ 的 run,用其齐备的 review.json 校验,避免误报「review.json 不存在」。注意不能用
|
|
191
|
+
// resolveLatestExecuteRunId——它见 marker 非空即原样返回(不校验目录),恰是本场景要绕开的值。
|
|
192
|
+
if (executeRunId && !existsSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'))) {
|
|
193
|
+
const relocated = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
194
|
+
if (relocated && relocated !== executeRunId) {
|
|
195
|
+
console.warn(`⚠️ execute run marker 漂移:${executeRunId} 无 tasks/,改用真实含 review 的 run ${relocated}`)
|
|
196
|
+
executeRunId = relocated
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
200
|
+
if (result.ok) return true
|
|
201
|
+
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
202
|
+
console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
|
|
203
|
+
console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
|
|
204
|
+
for (const f of result.failures) {
|
|
205
|
+
const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
|
|
206
|
+
console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
|
|
207
|
+
for (const e of f.errors) console.error(` - ${e}`)
|
|
208
|
+
}
|
|
209
|
+
console.error(' 修复:补全上述 review.json 字段后重跑 execute --done。')
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 阶段完成校验失败时回滚状态。
|
|
215
|
+
*
|
|
216
|
+
* completeStep 在跑 validator 之前就把 stageData.status 写成 'completed',
|
|
217
|
+
* 若校验失败不回滚,DB 会与真实产物不一致(hook/doctor/下游阶段全部误判),
|
|
218
|
+
* 且所有步骤都是 completed 时 agent 无法重新 --done("没有待完成的步骤")。
|
|
219
|
+
* 此处将 stage 回滚为 in-progress,最后一步重置为 pending,供修复产物后重做。
|
|
220
|
+
*/
|
|
221
|
+
function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
222
|
+
// 辅助阶段在 validator 前已被重置为 pending(steps 也换成了新数组),不要覆盖
|
|
223
|
+
if (stageData.status === 'completed') {
|
|
224
|
+
stageData.status = 'in-progress'
|
|
225
|
+
stageData.completedAt = null
|
|
226
|
+
}
|
|
227
|
+
if (steps[currentIdx] && steps[currentIdx].status === 'completed') {
|
|
228
|
+
steps[currentIdx].status = 'pending'
|
|
229
|
+
steps[currentIdx].completedAt = null
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 阶段完成校验失败后的统一收尾:回滚 stage/step 状态 + 落盘 + sync + 返回「未完成」。
|
|
235
|
+
*
|
|
236
|
+
* completeStep 的各 gate(runValidators / verify-test 对账 / plan→execute contract /
|
|
237
|
+
* Stage Review / Execute Task Review)失败时都走这套动作;原先每个失败分支手写重复
|
|
238
|
+
* ~7 次(含 lastActive 落盘 + triggerSync + return 结构),统一进来消坑,避免某分支
|
|
239
|
+
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
240
|
+
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
241
|
+
*/
|
|
242
|
+
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
243
|
+
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
244
|
+
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
245
|
+
pm._write(cwd, progress, changeName)
|
|
246
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
247
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 阶段完成校验 gate 级联(从 completeStep 抽出,行为保持)。仅当所有步骤确实标记为 completed 时
|
|
251
|
+
* 由 completeStep 调用。顺序:runValidators → verify-test 对账 → Plan→Execute contract →
|
|
252
|
+
* Stage Review Gate → Execute Task Review Gate。任一 gate 失败 → rollbackCompletionAndReturn
|
|
253
|
+
* (统一回滚 + 返回 early-return 对象);全部通过 → 返回 null(completeStep 继续收尾)。
|
|
254
|
+
*
|
|
255
|
+
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
256
|
+
*/
|
|
257
|
+
export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx = null }) {
|
|
258
|
+
const projectName = progress.project || basename(cwd)
|
|
259
|
+
const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
|
|
260
|
+
if (contractResult.errors.length > 0) {
|
|
261
|
+
console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
|
|
262
|
+
for (const err of contractResult.errors) {
|
|
263
|
+
console.error(` - ${err}`)
|
|
264
|
+
}
|
|
265
|
+
// 出路提示常放在 warnings(如「写明不改理由即可豁免」),此处即将阻断 return,
|
|
266
|
+
// 必须一并打出,否则 agent 只看到症状看不到怎么过。
|
|
267
|
+
for (const w of (contractResult.warnings || [])) {
|
|
268
|
+
console.error(` · ${w}`)
|
|
269
|
+
}
|
|
270
|
+
console.error(`\n 提示:修复缺失产物后重新完成此步骤(--skip-approval 只跳过阶段转换/审批检查,不能跳过产物校验)`)
|
|
271
|
+
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
272
|
+
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
273
|
+
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
274
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
275
|
+
}
|
|
276
|
+
if (contractResult.warnings.length > 0) {
|
|
277
|
+
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
278
|
+
for (const w of contractResult.warnings) {
|
|
279
|
+
console.warn(` - ${w}`)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
284
|
+
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
285
|
+
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
286
|
+
if (stageName === 'verify') {
|
|
287
|
+
const { runVerifyTestCheck, printVerifyTestCheck } = await import('../verify-postcheck.js')
|
|
288
|
+
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
289
|
+
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
290
|
+
const testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
|
|
291
|
+
printVerifyTestCheck(testCheck)
|
|
292
|
+
if (testCheck.status === 'failed') {
|
|
293
|
+
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
294
|
+
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
295
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
296
|
+
}
|
|
297
|
+
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
298
|
+
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
299
|
+
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
300
|
+
const parityRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
301
|
+
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: parityRuntimeRoot })
|
|
302
|
+
printVerifyParityCheck(parityCheck)
|
|
303
|
+
// ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
|
|
304
|
+
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
305
|
+
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
306
|
+
printVerifyDeletionCheck(deletionCheck)
|
|
307
|
+
// ── required-evidence 对账(advisory,不阻断):闭合 execute→verify evidence 死链 ──
|
|
308
|
+
// execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json,
|
|
309
|
+
// 本探针查每个 cannot_verify 任务是否在 verify-result.md 体现(CLI 只查提及,满足度 agent 自报告)。
|
|
310
|
+
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
|
|
311
|
+
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
|
|
312
|
+
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
313
|
+
// ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
|
|
314
|
+
// 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
|
|
315
|
+
// agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
|
|
316
|
+
// 类「verify PASS → archive 才发现 pending」的时序漏洞)。
|
|
317
|
+
const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
318
|
+
if (verifyChangeDir) {
|
|
319
|
+
const impactPath = join(verifyChangeDir, 'module-impact.md')
|
|
320
|
+
if (existsSync(impactPath)) {
|
|
321
|
+
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
322
|
+
const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
|
|
323
|
+
if (pendingRows.length > 0) {
|
|
324
|
+
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
325
|
+
for (const row of pendingRows) console.error(` - ${row}`)
|
|
326
|
+
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
327
|
+
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Plan postcheck contract:plan.md 必须满足 execute 契约 ──
|
|
335
|
+
if (stageName === 'plan') {
|
|
336
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
337
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
338
|
+
if (planPath && existsSync(planPath)) {
|
|
339
|
+
const { validatePlanForExecute } = await import('../stages/execute.js')
|
|
340
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
341
|
+
const planValidation = validatePlanForExecute(planContent)
|
|
342
|
+
if (!planValidation.ok) {
|
|
343
|
+
console.error(`\n❌ Plan → Execute Contract 校验失败:`)
|
|
344
|
+
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
345
|
+
console.error(`\n plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
346
|
+
// 阻断 completed
|
|
347
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
348
|
+
}
|
|
349
|
+
if (planValidation.warnings.length > 0) {
|
|
350
|
+
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
351
|
+
for (const w of planValidation.warnings) console.warn(` - ${w}`)
|
|
352
|
+
}
|
|
353
|
+
if (planValidation.ok) {
|
|
354
|
+
console.log(`\n✅ Plan → Execute Contract 校验通过(${planValidation.tasks.length} tasks, ${planValidation.waves.length} waves)`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Stage Review Gate:brainstorm/plan/execute 独立审查(按 tier 分级)──
|
|
360
|
+
// tier=self 放行+审计打印;tier=independent 必须 review.json 且 verdict 非 fail,fail-closed
|
|
361
|
+
if (['brainstorm', 'plan', 'execute'].includes(stageName)) {
|
|
362
|
+
try {
|
|
363
|
+
const { classifyReviewTier } = await import('../review-tier.js')
|
|
364
|
+
const { validateStageReview, getLatestStageReviewRunId, printStageReviewResult, generateStageReviewRunId, stageReviewMarkerPath } = await import('../stage-review.js')
|
|
365
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
366
|
+
const reviewChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
367
|
+
const designPath = reviewChangeDir ? join(reviewChangeDir, 'design.md') : null
|
|
368
|
+
let planLevel = null
|
|
369
|
+
if (reviewChangeDir) {
|
|
370
|
+
const planPath = join(reviewChangeDir, 'plan.md')
|
|
371
|
+
if (existsSync(planPath)) {
|
|
372
|
+
const fmLine = readFileSync(planPath, 'utf8').split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
373
|
+
if (fmLine) planLevel = fmLine.split(':')[1].trim()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const tier = classifyReviewTier({ planLevel, designPath })
|
|
377
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
378
|
+
|
|
379
|
+
if (tier.tier === 'self') {
|
|
380
|
+
console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
|
|
381
|
+
} else {
|
|
382
|
+
let reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
|
|
383
|
+
if (!reviewRunId) {
|
|
384
|
+
// marker 缺失(execute 批量完成跳过 prompt 渲染等场景)→ 自生 + 写盘
|
|
385
|
+
// 让 gate 读到确定 ID,错误从 execute-null 变 execute-review-<id>(可执行)
|
|
386
|
+
reviewRunId = generateStageReviewRunId()
|
|
387
|
+
try {
|
|
388
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
389
|
+
writeFileSync(stageReviewMarkerPath(runtimeRoot, stageName, changeName), reviewRunId + '\n')
|
|
390
|
+
} catch {}
|
|
391
|
+
}
|
|
392
|
+
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
393
|
+
: stageName === 'plan' ? 'plan'
|
|
394
|
+
: 'acceptance'
|
|
395
|
+
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
396
|
+
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
397
|
+
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot })
|
|
398
|
+
if (!reviewResult.ok) {
|
|
399
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (e) {
|
|
403
|
+
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
404
|
+
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
405
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
410
|
+
if (stageName === 'execute') {
|
|
411
|
+
try {
|
|
412
|
+
const { validateTaskReviews, printReviewResult, writeVerifyRequiredEvidence } = await import('../task-review.js')
|
|
413
|
+
const effectiveSpecBase = platformOpts?.specRoot || specBase
|
|
414
|
+
const planFile = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
415
|
+
const planPath = planFile ? join(planFile, 'plan.md') : null
|
|
416
|
+
|
|
417
|
+
if (planPath && existsSync(planPath)) {
|
|
418
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
419
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, effectiveSpecBase)
|
|
420
|
+
|
|
421
|
+
// execute run id:从变更专属标记文件读取(agent 可写内容,格式校验防注入/穿越)
|
|
422
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
423
|
+
let executeRunId = ''
|
|
424
|
+
try {
|
|
425
|
+
if (existsSync(runIdFile)) {
|
|
426
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
427
|
+
const { isValidExecuteRunId } = await import('../task-review.js')
|
|
428
|
+
if (c && !isValidExecuteRunId(c)) {
|
|
429
|
+
console.warn(`⚠️ execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),视为缺失回退扫描`)
|
|
430
|
+
} else {
|
|
431
|
+
executeRunId = c
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
if (!executeRunId) {
|
|
436
|
+
// marker 缺失:先扫描 execute-runs/ 既有目录找回真实 runId(与 getLatestStageReviewRunId
|
|
437
|
+
// 目录扫描兜底同语义),避免 marker 丢失而 agent 已用旧 runId 落盘时,直接 generate 新 ID
|
|
438
|
+
// 找不到旧 review、误判缺 review.json。仅当确实无既有 run 才 generate 新 ID 并落盘。
|
|
439
|
+
const { generateExecuteRunId, resolveLatestExecuteRunId } = await import('../task-review.js')
|
|
440
|
+
executeRunId = resolveLatestExecuteRunId({ runtimeRoot, changeName }) || ''
|
|
441
|
+
if (!executeRunId) {
|
|
442
|
+
executeRunId = generateExecuteRunId()
|
|
443
|
+
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
444
|
+
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
|
|
445
|
+
// marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
|
|
446
|
+
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
447
|
+
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
448
|
+
writeFileSync(runIdFile, executeRunId + '\n')
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库。
|
|
453
|
+
// 跨仓支持(task-07 / design §6 gates 行 + D-013):
|
|
454
|
+
// - 有 ctx(execute 启动入口 task-09 构造的 MultiRepoContext 实例)→ 用
|
|
455
|
+
// ctx.resolve('main').gitDir 作为主仓 task 的校验 cwd。ctx 的 main entry 已在
|
|
456
|
+
// _buildMainEntry 内统一编码 worktreePath/cwd + in-place-fallback 兜底(与 task-review.js:724
|
|
457
|
+
// generateTaskReviewDrafts 的 in-place 逻辑同源),避免两处漂移。Task Review Gate 内部循环
|
|
458
|
+
// 再按 review.repo ?? 'main' 从 ctx.resolve(repo).gitDir 切跨仓 task 的 gitDir(task-04 实现)。
|
|
459
|
+
// - 无 ctx(ctx=null 缺省,单仓 / 旧调用链)→ 走原逻辑(meta.worktreePath 或 cwd),零回归。
|
|
460
|
+
let reviewGitDir = cwd
|
|
461
|
+
if (ctx) {
|
|
462
|
+
const mainEntry = ctx.resolve('main')
|
|
463
|
+
if (mainEntry?.gitDir) reviewGitDir = mainEntry.gitDir
|
|
464
|
+
} else {
|
|
465
|
+
try {
|
|
466
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
467
|
+
const wm = new WorktreeManager({ cwd })
|
|
468
|
+
const meta = wm.getMeta(changeName)
|
|
469
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
470
|
+
reviewGitDir = meta.worktreePath
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir, ctx })
|
|
476
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
477
|
+
|
|
478
|
+
if (!reviewResult.ok) {
|
|
479
|
+
// Task review 校验失败,阻断 execute 完成
|
|
480
|
+
// 检查是否存在 checkbox 已勾但 review 不通过的情况
|
|
481
|
+
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
482
|
+
if (uncheckedTasks.length > 0) {
|
|
483
|
+
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
484
|
+
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
485
|
+
}
|
|
486
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
490
|
+
if (reviewResult.requiredEvidence.length > 0) {
|
|
491
|
+
const evidencePath = writeVerifyRequiredEvidence(join(effectiveSpecBase, 'changes', changeName), reviewResult.requiredEvidence)
|
|
492
|
+
if (evidencePath) {
|
|
493
|
+
console.log(`📄 verify-required-evidence.json 已写入: ${evidencePath}`)
|
|
494
|
+
console.log(' verify 阶段必须满足这些证据要求。')
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (e) {
|
|
499
|
+
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
500
|
+
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
501
|
+
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
502
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 读取 design.md frontmatter 的 scale 字段(brainstorm 末步写入 'small'|'large')。
|
|
510
|
+
* completeStep 的下一步提示据此分叉:small→quick,large/读不到→plan(fail-safe 走重流程)。
|
|
511
|
+
* 只解析首个 YAML frontmatter 块,避免误读正文里的 "scale:"。
|
|
512
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线 + completeStep brainstorm 提示消费)。
|
|
513
|
+
*/
|
|
514
|
+
export function readDesignScale(specBase, changeName) {
|
|
515
|
+
if (!changeName) return null
|
|
516
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
517
|
+
if (!existsSync(designPath)) return null
|
|
518
|
+
const text = readFileSync(designPath, 'utf8')
|
|
519
|
+
const fm = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
|
|
520
|
+
if (!fm) return null
|
|
521
|
+
const m = fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
|
|
522
|
+
return m ? m[1] : null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 校验变更目录下近 10 分钟新增的 .md/.yaml/.yml 文件含 author/created_at 元数据(advisory 打印)。
|
|
527
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
528
|
+
*/
|
|
529
|
+
export function validateMetadata(cwd, stageName, specBase) {
|
|
530
|
+
const changesDir = join(specBase, 'changes')
|
|
531
|
+
if (!existsSync(changesDir)) return
|
|
532
|
+
|
|
533
|
+
const cutoff = Date.now() - 10 * 60 * 1000
|
|
534
|
+
const missing = []
|
|
535
|
+
|
|
536
|
+
function walk(dir) {
|
|
537
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
538
|
+
const full = join(dir, entry.name)
|
|
539
|
+
try {
|
|
540
|
+
if (entry.isDirectory()) { walk(full); continue }
|
|
541
|
+
if (!/\.(md|yaml|yml)$/.test(entry.name)) continue
|
|
542
|
+
const mtime = statSync(full).mtimeMs
|
|
543
|
+
if (mtime < cutoff) continue
|
|
544
|
+
const content = readFileSync(full, 'utf-8')
|
|
545
|
+
if (!content.includes('author:') && !content.includes('author:')) missing.push(full)
|
|
546
|
+
if (!content.includes('created_at:') && !content.includes('created_at:')) missing.push(full)
|
|
547
|
+
} catch (e) { /* skip unreadable files */ }
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
walk(changesDir)
|
|
552
|
+
const unique = [...new Set(missing)]
|
|
553
|
+
if (unique.length > 0) {
|
|
554
|
+
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
555
|
+
unique.forEach(f => console.log(` - ${f.replace(cwd + '/', '')}`))
|
|
556
|
+
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 验证关键文件是否存在于正确的变更目录下(advisory 打印,防 AI 写错路径)。
|
|
562
|
+
* 从 complete.js 迁入(completeStageGates 共享收尾管线消费)。
|
|
563
|
+
*/
|
|
564
|
+
export function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
565
|
+
const effectiveChange = changeName || progress.currentChange
|
|
566
|
+
if (!effectiveChange) return
|
|
567
|
+
|
|
568
|
+
const changeDir = join(specBase, 'changes', effectiveChange)
|
|
569
|
+
if (!existsSync(changeDir)) return
|
|
570
|
+
|
|
571
|
+
// 每个阶段完成后预期存在的文件
|
|
572
|
+
// brainstorm:scale=small(小变更)只必产 design.md;large/未标 scale → 四件套全。
|
|
573
|
+
// 与 validateBrainstormOutputs 的 BRAINSTORM_RULES condition(scale≠small)同源,避免对合法 small 变更
|
|
574
|
+
// 误报"⬜ proposal/requirements/tasks 未找到"(本检查仅 advisory 打印不 gate,但误导输出仍要消除)。
|
|
575
|
+
const brainstormExpected = readDesignScale(specBase, effectiveChange) === 'small'
|
|
576
|
+
? ['design.md']
|
|
577
|
+
: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md']
|
|
578
|
+
const expectedFiles = {
|
|
579
|
+
brainstorm: brainstormExpected,
|
|
580
|
+
plan: ['plan.md'],
|
|
581
|
+
archive: ['module-impact.md'],
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const expected = expectedFiles[stageName]
|
|
585
|
+
if (!expected) return
|
|
586
|
+
|
|
587
|
+
const missing = []
|
|
588
|
+
for (const file of expected) {
|
|
589
|
+
if (!existsSync(join(changeDir, file))) {
|
|
590
|
+
missing.push(file)
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (missing.length > 0) {
|
|
595
|
+
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
596
|
+
console.log(` 变更目录:${changeDir.replace(cwd + '/', '')}/`)
|
|
597
|
+
for (const f of missing) {
|
|
598
|
+
// 检查是否写到了错误的位置
|
|
599
|
+
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
600
|
+
if (existsSync(wrongPath)) {
|
|
601
|
+
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${wrongPath.replace(cwd + '/', '')}`)
|
|
602
|
+
console.log(` 提示:应该写入 ${changeDir.replace(cwd + '/', '')}/${f}`)
|
|
603
|
+
} else {
|
|
604
|
+
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
console.log(`\n✅ 文件位置验证:所有 ${expected.length} 个预期文件均在变更目录中`)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* 读本变更 design.md §6「文件变更清单」表格,提取声明交付的文件路径(concurrent 预检 ownFiles 源,D-002)。
|
|
614
|
+
*
|
|
615
|
+
* 用途:execute --done 并发预检在 in-place-fallback 模式下,需把本变更交付文件从 foreignFiles 排除
|
|
616
|
+
* (否则自己产出的脏文件会被当他者)。worktree 模式下交付文件不在主仓 git status,ownFiles 用空数组即可。
|
|
617
|
+
*
|
|
618
|
+
* 解析:定位 `## 6.` 段(到下一个 `## N.` 前),逐行扫表格行第 2 列的反引号文件路径,跳过表头/分隔行。
|
|
619
|
+
* 设计容错(design §9 / R-02):design.md 缺失 / 无 §6 / 解析空 → 返回 [],foreignFiles 退化为保守噪音,
|
|
620
|
+
* otherActiveChanges 仍可靠。
|
|
621
|
+
*
|
|
622
|
+
* @param {string} specBase 规范根(.sillyspec/ 所在)
|
|
623
|
+
* @param {string} changeName 当前变更名
|
|
624
|
+
* @returns {string[]} 本变更声明交付的文件路径(git 路径,正斜杠)
|
|
625
|
+
*/
|
|
626
|
+
function readDesignOwnFiles(specBase, changeName) {
|
|
627
|
+
if (!specBase || !changeName) return []
|
|
628
|
+
let content
|
|
629
|
+
try {
|
|
630
|
+
content = readFileSync(join(specBase, 'changes', changeName, 'design.md'), 'utf8')
|
|
631
|
+
} catch {
|
|
632
|
+
return []
|
|
633
|
+
}
|
|
634
|
+
const files = []
|
|
635
|
+
let inSection6 = false
|
|
636
|
+
for (const line of content.split('\n')) {
|
|
637
|
+
const heading = line.match(/^##\s+(\d+)\./)
|
|
638
|
+
if (heading) {
|
|
639
|
+
inSection6 = heading[1] === '6'
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
if (!inSection6 || !line.startsWith('|')) continue
|
|
643
|
+
// 跳过表头分隔行(|---|---|)
|
|
644
|
+
if (/^\|[\s:|-]+\|?$/.test(line.trim())) continue
|
|
645
|
+
// 表格第 2 列反引号文件路径:`| 新增 | \`src/.../x.js\` | 说明 |`
|
|
646
|
+
const m = line.match(/^\|[^|]+\|\s*`([^`]+)`/)
|
|
647
|
+
if (m) files.push(m[1].trim())
|
|
648
|
+
}
|
|
649
|
+
return files
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 阶段完成收尾共享管线(从 completeStep 抽出,消除 noAI 末步 / continueStep 完成分支绕过 gate 的
|
|
654
|
+
* S1/S2/S3 三处不对称)。调用方已自行标记 stageData.status='completed' 并 pm._write 落盘后调用本函数。
|
|
655
|
+
*
|
|
656
|
+
* 序列:handleScanStageCompleted → validateMetadata → validateFileLocations[completed‖skipped 守卫] →
|
|
657
|
+
* auxiliary 重置 → runStageCompletionGates[同守卫] → handleExecuteWorktreeCleanup。
|
|
658
|
+
*
|
|
659
|
+
* 关键陷阱(design §5.4):auxiliary 重置把 stageData.steps 换成 freshSteps(全 pending);下方
|
|
660
|
+
* runStageCompletionGates 守卫与 rollbackStageCompletion 必须用入参 steps(pre-reset 原数组),
|
|
661
|
+
* 不得重读 stageData.steps(否则计数恒 0 → gate 永跳过)。本函数全程用入参 steps,未重读。
|
|
662
|
+
*
|
|
663
|
+
* @returns {Promise<{stageCompleted:false,currentIdx,nextPendingIdx:number}|null>}
|
|
664
|
+
* null = 全部通过,调用方继续自管收尾(如下一步提示);
|
|
665
|
+
* 非 null = gate/handler 失败已 rollback,调用方直接 return。
|
|
666
|
+
*/
|
|
667
|
+
export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx = null }) {
|
|
668
|
+
// 整体 try/catch(task-04 / review-2026-08-09 #2):收尾段(execute 预检 + scan handler + validate* +
|
|
669
|
+
// auxiliary 重置 + runStageCompletionGates)任一段抛非结构化异常 → rollbackCompletionAndReturn
|
|
670
|
+
// (回滚 in-progress + 落盘 + 返回未完成对象),不冒顶 exit 1。正常的 _scanResult/_gateEarlyReturn
|
|
671
|
+
// early-return 是 return 非 throw,不被 catch 拦;下方 handleExecuteWorktreeCleanup 在 try 外(副作用独立,失败不 rollback stage 状态)。
|
|
672
|
+
try {
|
|
673
|
+
// ── execute --done 并发他者改动预检(FR-06/FR-07,非阻断 advisory)──
|
|
674
|
+
// 仅 execute 触发,不影响 scan/plan/verify/archive 等 stage 的 completeStageGates。
|
|
675
|
+
// 纯副作用:console.warn 后照常推进,不改 stageData / 不阻断级联(FR-07)。
|
|
676
|
+
// 整个钩子 try/catch 兜底——任何异常吞掉,绝不影响 gate 级联(FR-07 不阻断铁律)。
|
|
677
|
+
if (stageName === 'execute') {
|
|
678
|
+
try {
|
|
679
|
+
// ownFiles 源(D-002 + B-002,钉死):动态 import WorktreeManager 取 meta.mode(复用上方
|
|
680
|
+
// Task Review Gate 的 ../worktree.js 动态 import 先例)。WorktreeManager 无 appliedFiles
|
|
681
|
+
// 字段(B-002 已证),不用模糊的「worktree applied 文件」。
|
|
682
|
+
// - worktree 模式(meta.mode 非 in-place-fallback):主仓 git status 看不见本变更交付文件
|
|
683
|
+
// → ownFiles=[](无害,dogfood 当前实际模式)
|
|
684
|
+
// - in-place-fallback 模式:交付文件就在主仓 dirty → ownFiles 读 design.md §6 文件清单排除
|
|
685
|
+
let ownFiles = []
|
|
686
|
+
// linkedChanges:execute 通常无(quick 管线概念,存 quick guard 文件);stageData 取不到则 []。
|
|
687
|
+
const linkedChanges = Array.isArray(stageData?.linkedChanges) ? stageData.linkedChanges : []
|
|
688
|
+
try {
|
|
689
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
690
|
+
const wm = new WorktreeManager({ cwd })
|
|
691
|
+
const meta = wm.getMeta(changeName)
|
|
692
|
+
if (meta?.mode === 'in-place-fallback') {
|
|
693
|
+
ownFiles = readDesignOwnFiles(specBase, changeName)
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
// getMeta 抛(meta.json 缺 / worktree 损坏)→ ownFiles=[] 兜底,不崩(FR-07)。
|
|
697
|
+
ownFiles = []
|
|
698
|
+
}
|
|
699
|
+
const detected = detectConcurrentChanges(cwd, { changeName, linkedChanges, ownFiles })
|
|
700
|
+
const warn = formatConcurrentWarning(detected)
|
|
701
|
+
if (warn) console.warn(warn)
|
|
702
|
+
} catch {
|
|
703
|
+
// FR-07 不阻断:钩子任何意外异常静默吞掉,gate 级联照常推进。
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// scan 平台 manifest + post-check(S1 平台受害者:noAI scanPostcheck 末步 / continueStep 现也走这里)
|
|
708
|
+
const _scanResult = await handleScanStageCompleted({ stageName, currentIdx, cwd, progress, pm, stageData, changeName, outputText, platformOpts })
|
|
709
|
+
if (_scanResult) return _scanResult
|
|
710
|
+
|
|
711
|
+
// 守卫计数:completed‖skipped === total(修 S3);用入参 steps(pre-reset 原数组,design §5.4)
|
|
712
|
+
const settledCount = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length
|
|
713
|
+
const total = steps.length
|
|
714
|
+
|
|
715
|
+
validateMetadata(cwd, stageName, specBase)
|
|
716
|
+
|
|
717
|
+
// 验证关键文件位置(仅当所有步骤已结案 completed‖skipped 时才校验)
|
|
718
|
+
if (settledCount === total && total > 0) {
|
|
719
|
+
validateFileLocations(cwd, stageName, progress, changeName, specBase)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// 辅助阶段完成后重置步骤(scan 等 auxiliary 阶段重置回 pending 可重跑)
|
|
723
|
+
const stageDef = stageRegistry[stageName]
|
|
724
|
+
if (stageDef?.auxiliary) {
|
|
725
|
+
const freshSteps = (stageDef.steps || []).map(s => ({
|
|
726
|
+
name: s.name,
|
|
727
|
+
status: 'pending',
|
|
728
|
+
output: null,
|
|
729
|
+
completedAt: null
|
|
730
|
+
}))
|
|
731
|
+
stageData.steps = freshSteps
|
|
732
|
+
stageData.status = 'pending'
|
|
733
|
+
stageData.completedAt = null
|
|
734
|
+
if (progress.currentStage === stageName) progress.currentStage = ''
|
|
735
|
+
pm._write(cwd, progress, changeName)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 阶段完成校验 gate 级联(runValidators → verify-test → Plan→Execute → Stage Review → Task Review)
|
|
739
|
+
if (settledCount === total && total > 0) {
|
|
740
|
+
const _gateEarlyReturn = await runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx })
|
|
741
|
+
if (_gateEarlyReturn) return _gateEarlyReturn
|
|
742
|
+
} else if (settledCount < total) {
|
|
743
|
+
console.log(`\n⚠️ 阶段校验跳过:${total} 步中仅 ${settledCount} 步标记为已结案(completed‖skipped),可能存在状态不同步。如确认阶段已完成,请运行 --status 确认。`)
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
console.error(`\n❌ ${stageName} 阶段完成收尾异常(已 rollback 为 in-progress,请修复后重新 --done):${e?.message || e}`)
|
|
747
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// execute worktree cleanup(completeStep / continueStep 完成分支统一调用,避免双清理)
|
|
751
|
+
await handleExecuteWorktreeCleanup({ stageName, changeName, cwd })
|
|
752
|
+
|
|
753
|
+
return null
|
|
754
|
+
}
|
|
755
|
+
|