sillyspec 3.20.2 → 3.20.4
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 +21 -21
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -21
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
- package/.husky/pre-push +13 -13
- package/CLAUDE.md +18 -18
- package/README.md +198 -188
- package/SKILL.md +90 -91
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -64
- package/docs/plan-execute-contract.md +123 -123
- package/docs/platform-scan-protocol.md +298 -298
- package/docs/revision-mode.md +115 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
- package/docs/sillyspec/file-lifecycle.md +125 -125
- package/docs/workflow-contract-regression.md +106 -106
- package/docs/worktree-isolation.md +252 -252
- package/package.json +40 -40
- 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/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -588
- package/packages/dashboard/server/parser.js +526 -526
- package/packages/dashboard/server/watcher.js +344 -344
- package/packages/dashboard/src/App.vue +558 -558
- package/packages/dashboard/src/components/ActionBar.vue +93 -93
- package/packages/dashboard/src/components/CommandPalette.vue +96 -96
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -95
- package/packages/dashboard/src/components/PipelineView.vue +156 -156
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/StageBadge.vue +67 -67
- package/packages/dashboard/src/components/StepCard.vue +94 -94
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +170 -170
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -158
- package/src/change-list.js +52 -52
- package/src/change-risk-profile.js +352 -352
- package/src/classify-change.js +73 -73
- package/src/constants.js +70 -70
- package/src/contract-matrix.js +278 -278
- package/src/db.js +201 -201
- package/src/endpoint-extractor.js +315 -315
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +653 -653
- package/src/index.js +922 -900
- package/src/init.js +431 -431
- package/src/knowledge-match.js +130 -130
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -482
- package/src/progress.js +1734 -1734
- package/src/run.js +3465 -3358
- package/src/scan-postcheck.js +387 -383
- package/src/setup.js +398 -398
- package/src/stage-contract.js +700 -700
- package/src/stages/archive.js +160 -160
- package/src/stages/brainstorm-auto.js +229 -229
- package/src/stages/brainstorm.js +645 -645
- package/src/stages/doctor.js +365 -365
- package/src/stages/execute.js +625 -625
- 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 +511 -513
- package/src/stages/plan.js +582 -582
- package/src/stages/propose.js +174 -174
- package/src/stages/quick.js +82 -82
- package/src/stages/scan.js +558 -558
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -322
- package/src/sync.js +497 -497
- package/src/task-review.js +346 -346
- package/src/workflow.js +785 -785
- package/src/worktree-apply.js +549 -549
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -932
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
- package/test/brainstorm-plan-contract.test.mjs +273 -273
- package/test/check-syntax.mjs +26 -26
- package/test/contract-artifacts.test.mjs +323 -323
- package/test/decision-supersede.test.mjs +277 -277
- package/test/knowledge-match.test.mjs +231 -231
- package/test/plan-execute-contract.test.mjs +330 -330
- package/test/plan-optimization.test.mjs +572 -572
- package/test/platform-artifacts.test.mjs +166 -166
- package/test/platform-failure-samples.test.mjs +199 -199
- package/test/platform-recovery-chain.test.mjs +167 -167
- package/test/platform-recovery.test.mjs +136 -136
- package/test/platform-scan-p0.test.mjs +168 -168
- package/test/revision-v1.test.mjs +1145 -1145
- package/test/run-scan-project-parse.test.mjs +200 -200
- package/test/run-tests.mjs +48 -48
- package/test/scan-knowledge.test.mjs +175 -175
- package/test/scan-paths.test.mjs +68 -68
- package/test/scan-postcheck.test.mjs +197 -197
- package/test/spec-dir.test.mjs +206 -206
- package/test/stage-contract.test.mjs +299 -299
- package/test/stage-definitions.test.mjs +39 -39
- package/test/wait-gates.test.mjs +496 -496
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +71 -71
- package/test/worktree-native-overlay.test.mjs +188 -188
package/src/task-review.js
CHANGED
|
@@ -1,346 +1,346 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SillySpec Task Review Gate — execute 阶段任务级评审校验
|
|
3
|
-
*
|
|
4
|
-
* execute 阶段每个 task 完成后,controller 必须写入 review.json。
|
|
5
|
-
* execute --done 时 CLI 硬校验:缺失 review 或 verdict 不通过则阻断。
|
|
6
|
-
*
|
|
7
|
-
* 目录结构:
|
|
8
|
-
* .sillyspec/.runtime/execute-runs/<runId>/tasks/<taskId>/review.json
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
|
|
12
|
-
import { join, resolve } from 'path'
|
|
13
|
-
|
|
14
|
-
// ── review.json schema version ──
|
|
15
|
-
export const REVIEW_SCHEMA_VERSION = 1
|
|
16
|
-
|
|
17
|
-
// ── 合法 verdict 枚举 ──
|
|
18
|
-
export const VALID_VERDICTS = ['pass', 'fail', 'cannot_verify']
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 解析 plan.md 中的 task 列表
|
|
22
|
-
* @param {string} planContent - plan.md 文件内容
|
|
23
|
-
* @returns {string[]} task id 列表,如 ['task-01', 'task-02']
|
|
24
|
-
*/
|
|
25
|
-
export function parseTaskIdsFromPlan(planContent) {
|
|
26
|
-
if (!planContent) return []
|
|
27
|
-
const ids = new Set()
|
|
28
|
-
const re = /^\s*[-*]\s*\[[ x]\]\s*task-(\d+)/gim
|
|
29
|
-
for (const m of planContent.matchAll(re)) {
|
|
30
|
-
ids.add(`task-${m[1].padStart(2, '0')}`)
|
|
31
|
-
}
|
|
32
|
-
return [...ids].sort()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 校验单个 review.json 文件
|
|
37
|
-
* @param {object} review - 解析后的 JSON 对象
|
|
38
|
-
* @returns {{ ok: boolean, errors: string[] }}
|
|
39
|
-
*/
|
|
40
|
-
export function validateReviewSchema(review) {
|
|
41
|
-
const errors = []
|
|
42
|
-
if (!review || typeof review !== 'object') {
|
|
43
|
-
errors.push('review.json 不是有效 JSON 对象')
|
|
44
|
-
return { ok: false, errors }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (review.schemaVersion !== REVIEW_SCHEMA_VERSION) {
|
|
48
|
-
errors.push(`schemaVersion 应为 ${REVIEW_SCHEMA_VERSION},实际为 ${review.schemaVersion}`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!review.task || typeof review.task !== 'string') {
|
|
52
|
-
errors.push('缺少 task 字段(应为 "task-XX" 格式)')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!VALID_VERDICTS.includes(review.specVerdict)) {
|
|
56
|
-
errors.push(`specVerdict 无效:${review.specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!VALID_VERDICTS.includes(review.qualityVerdict)) {
|
|
60
|
-
errors.push(`qualityVerdict 无效:${review.qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// cannot_verify 必须提供 requiredEvidence
|
|
64
|
-
if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
|
|
65
|
-
if (!Array.isArray(review.requiredEvidence) || review.requiredEvidence.length === 0) {
|
|
66
|
-
errors.push('cannot_verify 的 verdict 必须提供非空的 requiredEvidence 数组')
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// base/head 非空检查
|
|
71
|
-
if (!review.base || typeof review.base !== 'string') {
|
|
72
|
-
errors.push('缺少 base 字段(git commit hash)')
|
|
73
|
-
}
|
|
74
|
-
if (!review.head || typeof review.head !== 'string') {
|
|
75
|
-
errors.push('缺少 head 字段(git commit hash)')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { ok: errors.length === 0, errors }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 读取单个 task 的 review.json
|
|
83
|
-
* @param {string} reviewPath - review.json 文件路径
|
|
84
|
-
* @returns {{ ok: boolean, review: object|null, errors: string[] }}
|
|
85
|
-
*/
|
|
86
|
-
export function readReview(reviewPath) {
|
|
87
|
-
if (!existsSync(reviewPath)) {
|
|
88
|
-
return { ok: false, review: null, errors: ['review.json 不存在'] }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let raw
|
|
92
|
-
try {
|
|
93
|
-
raw = readFileSync(reviewPath, 'utf8')
|
|
94
|
-
} catch (e) {
|
|
95
|
-
return { ok: false, review: null, parseError: true, errors: [`review.json 读取失败: ${e.message}`] }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let parsed
|
|
99
|
-
try {
|
|
100
|
-
parsed = JSON.parse(raw)
|
|
101
|
-
} catch (e) {
|
|
102
|
-
// 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true
|
|
103
|
-
return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${e.message}`] }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const schemaResult = validateReviewSchema(parsed)
|
|
107
|
-
if (!schemaResult.ok) {
|
|
108
|
-
return { ok: false, review: parsed, schemaError: true, errors: schemaResult.errors }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { ok: true, review: parsed, errors: [] }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* execute --done 时的 task review 总校验
|
|
116
|
-
*
|
|
117
|
-
* 规则:
|
|
118
|
-
* - 每个 plan task 必须有 review.json
|
|
119
|
-
* - specVerdict 或 qualityVerdict 为 fail → 整体 fail
|
|
120
|
-
* - specVerdict 或 qualityVerdict 为 cannot_verify → warning(requiredEvidence 非空)
|
|
121
|
-
* - cannot_verify + requiredEvidence 为空 → fail(agent 逃避判断)
|
|
122
|
-
* - cannot_verify 的 requiredEvidence 汇总到 requiredEvidence 字段,供 verify 阶段消费
|
|
123
|
-
*
|
|
124
|
-
* @param {object} opts
|
|
125
|
-
* @param {string} opts.planContent - plan.md 内容
|
|
126
|
-
* @param {string} opts.runtimeRoot - .sillyspec/.runtime 的绝对路径
|
|
127
|
-
* @param {string} opts.executeRunId - execute run id(如 'exec-2026-06-23-131400')
|
|
128
|
-
* @param {boolean} [opts.allowCannotVerify=true] - 是否允许 cannot_verify(默认允许,给 warning)
|
|
129
|
-
* @returns {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array<{task: string, verdict: string, evidence: string[]}> }}
|
|
130
|
-
*/
|
|
131
|
-
export function validateTaskReviews(opts) {
|
|
132
|
-
const { planContent, runtimeRoot, executeRunId, allowCannotVerify = true } = opts
|
|
133
|
-
|
|
134
|
-
const taskIds = parseTaskIdsFromPlan(planContent)
|
|
135
|
-
|
|
136
|
-
// 如果 plan 里没有 task,跳过校验(向后兼容)
|
|
137
|
-
if (taskIds.length === 0) {
|
|
138
|
-
return { ok: true, errors: [], warnings: [], requiredEvidence: [] }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const errors = []
|
|
142
|
-
const warnings = []
|
|
143
|
-
const requiredEvidence = []
|
|
144
|
-
|
|
145
|
-
for (const taskId of taskIds) {
|
|
146
|
-
const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
|
|
147
|
-
const reviewPath = join(reviewDir, 'review.json')
|
|
148
|
-
|
|
149
|
-
const result = readReview(reviewPath)
|
|
150
|
-
|
|
151
|
-
if (!result.ok) {
|
|
152
|
-
if (result.parseError) {
|
|
153
|
-
// review.json 存在但 JSON 非法
|
|
154
|
-
errors.push(`${taskId}: review.json 解析失败 — ${result.errors.join('; ')}`)
|
|
155
|
-
} else if (result.schemaError) {
|
|
156
|
-
// review.json 存在且 JSON 合法,但 schema 校验失败
|
|
157
|
-
errors.push(`${taskId}: review.json 校验失败 — ${result.errors.join('; ')}`)
|
|
158
|
-
} else {
|
|
159
|
-
// review.json 不存在
|
|
160
|
-
errors.push(`${taskId}: 缺少 review.json — task 未经过评审`)
|
|
161
|
-
}
|
|
162
|
-
continue
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const review = result.review
|
|
166
|
-
|
|
167
|
-
// 检查 review.task 是否与 plan 中的 taskId 一致
|
|
168
|
-
if (review.task && review.task !== taskId) {
|
|
169
|
-
errors.push(`${taskId}: review.json 中的 task 字段为 "${review.task}",与 plan 不一致(应为 "${taskId}")— agent 可能复制模板未修改`)
|
|
170
|
-
continue
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 检查 fail verdict
|
|
174
|
-
if (review.specVerdict === 'fail' || review.qualityVerdict === 'fail') {
|
|
175
|
-
errors.push(`${taskId}: review 未通过 — spec: ${review.specVerdict}, quality: ${review.qualityVerdict}`)
|
|
176
|
-
if (review.reviewerNotes) {
|
|
177
|
-
errors.push(`${taskId}: ${review.reviewerNotes}`)
|
|
178
|
-
}
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 检查 cannot_verify
|
|
183
|
-
if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
|
|
184
|
-
if (!allowCannotVerify) {
|
|
185
|
-
errors.push(`${taskId}: cannot_verify 不被允许 — 必须提供评审结果`)
|
|
186
|
-
continue
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (review.requiredEvidence && review.requiredEvidence.length > 0) {
|
|
190
|
-
const verdicts = []
|
|
191
|
-
if (review.specVerdict === 'cannot_verify') verdicts.push('spec')
|
|
192
|
-
if (review.qualityVerdict === 'cannot_verify') verdicts.push('quality')
|
|
193
|
-
warnings.push(`${taskId}: ${verdicts.join('+')}=cannot_verify,requiredEvidence 必须在 verify 阶段满足`)
|
|
194
|
-
requiredEvidence.push({
|
|
195
|
-
task: taskId,
|
|
196
|
-
verdict: verdicts.join('+'),
|
|
197
|
-
evidence: review.requiredEvidence,
|
|
198
|
-
})
|
|
199
|
-
} else {
|
|
200
|
-
// cannot_verify + 空 requiredEvidence = agent 逃避判断
|
|
201
|
-
errors.push(`${taskId}: cannot_verify 但 requiredEvidence 为空 — 这是无效评审`)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 额外检查:扫描 execute-runs/<runId>/tasks/ 下是否有 plan 里没有的 task review
|
|
207
|
-
// (agent 可能写错了 task id)
|
|
208
|
-
try {
|
|
209
|
-
const tasksDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks')
|
|
210
|
-
if (existsSync(tasksDir)) {
|
|
211
|
-
const taskDirs = readdirSync(tasksDir, { withFileTypes: true })
|
|
212
|
-
.filter(e => e.isDirectory())
|
|
213
|
-
.map(e => e.name)
|
|
214
|
-
const taskIdSet = new Set(taskIds)
|
|
215
|
-
for (const dirName of taskDirs) {
|
|
216
|
-
if (!taskIdSet.has(dirName) && existsSync(join(tasksDir, dirName, 'review.json'))) {
|
|
217
|
-
warnings.push(`${dirName}: 存在 review.json 但不在 plan.md 的 task 列表中(可能是多余文件)`)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch (e) {
|
|
222
|
-
warnings.push(`task review extra-check 异常: ${e.message}`)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
ok: errors.length === 0,
|
|
227
|
-
errors,
|
|
228
|
-
warnings,
|
|
229
|
-
requiredEvidence,
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 将 cannot_verify 的 requiredEvidence 写入 change 目录
|
|
235
|
-
* 供 verify 阶段消费
|
|
236
|
-
*
|
|
237
|
-
* @param {string} changeDir - 变更目录(.sillyspec/changes/<name>)
|
|
238
|
-
* @param {Array<{task: string, verdict: string, evidence: string[]}>} requiredEvidence
|
|
239
|
-
* @returns {string|null} 写入的文件路径,null 表示无需写入
|
|
240
|
-
*/
|
|
241
|
-
export function writeVerifyRequiredEvidence(changeDir, requiredEvidence) {
|
|
242
|
-
if (!requiredEvidence || requiredEvidence.length === 0) return null
|
|
243
|
-
|
|
244
|
-
const filePath = join(changeDir, 'verify-required-evidence.json')
|
|
245
|
-
const data = {
|
|
246
|
-
generatedAt: new Date().toISOString(),
|
|
247
|
-
schemaVersion: 1,
|
|
248
|
-
items: requiredEvidence,
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
mkdirSync(changeDir, { recursive: true })
|
|
252
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n')
|
|
253
|
-
|
|
254
|
-
return filePath
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 生成 execute run id
|
|
259
|
-
* @returns {string} 如 'exec-2026-06-23-131400'
|
|
260
|
-
*/
|
|
261
|
-
export function generateExecuteRunId() {
|
|
262
|
-
const now = new Date()
|
|
263
|
-
const pad = (n) => String(n).padStart(2, '0')
|
|
264
|
-
return `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* 获取当前(或最新)execute run id
|
|
269
|
-
* 从 runtime 目录下查找 execute-runs/ 子目录
|
|
270
|
-
*
|
|
271
|
-
* @param {string} runtimeRoot - .sillyspec/.runtime 路径
|
|
272
|
-
* @returns {string|null} 最新 run id,null 表示无任何 run
|
|
273
|
-
*/
|
|
274
|
-
/**
|
|
275
|
-
* 获取当前 execute run id
|
|
276
|
-
* 优先从 current-execute-run-id 标记文件读取(execute 阶段启动时写入),
|
|
277
|
-
* fallback 到 execute-runs/ 下最新的 exec- 前缀目录。
|
|
278
|
-
*
|
|
279
|
-
* @param {string} runtimeRoot - .sillyspec/.runtime 路径
|
|
280
|
-
* @returns {string|null} 当前 run id,null 表示无任何 run
|
|
281
|
-
*/
|
|
282
|
-
export function getLatestExecuteRunId(runtimeRoot) {
|
|
283
|
-
// 优先读标记文件(execute 阶段启动时由 run.js 写入,生命周期内不变)
|
|
284
|
-
const markerPath = join(runtimeRoot, 'current-execute-run-id')
|
|
285
|
-
try {
|
|
286
|
-
if (existsSync(markerPath)) {
|
|
287
|
-
const content = readFileSync(markerPath, 'utf8').trim()
|
|
288
|
-
if (content) return content
|
|
289
|
-
}
|
|
290
|
-
} catch {}
|
|
291
|
-
|
|
292
|
-
// fallback:扫描 execute-runs/ 目录
|
|
293
|
-
const runsDir = join(runtimeRoot, 'execute-runs')
|
|
294
|
-
if (!existsSync(runsDir)) return null
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
const entries = readdirSync(runsDir, { withFileTypes: true })
|
|
298
|
-
.filter(e => e.isDirectory() && e.name.startsWith('exec-'))
|
|
299
|
-
.map(e => e.name)
|
|
300
|
-
.sort()
|
|
301
|
-
.reverse()
|
|
302
|
-
return entries[0] || null
|
|
303
|
-
} catch {
|
|
304
|
-
return null
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* 确保 task review 目录存在
|
|
310
|
-
* @param {string} runtimeRoot
|
|
311
|
-
* @param {string} executeRunId
|
|
312
|
-
* @param {string} taskId
|
|
313
|
-
* @returns {string} task review 目录路径
|
|
314
|
-
*/
|
|
315
|
-
export function ensureTaskReviewDir(runtimeRoot, executeRunId, taskId) {
|
|
316
|
-
const dir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
|
|
317
|
-
mkdirSync(dir, { recursive: true })
|
|
318
|
-
return dir
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* 打印校验结果
|
|
323
|
-
* @param {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array }} result
|
|
324
|
-
*/
|
|
325
|
-
export function printReviewResult(result) {
|
|
326
|
-
if (result.ok && result.warnings.length === 0) {
|
|
327
|
-
console.log('\n✅ Task Review Gate — 所有任务评审通过')
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (result.errors.length > 0) {
|
|
332
|
-
console.error('\n🚫 Task Review Gate — FAILED')
|
|
333
|
-
for (const err of result.errors) {
|
|
334
|
-
console.error(` - ${err}`)
|
|
335
|
-
}
|
|
336
|
-
console.error('\n 提示:为缺失/失败的任务补充 review.json,然后重新 --done')
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (result.warnings.length > 0) {
|
|
340
|
-
console.warn('\n⚠️ Task Review Gate — WARNING')
|
|
341
|
-
for (const w of result.warnings) {
|
|
342
|
-
console.warn(` - ${w}`)
|
|
343
|
-
}
|
|
344
|
-
console.warn('\n cannot_verify 的 requiredEvidence 将在 verify 阶段校验')
|
|
345
|
-
}
|
|
346
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SillySpec Task Review Gate — execute 阶段任务级评审校验
|
|
3
|
+
*
|
|
4
|
+
* execute 阶段每个 task 完成后,controller 必须写入 review.json。
|
|
5
|
+
* execute --done 时 CLI 硬校验:缺失 review 或 verdict 不通过则阻断。
|
|
6
|
+
*
|
|
7
|
+
* 目录结构:
|
|
8
|
+
* .sillyspec/.runtime/execute-runs/<runId>/tasks/<taskId>/review.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
|
|
12
|
+
import { join, resolve } from 'path'
|
|
13
|
+
|
|
14
|
+
// ── review.json schema version ──
|
|
15
|
+
export const REVIEW_SCHEMA_VERSION = 1
|
|
16
|
+
|
|
17
|
+
// ── 合法 verdict 枚举 ──
|
|
18
|
+
export const VALID_VERDICTS = ['pass', 'fail', 'cannot_verify']
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 解析 plan.md 中的 task 列表
|
|
22
|
+
* @param {string} planContent - plan.md 文件内容
|
|
23
|
+
* @returns {string[]} task id 列表,如 ['task-01', 'task-02']
|
|
24
|
+
*/
|
|
25
|
+
export function parseTaskIdsFromPlan(planContent) {
|
|
26
|
+
if (!planContent) return []
|
|
27
|
+
const ids = new Set()
|
|
28
|
+
const re = /^\s*[-*]\s*\[[ x]\]\s*task-(\d+)/gim
|
|
29
|
+
for (const m of planContent.matchAll(re)) {
|
|
30
|
+
ids.add(`task-${m[1].padStart(2, '0')}`)
|
|
31
|
+
}
|
|
32
|
+
return [...ids].sort()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 校验单个 review.json 文件
|
|
37
|
+
* @param {object} review - 解析后的 JSON 对象
|
|
38
|
+
* @returns {{ ok: boolean, errors: string[] }}
|
|
39
|
+
*/
|
|
40
|
+
export function validateReviewSchema(review) {
|
|
41
|
+
const errors = []
|
|
42
|
+
if (!review || typeof review !== 'object') {
|
|
43
|
+
errors.push('review.json 不是有效 JSON 对象')
|
|
44
|
+
return { ok: false, errors }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (review.schemaVersion !== REVIEW_SCHEMA_VERSION) {
|
|
48
|
+
errors.push(`schemaVersion 应为 ${REVIEW_SCHEMA_VERSION},实际为 ${review.schemaVersion}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!review.task || typeof review.task !== 'string') {
|
|
52
|
+
errors.push('缺少 task 字段(应为 "task-XX" 格式)')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!VALID_VERDICTS.includes(review.specVerdict)) {
|
|
56
|
+
errors.push(`specVerdict 无效:${review.specVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!VALID_VERDICTS.includes(review.qualityVerdict)) {
|
|
60
|
+
errors.push(`qualityVerdict 无效:${review.qualityVerdict}(应为 ${VALID_VERDICTS.join('/')})`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// cannot_verify 必须提供 requiredEvidence
|
|
64
|
+
if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
|
|
65
|
+
if (!Array.isArray(review.requiredEvidence) || review.requiredEvidence.length === 0) {
|
|
66
|
+
errors.push('cannot_verify 的 verdict 必须提供非空的 requiredEvidence 数组')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// base/head 非空检查
|
|
71
|
+
if (!review.base || typeof review.base !== 'string') {
|
|
72
|
+
errors.push('缺少 base 字段(git commit hash)')
|
|
73
|
+
}
|
|
74
|
+
if (!review.head || typeof review.head !== 'string') {
|
|
75
|
+
errors.push('缺少 head 字段(git commit hash)')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { ok: errors.length === 0, errors }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 读取单个 task 的 review.json
|
|
83
|
+
* @param {string} reviewPath - review.json 文件路径
|
|
84
|
+
* @returns {{ ok: boolean, review: object|null, errors: string[] }}
|
|
85
|
+
*/
|
|
86
|
+
export function readReview(reviewPath) {
|
|
87
|
+
if (!existsSync(reviewPath)) {
|
|
88
|
+
return { ok: false, review: null, errors: ['review.json 不存在'] }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let raw
|
|
92
|
+
try {
|
|
93
|
+
raw = readFileSync(reviewPath, 'utf8')
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return { ok: false, review: null, parseError: true, errors: [`review.json 读取失败: ${e.message}`] }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let parsed
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(raw)
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// 文件存在但 JSON 非法:review 设为 null 但标记 parseError=true
|
|
103
|
+
return { ok: false, review: null, parseError: true, errors: [`review.json 解析失败: ${e.message}`] }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const schemaResult = validateReviewSchema(parsed)
|
|
107
|
+
if (!schemaResult.ok) {
|
|
108
|
+
return { ok: false, review: parsed, schemaError: true, errors: schemaResult.errors }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { ok: true, review: parsed, errors: [] }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* execute --done 时的 task review 总校验
|
|
116
|
+
*
|
|
117
|
+
* 规则:
|
|
118
|
+
* - 每个 plan task 必须有 review.json
|
|
119
|
+
* - specVerdict 或 qualityVerdict 为 fail → 整体 fail
|
|
120
|
+
* - specVerdict 或 qualityVerdict 为 cannot_verify → warning(requiredEvidence 非空)
|
|
121
|
+
* - cannot_verify + requiredEvidence 为空 → fail(agent 逃避判断)
|
|
122
|
+
* - cannot_verify 的 requiredEvidence 汇总到 requiredEvidence 字段,供 verify 阶段消费
|
|
123
|
+
*
|
|
124
|
+
* @param {object} opts
|
|
125
|
+
* @param {string} opts.planContent - plan.md 内容
|
|
126
|
+
* @param {string} opts.runtimeRoot - .sillyspec/.runtime 的绝对路径
|
|
127
|
+
* @param {string} opts.executeRunId - execute run id(如 'exec-2026-06-23-131400')
|
|
128
|
+
* @param {boolean} [opts.allowCannotVerify=true] - 是否允许 cannot_verify(默认允许,给 warning)
|
|
129
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array<{task: string, verdict: string, evidence: string[]}> }}
|
|
130
|
+
*/
|
|
131
|
+
export function validateTaskReviews(opts) {
|
|
132
|
+
const { planContent, runtimeRoot, executeRunId, allowCannotVerify = true } = opts
|
|
133
|
+
|
|
134
|
+
const taskIds = parseTaskIdsFromPlan(planContent)
|
|
135
|
+
|
|
136
|
+
// 如果 plan 里没有 task,跳过校验(向后兼容)
|
|
137
|
+
if (taskIds.length === 0) {
|
|
138
|
+
return { ok: true, errors: [], warnings: [], requiredEvidence: [] }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const errors = []
|
|
142
|
+
const warnings = []
|
|
143
|
+
const requiredEvidence = []
|
|
144
|
+
|
|
145
|
+
for (const taskId of taskIds) {
|
|
146
|
+
const reviewDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
|
|
147
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
148
|
+
|
|
149
|
+
const result = readReview(reviewPath)
|
|
150
|
+
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
if (result.parseError) {
|
|
153
|
+
// review.json 存在但 JSON 非法
|
|
154
|
+
errors.push(`${taskId}: review.json 解析失败 — ${result.errors.join('; ')}`)
|
|
155
|
+
} else if (result.schemaError) {
|
|
156
|
+
// review.json 存在且 JSON 合法,但 schema 校验失败
|
|
157
|
+
errors.push(`${taskId}: review.json 校验失败 — ${result.errors.join('; ')}`)
|
|
158
|
+
} else {
|
|
159
|
+
// review.json 不存在
|
|
160
|
+
errors.push(`${taskId}: 缺少 review.json — task 未经过评审`)
|
|
161
|
+
}
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const review = result.review
|
|
166
|
+
|
|
167
|
+
// 检查 review.task 是否与 plan 中的 taskId 一致
|
|
168
|
+
if (review.task && review.task !== taskId) {
|
|
169
|
+
errors.push(`${taskId}: review.json 中的 task 字段为 "${review.task}",与 plan 不一致(应为 "${taskId}")— agent 可能复制模板未修改`)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 检查 fail verdict
|
|
174
|
+
if (review.specVerdict === 'fail' || review.qualityVerdict === 'fail') {
|
|
175
|
+
errors.push(`${taskId}: review 未通过 — spec: ${review.specVerdict}, quality: ${review.qualityVerdict}`)
|
|
176
|
+
if (review.reviewerNotes) {
|
|
177
|
+
errors.push(`${taskId}: ${review.reviewerNotes}`)
|
|
178
|
+
}
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 检查 cannot_verify
|
|
183
|
+
if (review.specVerdict === 'cannot_verify' || review.qualityVerdict === 'cannot_verify') {
|
|
184
|
+
if (!allowCannotVerify) {
|
|
185
|
+
errors.push(`${taskId}: cannot_verify 不被允许 — 必须提供评审结果`)
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (review.requiredEvidence && review.requiredEvidence.length > 0) {
|
|
190
|
+
const verdicts = []
|
|
191
|
+
if (review.specVerdict === 'cannot_verify') verdicts.push('spec')
|
|
192
|
+
if (review.qualityVerdict === 'cannot_verify') verdicts.push('quality')
|
|
193
|
+
warnings.push(`${taskId}: ${verdicts.join('+')}=cannot_verify,requiredEvidence 必须在 verify 阶段满足`)
|
|
194
|
+
requiredEvidence.push({
|
|
195
|
+
task: taskId,
|
|
196
|
+
verdict: verdicts.join('+'),
|
|
197
|
+
evidence: review.requiredEvidence,
|
|
198
|
+
})
|
|
199
|
+
} else {
|
|
200
|
+
// cannot_verify + 空 requiredEvidence = agent 逃避判断
|
|
201
|
+
errors.push(`${taskId}: cannot_verify 但 requiredEvidence 为空 — 这是无效评审`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 额外检查:扫描 execute-runs/<runId>/tasks/ 下是否有 plan 里没有的 task review
|
|
207
|
+
// (agent 可能写错了 task id)
|
|
208
|
+
try {
|
|
209
|
+
const tasksDir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks')
|
|
210
|
+
if (existsSync(tasksDir)) {
|
|
211
|
+
const taskDirs = readdirSync(tasksDir, { withFileTypes: true })
|
|
212
|
+
.filter(e => e.isDirectory())
|
|
213
|
+
.map(e => e.name)
|
|
214
|
+
const taskIdSet = new Set(taskIds)
|
|
215
|
+
for (const dirName of taskDirs) {
|
|
216
|
+
if (!taskIdSet.has(dirName) && existsSync(join(tasksDir, dirName, 'review.json'))) {
|
|
217
|
+
warnings.push(`${dirName}: 存在 review.json 但不在 plan.md 的 task 列表中(可能是多余文件)`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
warnings.push(`task review extra-check 异常: ${e.message}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
ok: errors.length === 0,
|
|
227
|
+
errors,
|
|
228
|
+
warnings,
|
|
229
|
+
requiredEvidence,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 将 cannot_verify 的 requiredEvidence 写入 change 目录
|
|
235
|
+
* 供 verify 阶段消费
|
|
236
|
+
*
|
|
237
|
+
* @param {string} changeDir - 变更目录(.sillyspec/changes/<name>)
|
|
238
|
+
* @param {Array<{task: string, verdict: string, evidence: string[]}>} requiredEvidence
|
|
239
|
+
* @returns {string|null} 写入的文件路径,null 表示无需写入
|
|
240
|
+
*/
|
|
241
|
+
export function writeVerifyRequiredEvidence(changeDir, requiredEvidence) {
|
|
242
|
+
if (!requiredEvidence || requiredEvidence.length === 0) return null
|
|
243
|
+
|
|
244
|
+
const filePath = join(changeDir, 'verify-required-evidence.json')
|
|
245
|
+
const data = {
|
|
246
|
+
generatedAt: new Date().toISOString(),
|
|
247
|
+
schemaVersion: 1,
|
|
248
|
+
items: requiredEvidence,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
mkdirSync(changeDir, { recursive: true })
|
|
252
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n')
|
|
253
|
+
|
|
254
|
+
return filePath
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 生成 execute run id
|
|
259
|
+
* @returns {string} 如 'exec-2026-06-23-131400'
|
|
260
|
+
*/
|
|
261
|
+
export function generateExecuteRunId() {
|
|
262
|
+
const now = new Date()
|
|
263
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
264
|
+
return `exec-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 获取当前(或最新)execute run id
|
|
269
|
+
* 从 runtime 目录下查找 execute-runs/ 子目录
|
|
270
|
+
*
|
|
271
|
+
* @param {string} runtimeRoot - .sillyspec/.runtime 路径
|
|
272
|
+
* @returns {string|null} 最新 run id,null 表示无任何 run
|
|
273
|
+
*/
|
|
274
|
+
/**
|
|
275
|
+
* 获取当前 execute run id
|
|
276
|
+
* 优先从 current-execute-run-id 标记文件读取(execute 阶段启动时写入),
|
|
277
|
+
* fallback 到 execute-runs/ 下最新的 exec- 前缀目录。
|
|
278
|
+
*
|
|
279
|
+
* @param {string} runtimeRoot - .sillyspec/.runtime 路径
|
|
280
|
+
* @returns {string|null} 当前 run id,null 表示无任何 run
|
|
281
|
+
*/
|
|
282
|
+
export function getLatestExecuteRunId(runtimeRoot) {
|
|
283
|
+
// 优先读标记文件(execute 阶段启动时由 run.js 写入,生命周期内不变)
|
|
284
|
+
const markerPath = join(runtimeRoot, 'current-execute-run-id')
|
|
285
|
+
try {
|
|
286
|
+
if (existsSync(markerPath)) {
|
|
287
|
+
const content = readFileSync(markerPath, 'utf8').trim()
|
|
288
|
+
if (content) return content
|
|
289
|
+
}
|
|
290
|
+
} catch {}
|
|
291
|
+
|
|
292
|
+
// fallback:扫描 execute-runs/ 目录
|
|
293
|
+
const runsDir = join(runtimeRoot, 'execute-runs')
|
|
294
|
+
if (!existsSync(runsDir)) return null
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const entries = readdirSync(runsDir, { withFileTypes: true })
|
|
298
|
+
.filter(e => e.isDirectory() && e.name.startsWith('exec-'))
|
|
299
|
+
.map(e => e.name)
|
|
300
|
+
.sort()
|
|
301
|
+
.reverse()
|
|
302
|
+
return entries[0] || null
|
|
303
|
+
} catch {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 确保 task review 目录存在
|
|
310
|
+
* @param {string} runtimeRoot
|
|
311
|
+
* @param {string} executeRunId
|
|
312
|
+
* @param {string} taskId
|
|
313
|
+
* @returns {string} task review 目录路径
|
|
314
|
+
*/
|
|
315
|
+
export function ensureTaskReviewDir(runtimeRoot, executeRunId, taskId) {
|
|
316
|
+
const dir = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId)
|
|
317
|
+
mkdirSync(dir, { recursive: true })
|
|
318
|
+
return dir
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 打印校验结果
|
|
323
|
+
* @param {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array }} result
|
|
324
|
+
*/
|
|
325
|
+
export function printReviewResult(result) {
|
|
326
|
+
if (result.ok && result.warnings.length === 0) {
|
|
327
|
+
console.log('\n✅ Task Review Gate — 所有任务评审通过')
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (result.errors.length > 0) {
|
|
332
|
+
console.error('\n🚫 Task Review Gate — FAILED')
|
|
333
|
+
for (const err of result.errors) {
|
|
334
|
+
console.error(` - ${err}`)
|
|
335
|
+
}
|
|
336
|
+
console.error('\n 提示:为缺失/失败的任务补充 review.json,然后重新 --done')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (result.warnings.length > 0) {
|
|
340
|
+
console.warn('\n⚠️ Task Review Gate — WARNING')
|
|
341
|
+
for (const w of result.warnings) {
|
|
342
|
+
console.warn(` - ${w}`)
|
|
343
|
+
}
|
|
344
|
+
console.warn('\n cannot_verify 的 requiredEvidence 将在 verify 阶段校验')
|
|
345
|
+
}
|
|
346
|
+
}
|