sillyspec 3.26.3 → 3.26.5

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 (68) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
  3. package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
  4. package/.claude/skills/sillyspec-quick/SKILL.md +18 -4
  5. package/package.json +1 -1
  6. package/src/db.js +11 -0
  7. package/src/dispatch/backends/local-agent.js +3 -0
  8. package/src/dispatch/backends/sillyhub-mcp.js +3 -0
  9. package/src/git-helper.js +21 -8
  10. package/src/index.js +2 -0
  11. package/src/quicklog.js +2 -0
  12. package/src/run/command.js +59 -7
  13. package/src/run/complete-handlers.js +95 -1
  14. package/src/run/complete.js +23 -5
  15. package/src/run/gates.js +1 -1
  16. package/src/run/prompt.js +34 -20
  17. package/src/run/quick-audit.js +5 -6
  18. package/src/run/shared.js +45 -11
  19. package/src/run/stage.js +10 -0
  20. package/src/run.js +5 -5
  21. package/src/stage-contract-spec.js +11 -0
  22. package/src/stage-contract.js +11 -1
  23. package/src/stage-review.js +9 -1
  24. package/src/stages/archive.js +12 -26
  25. package/src/stages/brainstorm.js +22 -20
  26. package/src/stages/execute.js +48 -0
  27. package/src/stages/plan.js +17 -1
  28. package/src/stages/quick.js +5 -1
  29. package/src/stages/scan.js +2 -2
  30. package/src/stages/verify.js +2 -1
  31. package/src/sync.js +1 -1
  32. package/src/worktree-apply.js +3 -3
  33. package/src/worktree.js +83 -8
  34. package/templates/prompts/testcase-design.md +12 -0
  35. package/templates/prompts/verify-probes.md +1 -0
  36. package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
  37. package/packages/dashboard/index.html +0 -15
  38. package/packages/dashboard/package-lock.json +0 -2384
  39. package/packages/dashboard/public/favicon.jpg +0 -0
  40. package/packages/dashboard/public/logo.jpg +0 -0
  41. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  42. package/packages/dashboard/public/prototype-overview.html +0 -256
  43. package/packages/dashboard/src/App.vue +0 -558
  44. package/packages/dashboard/src/components/ActionBar.vue +0 -93
  45. package/packages/dashboard/src/components/CommandPalette.vue +0 -96
  46. package/packages/dashboard/src/components/DetailPanel.vue +0 -137
  47. package/packages/dashboard/src/components/DocPreview.vue +0 -257
  48. package/packages/dashboard/src/components/DocTree.vue +0 -114
  49. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  50. package/packages/dashboard/src/components/LogStream.vue +0 -65
  51. package/packages/dashboard/src/components/PipelineStage.vue +0 -95
  52. package/packages/dashboard/src/components/PipelineView.vue +0 -156
  53. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  54. package/packages/dashboard/src/components/ProjectList.vue +0 -210
  55. package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
  56. package/packages/dashboard/src/components/StageBadge.vue +0 -67
  57. package/packages/dashboard/src/components/StepCard.vue +0 -94
  58. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  59. package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
  60. package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
  61. package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
  62. package/packages/dashboard/src/composables/useDashboard.js +0 -213
  63. package/packages/dashboard/src/composables/useKeyboard.js +0 -119
  64. package/packages/dashboard/src/composables/useLayout.js +0 -131
  65. package/packages/dashboard/src/composables/useWebSocket.js +0 -129
  66. package/packages/dashboard/src/main.js +0 -8
  67. package/packages/dashboard/src/style.css +0 -132
  68. package/packages/dashboard/vite.config.js +0 -18
package/src/run/prompt.js CHANGED
@@ -7,9 +7,10 @@
7
7
  * loadModuleContextIndex/buildModuleContextInjection/parseModuleMapSimple
8
8
  * (_module-map.yaml 模块上下文匹配注入,仅 outputStep 用)。
9
9
  *
10
- * 安全锚:run.js 始终 barrel。outputStep + applyRootPlaceholders 由 run.js import 回来;
11
- * _outputStepForTest(output-step-render 测试)+ applyRootPlaceholders(prompt-placeholders
12
- * 测试)被 test 直接 import,run.js barrel re-export 契约保留。
10
+ * 安全锚:run.js 始终 barrel。applyRootPlaceholders 由 run.js import 回来;
11
+ * outputStep(output-step-render / archive-task-completion-injection 测试)+ applyRootPlaceholders
12
+ * (prompt-placeholders 测试)被 test 直接 import 本模块(outputStep 不经 barrel,2026-08-13 起 test
13
+ * 直 import 源模块,`_outputStepForTest` 别名已移除)。
13
14
  *
14
15
  * 路径修正(相对 src/run/):
15
16
  * - stageRegistry 从 '../stages/index.js';resolvePromptIncludes/safeGit/WAIT_MARKER_RE 从 './shared.js'
@@ -33,18 +34,18 @@ import { REVIEW_SCHEMA_VERSION } from '../task-review.js'
33
34
  * @param {string} projectName - 项目名
34
35
  * @returns {object|null} 解析后的模块索引,null 表示无索引
35
36
  */
36
- function loadModuleContextIndex(specBase, projectName) {
37
+ export function loadModuleContextIndex(specBase, projectName) {
37
38
  try {
38
39
  const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
39
40
  if (!existsSync(mapPath)) return null
40
41
  const content = readFileSync(mapPath, 'utf8')
41
- // schema_version 校验(advisory):modules.js 写 schema_version: 2;缺失或非 2 → warn,
42
- // 提示 v1/v2 格式混用风险(旧格式字段解析会静默错位)。正常生成的文件不触发。
42
+ // schema_version 校验:仅缺失 schema_version(真 malformed,parseModuleMapSimple 必错位)时 warn;
43
+ // schema_version=1(旧格式)静默——读端 buildModuleContextInjection 已 v1/v2 双兼容
44
+ // (data.paths || data.core_files),v1 解析正常。原 v1 warn 是过激噪声(每步渲染 prompt 刷屏)。
45
+ // v1→v2 根因在 scan prompt 模板仍写 schema_version:1,升级 scan.js 是单独改动(见 troubleshooting)。
43
46
  const sv = content.match(/^schema_version:\s*(\d+)/m)
44
47
  if (!sv) {
45
48
  console.warn(`⚠️ _module-map.yaml 缺少 schema_version 声明(期望 2),模块解析可能错位:${mapPath}(跑 \`sillyspec modules rebuild\` 升级到 schema_version: 2 可消除此警告)`)
46
- } else if (sv[1] !== '2') {
47
- console.warn(`⚠️ _module-map.yaml schema_version=${sv[1]}(期望 2),可能为旧格式,模块解析可能错位:${mapPath}(跑 \`sillyspec modules rebuild\` 升级到 schema_version: 2 可消除此警告)`)
48
49
  }
49
50
  return parseModuleMapSimple(content)
50
51
  } catch {
@@ -116,6 +117,19 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
116
117
 
117
118
  // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
118
119
  // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
120
+ /**
121
+ * 提示词路径根的单一解析:specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)。
122
+ * 治 execute review.json 提示路径写 worktree 副本、--done 校验读主仓的分裂(坑 execute-prompt-spec-drift):
123
+ * CLI 自动锚定主仓 spec(command.js detectWorktreeSpecDrift → platformOpts.specDriftAnchor)只修正了
124
+ * resolveRuntimeRoot 的 runtime 落点,outputStep 拼 {SPEC_ROOT}/.runtime/execute-runs/… 若仍用 cwd
125
+ * (worktree)→ 提示的 review.json 路径落到 worktree 副本 .sillyspec,而 gate/checkbox 从主仓
126
+ * resolveRuntimeRoot 读 → agent 落盘错位,marker 漂移改用他 run / task 未勾。specDriftAnchor 非空即主仓
127
+ * specBase,优先于 cwd。平台模式 specRoot 优先语义不变。
128
+ */
129
+ export function resolvePromptSpecBase(platformOpts, cwd) {
130
+ return platformOpts?.specRoot || platformOpts?.specDriftAnchor || join(cwd, '.sillyspec')
131
+ }
132
+
119
133
  /**
120
134
  * 输出当前步骤的 prompt
121
135
  */
@@ -236,7 +250,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
236
250
  // 替换 <quicklog-id> 占位符(quick 阶段:从 session guard.json 读 CLI 分配的 ql-ID,
237
251
  // 供 agent 在模块文档变更索引等处引用)
238
252
  if (promptText.includes('<quicklog-id>')) {
239
- const specBaseQl = platformOpts?.specRoot || join(cwd, '.sillyspec')
253
+ const specBaseQl = resolvePromptSpecBase(platformOpts, cwd)
240
254
  let qlIdVal = ''
241
255
  try {
242
256
  const sessionGuardFile = join(specBaseQl, '.runtime', 'quick-sessions', changeName, 'guard.json')
@@ -248,7 +262,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
248
262
  }
249
263
  // 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
250
264
  if (promptText.includes('<linked-changes>')) {
251
- const specBaseLc = platformOpts?.specRoot || join(cwd, '.sillyspec')
265
+ const specBaseLc = resolvePromptSpecBase(platformOpts, cwd)
252
266
  let linkedChanges = []
253
267
  try {
254
268
  // D-002:guard 按 session 存。changeName == quick-<uuid8>(见 runStage 参数解析)。回退读旧单文件(兼容 task-03 前)
@@ -266,7 +280,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
266
280
  if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
267
281
  const projectName = dbProjectName || basename(cwd)
268
282
  // platformOpts.specRoot 现在指向 specDir 本身(可能是 cwd/.sillyspec 或外部路径)
269
- const specSillyspec = platformOpts.specRoot || join(cwd, '.sillyspec')
283
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
270
284
  const docsRoot = join(specSillyspec, 'docs', projectName)
271
285
  const projectsRoot = join(specSillyspec, 'projects')
272
286
  const changesRoot = join(specSillyspec, 'changes')
@@ -328,7 +342,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
328
342
  // 常规模式(无平台 specRoot):占位符替换为 cwd/.sillyspec 下对应路径
329
343
  // 让用 {SPEC_ROOT}/{DOCS_ROOT} 等占位符的 prompt(如 quick/scan)在常规模式也写到正确位置
330
344
  const projectName = dbProjectName || basename(cwd)
331
- const specSillyspec = join(cwd, '.sillyspec')
345
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
332
346
  promptText = applyRootPlaceholders(promptText, {
333
347
  specRoot: specSillyspec,
334
348
  docsRoot: join(specSillyspec, 'docs', projectName),
@@ -356,7 +370,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
356
370
 
357
371
  // scanProfile 分支也要替换占位符(非 platform 模式也会走到这里)
358
372
  const _pName = dbProjectName || basename(cwd)
359
- const _specSS = platformOpts?.specRoot || join(cwd, '.sillyspec')
373
+ const _specSS = resolvePromptSpecBase(platformOpts, cwd)
360
374
  const _docsRoot = join(_specSS, 'docs', _pName)
361
375
  promptText = applyRootPlaceholders(promptText, {
362
376
  specRoot: _specSS,
@@ -368,7 +382,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
368
382
  } else {
369
383
  // 非 platform 模式也要替换占位符
370
384
  const projectName = dbProjectName || basename(cwd)
371
- const specSillyspec = join(cwd, '.sillyspec')
385
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
372
386
  const docsRoot = join(specSillyspec, 'docs', projectName)
373
387
  const projectsRoot = join(specSillyspec, 'projects')
374
388
  const workflowsRoot = join(specSillyspec, 'workflows')
@@ -380,7 +394,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
380
394
  if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
381
395
  try {
382
396
  const { matchKnowledge } = await import('../knowledge-match.js')
383
- const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
397
+ const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
384
398
  const knowledgeDir = join(effectiveSpecBase, 'knowledge')
385
399
  // taskContext: changeName + plan.md task names for better matching
386
400
  let taskContext = changeName || ''
@@ -409,7 +423,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
409
423
  // Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
410
424
  if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
411
425
  let runId = ''
412
- const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
426
+ const execSpecBase = resolvePromptSpecBase(platformOpts, cwd)
413
427
  const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
414
428
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
415
429
  try {
@@ -433,7 +447,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
433
447
  try {
434
448
  const { classifyReviewTier } = await import('../review-tier.js')
435
449
  const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
436
- const tierSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
450
+ const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
437
451
  const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
438
452
  const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
439
453
  let planLevel = null
@@ -491,7 +505,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
491
505
  if (stageName === 'archive' && promptText.includes('{TASK_COMPLETION_REPORT}')) {
492
506
  try {
493
507
  const { summarizeTaskCompletion } = await import('../task-review.js')
494
- const tcrSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
508
+ const tcrSpecBase = resolvePromptSpecBase(platformOpts, cwd)
495
509
  const tcrRuntimeRoot = resolveRuntimeRoot(platformOpts, tcrSpecBase)
496
510
  const tcrChangeDir = changeName ? join(tcrSpecBase, 'changes', changeName) : null
497
511
  const summary = tcrChangeDir
@@ -505,7 +519,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
505
519
 
506
520
  // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
507
521
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
508
- const effectiveSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
522
+ const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
509
523
  const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
510
524
  if (moduleIndex && Object.keys(moduleIndex).length > 0) {
511
525
  // 尝试从 step prompt / changeName 匹配模块
@@ -563,7 +577,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
563
577
  if (platformOpts?.specRoot || platformOpts?.runtimeRoot || changeName) {
564
578
  if (stepIndex !== 0) console.log(`\n### ⚠️ 路径与平台规则(每步提醒,通用铁律见首步)`)
565
579
  if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
566
- const specSillyspec = platformOpts.specRoot || join(cwd, '.sillyspec')
580
+ const specSillyspec = resolvePromptSpecBase(platformOpts, cwd)
567
581
  console.log(`- **平台模式:所有文件只能写入 \`${specSillyspec}/\` 下的对应子目录,严禁写入源码目录。**`)
568
582
  console.log('- **平台模式:Write 工具失败时,不允许用 cat > / tee / heredoc 等方式绕过。先 Read 再 Write,仍失败则记录并停止。**')
569
583
  console.log('- **平台模式:local.yaml 中的 commands 必须在 package.json scripts 中真实存在,不存在的标记 unavailable。**')
@@ -24,17 +24,16 @@ export function printQuickAuditReview(review) {
24
24
  console.error(`\n quick 已停止:请恢复/拆分这些变更,或重新运行 quick 并显式声明范围。`)
25
25
  // 解锁咒语须在 BLOCKED 分支也给出 —— 原只在 confirm/review 分支打印,
26
26
  // 普通 --done 走这里只看到「恢复/拆分」反方向指引,正确解法(带 flag 重跑)根本不出现。
27
- // 但删除是例外:deletedFiles.length>0 是裸判定(auditQuickCompletion 判定行不受任何 flag
28
- // 门控),--force-baseline / --allow-new 都救不回。此时甩 flag 建议会误导调用者照加后继续
29
- // 被拦——改提示走完整流程 execute(有意设计:删除是高危操作,须 review 把关)。
27
+ // 删除:默认 fail-closed(高危操作),但 --allow-delete 显式 opt-in 可解锁(对称 --allow-new)。
30
28
  if (review.deletedFiles && review.deletedFiles.length > 0) {
31
29
  console.error(`\n ⛔ 本次含删除文件 ${review.deletedFiles.length} 个:${review.deletedFiles.join(', ')}`)
32
- console.error(` quick 不支持删除——--force-baseline / --allow-new 均无法解锁(有意设计,高危操作须走 review 把关)。`)
33
- console.error(` 请改走完整流程:sillyspec run execute(在 plan 的 task allowed_paths 内删除),或先 git restore 撤回删除后再 --done。`)
30
+ console.error(` 删除是破坏性操作默认 fail-closed,确认删除这些文件请带 --allow-delete 显式解锁:`)
31
+ console.error(` sillyspec run quick --done --allow-delete --change <id> --output "..."`)
32
+ console.error(` (--force-baseline / --allow-new 不能解锁删除;不确认请 git restore 撤回删除后再 --done,或走完整流程 execute 由 review 把关)`)
34
33
  } else {
35
34
  console.error(` 如确认接受这些变更,重新运行 --done 时带上对应 flag 即可解锁:`)
36
35
  console.error(` sillyspec run quick --done --force-baseline --allow-new --change <id> --output "..."`)
37
- console.error(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件)`)
36
+ console.error(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件;--allow-delete 允许删除文件)`)
38
37
  }
39
38
  } else if (review.status === 'warning') {
40
39
  console.warn(`\n⚠️ quick 变更边界审计 — WARNING:`)
package/src/run/shared.js CHANGED
@@ -17,6 +17,7 @@ import { fileURLToPath } from 'node:url'
17
17
  // safeGit 收口至 src/git-helper.js:import 建本地词法绑定(本模块内部 L128/130/431 调用)+
18
18
  // re-export 供 run/ 层现有调用方继续从 shared.js 引用(pure re-export 不建本地绑定,会致内部 ReferenceError)。
19
19
  import { safeGit } from '../git-helper.js'
20
+ import { createHash } from 'node:crypto'
20
21
  export { safeGit }
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -494,7 +495,7 @@ export function isQuickMetadata(p, linkedChanges = []) {
494
495
  * @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number }}
495
496
  */
496
497
  export async function auditQuickCompletion(cwd, guard, options = {}) {
497
- const { baselineFiles, allowedFiles = [], allowNew = false, forceBaseline = false } = guard
498
+ const { baselineFiles, allowedFiles = [], allowNew = false, forceBaseline = false, allowDelete = false } = guard
498
499
  const { isConfirm } = options
499
500
  // stagedTotal:当前所有非 quick 元数据的未提交条目(含前序 baseline 残留)。
500
501
  // 与 changedFiles(扣 baseline 后的本轮新增)区分,供审计文案同时展示「本轮新增 vs 累计暂存」,
@@ -506,7 +507,10 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
506
507
  // 外层 catch 吞成 warning(multi-agent-review Q3)。safeGit 返回 {value,error} 不抛;若仍有 error
507
508
  // 说明真读不到 git 状态,保守阻断(审计无锚点不能放行),不静默降级。
508
509
  // trim:false 必传:porcelain 首行前导空格是状态码一部分,trim 会削掉致 parsePorcelainPath 丢首字符。
509
- const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false })
510
+ // timeout 15000 + retryOnTimeout:机器忙时 git 子进程启动慢,默认 5s 偏紧易瞬时超时(ETIMEDOUT),
511
+ // 审计锚点失败即 blocked 中断 quick。加大到 15s 并对 ETIMEDOUT 重试一次(重试 30s),消化绝大多数
512
+ // 瞬时抖动,免去用户手工重跑(实测反馈:审计 git 偶发 ETIMEDOUT,重试即过)。
513
+ const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false, timeout: 15000, retryOnTimeout: true })
510
514
  if (statusResult.error) {
511
515
  result.reasons.push(`审计失败(git status): ${statusResult.error}`)
512
516
  result.status = 'blocked'
@@ -584,9 +588,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
584
588
  }
585
589
  }
586
590
 
587
- // 检查 deleted files
588
- for (const f of result.deletedFiles) {
589
- result.reasons.push(`删除文件: ${f}`)
591
+ // 检查 deleted files(--allow-delete 显式放行:删除是破坏性操作,默认 fail-closed,flag 即知情 opt-in)
592
+ if (!allowDelete) {
593
+ for (const f of result.deletedFiles) {
594
+ result.reasons.push(`删除文件: ${f}`)
595
+ }
590
596
  }
591
597
 
592
598
  // 检查 baseline hit(除非 force-baseline)
@@ -614,9 +620,10 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
614
620
  }
615
621
  }
616
622
 
617
- // 判定结果(force-baseline 降级 baselineHit → 非 blocked;allow-new 降级新增文件 → 非 warning。
618
- // reasons 文案本就受这两个 flag 控制,但原判定直接看数组长度,致 flag 对 status 失效。)
619
- if ((!forceBaseline && result.baselineHit.length > 0) || result.deletedFiles.length > 0 || result.reasons.some(r => r.startsWith('危险') || r.startsWith('删除'))) {
623
+ // 判定结果(force-baseline 降级 baselineHit → 非 blocked;allow-new 降级新增文件 → 非 warning;
624
+ // allow-delete 降级删除文件 → 非 blocked。reasons 文案本就受 flag 控制,但原判定直接看数组长度,
625
+ // 致 flag 对 status 失效。)
626
+ if ((!forceBaseline && result.baselineHit.length > 0) || (!allowDelete && result.deletedFiles.length > 0) || result.reasons.some(r => r.startsWith('危险') || r.startsWith('删除'))) {
620
627
  result.status = 'blocked'
621
628
  } else if ((!allowNew && result.newFiles.length > 0) || (allowedFiles.length > 0 && result.reasons.some(r => r.startsWith('超出')))) {
622
629
  result.status = 'warning'
@@ -637,6 +644,28 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
637
644
  }
638
645
  } catch {}
639
646
 
647
+ // task-02: 同文件并发检测——allowedFile 在 baseline(他者改过)且当前 hash ≠ step1 hash(我也改了)
648
+ // → commit 整文件 pathspec 会夹带他者 hunk,warn 给分离指引(advisory,不阻断,D-002)
649
+ if (allowedFiles.length > 0 && guard.allowedFilesHash) {
650
+ const sameFileHits = []
651
+ for (const f of allowedFiles) {
652
+ if (isBaselineFile(f) && guard.allowedFilesHash[f] !== undefined) {
653
+ try {
654
+ const cur = createHash('sha256').update(readFileSync(join(cwd, f))).digest('hex')
655
+ if (cur !== guard.allowedFilesHash[f]) sameFileHits.push(f)
656
+ } catch {} // 文件读失败(删除等)跳过
657
+ }
658
+ }
659
+ if (sameFileHits.length > 0) {
660
+ result.reasons.push(`同文件并发: ${sameFileHits.length} 个 allowedFile 含他者+你的改动(${sameFileHits.join(', ')})`)
661
+ console.warn(`\n⚠️ 同文件并发(${sameFileHits.length} 个 allowedFile 含他者改动+你的改动,commit 整文件会夹带他者 hunk):`)
662
+ for (const f of sameFileHits) {
663
+ console.warn(` - ${f}`)
664
+ console.warn(` 分离:git add -p ${f}(交互选你的 hunk)或 git diff ${f} > mine.patch(编辑留你的)+ git apply --cached mine.patch`)
665
+ }
666
+ }
667
+ }
668
+
640
669
  // --confirm 模式:展示 diff 并等待确认
641
670
  if (isConfirm && (result.status === 'warning' || result.status === 'blocked')) {
642
671
  console.log(`\n📋 quick 变更概览:`)
@@ -658,12 +687,17 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
658
687
  }
659
688
  }
660
689
  if (result.deletedFiles.length > 0) {
661
- console.log(`\n ⛔ 本次含删除文件 ${result.deletedFiles.length} 个——quick 不支持删除,--force-baseline / --allow-new 均无法解锁(有意设计,高危操作须走 review 把关)。`)
662
- console.log(` 请改走完整流程:sillyspec run execute(在 plan 的 task allowed_paths 内删除),或先 git restore 撤回删除后再 --done。`)
690
+ if (guard.allowDelete) {
691
+ console.log(`\n ℹ️ 已确认删除文件 ${result.deletedFiles.length} 个(--allow-delete 显式解锁):${result.deletedFiles.join(', ')}`)
692
+ } else {
693
+ console.log(`\n ⛔ 本次含删除文件 ${result.deletedFiles.length} 个——删除是破坏性操作默认 fail-closed,确认删除请带 --allow-delete 显式解锁:`)
694
+ console.log(` sillyspec run quick --done --allow-delete --change <id> --output "..."`)
695
+ console.log(` (--force-baseline / --allow-new 不能解锁删除;不确认请 git restore 撤回删除后再 --done,或走完整流程 execute 由 review 把关)`)
696
+ }
663
697
  } else {
664
698
  console.log(`\n 如确认接受这些变更,重新运行 --done 时带上对应 flag 即可解锁:`)
665
699
  console.log(` sillyspec run quick --done --force-baseline --allow-new --change <id> --output "..."`)
666
- console.log(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件)`)
700
+ console.log(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件;--allow-delete 允许删除文件)`)
667
701
  console.log(` 或在首个 sillyspec run quick 启动(step 1)时就声明这些 flag,持久化进 guard。`)
668
702
  }
669
703
  }
package/src/run/stage.js CHANGED
@@ -23,6 +23,7 @@ import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessi
23
23
  import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
24
24
  import { outputStep } from './prompt.js'
25
25
  import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
26
+ import { createHash } from 'node:crypto'
26
27
  import { checkTransition } from '../stage-contract.js'
27
28
  import { completeStageGates } from './gates.js'
28
29
 
@@ -248,6 +249,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
248
249
  .filter(Boolean)
249
250
  const allowedFiles = quickOpts?.quickFiles || []
250
251
  const allowNew = quickOpts?.isAllowNew || false
252
+ const allowDelete = quickOpts?.isAllowDelete || false
251
253
  const forceBaseline = quickOpts?.isForceBaseline || false
252
254
  const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
253
255
  // CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
@@ -273,7 +275,14 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
273
275
  baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
274
276
  baselineFiles,
275
277
  allowedFiles,
278
+ // task-01: 录每个 allowedFile 内容 sha256,供 --done auditQuickCompletion 检测同文件并发
279
+ // (allowedFile 在 baseline 且当前 hash ≠ 此值 = 我也改了 → 同文件并发 warn)。文件不存在/读失败跳过。
280
+ allowedFilesHash: Object.fromEntries(allowedFiles.flatMap(f => {
281
+ try { return [[f, createHash('sha256').update(readFileSync(join(cwd, f))).digest('hex')]] }
282
+ catch { return [] }
283
+ })),
276
284
  allowNew,
285
+ allowDelete,
277
286
  forceBaseline,
278
287
  linkedChanges,
279
288
  quicklogId: qlId,
@@ -286,6 +295,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
286
295
  const parts = [`${baselineFiles.length} 个已有脏文件`]
287
296
  if (allowedFiles.length > 0) parts.push(`${allowedFiles.length} 个 allowedFiles`)
288
297
  if (allowNew) parts.push('允许新增文件')
298
+ if (allowDelete) parts.push('允许删除文件')
289
299
  console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
290
300
  console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
291
301
  // 回填 DB changes 行的 title + quicklog_id,让 quick-<hex> 可读、DB↔QUICKLOG 可对账。
package/src/run.js CHANGED
@@ -6,9 +6,12 @@
6
6
  * 外部 import 契约通过 re-export 保留(run.js 是 index.js 懒加载入口,零入边叶子,
7
7
  * 外部 import 零感知)。
8
8
  *
9
- * test 直接 import 的契约(9 个):runCommand / ensureStageSteps /
10
- * _completeStepForTest / _outputStepForTest / parsePorcelainPath / auditQuickCompletion /
9
+ * test 直接 import 的契约(7 个):runCommand / ensureStageSteps /
10
+ * parsePorcelainPath / auditQuickCompletion /
11
11
  * applyRootPlaceholders / sanitizeProjectName / validateParsedProjects。
12
+ * (outputStep 由 test/output-step-render + archive-task-completion-injection 直接自
13
+ * ./run/prompt.js import,无需 barrel 转出;completeStep/outputStep 的 `_xxxForTest` 测试别名
14
+ * 2026-08-13 全部移除——completion 行为走 CLI 子进程,纯渲染器 outputStep 直 import 源模块。)
12
15
  */
13
16
  // W6 Step8c: 主命令分发(runCommand)+ auto 模式 + ensureStageSteps 搬至 ./run/command.js
14
17
  export { runCommand, ensureStageSteps } from './run/command.js'
@@ -18,6 +21,3 @@ export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata } from './run
18
21
  export { applyRootPlaceholders } from './run/prompt.js'
19
22
  // barrel re-export: sanitizeProjectName + validateParsedProjects 被 test 直接 import(随 handleScan 搬走,契约保留)
20
23
  export { sanitizeProjectName, validateParsedProjects } from './run/complete-handlers.js'
21
- // 测试专用导出:completeStep 是 step 完成处理核心、outputStep 是 prompt 渲染器,characterization 测试直接驱动
22
- export { completeStep as _completeStepForTest } from './run/complete.js'
23
- export { outputStep as _outputStepForTest } from './run/prompt.js'
@@ -262,6 +262,17 @@ const PLAN_RULES = [
262
262
  spec: '产出 `plan.md`(任务拆解与规划,缺失即阻断)',
263
263
  failMessage: 'plan.md 缺失: ${path}',
264
264
  },
265
+ // module-impact 首版(D-001@v2):large 变更(scale≠small)在 plan review_plan 步生成 module-impact.md
266
+ // 首版(活文档全程演化——execute/verify 更新、archive 终审)。small 豁免(走 quick,module-impact 无用)。
267
+ // condition 生效依赖 validatePlanOutputs 把 design.md scale 传入 ctx(stage-contract.js D-009)。
268
+ {
269
+ id: 'plan.module-impact.exists', stage: 'plan', source: 'validatePlanOutputs', severity: 'error', kind: 'file-exists',
270
+ target: { root: 'change', path: 'module-impact.md' },
271
+ data: {},
272
+ condition: { ctxField: 'scale', ne: 'small' },
273
+ spec: 'large 变更产出 `module-impact.md` 首版(plan review_plan 步生成;scale=small 豁免)',
274
+ failMessage: 'module-impact.md 缺失: ${path}(large 变更应在 plan 审查计划步生成模块影响分析首版)',
275
+ },
265
276
  // entry-point-wiring(custom):design.md 命中入口实例化/启动路径模式时,被提到的入口文件
266
277
  // (cli.ts/main.ts/server.(js|ts)/index.(js|ts))必须出现在某 task allowed_paths 或 plan.md
267
278
  // 文件变更清单中,否则需 design.md 紧邻写明豁免。判定算法(多源 allowed_paths 收集 + 逐文件对账 +
@@ -328,8 +328,18 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
328
328
  const changeDir = resolveChangeDir(cwd, changeName, specRoot)
329
329
  const planFile = join(changeDir, 'plan.md')
330
330
 
331
+ // 读 design.md frontmatter scale(若有)传入引擎 ctx,供 plan.module-impact.exists 的 condition
332
+ // (scale≠small)判定——large 要求 module-impact 首版,small 豁免(走 quick)。fail-safe:无 scale→null→
333
+ // condition(ne 'small')成立→保守要求 module-impact(同 brainstorm scale 读取模式,line 264-272)。
334
+ const designPathForScale = join(changeDir, 'design.md')
335
+ let scale = null
336
+ if (existsSync(designPathForScale)) {
337
+ const fm = readFileSync(designPathForScale, 'utf8').match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n/)
338
+ const sm = fm && fm[1].match(/^scale:[ \t]*['"]?(\w+)/m)
339
+ if (sm) scale = sm[1]
340
+ }
331
341
  // plan.md 存在性由引擎消费 manifest。entryPoint/id-trace/decisions 为 custom kind(下方保留)。
332
- const engineResult = evaluateRules('plan', { changeDir })
342
+ const engineResult = evaluateRules('plan', { changeDir, scale })
333
343
  const errors = [...engineResult.errors]
334
344
  const warnings = [...engineResult.warnings]
335
345
 
@@ -414,7 +414,7 @@ export function validateStageReview(opts) {
414
414
  * @param {object} context - { stage, reviewRunId } 用于错误提示路径
415
415
  */
416
416
  export function printStageReviewResult(result, context = {}) {
417
- const { stage } = context
417
+ const { stage, reviewRunId, runtimeRoot } = context
418
418
  if (result.ok && result.warnings.length === 0) {
419
419
  console.log(`\n✅ Stage Review Gate — ${stage} 阶段独立审查通过`)
420
420
  return
@@ -425,6 +425,14 @@ export function printStageReviewResult(result, context = {}) {
425
425
  for (const err of result.errors) {
426
426
  console.error(` - ${err}`)
427
427
  }
428
+ // echo 完整 review.json 路径:runId + marker 已由 CLI 自动生成写入(prompt.js review step
429
+ // 渲染 / gate 触发),agent 直接派独立子代理把 review.json 写到此路径,勿手算 runId、勿手写
430
+ // marker(历史坑:agent 撞 gate 后自己算 runId 猜错 review- 前缀,填 exec-/裸 ts 被 CLI 忽略)。
431
+ if (reviewRunId) {
432
+ const reviewDir = runtimeRoot ? `${runtimeRoot}/stage-reviews/${stage}-${reviewRunId}` : `.sillyspec/.runtime/stage-reviews/${stage}-${reviewRunId}`
433
+ console.error(`\n 📁 review.json 直接写此路径(runId/marker CLI 已生成写入,勿手算 runId、勿手写 marker):`)
434
+ console.error(` ${reviewDir}/review.json`)
435
+ }
428
436
  console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
429
437
  console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
430
438
  }
@@ -25,34 +25,20 @@ export const definition = {
25
25
  },
26
26
  {
27
27
  name: 'extract-module-impact',
28
- prompt: `按照 \`.sillyspec/workflows/archive-impact.yaml\` 中定义的 \`impact-analyzer\` 角色规则,分析本次变更影响的模块。
29
-
30
- ### 文件标题规则(sillyhub 平台解析识别用)
31
- module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
28
+ prompt: `最终确认 changes/<change>/module-impact.md(本次变更的模块影响分析)。该文档由 plan review_plan 步生成首版、execute 各 Wave 更新、verify 核对——本步是归档前最后一次确认(module-impact 已非 archive 生成,改活文档终审)。
32
29
 
33
30
  ### 操作
34
- 1. 读取 \`.sillyspec/workflows/archive-impact.yaml\`,了解角色定义和检查规则
35
- 2. 读取变更目录下的 proposal.md、design.md、tasks.md
36
- 3. 运行 \`git diff --name-only HEAD~1\`(或 \`git diff --name-only --cached\`)获取真实修改文件列表
37
- 4. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`
38
- - **如果不存在**:提示"建议运行 scan 生成模块映射",但继续执行。跳到步骤 7 生成只有 unmapped 部分的 module-impact.md
39
- 5. 三重交叉验证:
40
- - 声明范围:proposal.md / design.md 中的"变更范围"/"文件变更清单"
41
- - 任务范围:tasks.md / plan.md 中的任务文件路径
42
- - 真实变更:git diff 文件列表
43
- - **以 git diff 为准**(真实 > 声明)
44
- 6. 将 git diff 文件按 \`_module-map.yaml\` 的 paths glob 匹配到模块
45
- 7. 生成模块影响矩阵:
46
-
47
- | 模块 | 影响类型 | 相关文件 | 更新内容摘要 | needs_review |
48
- |------|----------|----------|-------------|-------------|
49
-
50
- 影响类型:逻辑变更 / 数据结构变更 / 接口变更 / 调用关系变更 / 配置变更 / 新增
51
- needs_review:如果影响无法完全确定,标记为 true
52
-
53
- 8. 未匹配到任何模块的文件归入"未匹配文件"表格
54
- 9. 生成 \`.sillyspec/changes/<change-name>/module-impact.md\``,
55
- outputHint: 'module-impact.md 路径 + 影响摘要',
31
+ 1. 读取 changes/<change>/module-impact.md(应已存在;large 变更 plan 阶段产出首版)
32
+ 2. 运行 git diff --name-only HEAD~1(或 git diff --name-only --cached)获取真实修改文件列表
33
+ 3. 三重核对:module-impact.md 记录的受影响模块/文件 vs 真实 git diff 文件列表 vs .sillyspec/docs/<project>/modules/_module-map.yaml 模块映射(不存在则仅核 unmapped 部分)。以 git diff 为准(真实 > 记录)
34
+ 4. 发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)→ 直接修正 module-impact.md,使其与实际变更一致
35
+ 5. module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
36
+
37
+ ### 降级(module-impact.md 不存在)
38
+ 若缺失(如旧变更未经新 plan 流程,或 small 变更从未生成),按原生成逻辑补一份:读 .sillyspec/workflows/archive-impact.yaml 的 impact-analyzer 角色规则 + git diff + _module-map.yaml,生成模块影响矩阵(模块 × 影响类型 × 相关文件 × 更新摘要 × needs_review,未匹配文件归入 unmapped 表)落盘 module-impact.md。
39
+
40
+ 确认完成后,下一步 sync-module-docs 会读 module-impact.md 更新模块卡片文档。`,
41
+ outputHint: 'module-impact.md 终审结果 + 修正摘要',
56
42
  optional: false
57
43
  },
58
44
  {
@@ -329,6 +329,8 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
329
329
  - 长期术语只在 archive/scan 时再提升到 \`{SPEC_ROOT}/docs/<project>/glossary.md\`
330
330
  4. 格式自检(只查章节齐全;语义一致性/可行性/YAGNI 不在本步查,交给下一步 Design Grill 独立审查):
331
331
  - design.md 含全部必填章节(背景/设计目标/非目标/总体方案/文件变更清单/接口定义/风险登记)
332
+ - design.md 头部 YAML frontmatter 字段齐全(author/created_at/scale 等)——step8 完成契约硬要求,审查前就补齐避免后续 docHash 漂移
333
+ - design.md 含「自审」字面命中章节(自审 / Self-Review / Self-review)——step8 完成契约 brainstorm.design.self-review 硬要求,写在本步自审段即可
332
334
  - 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
333
335
  - 涉及 session/lease/agent_run/daemon/lifecycle 等关键词时,是否含「生命周期契约表」
334
336
  5. 缺章节 → 补齐后重检;章节齐全 → 进入下一步(Design Grill 做语义层交叉审查)
@@ -476,10 +478,10 @@ created_at: <now-datetime>
476
478
 
477
479
  ### proposal.md 格式要求
478
480
  \`\`\`markdown
479
- ---
480
- author: <git-user>
481
- created_at: <now-datetime>
482
- ---
481
+ ---
482
+ author: <git-user>
483
+ created_at: <now-datetime>
484
+ ---
483
485
  # 提案书(Proposal)
484
486
 
485
487
  ## 动机
@@ -503,10 +505,10 @@ created_at: <now-datetime>
503
505
 
504
506
  ### requirements.md 格式要求
505
507
  \`\`\`markdown
506
- ---
507
- author: <git-user>
508
- created_at: <now-datetime>
509
- ---
508
+ ---
509
+ author: <git-user>
510
+ created_at: <now-datetime>
511
+ ---
510
512
  # 需求规格(Requirements)
511
513
 
512
514
  ## 角色
@@ -535,18 +537,18 @@ Then 期望结果
535
537
  | D-001@v1 | FR-01 | ... |
536
538
  \`\`\`
537
539
 
538
- ### tasks.md 格式要求(scale=large 时生成骨架,plan 阶段展开)
539
- \`\`\`markdown
540
- ---
541
- author: <git-user>
542
- created_at: <now-datetime>
543
- ---
544
- # 任务清单(Tasks)
545
-
546
- - [ ] task-01: <任务名>
547
- - [ ] task-02: <任务名>
548
- \`\`\`
549
-
540
+ ### tasks.md 格式要求(scale=large 时生成骨架,plan 阶段展开)
541
+ \`\`\`markdown
542
+ ---
543
+ author: <git-user>
544
+ created_at: <now-datetime>
545
+ ---
546
+ # 任务清单(Tasks)
547
+
548
+ - [ ] task-01: <任务名>
549
+ - [ ] task-02: <任务名>
550
+ \`\`\`
551
+
550
552
  ### decisions.md 格式要求(仅在有 Grill/重大决策时生成)
551
553
  \`\`\`markdown
552
554
  # 决策记录(Decisions)
@@ -33,7 +33,11 @@ export function validatePlanForExecute(planContent) {
33
33
 
34
34
  // 检查 1: 至少有一个 checkbox task
35
35
  if (allTasks.length === 0) {
36
+ // 诊断根因(坑 plan-md-format-contract-hidden):笼统报"没有找到 checkbox task"逼 agent 试错。
37
+ // 区分:无 Wave/Tasks 段 / Wave 标题格式不对 / task 用 ### 标题 / checkbox 被非 Wave 标题打断。
38
+ const diags = diagnoseNoTaskRootCause(planContent)
36
39
  errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
40
+ for (const d of diags) errors.push(` 诊断:${d}`)
37
41
  return { ok: false, errors, warnings, tasks: allTasks, waves }
38
42
  }
39
43
 
@@ -357,6 +361,39 @@ Apply Decision + 下一步建议
357
361
  }
358
362
  ]
359
363
 
364
+ /**
365
+ * 诊断 plan.md 无 checkbox task 的根因(坑 plan-md-format-contract-hidden)。
366
+ * 纯函数:读 planContent 文本,返回诊断提示数组。validatePlanForExecute 无 task 时追加到报错,
367
+ * 帮 agent 区分 4 条隐性格式契约(Wave 标题字面 / task checkbox / task-XX 编号 / ### 打断 Wave 段)。
368
+ * @param {string} planContent
369
+ * @returns {string[]}
370
+ */
371
+ function diagnoseNoTaskRootCause(planContent) {
372
+ const diags = []
373
+ const hasWaveHeading = /^#+\s*Wave\s+\d+/im.test(planContent) // "## Wave N"(字面)
374
+ const hasWaveWord = /^#+\s*(wave\s*\d+|w\d+|波次\s*\d+)/im.test(planContent) // 疑似 Wave 标题(含 W1/Wave1/波次1)
375
+ const hasCheckboxTask = /^[-*]\s*\[[ x]\]\s*task-\d+/im.test(planContent) // "- [ ] task-XX:" checkbox
376
+ const hasTaskHeading = /^#+\s*task-\d+/im.test(planContent) // "### task-XX:" 标题(非 checkbox)
377
+ const hasTasksSection = /^#+\s*(tasks?|任务)\s*$/im.test(planContent) // "## Tasks" / "## 任务"
378
+
379
+ if (!hasWaveHeading && !hasTasksSection) {
380
+ if (hasWaveWord) {
381
+ diags.push('Wave 标题格式不对:必须字面 "## Wave N"(Wave + 空格 + 数字),"## W1" / "## Wave1" / "## 波次1" 都不被识别')
382
+ } else if (hasCheckboxTask) {
383
+ diags.push('有 task checkbox 但不在 "## Wave N"(full)或 "## Tasks"(light/none)段内——非任务区的 checkbox 不收')
384
+ } else {
385
+ diags.push('plan.md 缺任务区:full 需 "## Wave 1" 标题,light/none 需 "## Tasks" 标题')
386
+ }
387
+ }
388
+ if (hasTaskHeading) {
389
+ diags.push('task 用了 "### task-XX:" 标题(非 checkbox):标题会打断 Wave 段导致后续 checkbox 全被跳过,请改用 "- [ ] task-XX: 名称"(英文冒号)')
390
+ }
391
+ if (hasWaveHeading && hasCheckboxTask && !hasTaskHeading) {
392
+ diags.push('有 Wave 段也有 checkbox task 但未收容:checkbox 可能落在非任务区(如 "## 自检" 段)或被 "###" 标题打断,请检查 task 是否紧跟在 "## Wave N" 下')
393
+ }
394
+ return diags
395
+ }
396
+
360
397
  /**
361
398
  * 从 plan 文件解析 Wave 分组
362
399
  */
@@ -801,6 +838,9 @@ ${taskSummary}
801
838
  2. 蓝图文件路径(让子代理自行读取详情)
802
839
  3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
803
840
  4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
841
+ 5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
842
+
843
+ {{include: testcase-design}}
804
844
 
805
845
  ### Wave 开始前
806
846
  1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑
@@ -865,6 +905,14 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
865
905
  - \`cannot_verify\` 只在确实无法验证且有待补充证据时使用,且 requiredEvidence 必须非空
866
906
  - \`sillyspec run execute --done\` 会校验所有 task 的 review.json,缺失或 fail 会阻断完成
867
907
 
908
+ ### module-impact.md 更新(主代理在本 Wave 所有 task 完成后汇总)
909
+ 本 Wave 内所有 task 子代理完成、review.json 写好后,**由你(主代理/调度者)**汇总本 Wave 的实际代码变更,更新 {SPEC_ROOT}/changes/<change>/module-impact.md(plan 阶段已生成首版):
910
+ - 基于本 Wave 各 task 的实际 git diff(不是计划)+ {SPEC_ROOT}/docs/<project>/modules/_module-map.yaml 对照
911
+ - 更新受影响模块的影响类型/说明(实际改动可能与 plan 首版预估不同,据实修正)
912
+ - **不由各 task 子代理分别改**(同 Wave 并行子代理改同一文件会互相覆盖)——只由主代理在 Wave 收尾统一更新一次
913
+ - 无 _module-map.yaml 时跳过模块匹配,仅按文件清单更新 unmapped 部分
914
+ 这是可选更新(不阻断 execute 完成),但保持 module-impact 与实际变更一致利于 verify 核对与 archive 终审。
915
+
868
916
  ### 完成后
869
917
  1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
870
918
  - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)