sillyspec 3.28.10 → 3.28.12

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 (52) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/.husky/pre-commit +4 -0
  6. package/.husky/pre-push +3 -0
  7. package/AGENTS.md +1 -1
  8. package/package.json +1 -1
  9. package/src/change-risk-profile.js +76 -6
  10. package/src/commit-guard.js +119 -0
  11. package/src/config-schema.js +23 -0
  12. package/src/design-facts.js +46 -9
  13. package/src/diagnostic-codes.js +95 -0
  14. package/src/docs-check.js +14 -10
  15. package/src/docs-debt.js +7 -2
  16. package/src/docs-gate.js +42 -15
  17. package/src/doctor-diagnostics.js +141 -1
  18. package/src/endpoint-extractor.js +50 -2
  19. package/src/foreign-declared.js +25 -16
  20. package/src/hooks/worktree-guard.js +75 -2
  21. package/src/index.js +129 -1
  22. package/src/machine-interface.js +63 -0
  23. package/src/probe7-anchor-check.js +20 -2
  24. package/src/progress/stage-machine.js +34 -5
  25. package/src/progress.js +1 -1
  26. package/src/quicklog.js +231 -1
  27. package/src/run/complete-handlers.js +172 -1
  28. package/src/run/complete.js +73 -3
  29. package/src/run/gate-snapshot.js +262 -0
  30. package/src/run/gates.js +41 -7
  31. package/src/run/prompt.js +79 -1
  32. package/src/run/shared.js +10 -0
  33. package/src/run/verify-quality-scan.js +151 -5
  34. package/src/stage-contract-spec.js +14 -0
  35. package/src/stage-contract.js +738 -8
  36. package/src/stage-review-checklist.js +16 -2
  37. package/src/stage-review.js +137 -2
  38. package/src/stages/archive.js +9 -5
  39. package/src/stages/brainstorm.js +7 -1
  40. package/src/stages/execute.js +62 -8
  41. package/src/stages/plan-postcheck.js +42 -5
  42. package/src/stages/plan.js +8 -1
  43. package/src/stages/quick.js +5 -5
  44. package/src/stages/verify.js +8 -3
  45. package/src/sync.js +69 -0
  46. package/src/task-review.js +9 -3
  47. package/src/verify-facts-schema.js +95 -10
  48. package/src/verify-postcheck.js +346 -28
  49. package/src/verify-probes.js +1221 -46
  50. package/src/worktree-apply.js +175 -3
  51. package/src/write-audit.js +55 -0
  52. package/.claude/full.json +0 -1
@@ -8,9 +8,13 @@
8
8
  *
9
9
  * 条目为纯文本,不含序号/checkbox/缩进等结构性前缀(渲染层负责前缀形态):
10
10
  * - brainstorm:前 3 条 =「交叉审查模型」三层检查(prompt 内渲染为「N. 」序号行),
11
- * 后 6 条 =「交叉点抽取」(渲染为「- 」列表行);
11
+ * 后 8 条 =「交叉点抽取」(渲染为「- 」列表行);
12
12
  * - plan:渲染为「- [ ] 」checkbox 行(审查清单段);
13
- * - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」)。
13
+ * - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」);
14
+ * - verify(2026-09-17-api-coverage-smoke task-06 / FR-07 新增键):smoke 纪律条目,
15
+ * 渲染为「输出验证报告」步「### 操作」第 8 条下的 3 空格缩进「- 」列表行;
16
+ * 命中条件以条目文本内嵌说明(local.yaml 配 commands.smoke 或判级 critical 时生效——
17
+ * review-dispatch worker_prompt 单独消费条目,无 prompt 上下文,须自含)。
14
18
  */
15
19
  export const REVIEW_CHECKLISTS = {
16
20
  brainstorm: [
@@ -23,6 +27,8 @@ export const REVIEW_CHECKLISTS = {
23
27
  'tasks.md 的执行范围与 design.md 的非目标',
24
28
  'decisions.md 的 D-xxx@vN 与 design.md 当前说法',
25
29
  'scan/module docs 或源码中的真实约束与 design.md 假设',
30
+ '涉及角色/权限/字典类数据时,实证不能只看静态定义:须以生产查询口径可解析到目标结果——用真实租户/库下可执行的查询(如按 enname+tenant 查询)确认返回非空,仅 enname 存在不算通过',
31
+ '涉及新页面/前端路由时,用户入口 × 菜单/注册 DML 对账:入口路由与菜单表、角色-权限注册 DML 逐一对账——能点到的入口必有对应菜单与授权注册行,菜单/注册 SQL 必须出现在交付清单(design.md 文件变更清单或 db/ 脚本)',
26
32
  ],
27
33
  plan: [
28
34
  'task 编号与 Wave checkbox 格式正确,execute 依赖此格式解析任务',
@@ -41,4 +47,12 @@ export const REVIEW_CHECKLISTS = {
41
47
  'design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)',
42
48
  '组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)',
43
49
  ],
50
+ // verify 键(2026-09-17-api-coverage-smoke task-06 / FR-07):smoke 纪律四段,逐字照 design §6 ①~④;
51
+ // 首条内嵌命中条件说明(worker_prompt 单独消费条目,须自含);②③④段尾分号/句号照 design 原文保留。
52
+ verify: [
53
+ 'smoke 纪律(命中条件:local.yaml 配置 commands.smoke 或本变更判级 integration/deployment-critical 时生效)——①断言派生表:design 接口表每行 ≥1 happy-path(含出参形状断言)/ 权限矩阵每行 ≥1 反例(非授权操作应拒)/ 契约表必填每项 ≥1 空值反例 / 转移表每边 ≥1 状态断言 / 需求字面(单号格式等)→ 格式断言;',
54
+ '②负向下界:每写端点 ≥1 权限反例(E4 类)+ 全链 ≥1 注错全量回滚断言(E5 类);',
55
+ '③执行口径:脚本后台起服+轮询就绪+finally 杀(墙钟增量≈冒烟本体)、DB 会话自设严格 sql_mode、载荷从消费端构造点导出(手写正确字段的测试抓不住字段漂移);',
56
+ '④断言锚点注释(每步挂依据 ID)与矩阵行一一对应。',
57
+ ],
44
58
  }
@@ -422,6 +422,111 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
422
422
  }
423
423
  }
424
424
 
425
+ /**
426
+ * 同阶段上一轮审查结论采集(ql-20260916-021,Superpowers scoped re-review 采纳①)。
427
+ *
428
+ * 复审派发 prompt 原本只有跨阶段 pass 结论注入(prompt.js {PRIOR_REVIEW_FACTS} 的前序阶段段),
429
+ * 同 stage 上一轮 FAIL 的 findings 与已实证 pass 面不回灌——独立复审子代理全量重读并重复报告
430
+ * 已修问题。本函数机械提取「该 stage 本变更最近一轮有效 review」的两份清单供回灌:
431
+ * - openFindings:checklist result=fail/gap 的未决项(verdict=fail 但 checklist 无明细时以
432
+ * reviewerNotes 合成一条,防 fail 轮零线索);
433
+ * - passItems:checklist result=pass 的已实证面(复审勿重复报告/重验,除非修复改动触及该面)。
434
+ *
435
+ * 采集规则:
436
+ * - runId 字典序 = 时间序(generateStageReviewRunId 时间戳格式),取最新有效轮——含 marker
437
+ * 当前指向 run 的已写 review(同目录覆盖式复审路径下它就是最新轮,无需调用方区分);
438
+ * - 跨变更过滤:reviewedFiles[0] 不含 `changes/<changeName>/` 的轮不采(与
439
+ * getLatestStageReviewRunId 的 cross-change 防护同口径);
440
+ * - 骨架轮跳过(register-stage-review 生成的双 cannot_verify 待审骨架);
441
+ * - 坏 JSON 轮跳过不抛(best-effort);两者皆空(无未决也无 pass 面)→ null。
442
+ *
443
+ * @param {string} runtimeRoot - .runtime 绝对路径
444
+ * @param {string} stage - brainstorm|plan|execute
445
+ * @param {string} changeName - 变更名(跨变更过滤锚)
446
+ * @returns {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[], notesPreview: string }|null}
447
+ */
448
+ export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
449
+ if (!runtimeRoot || !stage || !changeName) return null
450
+ const dir = join(runtimeRoot, 'stage-reviews')
451
+ if (!existsSync(dir)) return null
452
+ let entries
453
+ try {
454
+ entries = readdirSync(dir, { withFileTypes: true })
455
+ .filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
456
+ .map(e => e.name)
457
+ } catch { return null }
458
+ if (entries.length === 0) return null
459
+
460
+ const candidates = []
461
+ for (const name of entries) {
462
+ const rp = join(dir, name, 'review.json')
463
+ if (!existsSync(rp)) continue
464
+ try {
465
+ const review = JSON.parse(readFileSync(rp, 'utf8'))
466
+ const r0 = Array.isArray(review.reviewedFiles) ? String(review.reviewedFiles[0]) : ''
467
+ if (!r0.includes(`changes/${changeName}/`)) continue
468
+ // 骨架占位跳过(register-stage-review 待审骨架:双 cannot_verify + 骨架 notes 标记)
469
+ const isSkeleton = review.specVerdict === 'cannot_verify' && review.qualityVerdict === 'cannot_verify'
470
+ && String(review.reviewerNotes || '').includes('骨架由 register-stage-review')
471
+ if (isSkeleton) continue
472
+ candidates.push({ runId: name.slice(stage.length + 1), review })
473
+ } catch { continue }
474
+ }
475
+ if (candidates.length === 0) return null
476
+
477
+ candidates.sort((a, b) => a.runId.localeCompare(b.runId))
478
+ const latest = candidates[candidates.length - 1]
479
+ const openFindings = []
480
+ const passItems = []
481
+ for (const c of (Array.isArray(latest.review.checklist) ? latest.review.checklist : [])) {
482
+ if (!c || typeof c !== 'object' || !c.item) continue
483
+ const text = String(c.item).replace(/\s+/g, ' ').slice(0, 120)
484
+ if (c.result === 'fail' || c.result === 'gap') openFindings.push(`[${c.result}] ${text}`)
485
+ else if (c.result === 'pass') passItems.push(text)
486
+ }
487
+ // verdict=fail 但 checklist 无明细:fail 结论不丢线索——notes 合成一条(复审者据此核验)
488
+ if (openFindings.length === 0 && (latest.review.specVerdict === 'fail' || latest.review.qualityVerdict === 'fail')) {
489
+ const notes = String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160)
490
+ openFindings.push(`[verdict-fail] verdict=fail 但 checklist 无明细——以 reviewerNotes 为准:${notes || '(无 notes)'}`)
491
+ }
492
+ if (openFindings.length === 0 && passItems.length === 0) return null
493
+ return {
494
+ priorRunId: latest.runId,
495
+ verdicts: `spec=${latest.review.specVerdict}, quality=${latest.review.qualityVerdict}`,
496
+ openFindings,
497
+ passItems,
498
+ notesPreview: String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160),
499
+ }
500
+ }
501
+
502
+ /**
503
+ * renderPriorRoundFindingsMd——把 collectSameStagePriorReview 产物渲染为复审派发 prompt 的
504
+ * 回灌块(prompt.js 注入 {PRIOR_REVIEW_FACTS},与前序阶段 pass 结论段同串拼接)。
505
+ *
506
+ * 语义双向:未决项**必须**逐项核验(已修给证据、未解决如实再次 fail——漏放行=假通过);
507
+ * pass 面**勿重复报告/重验**(除非修复改动明确触及该面)——一堵一疏,复审以增量为主。
508
+ *
509
+ * @param {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[] }|null} collected
510
+ * @param {number} [cap=15] 每段展示封顶(防 prompt 灌爆,对齐前序实证段的 15 条惯例)
511
+ * @returns {string} markdown 块;collected 空 → 空串
512
+ */
513
+ export function renderPriorRoundFindingsMd(collected, cap = 15) {
514
+ if (!collected) return ''
515
+ const L = []
516
+ L.push(`**同阶段上一轮审查结论(run ${collected.priorRunId},verdict ${collected.verdicts})——本次为复审,以增量为主,不重演全量审查**:`)
517
+ if (collected.openFindings.length > 0) {
518
+ const shown = collected.openFindings.slice(0, cap)
519
+ L.push(`- **上一轮未决问题(逐项核验修复状态:已修给证据、未解决必须如实再次 fail——漏放行=假通过)**(${shown.length}/${collected.openFindings.length} 条):`)
520
+ for (const f of shown) L.push(` - ${f}`)
521
+ }
522
+ if (collected.passItems.length > 0) {
523
+ const shown = collected.passItems.slice(0, cap)
524
+ L.push(`- **上一轮已实证 pass 面(勿重复报告/重验——除非本次修复改动明确触及该面才定向复查)**(${shown.length}/${collected.passItems.length} 条):`)
525
+ for (const p of shown) L.push(` - ${p}`)
526
+ }
527
+ return `\n${L.join('\n')}\n`
528
+ }
529
+
425
530
  /**
426
531
  * 降级自审检测(2026-09-10 用户反馈①:PI agent 等宿主环境无 Agent tool,tier=independent
427
532
  * 硬要求子代理时只能降级自审——stage prompts 降级条款约定 reviewerNotes **首行**记
@@ -437,6 +542,19 @@ export function isDegradedSelfReview(review) {
437
542
  return /^降级[::]/.test(firstLine)
438
543
  }
439
544
 
545
+ /**
546
+ * 代落盘披露识别(C4,坑 delegated-write-audit-blind,2026-09-15 wp EHS 会话实证:平台写通道
547
+ * 故障下主流程 4 次代子代理落盘 review.json/交付物——独立性全靠 agent 自觉 + notes 留痕,gate
548
+ * 无任何可见标记。审查派发 prompt 已要求代落盘时 reviewerNotes 首行记「代落盘:…」,本识别让
549
+ * gate 侧补 ⚠️ 审计行(与 isDegradedSelfReview 同哲学:可见的折损优于静默的伪装——首行约定
550
+ * 识别,不承诺全文语义判定)。
551
+ */
552
+ export function hasDelegatedWriteDisclosure(review) {
553
+ const notes = review && typeof review.reviewerNotes === 'string' ? review.reviewerNotes : ''
554
+ const firstLine = notes.split(/\r?\n/, 1)[0].trim()
555
+ return /^代落盘[::]/.test(firstLine)
556
+ }
557
+
440
558
  /**
441
559
  * stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
442
560
  *
@@ -572,10 +690,20 @@ export function validateStageReviewWithAutoRefresh(opts) {
572
690
  if (!docHash) return { result, autoRefreshed: false }
573
691
 
574
692
  const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
693
+ // C3 声明化(坑 dochash-autorefresh-silent-drift,2026-09-15 wp EHS 会话实证:审查通过后
694
+ // 主文档多次改版——NEW: 前缀批注、需求调整改名、Reverse Sync——gate 每次都静默机械重算放行,
695
+ // verdict 原样保留,「审查后的变更不再见审查」零痕迹可读。旧内容不可复原(只有 hash),改用
696
+ // 可实现且更贴风险的两个信号做声明:① hash 漂移(旧→新短前缀)② 刷新序数——同一 review 被
697
+ // 反复刷新 = 主文档在审查后反复变更,序数 ≥2 时 gate 侧升级提醒人工确认结论续用。
698
+ const priorNotes = String(result.review.reviewerNotes || '')
699
+ const refreshOrdinal = (priorNotes.match(/docHash auto-refreshed at /g) || []).length + 1
700
+ const oldHashShort = String(result.review.docHash || '').slice(0, 8)
701
+ const newHashShort = String(docHash).slice(0, 8)
702
+ const auditLine = `docHash auto-refreshed at ${refreshedAt}(第 ${refreshOrdinal} 次;hash ${oldHashShort}→${newHashShort};gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用于新文档需人工确认${refreshOrdinal >= 2 ? '——已多次刷新:主文档在审查后反复变更,建议人工复核结论续用或重审' : ''})`
575
703
  const refreshed = {
576
704
  ...result.review,
577
705
  docHash,
578
- reviewerNotes: `${result.review.reviewerNotes || ''}\ndocHash auto-refreshed at ${refreshedAt}(gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用需人工确认)`.trim(),
706
+ reviewerNotes: `${priorNotes}\n${auditLine}`.trim(),
579
707
  }
580
708
  try {
581
709
  writeAtomicSync(reviewPath, JSON.stringify(refreshed, null, 2) + '\n')
@@ -583,7 +711,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
583
711
  return { result, autoRefreshed: false } // 写失败回落原失败结果,不静默放行
584
712
  }
585
713
  const recheck = validateStageReview(opts)
586
- return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors }
714
+ return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors, refreshOrdinal, hashDrift: `${oldHashShort}→${newHashShort}` }
587
715
  }
588
716
 
589
717
  /**
@@ -708,6 +836,13 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
708
836
  } catch (e) {
709
837
  throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
710
838
  }
839
+ // 幂等跳过(2026-09-17 用户反馈⑥-②,ql-20260917-005):docHash 已一致时 refresh 仍整文件
840
+ // 重写 + 追加审计行——会话内多次 refresh 每次都 bump mtime/改内容,agent 的 Read→Edit 窗口
841
+ // 被「file modified since read」打断(本会话两次实证)。hash 未变 = 无可刷新内容,直接
842
+ // no-op 返回(不写盘不 bump mtime);行为差异只在「少一次无意义重写」。
843
+ if (existing.docHash === docHash && JSON.stringify(existing.reviewedFiles) === JSON.stringify(reviewedFiles)) {
844
+ return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'noop-unchanged', mainDoc, review: existing }
845
+ }
711
846
  const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
712
847
  const review = {
713
848
  ...existing,
@@ -75,13 +75,17 @@ export const definition = {
75
75
 
76
76
  {SCOPE_AUDIT_TABLE}
77
77
 
78
- 3. 确保任务清单(tasks.md)所有 task checkbox 都已勾选;对照下方完成度快照做归档前最后注视(未完成/存疑 → 停下勿确认):
78
+ 3. 展示移交项清单(verify 阶段 facts.handover——CLI 机械注入如下,勿手改勿手算;blocking 置顶 + severity 标注,超封顶截断;确认前逐项核对 blocking 移交项已有处置安排(db-script 类须先在 verify-result.md 补执行声明——归档门会拦未声明/互斥态,env-blocked 确认复跑条件已登记,manual-acceptance 确认验收安排),advisory 项记录备查;无移交项时本区为空,注入异常时置换单行降级指引,全量见 changes/<变更名>/verify-facts.json):
79
+
80
+ {HANDOVER_SUMMARY}
81
+
82
+ 4. 确保任务清单(tasks.md)所有 task checkbox 都已勾选;对照下方完成度快照做归档前最后注视(未完成/存疑 → 停下勿确认):
79
83
  {TASK_COMPLETION_REPORT}
80
- 4. 若上一步机械决策提炼输出「⚠️ rejected 决策缺否决理由/复潮条件(needsWait)」:先按其指引处理——补录 decisions.md 缺失字段后重跑提炼命令(幂等),或经用户裁决跳过该条;处理结果写进本步 --output
81
- 5. **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
82
- 6. 让用户确认后,用 \`--confirm\` 完成本步骤:
84
+ 5. 若上一步机械决策提炼输出「⚠️ rejected 决策缺否决理由/复潮条件(needsWait)」:先按其指引处理——补录 decisions.md 缺失字段后重跑提炼命令(幂等),或经用户裁决跳过该条;处理结果写进本步 --output
85
+ 6. **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
86
+ 7. 让用户确认后,用 \`--confirm\` 完成本步骤:
83
87
  \`sillyspec run archive --done --confirm --output "确认归档"\`
84
- 7. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
88
+ 8. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
85
89
 
86
90
  ### 输出
87
91
  归档完成 + archive 目录路径`,
@@ -286,10 +286,11 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
286
286
 
287
287
  | 操作 | 文件路径 | 说明 |
288
288
  |---|---|---|
289
- | 新增 | src/xxx/NewFile.java | ... |
289
+ | 新增 | NEW:src/xxx/NewFile.java | ... |
290
290
  | 修改 | src/xxx/ExistingFile.java | 新增 xx 方法 |
291
291
  | 删除 | src/xxx/OldFile.java | 已被 xx 替代 |
292
292
 
293
+ **路径存在性核验(NEW: 前缀铁律,2026-09-17 用户反馈⑥-①前移)**:清单条目在「生成规范文件」步 \`--done\` 门禁会逐一核验仓内存在性——**计划新建的文件必须在路径前加 \`NEW:\` 前缀**(冒号后不加空格,\`NEW:src/foo.js\` 合法、\`NEW: src/foo.js\` 属书写错误);未加前缀的新建路径会以 \`design_file_ref_invalid\`(幻觉路径/书写错误)在末步被拦,返工一轮。已有文件写仓根相对正斜杠路径,禁绝对路径/glob。
293
294
  **字段数据流标注**(避免新增字段到 execute 才发现没透传):当清单含「新增/修改对外字段、接口、DTO、响应体、事件 payload、配置键」时,对应行「说明」列必须交代 producer→consumer 数据流——产出方(producer)→ 每跳流转/归一化点(序列化/反序列化/字段映射/默认值兜底)→ 消费方(consumer)。漏标 = 字段在某跳 dormant(声明了却没透传),到 execute/verify 才暴露,属设计层缺口。
294
295
  - 示例:「修改 daemon.ts:新增 budget_tokens,producer=backend/api-types.ts → ws_hub envelope 透传 → daemon.ts normalizeUsage(snake→camel)→ consumer=runtime/lease.ts 写 lease」
295
296
  - 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
@@ -385,8 +386,12 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
385
386
  - tier=self:当前 agent 直接执行下方交叉审查(小变更)
386
387
  - tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值):
387
388
  宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
389
+ {PRIOR_REVIEW_FACTS}
388
390
  {REVIEW_JSON_CONTRACT}
389
391
  子代理只产出 review + Unresolved Blockers,**是否调用 sillyspec run brainstorm --wait 仍由你(主 agent)根据其 verdict 决定**(子代理不直接操作 CLI 状态机)。
392
+ **子代理执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:独立审查 94 分钟无收敛被用户三催、写通道故障空转 35 分钟)**:
393
+ - 时间盒收敛:审查子代理的调研是收敛动作不是发散——必读材料读完即逐条出结论并落盘 review.json;仅个别结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 立即停止扩展、基于已读材料收敛)。
394
+ - 写通道降级:子代理写操作持续被平台拒绝(session not in running turn 类故障)→ 重试 ≤3 次即停,把完整审查结论(含 review.json 全文)作为最终文本回传主代理,由主代理代为落盘并在 reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
390
395
 
391
396
  ### 默认行为
392
397
  1. 默认必须执行一次交叉审查;不要让用户凭主观判断决定"要不要 Grill"。
@@ -413,6 +418,7 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(0, 3).map((item, index) => (index + 1) + '.
413
418
  ### 交叉点抽取
414
419
  重点找这些交叉点:
415
420
  ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
421
+ 后两条为命中条件注入项:变更涉及角色/权限/字典类数据或新页面/前端路由时必查。
416
422
 
417
423
  ### 问答处理
418
424
  1. 先自动交叉审查,不要一上来问用户。
@@ -197,14 +197,14 @@ export function validatePlanForExecute(tasksContent, planContent) {
197
197
  }
198
198
 
199
199
  // 检查 0.8: Wave 标题格式不对(W1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
200
- // 全并行——串行意图失效且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
200
+ // 串行执行——Wave 分组意图丢失且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
201
201
  // 且无任何显式 Wave 被解析时报,正常 Wave 10/带括号标题不误伤。
202
202
  const hasExplicitWave = waves.some(w => !w.implicit)
203
203
  const waveLikeHeading = /^#+\s*(?:wave\s*\d+|w\d+|波次\s*\d+)/im
204
204
  if (!hasExplicitWave && waveLikeHeading.test(plan)) {
205
205
  errors.push(
206
206
  `Wave 标题格式不对:必须字面 "## Wave N"(Wave + 空格 + 数字),"## W1" / "## Wave1" / "## 波次1" 都不被识别。` +
207
- `其下的 "- task-XX" 引用行不会被收容——任务将退化为单个隐式 Wave 全并行,串行意图失效`
207
+ `其下的 "- task-XX" 引用行不会被收容——所有任务将退化为单个隐式 Wave 串行执行(Wave 分组意图丢失,无法按组并行/验收)`
208
208
  )
209
209
  }
210
210
  // 检查 0.9(坑 wave-heading-undercount,2026-08-21 实证):部分 Wave 被识别 + 部分
@@ -404,6 +404,12 @@ worktree 路径 + 分支名 + 模式
404
404
  欠账处理:Wave 收尾时顺手同步对应模块卡,不必为此停下。变更索引类条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;可先跑 \`sillyspec modules split-changelog\` 迁出历史段)——勿把历史条目堆回模块卡正文(卡是子代理的读取税)。
405
405
  ⚠️ **铁律:模块卡 / sidecar 一律写主仓绝对路径 \`{SPEC_ROOT}/docs/<project>/modules/\`(CLI 已替换为绝对路径)**——在 worktree 内执行时**绝不**写 worktree checkout 出来的 \`.sillyspec\` 副本(该副本随 worktree cleanup 整目录蒸发,主仓永远读不到;2026-09-10 实证 task-13 子代理写副本、归档阶段被迫 checkout 补救)。
406
406
 
407
+ ### 歧义裁决权限(非破坏性自行裁决+记录,破坏性停人)
408
+ 执行中撞到 plan/design 歧义或冲突(任务描述与代码现实不符、两任务契约打架、design 未覆盖的边界情况)时按破坏性分级处理,不要一律停下等用户、也不要静默猜测(外部实测:可控歧义一律停人导致执行会话停摆 9 小时):
409
+ - **非破坏性歧义**(可逆、局部、不改既有 D-xxx 决策语义、不越 allowed_paths 边界、不删不改他人模块):自行裁决继续干。裁决落盘 \`{SPEC_ROOT}/changes/<变更名>/decisions.md\` 新条目(接续既有最大编号,格式 \`## D-xxx@v1: <短标题>\`,正文含歧义描述/选择/理由/影响 task),并在该 Wave 完成输出中披露「执行期裁决 N 条:D-xxx…」——review/verify 据此对账。⚠️ decisions.md 一律写**主仓** \`{SPEC_ROOT}\` 绝对路径(CLI 已替换),勿写 worktree checkout 的 \`.sillyspec\` 副本(随 cleanup 蒸发,同模块卡铁律)。
410
+ - **破坏性/不可逆动作**(删数据/重写他人模块、改共享配置或门禁、跳过测试、扩大 allowed_paths 边界、推翻既有 D-xxx 决策、跨仓联动):禁止自行裁决,\`sillyspec run execute --wait --reason "<冲突描述>" --options "<裁决A>,<裁决B>,需要更多上下文"\` 停人。
411
+ - 拿不准是否破坏性 → 按破坏性处理(fail-closed);同一歧义重复出现 → 引用既有 D-xxx,不重复裁决。
412
+
407
413
  ### 铁律
408
414
  - **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
409
415
  outputHint: 'Wave 分组 + 模型分配',
@@ -422,6 +428,7 @@ const acceptanceSteps = [
422
428
  tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
423
429
  - tier=self:当前 agent 汇总执行(对照 design.md 逐项检查 + 偏差说明)
424
430
  - tier=independent:必须用 Agent tool 启动一个独立的 QA 子代理(独立上下文,不共享实现者的分析),子代理对照 design.md 逐项检查实现一致性并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值;reviewedFiles 除主文档 design.md 外可追加 git diff 涉及的源码文件):
431
+ {PRIOR_REVIEW_FACTS}
425
432
  宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
426
433
  {REVIEW_JSON_CONTRACT}
427
434
  该 acceptance review 同时覆盖"代码审查"视角(风格/bug/安全/冗余),后续代码审查步骤仅需轻量复审。
@@ -430,6 +437,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
430
437
  ${REVIEW_CHECKLISTS.execute.map((item, index) => ' ' + (index + 1) + '. ' + item).join('\n')}
431
438
 
432
439
  **产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
440
+ **执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证)**:brainstorm/plan 阶段已实证的结论(既往 stage-review checklist pass 项、file:line 锚点)直接引用勿重验;必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。QA 子代理写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
433
441
  **gate 重试修复**:review.json 落盘后若 design.md 又有改版,gate 会**自动机械重算 docHash 放行**(verdict/checklist 保留,结论是否仍适用于新文档需人工确认)——**不要重做审查**(重做=同一材料第三遍);主文档路径错/缺失不会被自动修复,按 gate 报错修正 reviewedFiles[0],或跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\`。
434
442
 
435
443
  ### 操作
@@ -652,7 +660,8 @@ function parseWavesFromPlan(planContent, registry = []) {
652
660
  } catch { /* 告警失败不阻断解析 */ }
653
661
 
654
662
  // 无显式 Wave 结构但注册表非空(light 级:任务全在 tasks.md、plan.md 只留策略)→
655
- // 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行执行)
663
+ // 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行
664
+ // 执行——现为真实行为:buildWavePrompt 对 implicit Wave 下发串行调度指令,2026-09-17-feedback-hardening D-003@v1)
656
665
  if (waves.length === 0 && registry.length > 0) {
657
666
  waves.push({ index: 1, implicit: true, tasks: registry.map(t => ({ ...t })) })
658
667
  }
@@ -1013,6 +1022,30 @@ ${indexLines}
1013
1022
  }
1014
1023
  } catch { /* 热区提取 best-effort:失败不注入 */ }
1015
1024
 
1025
+ // ── 全局硬约束段(ql-20260917-002,Superpowers v6.0 writing-plans 采纳④):plan.md
1026
+ // 「## 全局硬约束」段机械提取,作为子代理 prompt 要点第 9 条逐字下发——实现者子代理
1027
+ // 只读自己的任务卡 + 本段,不重读 design 全文;designHotzone 只达 Wave 协调者层,本段
1028
+ // 补齐子代理触达。advisory:段缺省/空体/提取失败零注入零阻断(对齐 hotzone 模式);
1029
+ // 超 2400 字截断(全文让子代理读 plan.md 该段)。
1030
+ let globalConstraintsBlock = ''
1031
+ try {
1032
+ if (changeDir) {
1033
+ const planLines = memo('planLines', () => {
1034
+ const pp = path.join(changeDir, 'plan.md')
1035
+ return existsSync(pp) ? readFileSync(pp, 'utf8').replace(/\r\n/g, '\n').split('\n') : []
1036
+ })
1037
+ const gcStart = planLines.findIndex(l => /^##\s*全局硬约束/.test(l))
1038
+ if (gcStart >= 0) {
1039
+ const gcEnd = planLines.findIndex((l, i) => i > gcStart && /^##\s+/.test(l))
1040
+ const gcBody = planLines.slice(gcStart + 1, gcEnd < 0 ? planLines.length : gcEnd).join('\n').trim()
1041
+ if (gcBody) {
1042
+ const capped = gcBody.length > 2400 ? gcBody.slice(0, 2400) + '\n…(超 2400 字截断,全文读 plan.md 该段)' : gcBody
1043
+ globalConstraintsBlock = `\n9. **全局硬约束(plan.md 逐字下发,每个子代理必须遵守——与本 task 蓝图冲突时以本段为准并上报主代理)**:\n\n${capped}\n`
1044
+ }
1045
+ }
1046
+ }
1047
+ } catch { /* 全局硬约束提取 best-effort:失败不注入 */ }
1048
+
1016
1049
  // ── 决策锚点触碰事实(W-A 主渲染点,D-003):changedFiles 口径与 run/prompt.js {DOCS_DEBT}
1017
1050
  // 现算同源——collectExecuteChangedFiles(porcelain 未提交 ∪ baseline..HEAD)唯一实现,勿双写。
1018
1051
  // 单过流程前缀第 4 步渲染 {DOCS_DEBT} 时 changedFiles 恒空,Wave 步(重入/续跑时已含前置
@@ -1231,7 +1264,25 @@ ${workdirLines}
1231
1264
  }
1232
1265
  // dispatchMode === 'local'(无配置)或 worktreePath 为空 → dispatchSection = '' → 输出与改前字节一致(零回归)
1233
1266
 
1234
- return `## Wave ${waveIndex}: 执行以下任务
1267
+ // ── 隐式 Wave 串行化(2026-09-17-feedback-hardening D-003@v1):plan.md 无显式 Wave 划分时
1268
+ // parseWavesFromPlan 合成的 implicit Wave 未做过文件正交/契约链核查,并行不安全——prompt 层
1269
+ // 下发串行调度指令(头标注 + 角色清单 + 调度要求铁律 + batch 条件 2 括注收敛,防同 prompt
1270
+ // 自相矛盾)。显式 Wave 走原文条件变量,输出与改前逐字节一致(零回归)。
1271
+ const implicit = wave.implicit === true
1272
+ const waveHeader = implicit
1273
+ ? `## Wave ${waveIndex}(隐式合成——plan.md 无显式 Wave 划分,串行执行): 执行以下任务`
1274
+ : `## Wave ${waveIndex}: 执行以下任务`
1275
+ const batchCond2 = implicit
1276
+ ? `- 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理逐个(串行)处理或落在不同批次)`
1277
+ : `- 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理并行处理或落在不同 Wave)`
1278
+ const roleItem1 = implicit
1279
+ ? `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,逐个完成(串行——启动一个、等它完成并审查后再启动下一个,见「调度要求」串行铁律)`
1280
+ : `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行`
1281
+ const scheduleItem1 = implicit
1282
+ ? `1. **隐式 Wave 串行铁律**:本 Wave 由 plan.md 无显式 Wave 划分时合成——任务一律逐个完成(单子代理串行逐个实现,或逐个启动子代理并等待其完成再启动下一个),**禁止并行启动**(未做过文件正交/契约链核查,并行不安全;需要并行收益请在 plan.md 显式划分 Wave)。`
1283
+ : `1. **同一 Wave 的多个子代理(独立或 batch)必须并行启动,batch 内部串行**(batch 分组仅按文件正交 / 无契约链判定,不改变 Wave 依赖语义——Wave 定义=无依赖可并行;有依赖应在 plan.md 的不同 Wave 中)。`
1284
+
1285
+ return `${waveHeader}
1235
1286
 
1236
1287
  ## 执行方式
1237
1288
 
@@ -1239,13 +1290,13 @@ ${workdirLines}
1239
1290
 
1240
1291
  可选 batch(合并实现):同一 Wave 内,若一组任务同时满足以下三个条件,可把它们合并为一个 batch(最多 3 个 task),交给单个子代理串行逐个完成实现:
1241
1292
  - 组内任意两个 task 的 allowed_paths 无交集(文件正交)
1242
- - 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理并行处理或落在不同 Wave)
1293
+ ${batchCond2}
1243
1294
  - 组大小不超过 3 个 task
1244
1295
 
1245
1296
  任一条件不满足,该 task 走独立子代理(默认形态);拿不准就不合并。无论独立还是 batch,实现一律由子代理完成,你不要自己写代码。
1246
1297
 
1247
1298
  你的角色是调度者 + 审查者(batch 只合并实现、不合并审查):
1248
- 1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行
1299
+ ${roleItem1}
1249
1300
  2. 子代理完成后审查结果——batch 子代理只做实现与自验,task 审查、review.json 产出与 checkbox 勾选仍归你(主 agent),在子代理返回后逐 task 进行;审查 batch 报告时逐 task 对照 allowed_paths 检查改动文件清单有无越权
1250
1301
  3. checkbox 由 CLI 自动勾选(review write 落盘即按 verdict 勾选 tasks.md;勿手动勾选)
1251
1302
  4. 记录改动文件和测试结果
@@ -1267,7 +1318,7 @@ ${taskSummary}
1267
1318
  6. **增量落盘与中断接手指引**:每完成一个可见产出(代码/测试/文档),立即写盘并执行一次最小验证(如语法检查、单跑相关测试)。工作过程中如被 429/API 配额/会话中断,应在最终回复里输出「已完成清单」(含文件路径、测试命令、当前卡点),不要只输出结论——主代理会依据磁盘产物和该清单判断哪些部分已完成,哪些需接手补做,避免重做已落盘的工作
1268
1319
  7. **任务边界铁律**:严格只实现本 task 的 \`allowed_paths\` 内文件;若 design.md/plan.md 明确指定了接口/回调/钩子接入位置,必须逐字遵守;不允许顺手实现其他 task 的内容(如 task-01 不要把 task-02 的接入也做了)。如发现必须改其他 task 文件才能继续,先回到主代理由主代理决定是否重分 Wave 或调整 plan,禁止子代理私自越界
1269
1320
  8. **batch 子代理协议**(仅当按「执行方式」节条件合并 batch 时附加进该子代理 prompt):按 batch 内 task 顺序逐个完成实现闭环——读取 tasks/task-N.md → 实现 → 跑该 task 的 verify 命令 → 记录该 task 报告(改动文件清单 / verify 结果 / 卡点)→ 才开始下一个 task;最终回复输出逐 task 报告清单。禁止写 review.json、禁止勾选 tasks.md checkbox——task 审查与勾选归主 agent,在子代理返回后逐 task 进行。越权即停:发现必须改 batch 内其他 task 或任何 batch 外 task 的 allowed_paths 文件 → 立即停止本 task 及后续,报告冲突文件与卡点,回主 agent 裁决(重分 Wave / 调整 plan / 回退独立子代理)。第 7 条任务边界铁律在 batch 语境下的「本 task」= 当前正在实现的 task
1270
-
1321
+ ${globalConstraintsBlock}
1271
1322
  {{include: testcase-design}}
1272
1323
 
1273
1324
  ${designHotzone}${decisionTouchSection}${waveKnowledgeSection}
@@ -1281,6 +1332,9 @@ ${waveStartItem1}
1281
1332
  - 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
1282
1333
  - ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
1283
1334
 
1335
+ ### 歧义裁决(撞到 plan/design 歧义或冲突时)
1336
+ 非破坏性歧义(可逆、局部、不越界、不推翻 D-xxx)→ 主代理自行裁决继续:裁决写主仓 \`{SPEC_ROOT}/changes/<变更名>/decisions.md\` 新条目(\`## D-xxx@v1: <短标题>\` 接续编号,正文=歧义/选择/理由/影响 task),本 Wave 完成输出披露「执行期裁决 N 条」;review/verify 据此对账。破坏性/不可逆(删数据/重写他人模块、改共享配置或门禁、跳过测试、扩 allowed_paths、推翻既有决策、跨仓联动)→ \`sillyspec run execute --wait\` 停人。拿不准 → 按破坏性(fail-closed);重复歧义引用既有 D-xxx。子代理报告的冲突(越界类)回主代理裁决,**子代理不自行记 decisions**。
1337
+
1284
1338
  ### 中断续跑(如曾中断恢复)
1285
1339
  execute 按 Wave 持久化进度,task 级进度靠 tasks.md checkbox 勾选。若本 Wave 曾因 429/API 配额/崩溃中断:
1286
1340
  - tasks.md 中**已勾选 \`- [x]\` 的 task 已完成,跳过不重跑**(子代理也可能在完成前中断,重跑前先确认该 task 产出文件是否完整)
@@ -1292,7 +1346,7 @@ ${contractInjection}${prototypeInjection}
1292
1346
  ${taskList}
1293
1347
 
1294
1348
  ### 调度要求
1295
- 1. **同一 Wave 的多个子代理(独立或 batch)必须并行启动,batch 内部串行**(batch 分组仅按文件正交 / 无契约链判定,不改变 Wave 依赖语义——Wave 定义=无依赖可并行;有依赖应在 plan.md 的不同 Wave 中)。
1349
+ ${scheduleItem1}
1296
1350
  2. **Reverse Sync**:子代理报告实现与 design.md 不一致时,先检查是代码错了还是文档有遗漏
1297
1351
  3. **不要频繁编译!** 编译很慢,只在以下情况运行:
1298
1352
  - 写了大量代码后需要验证语法正确性
@@ -216,6 +216,32 @@ export function parseHeadCommit(content) {
216
216
  return parseFrontmatterScalar(content, 'head_commit')
217
217
  }
218
218
 
219
+ /**
220
+ * frontmatter 顶层键重复检测(纯函数,无 IO,2026-09-16-friction5-hardening FR-02 / D-004@v1)。
221
+ *
222
+ * 坑锚定(用户 2026-09-16 驾驭小结②):taskcard 骨架双来源反填 depends_on(tasks.md 行内
223
+ * 注解 + plan.md Wave 兜底)后 agent 又重复手填同键——js-yaml 4 对重复映射键 throw,本文件
224
+ * 四处 jsYaml.load 的 catch 全部静默降级(blueprint 契约空 / repo=null / 命令不校验 /
225
+ * Wave proposal best-effort 跳过 = 吞字段),parseDependsOn 正则又只取首个命中——重复键在
226
+ * 下游全链路静默失真,只能在 feasibility 入口单点拦截。行首 ^key: 顶格锚定:块列表项
227
+ * (- x)与缩进子键天然不匹配;goal: > 折叠块的缩进续行不匹配(顶格续行属罕见畸形,
228
+ * 宁报错不漏报——fail-closed)。
229
+ * @param {string} fmText frontmatter 文本(--- 之间的内容;CRLF 入口归一,同 parseDependsOn 惯例)
230
+ * @returns {Array<{key: string, lines: number[]}>} 出现 ≥2 次的顶层键及其行号(1-based,相对 fmText)
231
+ */
232
+ export function detectDuplicateTopKeys(fmText) {
233
+ const keyLines = new Map()
234
+ String(fmText || '').replace(/\r\n/g, '\n').split('\n').forEach((line, idx) => {
235
+ const m = line.match(/^([A-Za-z_][\w-]*):/)
236
+ if (!m) return
237
+ if (!keyLines.has(m[1])) keyLines.set(m[1], [])
238
+ keyLines.get(m[1]).push(idx + 1)
239
+ })
240
+ return [...keyLines.entries()]
241
+ .filter(([, lines]) => lines.length >= 2)
242
+ .map(([key, lines]) => ({ key, lines }))
243
+ }
244
+
219
245
  /**
220
246
  * 从 local.yaml 文本解析 repos: 段(跨仓 workspace 注册表)。
221
247
  * 与 parseLocalYamlModules 同风格(轻量行扫描,不引 yaml 依赖),结构 Map<repoKey, absolutePath>。
@@ -397,7 +423,8 @@ export function topoSortWaves(depMap) {
397
423
  * - `## Wave N` 标题下的 task-XX checkbox 归入 Wave N;
398
424
  * - 非 Wave 标题行(## 自检 等)退出当前 Wave 段,避免其 checkbox 混入。
399
425
  * 返回 null = plan.md 不存在或无显式 Wave 标题 → execute 会把所有 task 放进单个隐式 Wave
400
- * 全并行(execute.js:402-408),调用方据此判「同文件即冲突」。
426
+ * 串行执行(共享文件安全,2026-09-17-feedback-hardening D-003@v1),调用方据此降为 warning
427
+ * (显式划分 Wave 才有按组并行收益;同 Wave 内共享文件仍 error)。
401
428
  *
402
429
  * @param {string} planPath - plan.md 绝对路径
403
430
  * @returns {Map<string, number>|null} taskId → waveIndex;null = 无显式 Wave
@@ -562,15 +589,16 @@ export function validateBlueprintConsistency(changeDir, opts = {}) {
562
589
  // 子代理会互相覆盖该文件 → error。跨 Wave 同文件 → 串行执行安全 → warning。
563
590
  // 按 (repo, path) 二元组判冲突(约束③):跨仓 task 与主仓 task 同名路径不误判(不同 repo → 不同键)。
564
591
  // Wave 口径 = plan.md 显式 `## Wave N`(parseTaskWavesFromPlan,与 execute 同源,非 topoSort 建议值);
565
- // plan.md 无显式 Wave → execute 全并行(隐式单 Wave,execute.js:402-408)→ 同 repo 同文件即冲突。
592
+ // plan.md 无显式 Wave → execute 按单个隐式 Wave 串行执行(2026-09-17-feedback-hardening
593
+ // D-003@v1,共享文件安全)→ warning(显式划分 Wave 可获并行收益;同 Wave 内共享仍 error)。
566
594
  const waveOfTask = parseTaskWavesFromPlan(pJoin(changeDir, 'plan.md'))
567
595
  for (const [, { repo, path: p, owners }] of pathOwners) {
568
596
  if (owners.length < 2) continue
569
597
  if (waveOfTask === null) {
570
- errors.push(
598
+ warnings.push(
571
599
  `路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')} — plan.md 无显式 Wave 划分,` +
572
- `execute 会把它们放进同一隐式 Wave 全并行(execute.js:402-408),子代理互相覆盖。` +
573
- `解法:拆到不同 Wave(串行),或合并为单个 task。`
600
+ `execute 将按单个隐式 Wave 串行执行(共享文件安全)。` +
601
+ `显式划分 Wave 可获并行收益(同 Wave 内仍禁止共享文件);或合并为单个 task`
574
602
  )
575
603
  continue
576
604
  }
@@ -1263,6 +1291,15 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
1263
1291
  const fm = fmMatch[1]
1264
1292
  const body = content.slice(fmMatch[0].length)
1265
1293
 
1294
+ // 0. 顶层键重复检测(FR-02 / D-004@v1,坑见 detectDuplicateTopKeys jsdoc:骨架反填
1295
+ // depends_on 后 agent 重复手填 → js-yaml throw 被四处 catch 静默吞字段,只能在入口拦截)。
1296
+ // 先于必要字段检查——重复键会令下游 jsYaml.load 全链路失真,属最高优先级形态错误。
1297
+ const dupTaskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
1298
+ const dupKeys = detectDuplicateTopKeys(fm)
1299
+ for (const dup of dupKeys) {
1300
+ errors.push(`${dupTaskId || file}: frontmatter 顶层键 ${dup.key} 重复出现 ${dup.lines.length} 次(L${dup.lines.join('、L')})——骨架已自动反填的键(如 depends_on)勿重复手填,保留正确一处删除其余`)
1301
+ }
1302
+
1266
1303
  // 1. 必要字段检查
1267
1304
  const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
1268
1305
  const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
@@ -245,6 +245,10 @@ plan_level: full
245
245
  ## 关键路径
246
246
  task-01 → task-03(最长路径,决定最短交付周期)
247
247
 
248
+ ## 全局硬约束(从 design.md 逐字抄录,绑定所有 task)
249
+ - (逐字抄录 design.md 中绑定**每个** task 的硬约束:版本底线 / 依赖限制 / 命名与文案 / 精确值 / 兼容策略 / 非目标边界——执行期子代理只读自己的任务卡 + 本段、不重读 design 全文,抄录失真 = 执行期偏差源头;无跨 task 硬约束时可省略本段)
250
+ - 示例:Node ≥ 20;禁止新增运行时依赖;错误文案一律中文
251
+
248
252
  ## 全局验收标准
249
253
  1. 所有单元测试通过
250
254
  2. 集成敏感 task(路由/layout/跨进程装配)建议加集成冒烟验收——组件单测全绿 ≠ 集成正确
@@ -263,7 +267,7 @@ full 计划的约束:
263
267
  - **禁止**泛泛风险分析("需要充分测试"类废话转为具体验收条目)
264
268
  - Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
265
269
  - **Wave 段 ID 引用行必须保留**(\`- task-XX\` 纯 ID、无 checkbox 无任务名——机器解析依赖;任务名唯一真相在 tasks.md,plan.md 重抄即双写漂移)
266
- - plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局验收标准,**不放实现细节**
270
+ - plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局硬约束(design 有跨 task 硬约束时逐字抄录)+ 全局验收标准,**不放实现细节**
267
271
  - 如果存在 decisions.md,plan.md 建议包含当前版本 D-xxx@vN/FR-xxx 覆盖矩阵(CLI 只校验 D-xxx@vN/FR-xxx ID 字面出现在 plan.md,warning 不阻断;矩阵的 D→FR→task 映射完整性供人类追溯,CLI 不校验——勿以为画了矩阵就被结构校验)
268
272
  - 如果存在 P0/P1 unresolved blocker,不生成 plan.md,输出阻塞清单
269
273
  - 实现细节写到后续的 tasks/task-NN.md 中
@@ -338,12 +342,15 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
338
342
  ${REVIEW_CHECKLISTS.plan.map((item) => '- [ ] ' + item).join('\n')}
339
343
 
340
344
  ### tier=independent 时:启动 plan-review 子代理
345
+ {PRIOR_REVIEW_FACTS}
341
346
  用 Agent tool 启动子代理(subagent_type: general),prompt 要点:
342
347
  1. 独立读取 {SPEC_ROOT}/changes/<change>/plan.md + design.md + tasks/*.md(不要让生成者喂结论给你,自己读原始文件)
343
348
  2. 执行上方审查清单,每条给 pass/gap/fail + 证据
344
349
  3. 输出 review.json(CLI Stage Review Gate 将硬校验,契约如下 —— schema + 完整示例 + docHash 算法,照抄改值):
345
350
  {REVIEW_JSON_CONTRACT}
346
351
  4. verdict=fail 时在 reviewerNotes 写明阻断项
352
+ 5. 执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:plan 审查重核三仓源码 94 分钟不收敛——brainstorm 阶段已两轮实证的结论被全量重验):brainstorm 阶段 design review 已实证的结论(checklist pass 项、file:line 锚点)**直接引用勿重验**,本阶段只审 plan 特有面(任务拆分/依赖/契约/规模映射);必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。
353
+ 6. 写通道降级:写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)作为最终文本回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
347
354
 
348
355
  ### module-impact.md 首版(scale≠small 时:CLI 自动生成,审查步勿手写)
349
356
  module-impact.md 首版**由 CLI 在本阶段 --done 时自动生成**——文件×模块归属按 _module-map.yaml 前缀匹配机械预填,章节含「## 模块影响矩阵」「## 未匹配文件」「## 更新结果」表骨架(每受影响模块一行 pending),影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。已存在不覆盖。手写整份首版是历史返工根源(章节标题变体会被 archive contains_sections 硬拦),**本步不要手写**。
@@ -114,7 +114,7 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
114
114
 
115
115
  格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
116
116
 
117
- > ⚠️ --file-notes 只随 step3 --done 同一命令传才生效。CLI 是短进程,run 与 done 是独立进程——step1/step2 的 sillyspec run quick(仅拿提示词)即使带了 --file-notes,也只写进那个短进程的内存、进程结束即丢,不会带到 step3 --done。请在 step3 收尾时连同 --output 一起传。
117
+ > ⚠️ --file-notes 只随 step3 --done 同一命令传才生效。CLI 是短进程,run 与 done 是独立进程——step1/step2 的 sillyspec run quick(仅拿提示词)带 --file-notes 会被直接拒绝执行(去掉该参数重跑本条命令即可,进度不丢)。请在 step3 收尾时连同四字段一起传。
118
118
 
119
119
  ### 收尾推荐顺序(模块文档在 --done 前,QUICKLOG 在 --done 后,别记混)
120
120
  1. 【--done 前】命中模块 → 改模块文档 → \`git add -- <模块文档>\`(见下方「模块文档同步」)
@@ -153,13 +153,13 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
153
153
  CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需求:」自动提取、正文四字段自动分行、文件行用 --file-notes 时为多行括注。--done 后只需核对,多数情况无需手改:
154
154
 
155
155
  1. 标题:CLI 已从「需求:」提取(截到首个标点、超 80 字截断);按 step3 模板写成一句语义化短标题则无需手改,写成长句被截断时才手改。
156
- 2. 文件:用了 --file-notes 已是多行括注;没传则回填审计到的实际文件单行,可事后补括注(示例见下)。
156
+ 2. 文件:用了 --file-notes 已是多行括注;没传则回填审计到的实际文件单行,可事后补括注(格式见下)。
157
157
  3. 正文:四字段已分行落盘,按需扩写充实(不允许只留一段「结果:」)。
158
158
 
159
- 补括注示例(仅当没用 --file-notes、想事后补时):
159
+ 补括注格式(**虚构占位示例,非任何会话的真实条目**——多会话并行时勿把下面两行误认成本 quick 的改动文件,核对对象永远是 QUICKLOG 中本会话 <quicklog-id> 条目的「文件:」行;仅当没用 --file-notes、想事后补时参照此格式):
160
160
  文件:
161
- - backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
162
- - backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
161
+ - <path/to/改动文件>(一句话括注:该文件改了什么,可含串入的调用链)
162
+ - <path/to/新建文件>(新建:职责一句话)
163
163
 
164
164
  一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/quicklog/(git 跟踪,--done 后需提交),核对不影响 --done 已通过的边界审计。`,
165
165
  outputHint: '暂存和记录确认',