sillyspec 3.26.14 → 3.27.1

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 (70) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
  4. package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-explore/SKILL.md +10 -3
  7. package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
  8. package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
  9. package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
  10. package/.claude/skills/sillyspec-verify/SKILL.md +4 -0
  11. package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
  12. package/README.md +1 -1
  13. package/package.json +1 -1
  14. package/packages/dashboard/server/index.js +22 -6
  15. package/src/change-risk-profile.js +21 -6
  16. package/src/commit-suggest.js +202 -0
  17. package/src/config-cat.js +88 -0
  18. package/src/db.js +14 -3
  19. package/src/docs-check.js +598 -534
  20. package/src/doctor-diagnostics.js +102 -1
  21. package/src/endpoint-extractor.js +21 -2
  22. package/src/fs-atomic.js +4 -2
  23. package/src/git-helper.js +73 -5
  24. package/src/hooks/claude-pre-tool-use.cjs +10 -5
  25. package/src/hooks/worktree-guard.js +42 -4
  26. package/src/index.js +557 -12
  27. package/src/init.js +1 -1
  28. package/src/local-register.js +98 -0
  29. package/src/module-impact.js +166 -0
  30. package/src/module-resolve.js +211 -0
  31. package/src/modules.js +82 -2
  32. package/src/progress/change-registry.js +43 -4
  33. package/src/progress/stage-machine.js +10 -5
  34. package/src/progress/step-store.js +12 -8
  35. package/src/progress.js +19 -3
  36. package/src/quicklog.js +106 -1
  37. package/src/run/command.js +95 -1
  38. package/src/run/complete-handlers.js +73 -5
  39. package/src/run/complete.js +47 -14
  40. package/src/run/gates.js +144 -17
  41. package/src/run/multi-repo-context.js +3 -1
  42. package/src/run/next.js +128 -0
  43. package/src/run/prompt.js +36 -4
  44. package/src/run/quick-audit.js +5 -2
  45. package/src/run/shared.js +48 -20
  46. package/src/run/stage.js +37 -3
  47. package/src/scan-postcheck.js +142 -2
  48. package/src/stage-contract-spec.js +1 -1
  49. package/src/stage-contract.js +14 -1
  50. package/src/stage-review.js +15 -6
  51. package/src/stages/archive.js +4 -2
  52. package/src/stages/brainstorm.js +1 -0
  53. package/src/stages/execute.js +275 -44
  54. package/src/stages/explore.js +11 -2
  55. package/src/stages/knowledge.js +3 -2
  56. package/src/stages/quick.js +18 -9
  57. package/src/stages/scan.js +1 -0
  58. package/src/stages/verify.js +16 -51
  59. package/src/sync.js +44 -3
  60. package/src/task-review.js +273 -11
  61. package/src/taskcard.js +52 -22
  62. package/src/verify-postcheck.js +223 -6
  63. package/src/verify-probes.js +264 -0
  64. package/src/workspace.js +125 -0
  65. package/src/worktree-apply.js +43 -11
  66. package/src/worktree-deps.js +59 -5
  67. package/src/worktree.js +128 -14
  68. package/templates/claude-instruction.md +1 -1
  69. package/templates/prompts/taskcard-rules.md +1 -1
  70. package/templates/prompts/verify-probes.md +13 -47
@@ -7,6 +7,10 @@ import { isPathASupported } from '../dispatch/backends/sillyhub-mcp.js'
7
7
  import { readMcpConfig } from '../sillyhub-mcp/config.js'
8
8
  import { gitQuiet } from '../git-helper.js'
9
9
  import { parseRepo } from './plan-postcheck.js'
10
+ // 模块卡分级解析(token 成本优化 P0a)。注意:module-resolve.js 反向 import 本文件的
11
+ // parseTaskRegistry(注册表同口径)——双向引用均为函数声明 + 调用时解引(无模块初始化期
12
+ // 交叉求值),ESM live binding 下安全;两边都是纯读函数,无副作用序问题。
13
+ import { resolveChangeModuleCards } from '../module-resolve.js'
10
14
 
11
15
  /**
12
16
  * 任务注册表解析(2026-08-20-task-truth-unify D-001@v1:tasks.md 唯一任务真相)。
@@ -91,7 +95,26 @@ export function validatePlanForExecute(tasksContent, planContent) {
91
95
  const waves = parseWavesFromPlan(plan, registry)
92
96
  const allTasks = registry
93
97
 
94
- // 检查 0.8: Wave 标题格式不对(W1/Wave1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
98
+ // 检查 0.6: plan_level 锚点缺失(2026-08-21 审查 CLI-5):plan_level 是 review tier 的
99
+ // 判定主锚(classifyReviewTier)——漏写时静默退文件数启发式,full 级大变更会被降级成
100
+ // self 自审且无人报错(降级方向比 fail 更危险)。与 gates 读侧同口径:任意行 plan_level:。
101
+ const fmLine = plan.split('\n').find(l => l.trim().startsWith('plan_level:'))
102
+ if (!fmLine) {
103
+ errors.push(
104
+ `plan.md 缺 plan_level 锚点。brainstorm 的规模判定落盘(frontmatter 写 plan_level: none|light|full)是 review 分级的主锚,` +
105
+ `漏写会静默退文件数启发式——full 级变更可能被降级自审。修复:在 plan.md frontmatter 补一行,如 plan_level: full`
106
+ )
107
+ } else {
108
+ const planLevelValue = fmLine.split(':')[1].trim()
109
+ if (!['none', 'light', 'full'].includes(planLevelValue)) {
110
+ errors.push(
111
+ `plan_level 值非法:${JSON.stringify(planLevelValue)}(合法值 none|light|full)。` +
112
+ `非法值在 review 分级中被当作无锚点处理,同样会静默退文件数启发式`
113
+ )
114
+ }
115
+ }
116
+
117
+ // 检查 0.8: Wave 标题格式不对(W1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
95
118
  // 全并行——串行意图失效且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
96
119
  // 且无任何显式 Wave 被解析时报,正常 Wave 10/带括号标题不误伤。
97
120
  const hasExplicitWave = waves.some(w => !w.implicit)
@@ -102,6 +125,22 @@ export function validatePlanForExecute(tasksContent, planContent) {
102
125
  `其下的 "- task-XX" 引用行不会被收容——任务将退化为单个隐式 Wave 全并行,串行意图失效`
103
126
  )
104
127
  }
128
+ // 检查 0.9(坑 wave-heading-undercount,2026-08-21 实证):部分 Wave 被识别 + 部分
129
+ // wave-like 标题漏识别(6 标题解析 5 个)→ 漏掉 Wave 的任务静默落入后续步骤,收尾靠
130
+ // 批量完成兜住才没出事。逐个点名漏网标题,plan --done 即阻断修复。
131
+ if (hasExplicitWave) {
132
+ const strictRe = /^#+\s*Wave\s*\d+/i
133
+ const missed = plan.split('\n')
134
+ .map(l => l.replace(/\r$/, ''))
135
+ .filter(l => waveLikeHeading.test(l) && !strictRe.test(l))
136
+ if (missed.length > 0) {
137
+ errors.push(
138
+ `存在 ${missed.length} 个「疑似 Wave 标题但未被识别」的行(其余 Wave 已正常解析,这 ${missed.length} 个段的任务会漏进后续步骤):` +
139
+ missed.slice(0, 5).map(l => `"${l.trim().slice(0, 50)}"`).join('、') +
140
+ `。必须字面 "## Wave N" 格式(Wave+空格+数字),修正后重跑 plan --done`
141
+ )
142
+ }
143
+ }
105
144
 
106
145
  // 检查 1: 注册表非空(tasks.md 无 task-XX checkbox → 三类根因诊断)
107
146
  if (allTasks.length === 0) {
@@ -202,18 +241,20 @@ const fixedPrefix = [
202
241
  4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
203
242
  5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
204
243
 
205
- ### 模块文档加载
206
- 6. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
207
- 7. 根据 plan.md 中的任务文件路径匹配 _module-map.yaml 中的模块
208
- 8. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
209
- 9. 实现代码时遵循模块文档中描述的接口约定、数据流和依赖关系
210
- 10. **利用模块索引快速定位源码**:
244
+ ### 模块文档加载(细粒度卡优先——CLI 已按 tasks 卡 allowed_paths 级联匹配)
245
+ 6. 读取下方注入的 per-task 模块卡表({MODULE_RESOLVE_TABLE})——跨全部 _module-map.yaml 最长前缀匹配,子项目细粒度卡优先于根层大卡
246
+ 7. 主代理与子代理**只读表中命中的卡**:细粒度卡整卡可读(体量小、语义近);仅粗粒度大卡命中时**按节读**——只读「契约摘要」「注意事项」「定位」,跳过「变更索引」「人工备注」等历史累积段
247
+ 8. 实现代码时遵循模块卡中描述的接口约定、数据流和依赖关系
248
+ 9. **利用模块索引快速定位源码**:
211
249
  - 用 entrypoints 字段直接找到模块对外 API 的源码位置
212
250
  - 用 main_symbols 字段找到核心类/函数的定义位置
213
251
  - 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
252
+ 10. 表查询命令同源可复跑:\`sillyspec modules resolve --change <change-name>\`(任务卡改动后重跑刷新表)
214
253
 
215
254
  ### 符号影响面扩展检查
216
255
  11. **符号影响面扫描**(Critical — execute 前必做):
256
+ - **重入沿用(省重复消耗)**:若 symbol-impact.md 已存在、结论完整且其文件头「tasks.md 内容指纹」与当前 tasks.md 一致(中断续跑/阶段重开场景),直接复核沿用既有结论,**不重做调用点扫描**
257
+ - **报告骨架勿手写**:先跑 \`sillyspec symbol-impact --change <change-name>\`——CLI 从 tasks.md 生成逐 task \`<!--TODO-->\` 骨架(gate 拦截时也会自动落一份);把每行占位替换为真实结论(**未替换的 TODO 占位会被 gate 拒绝**,骨架不能直接过门)
217
258
  - 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
218
259
  - 对每个修改文件,检查是否涉及以下变更类型:
219
260
  - class 构造函数参数变更(新增/删除/修改参数)
@@ -221,7 +262,7 @@ const fixedPrefix = [
221
262
  - DTO / 类型定义变更
222
263
  - API client 方法签名变更
223
264
  - 函数/方法签名变更(参数增删改)
224
- - 如果涉及上述变更类型,执行调用点搜索:
265
+ - 如果涉及上述变更类型,执行调用点搜索(用于补骨架外的调用点证据,不是生成报告的方式):
225
266
  \`\`\`bash
226
267
  rg "new ClassName\(" src/
227
268
  rg "ClassName\(" src/
@@ -230,7 +271,7 @@ const fixedPrefix = [
230
271
  \`\`\`
231
272
  - 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
232
273
  - **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
233
- - **报告落盘(CLI 硬校验)**:把检查结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
274
+ - **报告落盘(CLI 硬校验)**:结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
234
275
  - 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
235
276
 
236
277
  ### 输出
@@ -281,7 +322,7 @@ worktree 路径 + 分支名 + 模式
281
322
  {DOCS_DEBT}
282
323
 
283
324
  如上为 CLI 用 git 事实算出的本变更触及模块文档欠账(无输出=无欠账或归属数据缺失)。
284
- 欠账处理:Wave 收尾时顺手同步对应模块卡(变更索引追加条目),不必为此停下。
325
+ 欠账处理:Wave 收尾时顺手同步对应模块卡,不必为此停下。变更索引类条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;可先跑 \`sillyspec modules split-changelog\` 迁出历史段)——勿把历史条目堆回模块卡正文(卡是子代理的读取税)。
285
326
 
286
327
  ### 铁律
287
328
  - **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
@@ -309,14 +350,17 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
309
350
  2. design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)
310
351
  3. 组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)
311
352
 
353
+ **产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
354
+ **gate 重试修复**:review.json 落盘后若后续 gate 报 docHash 失配 / 路径错,跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\` 一键重算修复——**不要重做审查**(重做=同一材料第三遍)。
355
+
312
356
  ### 操作
313
- 1. 读取 design.md(技术方案)
357
+ 1. 读取 design.md(技术方案)——按章节精准读(design-check 对照表可借 tasks 卡 §锚点定位),避免反复整读全文
314
358
  2. 逐一对照 design.md 中的设计要点与实际代码实现
315
359
  3. 检查接口签名、数据结构、模块划分是否一致
316
360
  4. 记录偏差项(偏差 ≠ 错误,可能是合理的实现调整)
317
361
 
318
362
  ### 输出
319
- 检查清单:每项设计要点的实现状态 ✅/⚠️/❌ + 偏差说明`,
363
+ review.json 已落盘(checklist=逐项核验表)+ checklist 摘要与偏差说明;无独立长文产物`,
320
364
  outputHint: '设计对照检查清单',
321
365
  optional: false
322
366
  },
@@ -488,9 +532,14 @@ function parseWavesFromPlan(planContent, registry = []) {
488
532
  const waves = []
489
533
  const regById = new Map(registry.map(t => [t.id, t]))
490
534
  let currentWave = null
535
+ // Wave 标题正则(坑 wave-heading-undercount,2026-08-21 实证:plan 6 个 Wave 只解析出 5 个,
536
+ // 末 Wave 任务静默落入「运行测试」验收步靠批量完成兜住)。空格可选("Wave6"/"Wave 6")、
537
+ // 编号后缀任意("## Wave 6(测试)")——解析侧宁可多收(字母后缀重复编号的硬拦仍在
538
+ // validatePlanForExecute),不可静默丢。
539
+ const WAVE_HEADING_RE = /^#+\s*Wave\s*(\d+)/i
491
540
 
492
541
  for (const line of String(planContent || '').split('\n')) {
493
- const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
542
+ const waveMatch = line.match(WAVE_HEADING_RE)
494
543
  if (waveMatch) {
495
544
  currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
496
545
  waves.push(currentWave)
@@ -508,6 +557,20 @@ function parseWavesFromPlan(planContent, registry = []) {
508
557
  }
509
558
  }
510
559
 
560
+ // wave-like 漂移告警(坑 wave-heading-undercount 运行时防线):比解析正则更宽的「像 Wave
561
+ // 标题」形态(波次/W+数字等)若未被收容,说明该 Wave 段的任务会被静默漏进后续步骤——
562
+ // execute 启动即 warn(agent 当场修 plan),而非收尾才发现步骤表缺 Wave。
563
+ try {
564
+ const likeRe = /^#{1,6}\s*(?:wave|w|波次)\s*\d+/i
565
+ const likeHeadings = String(planContent || '').split('\n')
566
+ .map(l => l.replace(/\r$/, ''))
567
+ .filter(l => likeRe.test(l) && !WAVE_HEADING_RE.test(l))
568
+ if (likeHeadings.length > 0) {
569
+ console.warn(`⚠️ plan 存在 ${likeHeadings.length} 个「疑似 Wave 标题但未被识别」的行(其任务会漏进后续步骤):`)
570
+ for (const h of likeHeadings.slice(0, 5)) console.warn(` ${h.trim().slice(0, 60)}(须字面 "## Wave N" 格式)`)
571
+ }
572
+ } catch { /* 告警失败不阻断解析 */ }
573
+
511
574
  // 无显式 Wave 结构但注册表非空(light 级:任务全在 tasks.md、plan.md 只留策略)→
512
575
  // 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行执行)
513
576
  if (waves.length === 0 && registry.length > 0) {
@@ -558,19 +621,20 @@ function resolveTaskRepo(task, changeDir) {
558
621
  }
559
622
 
560
623
  /**
561
- * 把 base_commit 锡点写入 task 卡 frontmatter(W3 task-08,D-010)。
562
- * 派发跨仓 task 前 CLI 实时 git rev-parse HEAD 落盘——锁定子代理起改的 base 锚点,
563
- * 防止同 Wave 多 task 改同一跨仓仓时 HEAD 推进致 diff 范围漂移(design §5.3 约束① / R-01)。
624
+ * 把 commit 锡点(base_commit/head_commit)写入 task 卡 frontmatter(W3 task-08,D-010;
625
+ * head 侧 2026-08-21 审计项③ 补对称)。
564
626
  *
565
- * 策略:幂等写——frontmatter 有 base_commit 行则就地替换,无则紧跟 repo 行后插入(
566
- * 无 frontmatter 时整段创建)。仅改 frontmatter 不动正文。已存在相同值不重复写。
627
+ * 策略:幂等写——frontmatter 有该字段行则就地替换,无则按 D-010 协议顺序插入:
628
+ * head_commit 跟在 base_commit 行后;base_commit 跟在 repo 行后;均无则插 frontmatter
629
+ * 首部。仅改 frontmatter 不动正文。已存在相同值不重复写。
567
630
  *
568
631
  * @param {string} taskFilePath - task-NN.md 绝对路径
569
- * @param {string} baseCommit - 跨仓仓 HEAD sha
632
+ * @param {'base_commit'|'head_commit'} field - 锚点字段名
633
+ * @param {string} commit - commit sha
570
634
  * @returns {boolean} 是否实际写入(false = 文件不存在 / 写失败 / 值未变)
571
635
  */
572
- function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
573
- if (!taskFilePath || !baseCommit) return false
636
+ export function writeCommitAnchorToTaskCard(taskFilePath, field, commit) {
637
+ if (!taskFilePath || !commit || (field !== 'base_commit' && field !== 'head_commit')) return false
574
638
  if (!existsSync(taskFilePath)) return false
575
639
  let content
576
640
  try {
@@ -579,27 +643,28 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
579
643
  return false
580
644
  }
581
645
  // CRLF 归一(坑 base-commit-crlf-frontmatter,plan-postcheck.js 同款):跨仓 task 卡被
582
- // 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → base_commit 锚点静默不落盘,而 Wave
646
+ // 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → 锚点静默不落盘,而 Wave
583
647
  // prompt 仍声称「CLI 已落盘」。归一后按 LF 写回(taskcard 生成侧本就强制 LF)。
584
648
  content = content.replace(/\r\n?/g, '\n')
585
649
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
586
650
  if (!fmMatch) return false
587
651
  const fm = fmMatch[1]
588
- const baseLineRe = /^base_commit:\s*.*$/m
652
+ const lineRe = new RegExp(`^${field}:\\s.*$`, 'm')
589
653
  let newFm
590
- if (baseLineRe.test(fm)) {
591
- const existing = (fm.match(baseLineRe) || [''])[0]
592
- if (existing === `base_commit: ${baseCommit}`) return false // 值未变,幂等跳过
593
- newFm = fm.replace(baseLineRe, `base_commit: ${baseCommit}`)
654
+ if (lineRe.test(fm)) {
655
+ const existing = (fm.match(lineRe) || [''])[0]
656
+ if (existing === `${field}: ${commit}`) return false // 值未变,幂等跳过
657
+ newFm = fm.replace(lineRe, `${field}: ${commit}`)
594
658
  } else {
595
- // 紧跟 repo 行后插入(与 D-010 协议同源顺序 repo→base_commit→head_commit)
596
- const repoLineRe = /^repo:\s*.*$/m
597
- const anchor = `base_commit: ${baseCommit}\n`
598
- if (repoLineRe.test(fm)) {
599
- newFm = fm.replace(repoLineRe, m => m + '\n' + anchor.trimEnd())
659
+ // 插入锚点(D-010 协议顺序 repo→base_commit→head_commit):head 跟 base 后,
660
+ // base 跟 repo 后,都无则 frontmatter 首字段
661
+ const anchor = `${field}: ${commit}`
662
+ const prevField = field === 'head_commit' ? 'base_commit' : 'repo'
663
+ const prevRe = new RegExp(`^${prevField}:\\s.*$`, 'm')
664
+ if (prevRe.test(fm)) {
665
+ newFm = fm.replace(prevRe, m => m + '\n' + anchor)
600
666
  } else {
601
- // 无 repo 行:插到 frontmatter 首部(frontmatter 首字段)
602
- newFm = anchor.trimEnd() + '\n' + fm
667
+ newFm = anchor + '\n' + fm
603
668
  }
604
669
  }
605
670
  const newContent = content.replace(/^---\n[\s\S]*?\n---/, `---\n${newFm}\n---`)
@@ -612,6 +677,75 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
612
677
  }
613
678
  }
614
679
 
680
+ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
681
+ return writeCommitAnchorToTaskCard(taskFilePath, 'base_commit', baseCommit)
682
+ }
683
+
684
+ /**
685
+ * 跨仓 task head_commit 锡点自动落盘(2026-08-21 agent-手工产出审计项③,D-010 补对称)。
686
+ *
687
+ * base_commit 已在派发时 CLI 落盘,head_commit 此前靠主 agent 按 Wave prompt 指引手跑
688
+ * `rev-parse` 手写(漏抄/抄错直接炸 Task Review Gate 真实性校验)。本函数在 execute
689
+ * --done 时机补齐另一半:扫描 task 卡,跨仓 task(repo≠main)缺 head_commit 的,实时
690
+ * `git -C <跨仓仓根> rev-parse HEAD` 幂等写入(**已存在不覆盖**——agent 手写的精确锚点优先)。
691
+ *
692
+ * best-effort:MultiRepoContext 构造失败 / git 不可达 → 跳过该 task 记 reason,绝不抛。
693
+ *
694
+ * @param {{ changeName: string, cwd: string, specBase?: string, platformOpts?: object }} opts
695
+ * @returns {Promise<{ stamped: number, stampedTasks: string[], skipped: number, reasons: string[] }>}
696
+ */
697
+ export async function stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts = {} }) {
698
+ const base = specBase || platformOpts.specRoot || path.join(cwd, '.sillyspec')
699
+ const reasons = []
700
+ const stampedTasks = []
701
+ if (!changeName) {
702
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 changeName'] }
703
+ }
704
+ const tasksDir = path.join(base, 'changes', changeName, 'tasks')
705
+ if (!existsSync(tasksDir)) {
706
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 tasks/ 目录(单 task 变更,无跨仓锡点)'] }
707
+ }
708
+ let ctx = null
709
+ try {
710
+ const { getOrCreateMultiRepoContext } = await import('../run/shared.js')
711
+ ctx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
712
+ } catch (e) {
713
+ return { stamped: 0, stampedTasks, skipped: 0, reasons: [`MultiRepoContext 构造失败: ${e.message}`] }
714
+ }
715
+ let stamped = 0
716
+ let skipped = 0
717
+ for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
718
+ const taskFile = path.join(tasksDir, f)
719
+ let content
720
+ try {
721
+ content = readFileSync(taskFile, 'utf8')
722
+ } catch {
723
+ skipped++
724
+ continue
725
+ }
726
+ if (/^head_commit:/m.test(content)) continue // 已有(手写或前次落盘)不覆盖
727
+ const repoKey = parseRepo(content)
728
+ if (!repoKey || repoKey === 'main') continue // 主仓 task 锚 meta,无 head_commit 字段
729
+ const entry = ctx && ctx.resolve ? ctx.resolve(repoKey) : null
730
+ if (!entry || !entry.gitDir) {
731
+ skipped++
732
+ reasons.push(`${f}: repo "${repoKey}" 无法解析 gitDir(local.yaml repos 未注册?)`)
733
+ continue
734
+ }
735
+ const head = gitQuiet(entry.gitDir, ['rev-parse', 'HEAD'])
736
+ if (!head) {
737
+ skipped++
738
+ reasons.push(`${f}: git rev-parse HEAD 失败(${entry.gitDir})`)
739
+ continue
740
+ }
741
+ if (writeCommitAnchorToTaskCard(taskFile, 'head_commit', head)) {
742
+ stamped++
743
+ stampedTasks.push(f.replace(/\.md$/, ''))
744
+ }
745
+ }
746
+ return { stamped, stampedTasks, skipped, reasons }
747
+ }
748
+
615
749
  /**
616
750
  * 为 Wave 生成 prompt(强制子代理执行)
617
751
  *
@@ -700,6 +834,94 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
700
834
  } catch {}
701
835
  }
702
836
 
837
+ // ── 模块卡分级(P0a):本 Wave per-task 最优卡表,细卡优先——治「根层大卡被每个子代理
838
+ // 整读」(multi-agent-platform backend.md 58KB 实测单卡一项 20 万+ tokens/全流程)。
839
+ // best-effort:解析失败/无 map 不注入(零回归),可经 `sillyspec modules resolve` 手查。
840
+ let moduleSection = ''
841
+ try {
842
+ if (changeDir) {
843
+ const specBase = path.dirname(path.dirname(changeDir))
844
+ // repo root 推导:默认 spec 布局(<repo>/.sillyspec/changes/<change>)从 specBase 反推,
845
+ // 平台模式(specRoot 异位)回退 process.cwd()——子项目目录前缀判定用,误差只降级匹配粒度
846
+ const repoRoot = path.basename(specBase) === '.sillyspec' ? path.dirname(specBase) : process.cwd()
847
+ const waveTaskIds = new Set(wave.tasks.map((t, ti) => `task-${String(t.index || (ti + 1)).padStart(2, '0')}`))
848
+ const { hasMaps, rows } = resolveChangeModuleCards({
849
+ cwd: repoRoot,
850
+ specBase,
851
+ changeName: path.basename(changeDir),
852
+ })
853
+ const hits = rows.filter(r => waveTaskIds.has(r.taskId) && r.moduleId)
854
+ if (hasMaps && hits.length > 0) {
855
+ const kb = n => (n > 0 ? `${(n / 1024).toFixed(1)}KB` : '?')
856
+ const cardLines = hits.map(r =>
857
+ `- ${r.taskId} → \`${r.cardPath}\`(${r.moduleId},${r.granularity === 'fine' ? '细卡' : '粗卡'} ${kb(r.cardBytes)}:${r.advice})`
858
+ ).join('\n')
859
+ moduleSection = `
860
+ ### 模块卡分级(本 Wave 子代理按表引用,勿按目录漫游 / 整读根层大卡)
861
+
862
+ ${cardLines}
863
+
864
+ 细粒度卡整卡读;粗粒度大卡只读「契约摘要/注意事项/定位」节(跳过变更索引/人工备注历史段)。为子代理写 prompt 时把对应卡路径直接写入。
865
+ `
866
+ }
867
+ }
868
+ } catch { /* 模块卡分级是 best-effort:失败不注入不阻断 */ }
869
+
870
+ // ── design.md 热区(P1a):Wave 前置只需「非目标/兼容策略」两节,CLI 提取直供——免每
871
+ // Wave 整读全文(28KB 级 design × 8 Wave 重复读)。无 design.md / 无对应节 → 不注入零回归。
872
+ let designHotzone = ''
873
+ let hasDesignHotzone = false
874
+ try {
875
+ if (changeDir) {
876
+ const designPath = path.join(changeDir, 'design.md')
877
+ if (existsSync(designPath)) {
878
+ const SECTION_CHAR_LIMIT = 2400
879
+ const dLines = readFileSync(designPath, 'utf8').replace(/\r\n/g, '\n').split('\n')
880
+ const sections = []
881
+ let cur = null
882
+ dLines.forEach((l, i) => {
883
+ const m = l.match(/^##\s+(.+)$/)
884
+ if (m) {
885
+ if (cur) cur.end = i
886
+ cur = { title: m[1].trim(), start: i, end: dLines.length }
887
+ sections.push(cur)
888
+ }
889
+ })
890
+ const grab = kwRe => {
891
+ const s = sections.find(s => kwRe.test(s.title))
892
+ if (!s) return null
893
+ const body = dLines.slice(s.start + 1, s.end).join('\n').trim()
894
+ if (!body) return null
895
+ return { title: s.title, body: body.length > SECTION_CHAR_LIMIT ? body.slice(0, SECTION_CHAR_LIMIT) + '\n…(超长截断,原文见 design.md)' : body }
896
+ }
897
+ const nonGoals = grab(/非目标/)
898
+ const compat = grab(/兼容/)
899
+ if (nonGoals || compat) {
900
+ hasDesignHotzone = true
901
+ const fmt = s => (s ? `#### ${s.title}\n${s.body}\n` : '')
902
+ const indexLines = sections.map(s => `- L${s.start + 1} ## ${s.title}`).join('\n')
903
+ designHotzone = `
904
+ ### design.md 热区(CLI 提取——Wave 前置只读这两节,勿整读全文)
905
+
906
+ ${fmt(nonGoals)}${fmt(compat)}
907
+ ### design.md 章节行号索引(需要其余章节时按行号精准读,Read offset/limit)
908
+
909
+ ${indexLines}
910
+ `
911
+ }
912
+ }
913
+ }
914
+ } catch { /* 热区提取 best-effort:失败不注入 */ }
915
+
916
+ // 子代理要点 4 / Wave 开始前第 1 条:模块表 / 热区存在时换成「按注入内容执行」版文案,
917
+ // 否则保留原文(零回归——无 map / 无 design 的项目 prompt 不变)
918
+ const moduleDocPoint = moduleSection
919
+ ? '4. **模块卡分级**:把上方「模块卡分级」表中该 task 命中的卡路径写进子代理 prompt(细卡整读;粗大卡只读「契约摘要/注意事项/定位」节)——勿让子代理按目录漫游或整读根层大卡'
920
+ : '4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流'
921
+ const waveStartItem1 = hasDesignHotzone
922
+ ? '1. 「非目标」「兼容策略」两节已由 CLI 提取附在上方「design.md 热区」——**不要再整读 design.md**;需要其余章节时按热区下方行号索引精准读取(Read offset/limit)。确保子代理不超范围、不破坏旧逻辑'
923
+ : '1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑'
924
+
703
925
  // 构建任务摘要(不再内联完整蓝图,减少上下文污染)
704
926
  const taskSummary = wave.tasks.map((t, ti) => {
705
927
  const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
@@ -768,9 +990,9 @@ ${crossLines}
768
990
  **派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
769
991
  - 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
770
992
 
771
- **回收跨仓 task review 前(head 锡点,你必须在子代理完成后落盘):**
772
- - 跨仓 task 子代理完成 commit 后、写 review.json / 勾选 checkbox **之前**,你必须运行 \`git -C <跨仓仓根> rev-parse HEAD\` 把结果写入该 task 卡 frontmatter \`head_commit:\` 字段(与 \`base_commit:\` 同源 frontmatter 锡点写入)。
773
- - review.json 的 \`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`(**非瞬时 HEAD**),锁 diff 范围不漂移。
993
+ **回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
994
+ - 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动 \`git -C <跨仓仓根> rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
995
+ - review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
774
996
 
775
997
  **跨仓 task 子代理 prompt 必须注入:**
776
998
  > 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
@@ -874,7 +1096,7 @@ ${workdirLines}
874
1096
 
875
1097
  ${worktreeSection}${crossRepoCommitSection}${dispatchSection}
876
1098
  **SillyHub 派发互斥**:SillyHub 派发模式下按派发段执行(一 Wave 一 mission),不按 batch 分组;batch 分组指导仅适用于本地 Agent tool 派发。
877
-
1099
+ ${moduleSection}
878
1100
  ### 任务摘要(按需读取完整蓝图)
879
1101
  为每个任务启动子代理时,**只需告知任务目标和蓝图文件路径,让子代理按需读取**:
880
1102
 
@@ -884,7 +1106,7 @@ ${taskSummary}
884
1106
  1. 任务目标(简短描述)
885
1107
  2. 蓝图文件路径(让子代理自行读取详情)
886
1108
  3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
887
- 4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
1109
+ 4. ${moduleDocPoint}
888
1110
  5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
889
1111
  6. **增量落盘与中断接手指引**:每完成一个可见产出(代码/测试/文档),立即写盘并执行一次最小验证(如语法检查、单跑相关测试)。工作过程中如被 429/API 配额/会话中断,应在最终回复里输出「已完成清单」(含文件路径、测试命令、当前卡点),不要只输出结论——主代理会依据磁盘产物和该清单判断哪些部分已完成,哪些需接手补做,避免重做已落盘的工作
890
1112
  7. **任务边界铁律**:严格只实现本 task 的 \`allowed_paths\` 内文件;若 design.md/plan.md 明确指定了接口/回调/钩子接入位置,必须逐字遵守;不允许顺手实现其他 task 的内容(如 task-01 不要把 task-02 的接入也做了)。如发现必须改其他 task 文件才能继续,先回到主代理由主代理决定是否重分 Wave 或调整 plan,禁止子代理私自越界
@@ -892,8 +1114,9 @@ ${taskSummary}
892
1114
 
893
1115
  {{include: testcase-design}}
894
1116
 
1117
+ ${designHotzone}
895
1118
  ### Wave 开始前
896
- 1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑
1119
+ ${waveStartItem1}
897
1120
  2. 读取 plan.md 了解全局任务划分和依赖关系
898
1121
  3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
899
1122
  4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
@@ -963,11 +1186,19 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
963
1186
  - 无 _module-map.yaml 时跳过模块匹配,仅按文件清单更新 unmapped 部分
964
1187
  这是可选更新(不阻断 execute 完成),但保持 module-impact 与实际变更一致利于 verify 核对与 archive 终审。
965
1188
 
1189
+ ### 知识沉淀(主代理在本 Wave 收尾,与 module-impact.md 同批)
1190
+ 本 Wave 各 task 子代理报告里若提到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节),由你在 Wave 收尾统一追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
1191
+ - 条目格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源(task-XX)
1192
+ - 不由各 task 子代理分别写(并行改同一文件会互相覆盖),只由主代理在 Wave 收尾统一追加——与 module-impact.md 同一收尾节奏
1193
+ - 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写;拿不准时不写,宁缺毋滥
1194
+ - execute 末步「知识库审阅」会检查待确认条目并提示用户归类
1195
+
966
1196
  ### 完成后
967
- 1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
968
- - 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
969
- - 将端点清单写入 {SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
970
- - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
1197
+ 1. 为每个后端 router task 生成 API 端点 artifact——**用 CLI 命令,勿手扫装饰器手写(易漏 endpoint)**:
1198
+ - 逐 task 运行:sillyspec endpoints extract --change <change> --task <task-NN>(<change> = 当前变更名)
1199
+ - CLI 静态扫描变更文件(FastAPI @router.* / Express router.* / Spring @*Mapping)生成
1200
+ {SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
1201
+ - 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }(CLI 已按此格式写好,verify 探针 5 直接消费)
971
1202
  `
972
1203
  }
973
1204
 
@@ -18,14 +18,23 @@ export const definition = {
18
18
  3. 可以用 \`rg\` / \`ls\` / \`cat\` 调查已有结构和集成点
19
19
  4. 输出 2-3 个有价值方向、关键风险和下一步建议
20
20
  5. 如果用户要求保存结论,先明确保存位置,再写入对应文档
21
+ 6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
22
+
23
+ ### HTML 原型生成(仅用户明确要求时)
24
+ 探索保持只读姿态:不主动写原型文件;UI 讨论成形时可以提议("要不要出个原型看效果"),用户同意后再生成。生成要求与 brainstorm 阶段的原型对齐:
25
+ - 单文件 HTML(内联 CSS + JS),浏览器直接打开
26
+ - 高保真呈现讨论中的布局/组件/交互流程——用户看到的是"这个方案做出来长什么样",不是示意图
27
+ - 与项目现有界面风格一致:先读 scan 文档与现有前端代码,复用现有组件库/设计 token 的观感,不另起风格
28
+ - 保存到 \`{SPEC_ROOT}/explore/prototype-<名称>.html\`(目录不存在则先创建;用户指定了其他位置则从其指定)
29
+ - 原型是探索讨论工具、不是交付物,也不是实现代码——不要顺手把原型接进项目源码或构建;话题后续进入 brainstorm 时,把原型文件复制到 \`{SPEC_ROOT}/changes/<变更名>/\` 可直接复用为该变更的原型
21
30
 
22
31
  ### 输出
23
- 探索结论、选项对比、风险清单或 ASCII 图
32
+ 探索结论、选项对比、风险清单或 ASCII 图;如生成了原型,附原型文件路径
24
33
 
25
34
  ### 铁律
26
35
  - 不写实现代码
27
36
  - 不安装依赖
28
- - 不修改文件,除非用户明确要求保存探索结论
37
+ - 不修改文件,除非用户明确要求保存探索结论或生成原型
29
38
  - 不强行推进到 brainstorm/plan/execute`,
30
39
  outputHint: '探索结论',
31
40
  optional: false
@@ -232,11 +232,12 @@ export async function cmdValidate(dir, args, opts = {}) {
232
232
  }
233
233
  }
234
234
 
235
- // 4. uncategorized.md 条目数(多了提示需清理)
235
+ // 4. uncategorized.md 条目数(多了提示需清理)。条目行契约是 `## <qlId> | <标题>`
236
+ //(quick 自动候选与手工条目同格式),h3 形态一并兼容;原 `^###` 只数 h3 恒为 0
236
237
  const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
237
238
  if (existsSync(uncategorizedPath)) {
238
239
  const content = readFileSync(uncategorizedPath, 'utf8')
239
- const entryCount = (content.match(/^###\s+/gm) || []).length
240
+ const entryCount = (content.match(/^#{2,3}\s+\S/gm) || []).length
240
241
  if (entryCount >= 10) {
241
242
  warnings.push({
242
243
  code: 'too_many_uncategorized',
@@ -81,7 +81,14 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
81
81
  - 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
82
82
 
83
83
  ### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
84
- \`--output\` 是 QUICKLOG「结果:」归档的唯一来源,**必须按此结构给全四项**(逐项一句话,不可用「见前述」替代):
84
+
85
+ 🔧 **推荐四参数形式**(CLI 自动合成结构化模板,杜绝嵌套冒号/缺字段/截断规则踩坑):
86
+
87
+ \`\`\`
88
+ sillyspec run quick --done --change <id> --req "一句话语义化短标题(写「改了什么」)" --cause "为什么这样改(纯新增/配置/样式写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"
89
+ \`\`\`
90
+
91
+ 兼容旧形式 \`--output "需求:… 根因:… 方案:… 结果:…"\`(**必须按此结构给全四项**,逐项一句话,不可用「见前述」替代;缺任一项 \`--done\` 会被拒并提示缺失字段,补全后重跑即可不丢进度):
85
92
 
86
93
  \`\`\`
87
94
  需求:一句话语义化短标题(写「改了什么」)
@@ -90,15 +97,13 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
90
97
  结果:验证情况(测试数 / lint / typecheck / 部署状态)
91
98
  \`\`\`
92
99
 
93
- ⚠️ **「需求:」就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。
94
-
95
- 缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
100
+ ⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。旧形式正文内避免嵌套全角冒号(标签后紧接嵌套冒号会被拆分判定缺字段)——这正是四参数形式消灭的事故面。
96
101
 
97
102
  ### 📎 文件括注 --file-notes(可选,省事后手改文件行)
98
103
  若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
99
104
 
100
105
  \`\`\`
101
- sillyspec run quick --done --change <id> --output "需求:… 根因:… 方案:… 结果:…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
106
+ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "…" --result "…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
102
107
  \`\`\`
103
108
 
104
109
  格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
@@ -116,7 +121,10 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
116
121
  - 禁止使用 \`git add -A\`
117
122
  - 不要暂存 quick 开始前就已存在的无关改动
118
123
  3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——**QUICKLOG 核对按需**(见下方「QUICKLOG 正文核对」;文件行可用 --file-notes 免手改)
119
- 4. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
124
+ 4. 只有遇到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节)时,才追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
125
+ - 格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源 \`(<quicklog-id>)\`
126
+ - 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写
127
+ - 拿不准时不写,宁缺毋滥
120
128
  5. 任务比预期复杂 → 建议用完整流程
121
129
 
122
130
  ### 模块文档同步
@@ -124,10 +132,11 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
124
132
  7. 对比本次修改的文件(\`git diff --name-only HEAD\`)与模块映射
125
133
  8. 如果命中模块 → 直接同步模块文档:
126
134
  - 读取对应的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`(如不存在则新建)
127
- - 根据本次改动内容更新模块文档(正文描述当前状态,底部变更索引追加本步骤预注入的 ql-ID)
135
+ - 根据本次改动内容更新模块文档(正文描述当前状态;**勿把历史条目堆进正文——卡是子代理的读取税**)
136
+ - 变更索引条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;历史段可跑 \`sillyspec modules split-changelog\` 迁出)
128
137
  - 变更索引格式:\`- <quicklog-id> | <一句话描述>\`
129
- - 写入模块文档
130
- - 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
138
+ - 写入模块文档与 sidecar
139
+ - 使用 \`git add -- <module-doc>\` 暂存更新的模块文件(sidecar 一并暂存)
131
140
  9. 未命中任何模块 → 跳过,不做额外操作
132
141
 
133
142
  ### 输出
@@ -154,6 +154,7 @@ export const definition = {
154
154
  generator: sillyspec-scan
155
155
  ---
156
156
  \`\`\`
157
+ (\`<git-head-short>\` / \`<now-iso-datetime>\` 两占位符由 CLI 注入 prompt 时自动替换为当前 HEAD 短哈希与时间——照 prompt 里已替换的真实值抄写,勿自跑 git rev-parse;漏 header 事后 \`sillyspec scan-fix-headers\` 一键补 author/created_at)
157
158
  - 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
158
159
  - 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
159
160