sillyspec 3.26.7 → 3.26.9

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 (62) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -2
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
  3. package/.claude/skills/sillyspec-doctor/SKILL.md +2 -0
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
  8. package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
  9. package/.claude/skills/sillyspec-status/SKILL.md +0 -1
  10. package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
  11. package/.husky/pre-push +9 -2
  12. package/README.md +5 -9
  13. package/bin/sillyspec.js +1 -1
  14. package/package.json +2 -2
  15. package/packages/dashboard/server/index.js +14 -8
  16. package/src/change-risk-profile.js +5 -207
  17. package/src/config-schema.js +310 -287
  18. package/src/constants.js +97 -106
  19. package/src/db-engine.js +1 -1
  20. package/src/docs-check.js +362 -0
  21. package/src/docs-debt.js +226 -0
  22. package/src/docs-gate.js +106 -0
  23. package/src/doctor-diagnostics.js +23 -2
  24. package/src/git-helper.js +2 -2
  25. package/src/hooks/worktree-guard.js +1 -1
  26. package/src/index.js +1963 -1787
  27. package/src/init.js +95 -17
  28. package/src/machine-interface.js +7 -4
  29. package/src/modules.js +490 -486
  30. package/src/progress/stage-machine.js +3 -0
  31. package/src/progress.js +37 -13
  32. package/src/quicklog.js +182 -1
  33. package/src/run/command.js +154 -26
  34. package/src/run/complete-handlers.js +1254 -1128
  35. package/src/run/complete.js +9 -33
  36. package/src/run/gates.js +85 -1
  37. package/src/run/prompt.js +82 -1
  38. package/src/run/quick-audit.js +23 -0
  39. package/src/run/shared.js +273 -5
  40. package/src/run/stage.js +17 -1
  41. package/src/scan-diff.js +388 -0
  42. package/src/scan-staleness.js +95 -0
  43. package/src/stage-contract-spec.js +16 -1
  44. package/src/stage-contract.js +8 -5
  45. package/src/stage-review.js +1 -1
  46. package/src/stages/brainstorm-auto.js +0 -1
  47. package/src/stages/brainstorm.js +3 -3
  48. package/src/stages/execute.js +35 -11
  49. package/src/stages/plan-postcheck.js +93 -36
  50. package/src/stages/plan.js +22 -7
  51. package/src/stages/quick.js +1 -1
  52. package/src/stages/scan.js +2 -2
  53. package/src/stages/verify.js +11 -12
  54. package/src/sync.js +39 -17
  55. package/src/task-review.js +14 -5
  56. package/src/workflow.js +8 -185
  57. package/src/worktree-apply.js +128 -3
  58. package/src/worktree.js +2 -2
  59. package/templates/claude-instruction.md +1 -1
  60. package/templates/prompts/taskcard-rules.md +1 -0
  61. package/templates/prompts/verify-probes.md +2 -2
  62. package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
@@ -23,6 +23,18 @@ export function validatePlanForExecute(planContent) {
23
23
 
24
24
  const waves = parseWavesFromPlan(planContent)
25
25
 
26
+ // 检查 0: Wave 编号带字母后缀(如 "## Wave 2b")→ 显式报错(坑 plan-wave-letter-suffix)
27
+ // 解析正则 ^#+\s*Wave\s+(\d+) 不锚定结尾,"Wave 2b" 会被 parseInt 截断成 Wave 2 静默收容,
28
+ // 与显式 Wave 2 合并为同一 Wave 强制并行——串行意图静默失效且无任何提示,排查到源码才明白。
29
+ const letterSuffixWaves = [...planContent.matchAll(/^#+\s*Wave\s+(\d+)([a-z])(?![a-z0-9])/gim)]
30
+ for (const m of letterSuffixWaves) {
31
+ errors.push(
32
+ `Wave 编号带字母后缀:${m[0].trim()}("${m[1]}${m[2]}")不被支持。` +
33
+ `解析器只认纯数字编号("## Wave N"),字母后缀会被截断为 "${m[1]}" 与显式 Wave ${m[1]} 静默合并强制并行。` +
34
+ `解法:改顺序纯数字编号(Wave 3/4…)或并入相邻 Wave`
35
+ )
36
+ }
37
+
26
38
  // 收集所有 task
27
39
  const allTasks = []
28
40
  for (const wave of waves) {
@@ -156,12 +168,12 @@ const fixedPrefix = [
156
168
  \`\`\`
157
169
  - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
158
170
  - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
159
- - 报告:列出每个受影响符号、调用点位置、是否在任务范围内
171
+ - **报告落盘(CLI 硬校验)**:把检查结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
160
172
  - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
161
173
 
162
174
  ### 输出
163
- 已加载的上下文摘要(含模块文档 + 源码锚点)`,
164
- outputHint: '上下文摘要',
175
+ 已加载的上下文摘要(含模块文档 + 源码锚点)+ 符号影响面结论摘要`,
176
+ outputHint: '上下文摘要 + 符号影响面结论',
165
177
  optional: false
166
178
  },
167
179
  {
@@ -172,7 +184,7 @@ const fixedPrefix = [
172
184
  1. 运行 \`sillyspec worktree meta <change-name>\` 读取 meta.json
173
185
  2. 从输出中提取 worktreePath、branch、mode 字段
174
186
  3. 确认 worktree 目录存在(如果是 worktree/native-worktree 模式)
175
- 4. **确认工具链可用**:worktree 内项目工具链(lint/format/test 二进制,如 ruff / prettier / uv)可能不全——对本次会用到的工具先跑一次 \`--version\` 确认;缺失则按项目方式安装(Python 项目 \`uv tool install ruff\` / \`uv sync\`,Node 项目 node_modules 已由 CLI 链接主仓)。不要等到 commit 才发现二进制不在 PATH 被 hook 拦
187
+ 4. **确认工具链可用**:worktree 内项目工具链(lint/format/test 二进制,如 ruff / prettier / uv)可能不全——对本次会用到的工具先跑一次 \`--version\` 确认;缺失则按项目方式安装(Python 项目 \`uv tool install ruff\` / \`uv sync\`,Node 项目 node_modules 已由 CLI 链接主仓)。不要等到 commit 才发现二进制不在 PATH 被 hook 拦。**Python 项目注意**:worktree 自建 .venv 只含 pyproject 声明依赖,pytest 等 dev 工具可能缺失——优先在 worktree 内补装(\`uv sync --group dev\` / \`uv pip install pytest\`),**不要回退用主仓 venv 跑测试**(主仓 venv 加载的可能是主仓代码而非 worktree 代码,环境不一致会掩真 bug)
176
188
 
177
189
  ### 铁律
178
190
  - **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
@@ -203,6 +215,12 @@ worktree 路径 + 分支名 + 模式
203
215
  如上所示的知识条目与本次任务相关。请阅读这些条目以获取项目约定和已知模式。
204
216
  如无命中条目(Status: no matches),跳过本节。
205
217
 
218
+ ### 模块文档欠账(CLI 算事实)
219
+ {DOCS_DEBT}
220
+
221
+ 如上为 CLI 用 git 事实算出的本变更触及模块文档欠账(无输出=无欠账或归属数据缺失)。
222
+ 欠账处理:Wave 收尾时顺手同步对应模块卡(变更索引追加条目),不必为此停下。
223
+
206
224
  ### 铁律
207
225
  - **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
208
226
  outputHint: 'Wave 分组 + 模型分配',
@@ -224,6 +242,11 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
224
242
  {REVIEW_JSON_CONTRACT}
225
243
  该 acceptance review 同时覆盖"代码审查"视角(风格/bug/安全/冗余),后续代码审查步骤仅需轻量复审。
226
244
 
245
+ **审查范围分级(省重复消耗,task review 已覆盖的不全量重审)**:每个 task 在 Task Review Gate 已产出 review.json({SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json,specVerdict/qualityVerdict 双 pass)。QA 子代理按它分层:双 pass 的 task 只**抽查**(读 1-2 个核心 diff 文件抽验 reviewerNotes 与实际改动相符,不必逐文件重审);未双 pass(fail/cannot_verify/缺失)的 task 必须全量重审。无论抽查还是全量,以下三项始终必查——task review 铁律是"只看当前 task 的 diff",这三项是它覆盖不到、只有 stage review 能兜住的:
246
+ 1. 跨 task 交界(A 产出的接口/数据结构与 B 的消费是否对得上)
247
+ 2. design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)
248
+ 3. 组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)
249
+
227
250
  ### 操作
228
251
  1. 读取 design.md(技术方案)
229
252
  2. 逐一对照 design.md 中的设计要点与实际代码实现
@@ -305,8 +328,9 @@ const fixedSuffix = [
305
328
 
306
329
  先检查当前 worktree 的隔离模式:
307
330
  \`\`\`bash
308
- node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManager(); const m = wm.getMeta('<change-name>'); console.log(m ? JSON.stringify({mode: m.mode, path: m.worktreePath}) : 'no meta'); })"
331
+ sillyspec worktree meta <change-name>
309
332
  \`\`\`
333
+ (CLI 子命令,输出 meta.json 的 mode / worktreePath 字段;无 meta 输出 no meta)
310
334
 
311
335
  ### 操作(mode = worktree,SillySpec 创建的隔离 worktree)
312
336
 
@@ -721,7 +745,7 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
721
745
  .map(i => `- task-${i.taskNum} → repo \`${i.repo}\`,workdir=\`${i.workdir}\``)
722
746
  .join('\n')
723
747
  crossRepoCommitSection = `
724
- ### 跨仓 task 派发与双锡点(必须严格遵守)
748
+ ### 跨仓 task 派发与双锡点
725
749
 
726
750
  本 Wave 含跨仓 task(直接改跨仓仓主干,**不经主仓 worktree**):
727
751
 
@@ -746,7 +770,7 @@ ${crossLines}
746
770
  ` - task-${i.taskNum} (repo: ${i.repo}) → workdir: "${i.workdir}"`
747
771
  ).join('\n')
748
772
  worktreeSection = `
749
- ### 工作目录(必须严格遵守,per-task)
773
+ ### 工作目录(per-task)
750
774
 
751
775
  调用 Task 工具启动子代理时,**workdir 参数是强制必传的**,且本 Wave 内不同 task 的 workdir 不同(主仓 task vs 跨仓 task)。
752
776
 
@@ -770,7 +794,7 @@ ${workdirLines}
770
794
  } else {
771
795
  worktreeSection = (worktreePath)
772
796
  ? `
773
- ### 工作目录(必须严格遵守)
797
+ ### 工作目录
774
798
 
775
799
  调用 Task 工具启动子代理时,**workdir 参数是强制必传的**。
776
800
  不传 workdir 会导致子代理把文件写到主工作区而非 worktree,破坏隔离。
@@ -817,7 +841,7 @@ ${workdirLines}
817
841
 
818
842
  return `## Wave ${waveIndex}: 执行以下任务
819
843
 
820
- ## 执行方式(必须严格遵守)
844
+ ## 执行方式
821
845
 
822
846
  **每个任务必须由独立子代理执行,你不要自己写代码。**
823
847
 
@@ -863,7 +887,7 @@ ${contractInjection}${prototypeInjection}
863
887
  ${taskList}
864
888
 
865
889
  ### 调度要求
866
- 1. **同一 Wave 内的任务必须并行启动子代理,禁止串行等待。** Wave 的定义就是"无依赖、可并行",不要自行分析依赖关系。如果有依赖应该在 plan.md 的不同 Wave 中。
890
+ 1. **同一 Wave 内的任务必须并行启动子代理**(Wave 定义=无依赖可并行,不自行分析依赖关系;有依赖应在 plan.md 的不同 Wave 中)。
867
891
  2. **Reverse Sync**:子代理报告实现与 design.md 不一致时,先检查是代码错了还是文档有遗漏
868
892
  3. **不要频繁编译!** 编译很慢,只在以下情况运行:
869
893
  - 写了大量代码后需要验证语法正确性
@@ -875,7 +899,7 @@ ${taskList}
875
899
  - 记录改动文件和测试结果
876
900
  5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
877
901
 
878
- ### Task Review Gate(必须执行,不可跳过)
902
+ ### Task Review Gate
879
903
 
880
904
  每个子代理完成后、勾选 checkbox **之前**,你必须创建 task review。
881
905
 
@@ -31,6 +31,7 @@ import { validateScriptCommands } from './cmd-existence.js'
31
31
  * @returns {string[]}
32
32
  */
33
33
  function parseDependsOn(content) {
34
+ content = content.replace(/\r\n/g, '\n') // 外部调用方(worktree-apply 等)原生 readFileSync 喂 CRLF 时 ^---\n 锚点失配
34
35
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
35
36
  if (!fmMatch) return []
36
37
  const fm = fmMatch[1]
@@ -38,9 +39,11 @@ function parseDependsOn(content) {
38
39
  if (inlineMatch) {
39
40
  return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
40
41
  }
41
- const blockMatch = fm.match(/depends_on:\s*\n((?:\s+-\s+.+\n?)+)/)
42
+ // 块列表:列表项允许顶格或缩进([ \t]*,坑6——原 \s* 贪婪吃掉换行与前导空白,
43
+ // 顶格 `depends_on:\n- task-01` 标准 YAML 写法永远失配,静默判「无依赖」)
44
+ const blockMatch = fm.match(/depends_on:[ \t]*\n((?:[ \t]*-[ \t]+.+\n?)+)/)
42
45
  if (blockMatch) {
43
- return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
46
+ return blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)?.map(s => s.replace(/^[ \t]*-[ \t]+/, '').trim().replace(/['"]/g, '')) || []
44
47
  }
45
48
  return []
46
49
  }
@@ -67,16 +70,25 @@ function parseTaskId(content, filename) {
67
70
  * @returns {string[]}
68
71
  */
69
72
  export function parseAllowedPaths(content) {
73
+ content = content.replace(/\r\n/g, '\n') // 同 parseDependsOn:入口归一,保护喂原始 CRLF 内容的外部调用方
70
74
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
71
75
  if (!fmMatch) return []
72
76
  const fm = fmMatch[1]
73
77
  const inlineMatch = fm.match(/allowed_paths:\s*\[([^\]]*)\]/)
74
78
  if (inlineMatch) {
75
- return inlineMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean)
79
+ return inlineMatch[1].split(',').map(s => s.trim().replace(/['"`]/g, '')).filter(Boolean)
76
80
  }
77
- const blockMatch = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)
81
+ // 块列表:列表项允许顶格或缩进(坑6①——原 `\s*\n` 的 \s 贪婪匹配换行+前导空白,
82
+ // 标准 YAML 块列表 `allowed_paths:\n- src/a.js`(顶格)永远失配,静默判「缺 allowed_paths」;
83
+ // 缩进式 \s+- 恰可命中是巧合幸存。改 [ \t]*(不吃换行)+ 行内 [ \t]*-[ \t]+ 顶格缩进通吃)
84
+ const blockMatch = fm.match(/allowed_paths:[ \t]*\n((?:[ \t]*-[ \t]+.+\n?)+)/)
78
85
  if (blockMatch) {
79
- return blockMatch[1].match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim().replace(/['"]/g, '')) || []
86
+ // 剥首尾成对反引号(坑7 家族:`src/a.js` 整项以反引号开头时 js-yaml/正则路径带 ` 匹配失败报未覆盖;
87
+ // 与 change-list.js 的 cell 归一化同语义——剥两端包裹引号,不动路径内部字符)
88
+ return blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)?.map(s => {
89
+ const raw = s.replace(/^[ \t]*-[ \t]+/, '').trim()
90
+ return raw.replace(/^`([^`]*)`$/, '$1').replace(/^['"]|['"]$/g, '').trim()
91
+ }).filter(Boolean) || []
80
92
  }
81
93
  return []
82
94
  }
@@ -96,6 +108,7 @@ export function parseAllowedPaths(content) {
96
108
  * @returns {string|null}
97
109
  */
98
110
  function parseFrontmatterScalar(content, field) {
111
+ content = content.replace(/\r\n/g, '\n') // 同 parseAllowedPaths:入口归一 CRLF
99
112
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
100
113
  if (!fmMatch) return null
101
114
  let fm
@@ -439,6 +452,39 @@ export function validateBlueprintConsistency(changeDir) {
439
452
  }
440
453
  }
441
454
 
455
+ // plan.md 声明任务 ↔ tasks/ 卡片双向对账(债单 D-2b):
456
+ // plan 列 17 任务只出 12 卡(尾部文档同步类任务漏卡)时,无卡任务不进 Wave、
457
+ // 不受 execute 审计 → 零失败信号漏做。口径与 plan.js parseTaskCount/parseTaskNames 一致
458
+ // (`- [ ] task-NN` checkbox;file 命名 task-NN.md + 卡内 id task-NN 均取,缺一即漏卡)。
459
+ const planMdPath = pJoin(changeDir, 'plan.md')
460
+ if (existsSync(planMdPath)) {
461
+ const declaredIds = new Set()
462
+ for (const line of readFileSync(planMdPath, 'utf8').split('\n')) {
463
+ const m = line.match(/^[-*]\s*\[[ x]\]\s*(task-\d+)\b/i)
464
+ if (m) declaredIds.add(m[1].toLowerCase())
465
+ }
466
+ if (declaredIds.size > 0) {
467
+ const tprRule = getRule('plan.task-plan-reconciliation')
468
+ const cardIds = new Set([...taskInfo.keys()].map(id => id.toLowerCase()))
469
+ const missing = [...declaredIds].filter(id => !cardIds.has(id))
470
+ if (missing.length > 0) {
471
+ errors.push(
472
+ tprRule.data.messageMissingCards
473
+ .replaceAll('${declared}', declaredIds.size)
474
+ .replaceAll('${missing}', missing.join(', '))
475
+ )
476
+ }
477
+ const orphans = [...cardIds].filter(id => !declaredIds.has(id))
478
+ if (orphans.length > 0) {
479
+ errors.push(
480
+ tprRule.data.messageOrphanCards.replaceAll('${orphans}', orphans.join(', '))
481
+ )
482
+ }
483
+ }
484
+ // declaredIds.size === 0:plan.md 无 task checkbox 声明(极端格式)→ 跳过对账,
485
+ // 由 validatePlanArtifacts/validatePlanForExecute 把关,不在此误伤。
486
+ }
487
+
442
488
  // 拓扑排序 + 循环依赖
443
489
  const depMap = new Map()
444
490
  for (const [taskId, info] of taskInfo) {
@@ -691,8 +737,10 @@ function parseDesignCoverageByRepo(designPath) {
691
737
  }
692
738
 
693
739
  if (!hasSegmentHeader) {
694
- // 无段头 → 整章节归 main(等价原 parseFileChangeList 行为)
695
- const main = new Set(parseFileChangeList(designPath))
740
+ // 无段头 → 整章节归 main(等价原 parseFileChangeList 行为)。
741
+ // keepSillyspecDocs=true:.sillyspec/docs/ 模块文档 = 交付物,与 apply 阶段
742
+ // resolveApplyAllowSet 口径一致(债单 D-2)——design 声明的模块文档更新必须被 task 认领。
743
+ const main = new Set(parseFileChangeList(designPath, { keepSillyspecDocs: true }))
696
744
  if (main.size > 0) byRepo.set('main', main)
697
745
  for (const p of main) allFiles.push(p)
698
746
  return { byRepo, hasSegmentHeader: false, allFiles }
@@ -731,7 +779,7 @@ function parseDesignCoverageByRepo(designPath) {
731
779
  const virtualDesign = ['## 文件变更清单', '', ...segLines, ''].join('\n')
732
780
  const tmpPath = pJoin(tmpDir, `${repo}.md`)
733
781
  writeFileSync(tmpPath, virtualDesign)
734
- const segFiles = [...parseFileChangeList(tmpPath)]
782
+ const segFiles = [...parseFileChangeList(tmpPath, { keepSillyspecDocs: true })]
735
783
  if (segFiles.length === 0) continue
736
784
  const set = byRepo.get(repo) || new Set()
737
785
  for (const p of segFiles) { set.add(p); allFiles.push(p) }
@@ -1062,41 +1110,41 @@ export async function executePlanPostcheck(context) {
1062
1110
 
1063
1111
  console.log(` 📂 变更目录: ${changeDir}`)
1064
1112
 
1113
+ // ── 报错聚合(坑6③):六个检查全部跑完后统一输出全部失败项,一轮 --done 暴露全部问题,
1114
+ // 不再「失败一个抛一个、修一个冒一个」的迭代盲盒。各检查相互独立(都只读 changeDir 产物),
1115
+ // 前置检查失败不使后续检查失真——唯一例外是 tasks/ 目录缺失时后续检查都无意义,仍提前短路。
1116
+ const failures = [] // { name, errors, hint }
1117
+ const printSectionWarnings = (label, warnings) => {
1118
+ if (warnings.length > 0) {
1119
+ console.warn(`\n⚠️ ${label}警告(不阻断):`)
1120
+ for (const w of warnings) console.warn(` - ${w}`)
1121
+ }
1122
+ }
1123
+
1065
1124
  // ── 1. 一致性校验 ──
1066
1125
  const consistency = validateBlueprintConsistency(changeDir)
1067
1126
  if (consistency.errors.length > 0) {
1068
- console.error('\n❌ 蓝图一致性校验失败:')
1069
- for (const err of consistency.errors) console.error(` - ${err}`)
1070
- console.error('\n 请修复上述问题后重新完成此步骤。')
1071
- throw new Error('planPostcheck: blueprint consistency check failed')
1072
- }
1073
- if (consistency.warnings.length > 0) {
1074
- console.warn('\n⚠️ 蓝图一致性警告(不阻断):')
1075
- for (const w of consistency.warnings) console.warn(` - ${w}`)
1127
+ failures.push({ name: '蓝图一致性校验', errors: consistency.errors, hint: '请修复上述问题后重新完成此步骤。' })
1076
1128
  }
1129
+ printSectionWarnings('蓝图一致性', consistency.warnings)
1077
1130
 
1078
1131
  // ── 1b. 可行性校验 ──
1079
1132
  const feasibility = validatePlanFeasibility(changeDir, context.cwd)
1080
1133
  if (feasibility.errors.length > 0) {
1081
- console.error('\n❌ Plan 可行性校验失败:')
1082
- for (const err of feasibility.errors) console.error(` - ${err}`)
1083
- throw new Error('planPostcheck: feasibility check failed')
1084
- }
1085
- if (feasibility.warnings.length > 0) {
1086
- console.warn('\n⚠️ Plan 可行性警告(不阻断):')
1087
- for (const w of feasibility.warnings) console.warn(` - ${w}`)
1134
+ failures.push({ name: 'Plan 可行性校验', errors: feasibility.errors, hint: null })
1088
1135
  }
1136
+ printSectionWarnings('Plan 可行性', feasibility.warnings)
1089
1137
 
1090
1138
  // ── 1c. 跨任务契约校验 ──
1091
1139
  // 对账 consumer.expects_from ↔ provider.provides,拦截「consumer 期望字段
1092
1140
  // 但 provider 未承诺」的契约断裂(避免到 execute/verify 才暴露成 403/500)
1093
1141
  const crossTask = validateCrossTaskContracts(changeDir)
1094
1142
  if (crossTask.errors.length > 0) {
1095
- console.error('\n❌ 跨任务契约校验失败(consumer 期望的字段未被 provider 承诺):')
1096
- for (const err of crossTask.errors) console.error(` - ${err}`)
1097
- console.error('\n 修复方式:要么在 provider task 的 provides.fields 补上缺失字段,')
1098
- console.error(' 要么修正 consumer task 的 expects_from.needs(确认依赖是否真实)。')
1099
- throw new Error('planPostcheck: cross-task contract check failed')
1143
+ failures.push({
1144
+ name: '跨任务契约校验(consumer 期望的字段未被 provider 承诺)',
1145
+ errors: crossTask.errors,
1146
+ hint: '修复方式:要么在 provider task 的 provides.fields 补上缺失字段,要么修正 consumer task 的 expects_from.needs(确认依赖是否真实)。',
1147
+ })
1100
1148
  }
1101
1149
 
1102
1150
  // ── 1c-b. TaskCard 命令存在性校验 ──
@@ -1111,11 +1159,11 @@ export async function executePlanPostcheck(context) {
1111
1159
  }
1112
1160
  const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules)
1113
1161
  if (taskCmds.errors.length > 0) {
1114
- console.error('\n❌ TaskCard 命令存在性校验失败(verify/implementation 的 npm/pnpm/yarn run <script> 不存在):')
1115
- for (const err of taskCmds.errors) console.error(` - ${err}`)
1116
- console.error('\n 修复方式:要么在 package.json scripts 补上缺失命令(monorepo 注意子包路径),')
1117
- console.error(' 要么修正 TaskCard 的 verify/implementation(确认命令是否真实、是否需 cd 子包)。')
1118
- throw new Error('planPostcheck: task command existence check failed')
1162
+ failures.push({
1163
+ name: 'TaskCard 命令存在性校验(verify/implementation 的 npm/pnpm/yarn run <script> 不存在)',
1164
+ errors: taskCmds.errors,
1165
+ hint: '修复方式:要么在 package.json scripts 补上缺失命令(monorepo 注意子包路径),要么修正 TaskCard 的 verify/implementation(确认命令是否真实、是否需 cd 子包)。',
1166
+ })
1119
1167
  }
1120
1168
 
1121
1169
  // ── 1d. design 文件覆盖对账 ──
@@ -1124,14 +1172,23 @@ export async function executePlanPostcheck(context) {
1124
1172
  const coverage = validateDesignFileCoverage(changeDir)
1125
1173
  for (const w of coverage.warnings) console.warn(`\n ⚠️ ${w}`)
1126
1174
  if (coverage.errors.length > 0) {
1127
- console.error('\n❌ design.md 文件覆盖对账失败(清单中的文件未被任何 task 覆盖):')
1128
- for (const err of coverage.errors) console.error(` - ${err}`)
1129
- throw new Error('planPostcheck: design file coverage check failed')
1175
+ failures.push({ name: 'design.md 文件覆盖对账(清单中的文件未被任何 task 覆盖)', errors: coverage.errors, hint: null })
1130
1176
  }
1131
1177
  if (coverage.designFiles.length > 0 && coverage.uncovered.length === 0) {
1132
1178
  console.log(` ✅ design.md ${coverage.designFiles.length} 个文件全部被 task allowed_paths 覆盖`)
1133
1179
  }
1134
1180
 
1181
+ // ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
1182
+ if (failures.length > 0) {
1183
+ console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
1184
+ for (const f of failures) {
1185
+ console.error(`\n 【${f.name}】`)
1186
+ for (const err of f.errors) console.error(` - ${err}`)
1187
+ if (f.hint) console.error(` ${f.hint}`)
1188
+ }
1189
+ throw new Error(`planPostcheck: ${failures.length} 组校验失败(${failures.map(f => f.name).join(';')})`)
1190
+ }
1191
+
1135
1192
  // ── 2. Wave 重排 ──
1136
1193
  const tasksDir = pJoin(changeDir, 'tasks')
1137
1194
  if (existsSync(tasksDir)) {
@@ -113,6 +113,9 @@ needs_human_review: true | false
113
113
 
114
114
  然后列出已加载的文件清单(含 decisions.md 当前版本/未决项状态、模块文档 + 模块依赖关系摘要)。
115
115
 
116
+ ### 落盘锚点(必须执行)
117
+ 把 plan_level 写进 plan.md frontmatter(\`plan_level: <none|light|full>\`)——上一步分类结果在本步落盘为持久锚点,后续步骤(及 context 压缩后的恢复)一律读 plan.md frontmatter,不依赖对话记忆传递。
118
+
116
119
  分类完成后,继续进入下一步。`,
117
120
  outputHint: '复杂度分类结果 + 文件清单',
118
121
  optional: false
@@ -125,13 +128,13 @@ needs_human_review: true | false
125
128
  const stepGeneratePlan = {
126
129
  id: 'generate_plan',
127
130
  name: '生成分级计划',
128
- prompt: `根据上一步的 plan_level 结果,按对应级别生成计划。
131
+ prompt: `根据 plan.md frontmatter 的 plan_level 结果,按对应级别生成计划。
129
132
 
130
133
  ### 操作
131
- 1. 读取上一步输出的 plan_level 分类结果
134
+ 1. 读取 plan.md frontmatter 的 \`plan_level:\` 字段(上一步已落盘为持久锚点;文件不存在或无该字段时回退读上一步输出的分类结果)
132
135
  2. 读取 tasks.md 和 design.md 了解需求范围
133
136
  3. 按 plan_level 选择对应模板输出
134
- 4. 保存 plan.md(审查在下一步"审查计划"独立进行,不在本步自审——避免生成与自审同一次输出)
137
+ 4. 保存 plan.md(审查在下一步"审查计划"独立进行,不在本步自审——避免生成与自审同一次输出);frontmatter 保留 plan_level 字段不删
135
138
 
136
139
  ---
137
140
 
@@ -283,7 +286,7 @@ full 计划的约束:
283
286
  ### 通用操作(所有级别)
284
287
  1. 读取 tasks.md 获取任务列表
285
288
  2. 读取 design.md 获取文件变更清单
286
- 3. 读取上一步的 plan_level 分类结果
289
+ 3. 读取 plan.md frontmatter 的 \`plan_level:\` 字段(持久锚点;缺失时回退上一步输出)
287
290
  4. 按对应级别模板生成内容
288
291
  5. 保存到变更目录下的 plan.md(路径格式:\`{SPEC_ROOT}/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`{SPEC_ROOT}/changes/2026-05-28-agent-log-streaming/plan.md\`)
289
292
  **plan_level 为 none 时生成最小 plan.md(占位),不生成完整蓝图。**
@@ -346,15 +349,25 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
346
349
  ### 生成 module-impact.md 首版(scale≠small 时)
347
350
  计划审查通过后,顺带生成 module-impact.md 首版(本次变更的模块影响分析,供 execute/verify 阶段更新、archive 阶段终审)。scale=small 不生成(small 走 quick,module-impact 对 quick 无用)。
348
351
 
349
- 输入(此时 TaskCard/allowed_paths 尚未生成——在下一步 generate_blueprints,故用以下两项作输入,粒度与 archive 现状一致):
352
+ 输入(此时 TaskCard/allowed_paths 尚未生成——在下一步「生成 TaskCard」才产出,故用以下两项作输入,粒度与 archive 现状一致):
350
353
  - design.md 的「文件变更清单」(本次计划改哪些源码文件)
351
354
  - plan.md 的任务列表(每个 task 改的范围)
352
355
 
353
- 步骤(复用 archive extract-module-impact 的核心指引,同源——改 archive step2 prompt 时保持一致):
356
+ 步骤(module-impact 的生成口径与 archive 终审一致):
354
357
  1. 读 {SPEC_ROOT}/docs/<project>/modules/_module-map.yaml(模块→文件路径映射)。**不存在 → 降级**:生成只含 unmapped 部分的 module-impact.md + 提示「建议运行 scan 生成模块映射」,不阻断
355
358
  2. 对照 design 文件变更清单 + plan 任务列表,逐文件匹配所属模块(命中的归 mapped,未命中的归 unmapped)
356
359
  3. 生成模块影响矩阵(模块 × 影响类型[新增/修改/删除/依赖变更] × 说明),落盘 {SPEC_ROOT}/changes/<change>/module-impact.md
357
360
  4. 首行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
361
+ 5. **必须含「更新结果」表骨架**(本表是 verify/archive 死信门控的收口目标——CLI 硬校验表内无 pending/待办行,漏写此表则 agent 只能从 gate 报错反推格式):
362
+ \`\`\`markdown
363
+ ## 更新结果
364
+
365
+ | 目标 | 操作 | 状态 |
366
+ |------|------|------|
367
+ | \`modules/<id>.md\` | 更新<模块名>模块卡(本次变更涉及) | pending |
368
+ | \`_module-map.yaml\` | 无变化(未增删模块) | skipped |
369
+ \`\`\`
370
+ 规则:每个受影响模块文档一行,状态列初始化 pending;无变化的行直接写 skipped。execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。**末列是状态列**(done/skipped/pending),不要在表尾追加其他列。
358
371
 
359
372
  execute/verify 阶段会按实际代码变更更新此文档;archive 阶段会最终确认它。
360
373
 
@@ -470,7 +483,9 @@ ${subagentPrompts}
470
483
 
471
484
  ## 验收(生成后自查,不另开步骤)
472
485
  - 每个 task-N.md 文件存在且非空
473
- - frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
486
+ - frontmatter 字段分两组对照(与 plan-postcheck / taskcard-rules.md 同源,三处清单以此为准):
487
+ - **硬校验 9 字段**(缺失 plan-postcheck 直接报错阻断):id、title、title_zh、allowed_paths、goal、implementation、acceptance、verify、constraints
488
+ - **规范约定 5 字段**(应填但缺失只影响规范性,不阻断):author、created_at、priority、depends_on、blocks
474
489
  - 每个 task 总长度 20~40 行
475
490
  - **一致性自查**:
476
491
  - allowed_paths 有无冲突
@@ -135,7 +135,7 @@ CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需
135
135
  - backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
136
136
  - backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
137
137
 
138
- 一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(gitignore),核对不影响 --done 已通过的边界审计。`,
138
+ 一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/quicklog/(git 跟踪,--done 后需提交),核对不影响 --done 已通过的边界审计。`,
139
139
  outputHint: '暂存和记录确认',
140
140
  optional: false
141
141
  }
@@ -138,7 +138,7 @@ export const definition = {
138
138
  e. 等待该项目所有子代理完成后,验证文件是否生成且非空
139
139
  f. 该项目完成后,继续下一个项目
140
140
  3. 所有项目完成后,运行以下命令检查产物:
141
- \`node -e "import('./src/workflow.js').then(w => { const r = w.runPostCheck(w.loadWorkflow('.', 'scan-docs'), '.', '<project>'); console.log(w.formatCheckReport(r)) })\`
141
+ \`sillyspec workflow check scan-docs --project <project>\`(CLI 子命令,任意项目根可用)
142
142
  对每个项目分别执行(将 \`<project>\` 替换为实际项目名)
143
143
  4. 如果检查报告有失败项,按报告中的角色和文件重试失败的部分
144
144
 
@@ -165,7 +165,7 @@ export const definition = {
165
165
  - 断点续扫步骤列出的缺失文档列表
166
166
  - 环境探测结果摘要(如有 _env-detect.md,直接贴入)
167
167
  - **⚠️ 必须强调:子代理必须用 write 工具将文件写入磁盘**
168
- - **文件标题用中文**(sillyhub 平台解析识别用):frontmatter 必须在文件最前(第 1 行 --- 起),frontmatter 结束的 --- 之后空一行,再写 # 中文名(English)作为标题。严禁把 # 标题放到 frontmatter 之前(否则 frontmatter 不在头部、Step11 检查项9 会报缺 author/created_at)。各文档标准标题格式:
168
+ - **文件标题用中文**(sillyhub 平台解析识别用):frontmatter 必须在文件最前(第 1 行 --- 起),frontmatter 结束的 --- 之后空一行,再写 # 中文名(English)作为标题。严禁把 # 标题放到 frontmatter 之前(否则 frontmatter 不在头部、「检查产物完整性」步的 frontmatter 检查会报缺 author/created_at)。各文档标准标题格式:
169
169
  - STRUCTURE.md = # 目录结构(Structure)
170
170
  - CONVENTIONS.md = # 代码约定(Conventions)
171
171
  - ARCHITECTURE.md = # 架构(Architecture)
@@ -75,7 +75,7 @@ export const definition = {
75
75
  ### Execute Evidence 传递检查
76
76
  7. 检查 verify-required-evidence.json 是否存在(由 execute 阶段 Task Review Gate 写入)
77
77
  - 路径:变更目录下的 verify-required-evidence.json
78
- - 文件 schema:\`{ items: [{ task, verdict, evidence: string[] }] }\`——顶层是 \`items\`,每项的 \`evidence\` 是字符串数组(**不是** \`requiredEvidence\` 键;旧 prompt 写 requiredEvidence 是字段名错配,照读必落空)
78
+ - 文件 schema:\`{ items: [{ task, verdict, evidence: string[] }] }\`——顶层是 \`items\`,每项的 \`evidence\` 是字符串数组(不是 \`requiredEvidence\` 键)
79
79
  - 如果存在 → 逐项读取 \`items\`,对每个 cannot_verify 任务逐条核对其 \`evidence\` 数组是否已满足
80
80
  - 每条 evidence 必须在 verify-result.md 中给出明确结论(satisfied / missing / partial)
81
81
  - 如果有任何 evidence 为 missing → verify 结论不能为 PASS
@@ -134,7 +134,7 @@ export const definition = {
134
134
  3. 数据模型是否符合
135
135
  4. API 设计是否符合
136
136
  5. **Reverse Sync 检查**:如果发现实现合理但 design.md 未覆盖,先更新 design.md 补充遗漏
137
- 6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不符合时标记 ⚠️(不阻断,模块文档可能未及时更新)
137
+ 6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不一致时**当场同步模块文档**(这是 verify 的收尾义务,不是可选项);module-impact.md「更新结果」表的文档同步项也须在本阶段回填 done/skipped——CLI 在 verify --done 时硬校验该表无 pending/待办死信行,未清即阻断完成
138
138
  7. **决策链路检查**:如果存在 decisions.md,输出 D-xxx@vN → FR-xxx → task-xx → evidence 的追踪矩阵;缺失项必须列为风险(CLI 只校验每个 D-xxx@vN ID 字面出现在 verify-result.md,warning 不阻断;矩阵的 D→FR→task→evidence 映射完整性供人类追溯,CLI 不校验——是否真覆盖由你诚实判定)
139
139
 
140
140
  ### 输出
@@ -149,9 +149,8 @@ export const definition = {
149
149
  ### 操作
150
150
  1. 检查变更目录下 tasks/ 是否存在
151
151
  2. 如果存在:
152
- - 逐个读取 tasks/task-NN.md
153
- - 检查每个文件的「验收标准」checkbox 是否全部勾选
154
- - 未勾选的项列为不通过
152
+ - 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
153
+ - 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
155
154
  3. 如果不存在:跳过此步骤
156
155
 
157
156
  ### 输出
@@ -235,13 +234,13 @@ PASS / PASS WITH NOTES / FAIL
235
234
  (自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
236
235
 
237
236
  ## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
238
- - daemon 启动命令:
239
- - backend 地址:
240
- - 创建 session / 调用核心 API 的请求:
241
- - daemon 日志关键片段(不能出现 session_control_no_manager / fallback to task_runner / submitMessages agent_run_id empty / 422):
242
- - backend 状态(AgentRun running -> completed/failed):
243
- - session / lease end 状态:
244
- - 失败模式排除:
237
+ 按本次变更实际触碰的运行时组件填写(模板是通用形态——长驻进程 / 服务端点 / 生命周期状态机的证据结构;未涉及的行写「不涉及」并注明原因,勿堆砌无关关键词):
238
+ - 长驻进程/服务 启动命令:
239
+ - 服务地址(本变更触碰的端点):
240
+ - 触发核心路径的请求/命令(附关键响应):
241
+ - 进程日志关键片段(证明走了新路径、无降级/回退分支):
242
+ - 生命周期终态断言(本变更涉及的状态机:初始态 → 运行态 → 终态,逐个确认到达):
243
+ - 失败模式排除(针对本变更声明的风险,逐条说明为何未触发):
245
244
 
246
245
  ## 代码审查
247
246
  (问题列表 + 总体评价)
package/src/sync.js CHANGED
@@ -12,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlink
12
12
  import { join } from 'path';
13
13
  import { resolvePlatformSpecDir } from './progress.js';
14
14
  import { safeGit } from './git-helper.js';
15
+ import { PLATFORM_MANAGED_FILENAME } from './run/shared.js';
15
16
 
16
17
  // sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
17
18
  function safePlatformSpecDir(cwd) {
@@ -327,28 +328,37 @@ export class SyncManager {
327
328
  }
328
329
 
329
330
  /**
330
- * 断开平台连接。
331
- * 从 local.yaml 删除 platform 配置段。
331
+ * 断开平台连接(三清,D-C@v2)。
332
+ * 1. 从 local.yaml 删除 platform 配置段;
333
+ * 2. 删恢复指针 <cwd>/.sillyspec-platform.json;
334
+ * 3. 删平台接管声明 <cwd>/.sillyspec-platform-managed。
335
+ * 不删后两者则"disconnect 后恢复本地模式"不可达(指针健在时 resolvePlatformSpecDir
336
+ * 仍解析平台 specRoot;声明健在时指针缺失会 fail-closed)。
332
337
  */
333
338
  disconnect() {
334
339
  const p = join(this.cwd, LOCAL_YAML);
335
- if (!existsSync(p)) {
336
- console.log('[sync] 已断开连接(无配置文件)');
337
- return;
338
- }
339
- const text = readFileSync(p, 'utf8');
340
- if (findTopLevelSectionRange(text, 'platform') === null) {
341
- console.log('[sync] 已断开连接(未连接)');
342
- return;
340
+ if (existsSync(p)) {
341
+ const text = readFileSync(p, 'utf8');
342
+ if (findTopLevelSectionRange(text, 'platform') !== null) {
343
+ // 文本级删除 platform 段,保留注释/其他段/数组/深嵌套/CRLF
344
+ const newText = replaceTopLevelSection(text, 'platform', null);
345
+ if (newText.trim() === '') {
346
+ // 删段后纯空白(无任何段也无注释)→ 删除整个文件;注释算内容,有注释则保留
347
+ try { unlinkSync(p); } catch { /* best effort */ }
348
+ } else {
349
+ writeFileSync(p, newText, 'utf8');
350
+ }
351
+ }
343
352
  }
344
- // 文本级删除 platform 段,保留注释/其他段/数组/深嵌套/CRLF
345
- const newText = replaceTopLevelSection(text, 'platform', null);
346
- if (newText.trim() === '') {
347
- // 删段后纯空白(无任何段也无注释)→ 删除整个文件;注释算内容,有注释则保留
348
- try { unlinkSync(p); } catch { /* best effort */ }
349
- } else {
350
- writeFileSync(p, newText, 'utf8');
353
+ // 三清之二/三:指针 + 接管声明(disconnect 是声明的唯一退出路径——design.md §5.4)
354
+ let cleaned = [];
355
+ for (const f of ['.sillyspec-platform.json', PLATFORM_MANAGED_FILENAME]) {
356
+ const fp = join(this.cwd, f);
357
+ if (existsSync(fp)) {
358
+ try { unlinkSync(fp); cleaned.push(f); } catch { /* best effort */ }
359
+ }
351
360
  }
361
+ if (cleaned.length > 0) console.log(`[sync] 已清理平台指针与接管声明: ${cleaned.join(', ')}`);
352
362
  console.log('[sync] 已断开连接');
353
363
  }
354
364
 
@@ -442,6 +452,18 @@ export class SyncManager {
442
452
  }
443
453
 
444
454
  console.log(`[sync] 已同步变更: ${changeName}`);
455
+
456
+ // 2026-08-16-auto-sync-from-repo:进度上行成功后顺带推四件套文档(best-effort)。
457
+ // 根因:run 流程此前只推进度不推文档(本文件头注释),本地直跑 sillyspec 的产出
458
+ // 文档永远不自动到平台(变更中心"进度到了文档没到")。生产者直连,不依赖 daemon
459
+ // 缓存链路。失败仅 debugLog——文档同步失败不得影响进度上行的返回值与流程;
460
+ // 四件套全缺失时 syncDocuments 内部已有跳过(syncedCount===0 提前返回,不调端点)。
461
+ try {
462
+ await this.syncDocuments(changeName);
463
+ } catch (err) {
464
+ debugLog(`[sync] 文档同步失败(不影响进度): ${changeName}: ${err.message}`);
465
+ }
466
+
445
467
  return { synced: 1, errors: [] };
446
468
  }
447
469