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.
Files changed (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -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
+ }