sillyspec 3.22.0 → 3.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,8 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
48
48
 
49
49
  某些步骤(如"对话式探索")需要用户输入。两种方式:
50
50
 
51
+ > 自动去重:若前置 step 已对同一问题(waitReason 归一化后相同,如"确认设计方案" vs "最终确认设计方案")确认过,后续重复 wait 会自动跳过,无需再 `--wait`。
52
+
51
53
  - **方式一(推荐)**:AI 自行与用户交互后,一步完成:
52
54
  ```bash
53
55
  sillyspec run brainstorm --done --change <名> --answer "用户回答" --output "需求已澄清"
@@ -57,7 +57,7 @@ sillyspec worktree doctor --fix --change <变更名>
57
57
 
58
58
  ### Task Review Gate
59
59
 
60
- execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否则阻断完成。`cannot_verify` 的 task 会写入 `verify-required-evidence.json`,由 verify 阶段消费。
60
+ execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否则阻断完成。**例外**:task 在 `tasks/task-XX.md` frontmatter 声明 `low_risk: true`(type-only / 机械迁移等低逻辑风险)时,缺 review.json 只发 warning 不阻断。`cannot_verify` 的 task 会写入 `verify-required-evidence.json`,由 verify 阶段消费。
61
61
 
62
62
  ## worktree 子命令(execute 相关)
63
63
 
@@ -53,7 +53,7 @@ verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报
53
53
  ```markdown
54
54
  # 验证报告
55
55
  ## 结论
56
- PASS / PASS WITH NOTES / FAIL ← 必须有此章节,FAIL 会阻断 verify 完成
56
+ PASS / PASS WITH NOTES / FAIL ← 必须有结论章节(标题含"结论/Conclusion/Result/结果"即可,不限于确切"## 结论"),FAIL 会阻断 verify 完成
57
57
  ## 任务完成度
58
58
  ## 设计一致性
59
59
  ## 探针结果
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.22.0",
3
+ "version": "3.22.2",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/run.js CHANGED
@@ -2481,7 +2481,19 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2481
2481
  if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
2482
2482
  // 检查 --done 是否带了 --answer:如果是,自动补全 waitAnswer 状态,一步完成
2483
2483
  const doneAnswer = typeof options !== 'undefined' && options.doneAnswer ? options.doneAnswer : null
2484
- if (doneAnswer) {
2484
+ // B4: 前置 step 已对同一问题确认则自动跳过重复 wait。
2485
+ // 归一化去掉「最终/再次/重复」等修饰词,避免 step N「确认 X」与 step M「最终确认 X」重复打断。
2486
+ const normalizeReason = (r) => (r || '').replace(/(最终|再次|重复|最后|首|初次|首次)/g, '').trim()
2487
+ const currentReason = normalizeReason(currentStepDef.waitReason)
2488
+ const priorConfirmed = currentReason && steps.slice(0, currentIdx).some(s =>
2489
+ s.waitAnswer && s.status === 'completed' && normalizeReason(s.waitReason) === currentReason
2490
+ )
2491
+ if (priorConfirmed) {
2492
+ currentStep.status = 'waiting'
2493
+ currentStep.waitAnswer = '前置步骤已对同一问题确认 — 自动跳过重复 wait'
2494
+ currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
2495
+ console.log(`⚠️ Step "${currentStep.name}" 的确认(${currentStepDef.waitReason})已在前置步骤完成,自动跳过。`)
2496
+ } else if (doneAnswer) {
2485
2497
  currentStep.status = 'waiting'
2486
2498
  currentStep.waitAnswer = doneAnswer
2487
2499
  currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
@@ -3096,7 +3108,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
3096
3108
  executeRunId = generateExecuteRunId()
3097
3109
  }
3098
3110
 
3099
- const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId })
3111
+ const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile })
3100
3112
  printReviewResult(reviewResult)
3101
3113
 
3102
3114
  if (!reviewResult.ok) {
@@ -253,14 +253,21 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
253
253
  warnings.push('design.md 缺少「自审」章节')
254
254
  }
255
255
 
256
- // P1: 涉及生命周期关键词时,design.md 必须包含生命周期契约表
256
+ // P1: 涉及生命周期关键词时,design.md 必须包含生命周期契约表(除非显式声明不涉及)
257
257
  const hasLifecycleKeyword = /\b(session|lease|agent[._-]?run|daemon|lifecycle|state[._-]?transition|claim|heartbeat)\b/i.test(content)
258
258
  if (hasLifecycleKeyword) {
259
- const hasLifecycleTable =
260
- /生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
261
- /事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
262
- if (!hasLifecycleTable) {
263
- errors.push('design.md 涉及生命周期关键词(session/lease/agent_run/daemon/lifecycle)但缺少「生命周期契约表」— 必须列出完整的事件×状态转换矩阵')
259
+ // 显式声明本变更不涉及生命周期契约(覆盖字段名/错误码/否定声明场景):
260
+ // 历史教训:design 提到 daemon_id 字段名或 daemon_not_owned 错误码就触发,被迫加空表(B3a)
261
+ const declaresNotApplicable = /(生命周期|lifecycle)[^\n]{0,40}(n\/?a|不涉及|无|none|not[ _-]?applicable)|(不涉及|无需|没有)[^\n]{0,40}(生命周期|lifecycle)/i.test(content)
262
+ if (declaresNotApplicable) {
263
+ warnings.push('design.md 显式声明不涉及生命周期契约 — 已豁免「生命周期契约表」要求')
264
+ } else {
265
+ const hasLifecycleTable =
266
+ /生命周期契约表|lifecycle[._-]?contract|lifecycle[._-]?matrix|Lifecycle Contract/i.test(content) ||
267
+ /事件.*发起方.*接收方.*必需字段.*状态变化/.test(content)
268
+ if (!hasLifecycleTable) {
269
+ errors.push('design.md 涉及生命周期关键词(session/lease/agent_run/daemon/lifecycle)但缺少「生命周期契约表」— 必须列出完整的事件×状态转换矩阵;或显式声明「不涉及生命周期契约」并附理由豁免')
270
+ }
264
271
  }
265
272
  }
266
273
  }
@@ -380,6 +387,22 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
380
387
 
381
388
  return { ok: errors.length === 0, errors, warnings }
382
389
  }
390
+ /**
391
+ * 从 verify-result.md 提取结论关键词(PASS / PASS WITH NOTES / FAIL)。
392
+ * 标题放宽:含「结论/Conclusion/Result/结果」的二级标题均可(B3c),
393
+ * PASS/FAIL 可在标题行本身(如「## 验收结论:✅ PASS」)或紧邻标题的正文里。
394
+ * 历史教训:原正则锚定确切「## 结论」,用户写「## 验收结论:✅ PASS」不被识别。
395
+ */
396
+ function extractVerifyConclusion(verify) {
397
+ const headingRe = /^##\s[^\n]*(?:结论|conclusion|result|结果)/im
398
+ const headingMatch = verify.match(headingRe)
399
+ if (!headingMatch) return ''
400
+ const start = headingMatch.index
401
+ const slice = verify.slice(start, start + 400)
402
+ const kw = slice.match(/\b(PASS(?:\s+WITH\s+NOTES)?|FAIL)\b/i)
403
+ return kw ? kw[1].toUpperCase().replace(/\s+/g, ' ') : ''
404
+ }
405
+
383
406
  function validateVerifyOutputs(cwd, changeName, context = {}) {
384
407
  const { specRoot } = context
385
408
  const changeDir = resolveChangeDir(cwd, changeName, specRoot)
@@ -419,12 +442,11 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
419
442
  // ── FAIL 结论门控(适用于所有变更,不限风险等级)──
420
443
  // verify-result.md 结论为 FAIL 时,verify 阶段不能 completed。
421
444
  // 历史教训:CLI 曾不校验结论,AI 写 FAIL 后 verify 仍被标记完成并提示"验证通过可以归档"。
422
- const conclusionLine = verify.match(/^##\s*结论\s*\n\s*(PASS(?:\s+WITH\s+NOTES)?|FAIL)/im)
423
- const conclusionStr = conclusionLine ? conclusionLine[1].toUpperCase().replace(/\s+/g, ' ') : ''
445
+ const conclusionStr = extractVerifyConclusion(verify)
424
446
  if (conclusionStr === 'FAIL') {
425
447
  errors.push('verify-result.md 结论为 FAIL — 验证未通过,不能标记 verify 完成;请修复后重新运行验证')
426
448
  } else if (!conclusionStr) {
427
- warnings.push('verify-result.md 未识别到「## 结论」章节(应为 PASS / PASS WITH NOTES / FAIL)')
449
+ warnings.push('verify-result.md 未识别到结论章节(含 结论/Conclusion/Result/结果 的二级标题,后跟 PASS / PASS WITH NOTES / FAIL)')
428
450
  }
429
451
 
430
452
  // ── P0: Change Risk Gate — 核心功能缺少真实集成验证时 FAIL ──
@@ -433,8 +455,7 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
433
455
  planContent: readIfExists(join(changeDir, 'plan.md')),
434
456
  })
435
457
  if (['integration-critical', 'deployment-critical'].includes(changeRiskProfile.level)) {
436
- const conclusionMatch = verify.match(/^## 结论\s*\n\s*(PASS|PASS WITH NOTES|FAIL)/im)
437
- const conclusion = conclusionMatch ? conclusionMatch[1] : ''
458
+ const conclusion = extractVerifyConclusion(verify)
438
459
  if (conclusion === 'PASS WITH NOTES' || conclusion === 'PASS') {
439
460
  const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification)
440
461
  if (!evidenceCheck.ok) {
@@ -361,30 +361,48 @@ function parseWavesFromPlan(planContent) {
361
361
  const lines = planContent.split('\n')
362
362
  let currentWave = null
363
363
  let currentTask = null
364
+ // light/none plan.md 用 `## Tasks`(无 `## Wave N`)包任务,需识别为隐式任务区,
365
+ // 让其中的 task checkbox 能被收进惰性创建的隐式 Wave(见下方 taskMatch 分支)。
366
+ let inImplicitTaskSection = false
364
367
 
365
368
  for (const line of lines) {
366
369
  const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
367
370
  if (waveMatch) {
368
371
  currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
369
372
  currentTask = null
373
+ inImplicitTaskSection = false
370
374
  waves.push(currentWave)
371
375
  continue
372
376
  }
373
377
 
374
- // 遇到任何非 Wave 的标题行(## 自检、## 备注 等)时退出当前 Wave 段,
375
- // 避免「## 自检」段里的 - [x] checkbox 被误当 task 定义解析(导致
376
- // Contract 校验报 task id 重复/不连续)。详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
377
- if (/^#+\s/.test(line)) {
378
+ // 任何非 Wave 的标题行:
379
+ // 1) 退出当前显式 Wave 段,避免「## 自检」段里的 - [x] checkbox 被误当 task 定义解析
380
+ // (导致 Contract 校验报 task id 重复/不连续,详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
381
+ // 2) 识别 light/none 的 `## Tasks`/`## 任务` 任务区(无 Wave 标题),置位隐式任务区标志
382
+ const headingMatch = line.match(/^#+\s+(.+?)\s*$/)
383
+ if (headingMatch) {
378
384
  currentWave = null
379
385
  currentTask = null
386
+ const headingText = headingMatch[1].trim().toLowerCase()
387
+ inImplicitTaskSection = /^(tasks?|任务)$/.test(headingText)
380
388
  continue
381
389
  }
382
390
 
383
- if (!currentWave) continue
384
-
385
391
  const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
386
392
  if (taskMatch) {
387
393
  const taskNoMatch = taskMatch[1].match(/\btask-(\d+)\b/i)
394
+ // full plan.md:task 必须在显式 Wave 段内(currentWave 非 null),正常收容。
395
+ // light/none plan.md(无 Wave 标题,任务在 `## Tasks` 下):在隐式任务区内,遇含
396
+ // task-XX 编号的 checkbox 时惰性创建隐式 Wave 收容,否则 validatePlanForExecute 会报
397
+ // "没有找到 checkbox task"。详见 docs/sillyspec/plan-light-needs-wave-heading.md
398
+ // 不收的情况:非任务区(## 自检/## 验收 等)的 checkbox,或任务区内无 task-XX 编号的 checkbox。
399
+ if (!currentWave) {
400
+ if (!inImplicitTaskSection || !taskNoMatch) continue
401
+ const nextIndex = waves.length === 0 ? 1 : (waves[waves.length - 1].index || waves.length) + 1
402
+ currentWave = { index: nextIndex, tasks: [], implicit: true }
403
+ currentTask = null
404
+ waves.push(currentWave)
405
+ }
388
406
  currentTask = {
389
407
  index: taskNoMatch ? parseInt(taskNoMatch[1], 10) : null,
390
408
  name: taskMatch[1].trim(),
@@ -549,12 +567,12 @@ ${taskSummary}
549
567
  4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
550
568
 
551
569
  ### Wave 开始前
552
- 1. 读取 design.md 的「编码铁律」章节(如果存在),严格遵守
570
+ 1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑
553
571
  2. 读取 plan.md 了解全局任务划分和依赖关系
554
572
  3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
555
573
  4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
556
574
  5. **上下文分层加载**:
557
- - 🔥 热上下文:design.md 编码铁律 + 当前 Wave 任务(必须加载)
575
+ - 🔥 热上下文:design.md 非目标/兼容策略 + 当前 Wave 任务(必须加载)
558
576
  - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
559
577
  - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
560
578
  ${contractInjection}
@@ -114,10 +114,10 @@ export const definition = {
114
114
  ### 自动探针(必须先执行)
115
115
  在检查前,依次运行以下三个探针,将结果作为验证输入:
116
116
 
117
- **探针 1:未实现标记扫描**
118
- 在项目源码目录中搜索未实现标记:
117
+ **探针 1:未实现标记扫描(仅变更文件)**
118
+ 只扫描本次变更涉及的文件,不要全项目扫描——历史 TODO 与本次变更无关,徒增噪音与 token。变更文件 = design.md「文件变更清单」列出的源码文件(你已在「加载规范」步骤读取 design.md,glob 路径需展开为具体文件):
119
119
  \`\`\`bash
120
- grep -rn "尚未实现\|TODO\|FIXME\|HACK\|XXX" <源码目录>/ --include="*.java" --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" --include="*.py"
120
+ grep -n "尚未实现\|TODO\|FIXME\|HACK\|XXX" <design 清单中的源码文件>
121
121
  \`\`\`
122
122
  记录每个匹配的文件、行号和内容。
123
123
 
@@ -128,8 +128,26 @@ export function readReview(reviewPath) {
128
128
  * @param {boolean} [opts.allowCannotVerify=true] - 是否允许 cannot_verify(默认允许,给 warning)
129
129
  * @returns {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array<{task: string, verdict: string, evidence: string[]}> }}
130
130
  */
131
+ /**
132
+ * 读取 task-XX.md frontmatter,判断是否声明 low_risk: true(type-only/机械迁移等低逻辑风险)。
133
+ * 声明 low_risk 的 task 缺 review.json 时只 warning 不 error(B2:免逐个评审仪式)。
134
+ */
135
+ function isTaskLowRisk(changeDir, taskId) {
136
+ if (!changeDir) return false
137
+ const taskFile = join(changeDir, 'tasks', `${taskId}.md`)
138
+ if (!existsSync(taskFile)) return false
139
+ try {
140
+ const content = readFileSync(taskFile, 'utf8')
141
+ const fm = content.match(/^---\n([\s\S]*?)\n---/)
142
+ if (!fm) return false
143
+ return /^low_risk:\s*true\s*$/im.test(fm[1])
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
131
149
  export function validateTaskReviews(opts) {
132
- const { planContent, runtimeRoot, executeRunId, allowCannotVerify = true } = opts
150
+ const { planContent, runtimeRoot, executeRunId, allowCannotVerify = true, changeDir = null } = opts
133
151
 
134
152
  const taskIds = parseTaskIdsFromPlan(planContent)
135
153
 
@@ -156,8 +174,12 @@ export function validateTaskReviews(opts) {
156
174
  // review.json 存在且 JSON 合法,但 schema 校验失败
157
175
  errors.push(`${taskId}: review.json 校验失败 — ${result.errors.join('; ')}`)
158
176
  } else {
159
- // review.json 不存在
160
- errors.push(`${taskId}: 缺少 review.json — task 未经过评审`)
177
+ // review.json 不存在:声明 low_risk 的 task 豁免(B2:type-only/机械迁移免逐个评审)
178
+ if (isTaskLowRisk(changeDir, taskId)) {
179
+ warnings.push(`${taskId}: 缺少 review.json — task 声明 low_risk: true,已豁免评审(type-only/机械迁移)`)
180
+ } else {
181
+ errors.push(`${taskId}: 缺少 review.json — task 未经过评审`)
182
+ }
161
183
  }
162
184
  continue
163
185
  }
package/.claude/CLAUDE.md DELETED
@@ -1,23 +0,0 @@
1
- # Claude Code 指引
2
-
3
- # SillySpec工具定位
4
- 1. SillySpec 是给 Agent 调用的 CLI 工具,不是给人类直接使用的产品
5
- 2. SillySpec 是管理 Agent 工作流的 CLI 工具,不是处理业务逻辑的工具
6
- 3. 你是 Agent。SillySpec 是你的流程控制器。你通过 CLI 命令告诉它"我在哪",它告诉你"下一步该做什么"。你执行步骤,它校验产出、推进状态。人类用户只在关键决策点介入审批
7
-
8
- ## 文件生命周期文档同步
9
- 每次修改 `src/stages/` 下的阶段定义(prompt、步骤、输出文件名等)或 `src/run.js`、`src/progress.js` 等影响文件生命周期的代码后,**必须同步更新** `docs/sillyspec/file-lifecycle.md`,确保文档与代码一致。
10
-
11
- ### 触发更新的典型改动
12
- - 新增/删除/重命名阶段步骤
13
- - 修改步骤 prompt 中的输出文件名(如 verify-result.md)
14
- - 修改阶段间的流转逻辑(如 archive 归档方式)
15
- - 新增/删除运行时文件类型(如 gate-status.json)
16
- - 修改 ProgressManager 的数据存储方式(如 SQLite 表结构变更)
17
-
18
- ### 更新检查清单
19
- - [ ] 文件名引用一致(prompt 输出的文件名 == validateFileLocations 期望的文件名)
20
- - [ ] 阶段步骤描述与 `src/stages/*.js` 一致
21
- - [ ] 归档/清理流程描述与实际代码逻辑一致
22
- - [ ] 数据库 Schema 描述与 `src/db.js` 一致
23
- - [ ] 更新文档头部 `updated_at` 时间戳