sillyspec 3.27.2 → 3.27.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +8 -0
  2. package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
  3. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  4. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
  5. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  6. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  7. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
  8. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  9. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
  10. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
  11. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
  12. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
  13. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  14. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  15. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  16. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
  17. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  18. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
  19. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
  20. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  21. package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
  22. package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
  23. package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
  24. package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
  25. package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
  26. package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
  27. package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
  28. package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
  29. package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
  30. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
  31. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  32. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
  33. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
  34. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
  35. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
  36. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
  37. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
  38. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
  39. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
  40. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
  41. package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
  42. package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
  43. package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
  44. package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
  45. package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
  46. package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
  47. package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
  48. package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
  49. package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
  50. package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
  51. package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
  52. package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
  53. package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
  54. package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
  55. package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
  56. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
  57. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
  58. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
  59. package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
  60. package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
  61. package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
  62. package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
  63. package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
  64. package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
  65. package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
  66. package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
  67. package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
  68. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
  69. package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
  70. package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
  71. package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
  72. package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
  73. package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
  74. package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
  75. package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
  76. package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
  77. package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
  78. package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
  79. package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
  80. package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
  81. package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
  82. package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
  83. package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
  84. package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
  85. package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
  86. package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
  87. package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
  88. package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
  89. package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
  90. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
  91. package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
  92. package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
  93. package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
  94. package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
  95. package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
  96. package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
  97. package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
  98. package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
  99. package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
  100. package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
  101. package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
  102. package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
  103. package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
  104. package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
  105. package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
  106. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
  107. package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
  108. package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
  109. package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
  110. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
  111. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
  112. package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
  113. package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
  114. package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
  115. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
  116. package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
  117. package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
  118. package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
  119. package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
  120. package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
  121. package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
  122. package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
  123. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
  124. package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
  125. package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
  126. package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
  127. package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
  128. package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
  129. package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
  130. package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
  131. package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
  132. package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
  133. package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
  134. package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
  135. package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
  136. package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
  137. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
  138. package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
  139. package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
  140. package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
  141. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
  142. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
  143. package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
  144. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
  145. package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
  146. package/eval/README.md +86 -0
  147. package/eval/eval.config.json +28 -0
  148. package/eval/lib/adapters/cli.mjs +63 -0
  149. package/eval/lib/adapters/dryrun.mjs +27 -0
  150. package/eval/lib/config.mjs +82 -0
  151. package/eval/lib/db.mjs +76 -0
  152. package/eval/lib/report.mjs +120 -0
  153. package/eval/lib/runner.mjs +157 -0
  154. package/eval/lib/tasks.mjs +73 -0
  155. package/eval/lib/util.mjs +107 -0
  156. package/eval/lib/workdir.mjs +30 -0
  157. package/eval/run.mjs +83 -0
  158. package/eval/tasks/001-slugify/files/package.json +6 -0
  159. package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
  160. package/eval/tasks/001-slugify/task.json +6 -0
  161. package/eval/tasks/001-slugify/verify.mjs +28 -0
  162. package/eval/tasks/002-each-day/files/package.json +6 -0
  163. package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
  164. package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
  165. package/eval/tasks/002-each-day/task.json +6 -0
  166. package/eval/tasks/002-each-day/verify.mjs +31 -0
  167. package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
  168. package/eval/tasks/003-word-report/files/package.json +6 -0
  169. package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
  170. package/eval/tasks/003-word-report/task.json +6 -0
  171. package/eval/tasks/003-word-report/verify.mjs +38 -0
  172. package/package.json +4 -2
  173. package/src/agent-session-log.js +45 -5
  174. package/src/change-risk-profile.js +83 -14
  175. package/src/config-schema.js +18 -3
  176. package/src/contract-matrix.js +90 -29
  177. package/src/datetime.js +20 -0
  178. package/src/decision-distill.js +517 -0
  179. package/src/dispatch/backends/local-agent.js +4 -4
  180. package/src/docs-check.js +227 -0
  181. package/src/docs-debt.js +187 -24
  182. package/src/doctor-diagnostics.js +102 -2
  183. package/src/foreign-declared.js +78 -0
  184. package/src/index.js +89 -9
  185. package/src/init.js +9 -7
  186. package/src/knowledge-match.js +107 -6
  187. package/src/local-register.js +7 -1
  188. package/src/module-resolve.js +12 -1
  189. package/src/modules.js +2 -1
  190. package/src/plan-adopt-waves.js +126 -0
  191. package/src/quicklog.js +50 -4
  192. package/src/run/command.js +122 -43
  193. package/src/run/complete-handlers.js +48 -19
  194. package/src/run/complete.js +5 -2
  195. package/src/run/gates.js +87 -22
  196. package/src/run/prompt.js +84 -42
  197. package/src/run/quick-audit.js +11 -0
  198. package/src/run/shared.js +152 -10
  199. package/src/run.js +2 -2
  200. package/src/scan-postcheck.js +30 -17
  201. package/src/stage-contract-spec.js +2 -1
  202. package/src/stage-contract.js +68 -4
  203. package/src/stages/archive.js +53 -12
  204. package/src/stages/brainstorm-auto.js +7 -0
  205. package/src/stages/brainstorm.js +11 -0
  206. package/src/stages/doctor.js +157 -1
  207. package/src/stages/execute.js +32 -6
  208. package/src/stages/plan-postcheck.js +76 -17
  209. package/src/stages/plan.js +1 -0
  210. package/src/stages/quick.js +6 -1
  211. package/src/stages/verify.js +31 -14
  212. package/src/sync.js +41 -3
  213. package/src/task-review.js +2 -2
  214. package/src/taskcard-placeholders.js +21 -0
  215. package/src/taskcard.js +10 -1
  216. package/src/verify-postcheck.js +315 -13
  217. package/src/verify-probes.js +46 -4
  218. package/src/worktree-apply.js +184 -7
  219. package/src/worktree-deps.js +57 -0
  220. package/src/worktree.js +201 -58
  221. package/templates/prompts/taskcard-rules.md +1 -0
@@ -19,6 +19,7 @@ import { tmpdir } from 'os'
19
19
  import jsYaml from 'js-yaml'
20
20
  import { parseFileChangeList, pathMatches } from '../change-list.js'
21
21
  import { getRule } from '../stage-contract-spec.js'
22
+ import { TASKCARD_PLACEHOLDERS } from '../taskcard-placeholders.js'
22
23
  import { validateScriptCommands } from './cmd-existence.js'
23
24
 
24
25
  // ═══════════════════════════════════════════════════════════════
@@ -173,7 +174,12 @@ export function parseHeadCommit(content) {
173
174
  export function parseRepoRegistry(yamlText) {
174
175
  const reg = new Map()
175
176
  if (!yamlText) return reg
176
- const lines = yamlText.split('\n')
177
+ // CRLF 归一(坑 register-repo-crlf-idempotent-loop,2026-08-23 实证:Windows 下 agent Write
178
+ // 工具/编辑器写出的 local.yaml 带 \r——条目正则 `(.*)$` 的 `.` 不匹配 \r、`$` 又要求真串尾,
179
+ // 行尾残留 \r 时整条失配 → 空 Map → MultiRepoContext fail-closed 报「未注册」→ register-repo
180
+ // 幂等跳过不落盘 → 死循环。同文件 parseAllowedPaths/parseDependsOn/parseBaseCommit 均有此
181
+ // 归一,本函数是唯一缺口)
182
+ const lines = String(yamlText).replace(/\r\n?/g, '\n').split('\n')
177
183
  let startIdx = -1
178
184
  for (let i = 0; i < lines.length; i++) {
179
185
  if (/^repos:\s*(?:#.*)?$/.test(lines[i])) { startIdx = i; break }
@@ -356,6 +362,28 @@ function parseTaskWavesFromPlan(planPath) {
356
362
  return map
357
363
  }
358
364
 
365
+ /**
366
+ * 收集任务卡 depends_on → Map<taskId, deps>(Wave 重排 / plan-adopt-waves 共用)。
367
+ * taskId 优先取卡内 frontmatter id:(与文件名解耦——文件名 task-01 但 id 改写过时以 id 为准)。
368
+ * @param {string} tasksDir - changes/<name>/tasks 目录
369
+ * @returns {Map<string, string[]>}
370
+ */
371
+ export function collectTaskDepMap(tasksDir) {
372
+ const depMap = new Map()
373
+ if (!existsSync(tasksDir)) return depMap
374
+ for (const file of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
375
+ const content = readFileSync(pJoin(tasksDir, file), 'utf8')
376
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
377
+ let taskId = file.replace('.md', '')
378
+ if (fmMatch) {
379
+ const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
380
+ if (idMatch) taskId = idMatch[1].trim()
381
+ }
382
+ depMap.set(taskId, parseDependsOn(content))
383
+ }
384
+ return depMap
385
+ }
386
+
359
387
  /**
360
388
  * 本地一致性校验器
361
389
  * @param {string} changeDir - 变更目录
@@ -1002,6 +1030,22 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
1002
1030
  if (!hasVerify) errors.push(fsRule.data.messageVerify.replaceAll('${id}', taskId || file))
1003
1031
  if (!hasConstraints) errors.push(fsRule.data.messageConstraints.replaceAll('${id}', taskId || file))
1004
1032
 
1033
+ // 5b. 骨架占位符拦截(坑 taskcard-placeholder-slip,2026-08-24 用户实证:task 卡留成
1034
+ // 空骨架、九字段「存在性」全过,直到人工审计才发现)。占位标记是 taskcard.js 生成的
1035
+ // 封闭集合(TASKCARD_PLACEHOLDERS 同源导出),精确匹配;verify 字段的占位命令刻意
1036
+ // 不拦(可能是真实 tsc 命令)。占位符视同缺字段,与 9 字段缺失同级 error。
1037
+ // 先剥 HTML 注释再匹配——骨架尾部注释自身罗列了占位标记清单(填充指引),不剥会把
1038
+ // 已填充卡的注释误判成占位残留。
1039
+ const contentNoComments = content.replace(/<!--[\s\S]*?-->/g, '')
1040
+ const placeholderHits = TASKCARD_PLACEHOLDERS.filter(p => contentNoComments.includes(p.marker))
1041
+ if (placeholderHits.length > 0) {
1042
+ errors.push(
1043
+ (fsRule.data.messagePlaceholder || '${id}: task 卡仍是未填充的生成骨架')
1044
+ .replaceAll('${id}', taskId || file)
1045
+ .replaceAll('${fields}', placeholderHits.map(p => p.field).join('、'))
1046
+ )
1047
+ }
1048
+
1005
1049
  // 6. acceptance best-effort 字段 grep(D-05 软约束,warning 不阻断)
1006
1050
  // 从 acceptance 文本提取 snake_case/camelCase 标识符,grep allowed_paths 指向的源文件;
1007
1051
  // 找不到 → warning(给 LLM 审查提线索,不阻断 execute)。宁漏不噪:只取 snake/camel,
@@ -1248,18 +1292,7 @@ export async function executePlanPostcheck(context) {
1248
1292
  const tasksDir = pJoin(changeDir, 'tasks')
1249
1293
  if (existsSync(tasksDir)) {
1250
1294
  const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))
1251
- const depMap = new Map()
1252
-
1253
- for (const file of taskFiles) {
1254
- const content = readFileSync(pJoin(tasksDir, file), 'utf8')
1255
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
1256
- let taskId = file.replace('.md', '')
1257
- if (fmMatch) {
1258
- const idMatch = fmMatch[1].match(/^id:\s*(.+)/m)
1259
- if (idMatch) taskId = idMatch[1].trim()
1260
- }
1261
- depMap.set(taskId, parseDependsOn(content))
1262
- }
1295
+ const depMap = collectTaskDepMap(tasksDir)
1263
1296
 
1264
1297
  const { waves, error: topoError } = topoSortWaves(depMap)
1265
1298
  if (topoError) {
@@ -1272,8 +1305,10 @@ export async function executePlanPostcheck(context) {
1272
1305
  console.log(` Wave ${i + 1}: ${wave.join(', ')}`)
1273
1306
  })
1274
1307
 
1275
- // 比较 plan.md 现有 Wave 分组
1276
- if (waves.length > 1 && taskFiles.length > 1) {
1308
+ // 比较 plan.md 现有 Wave 分组 + 依赖方向硬校验(坑 wave-dep-direction-unchecked,
1309
+ // 2026-08-24 用户反馈二期:主控手排 Wave 与拓扑比对只警告不阻断,而真正的正确性违规
1310
+ // ——depends_on 落同 Wave / 后置 Wave——此前完全没有校验)
1311
+ if (taskFiles.length > 0) {
1277
1312
  const planPath = pJoin(changeDir, 'plan.md')
1278
1313
  if (existsSync(planPath)) {
1279
1314
  const planContent = readFileSync(planPath, 'utf8')
@@ -1305,6 +1340,30 @@ export async function executePlanPostcheck(context) {
1305
1340
  }
1306
1341
  if (currentWaveTasks) existingWaves.push(currentWaveTasks)
1307
1342
 
1343
+ // ── 依赖方向硬校验 ──
1344
+ // Wave 契约(execute.js Wave 要点 1):同一 Wave 的子代理强制并行;有依赖的 task
1345
+ // 必须落在其依赖的后置 Wave。同 Wave / 后置 Wave 引用都是正确性违规(并行破坏依赖
1346
+ // / 顺序颠倒),硬拦;仅分组数差异(手工过度串行化)合法,保留下方 warning。
1347
+ const waveOf = new Map()
1348
+ existingWaves.forEach((ws, i) => { for (const t of ws) waveOf.set(t, i) })
1349
+ const directionViolations = []
1350
+ for (const [taskId, deps] of depMap) {
1351
+ const w = waveOf.get(taskId)
1352
+ if (w === undefined) continue // 卡片未列入任何 Wave → 由 validatePlanForExecute 覆盖校验兜底
1353
+ for (const dep of deps) {
1354
+ const dw = waveOf.get(dep)
1355
+ if (dw === undefined) continue // 依赖不存在的 task → feasibility depends_on 引用校验兜底
1356
+ if (dw === w) directionViolations.push(`${taskId} depends_on ${dep},但两者同在 Wave ${w + 1}(同 Wave 强制并行,依赖被破坏)`)
1357
+ else if (dw > w) directionViolations.push(`${taskId} depends_on ${dep},但 ${dep} 在后置 Wave ${dw + 1}(顺序颠倒)`)
1358
+ }
1359
+ }
1360
+ if (directionViolations.length > 0) {
1361
+ console.error(`\n❌ Wave 依赖方向违规(${directionViolations.length} 处):`)
1362
+ for (const v of directionViolations) console.error(` ${v}`)
1363
+ console.error(' 解法:sillyspec plan-adopt-waves --change <变更名> 一键按 depends_on 拓扑重排 plan.md Wave 段,或手动调整分组')
1364
+ throw new Error('planPostcheck: Wave 依赖方向违规(depends_on 同 Wave / 后置 Wave)——' + directionViolations.join(';'))
1365
+ }
1366
+
1308
1367
  const sameStructure = waves.length === existingWaves.length &&
1309
1368
  waves.every((w, i) => {
1310
1369
  const a = [...w].sort().join(',')
@@ -1315,8 +1374,8 @@ export async function executePlanPostcheck(context) {
1315
1374
  if (sameStructure) {
1316
1375
  console.log(' ✅ Wave 分组与拓扑排序一致,无需更新 plan.md')
1317
1376
  } else {
1318
- console.log(' ⚠️ Wave 分组与拓扑排序不一致,建议手动调整 plan.md')
1319
- console.log(' 拓扑排序建议的 Wave 分组见上方')
1377
+ console.log(' ⚠️ Wave 分组与拓扑排序不一致(依赖方向已校验合法——手工比拓扑更细的串行化是安全做法,可保持现状)')
1378
+ console.log(' 标准分组见上方 📊;如需对齐:sillyspec plan-adopt-waves --change <变更名>(重排 plan.md Wave 段并同步任务总表 W 列)')
1320
1379
  }
1321
1380
  }
1322
1381
  }
@@ -510,6 +510,7 @@ ${taskcardTemplate}
510
510
  - 所有 task 都被某个 batch 覆盖、无遗漏、无重复
511
511
  - frontmatter 字段分两组对照(与 plan-postcheck / taskcard-rules.md 同源,三处清单以此为准):
512
512
  - **硬校验 9 字段**(缺失 plan-postcheck 直接报错阻断):id、title、title_zh、allowed_paths、goal、implementation、acceptance、verify、constraints
513
+ - **占位符硬拦**(骨架占位值未替换视同缺字段,plan --done 报错阻断):FR-XX、D-XXX、src/example/file.ts、一句话说明这个 task、具体步骤 1、可验证的验收条件 1、边界约束 1
513
514
  - **规范约定 5 字段**(应填但缺失只影响规范性,不阻断):author、created_at、priority、depends_on、blocks
514
515
  - 每个 task 总长度 20~40 行
515
516
  - **一致性自查**:
@@ -66,6 +66,9 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
66
66
  ### 输出
67
67
  实现摘要 + 修改文件列表
68
68
 
69
+ ### 📌 末步预告:结果摘要四字段(step3 --done 硬校验,缺一被拒)
70
+ 下一步(step3)--done 必须给四字段结果摘要(推荐 \`--req/--cause/--solution/--result\` 四参数,CLI 自动合成;兼容 \`--output "需求:… 根因:… 方案:… 结果:…"\`)。其中「结果:验证情况(测试数 / lint / typecheck / 部署状态)」的素材在**本步**产生——实现摘要里请顺手记下具体验证数据(如「3 个单测通过、lint 0 告警」),step3 直接引用,避免到收尾时回忆不起来或被硬校验打回重跑。
71
+
69
72
  ### 铁律
70
73
  - 不要修改无关文件
71
74
  - 不要编造不存在的 CLI 子命令
@@ -97,7 +100,9 @@ sillyspec run quick --done --change <id> --req "一句话语义化短标题(
97
100
  结果:验证情况(测试数 / lint / typecheck / 部署状态)
98
101
  \`\`\`
99
102
 
100
- ⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。旧形式正文内避免嵌套全角冒号(标签后紧接嵌套冒号会被拆分判定缺字段)——这正是四参数形式消灭的事故面。
103
+ ⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。单行旧形式的风险仅在「正文里再次出现顶层标签字样」——需求:/根因:/方案:/结果: 字样前导恰是空白或句末标点(。;!?)时,会被边界扫描误当真实字段边界(错位/缺字段被拒)——这正是四参数形式消灭的事故面。注意区分:根因块内以列表行形态出现的嵌套子字段(\`- 现象:\` \`- 根因:\` \`- 护栏:\` \`- 证据:\`)是**合法形态**——「- 」前缀不构成顶层标签、不参与拆分判定,顶层四字段边界不受影响(postmortem 用法见下方可选提示)。
104
+
105
+ > 💡 **可选(非必填)**——根因字段属 postmortem 场景(踩坑修复、值得留档复盘)时,可改用多行 \`--output\` 形式,在根因块内按列表行补四子字段:\`根因:\` 行后换行接 \`- 现象:…\`(出了什么问题)、\`- 根因:…\`(深层原因)、\`- 护栏:…\`(防再犯措施)、\`- 证据:…\`(可追溯路径,如 sillyspec agent-log --json 输出的会话日志 jsonl 路径、review.json、verify-result.md)。该列表行形态为合法格式(顶层标签边界不受影响,详见上方说明);常规场景仍推荐四参数形式。
101
106
 
102
107
  ### 📎 文件括注 --file-notes(可选,省事后手改文件行)
103
108
  若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
@@ -27,13 +27,17 @@ export const definition = {
27
27
  ### 只允许的操作
28
28
  - git status / git diff / git show / git log / git stash list(只读)
29
29
  - cat / head / grep / find / wc(只读检查)
30
- - 写入 .sillyspec/changes/ 下的报告文件(verify-result.md)
30
+ - 写入 {SPEC_ROOT}/changes/ 下的报告文件(verify-result.md)——一律用 CLI 替换出的**主仓绝对路径**落盘,绝不写进 worktree 的 .sillyspec 副本(CLI 校验读主仓,副本随 worktree 清理蒸发)
31
31
  - 运行测试命令(不修改源码)
32
32
  - 运行 lint 命令(不自动修复)
33
33
 
34
34
  ### 长测试/构建执行铁律
35
35
  - 长测试/构建/lint 命令必须**前台同步执行**,禁止 run_in_background:true / & / nohup / disown——后台任务易被会话生命周期回收导致中断无果
36
36
 
37
+ ### 检查选择与重复执行纪律(FR-12)
38
+ - 不得为凑检查而重复执行已通过的检查——同一检查通过一次即为有效证据,重复执行只耗时不增信
39
+ - 本地验证聚焦本次变更范围(模块子集 / 针对性检查);全量测试与全仓扫描留给 CI 或用户明确要求时执行
40
+
37
41
  如果发现文件缺失或异常,**只报告问题,不尝试修复**。
38
42
  `,
39
43
 
@@ -65,11 +69,11 @@ export const definition = {
65
69
  2. 如果存在 decisions.md,必须读取并提取所有当前版本 D-xxx@vN 决策 ID
66
70
  - 如果存在 P0/P1 unresolved/blocking 决策,验证结论不能为 PASS
67
71
  - 如果发现 superseded 决策被下游引用,标记为 ⚠️ stale decision reference
68
- 3. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
69
- 4. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
70
- 5. 加载代码规范:\`cat .sillyspec/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
71
- - 测试现状:\`cat .sillyspec/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
72
- - 技术债清单:\`cat .sillyspec/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
72
+ 3. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
73
+ 4. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`(构建命令、测试命令、lint 命令等)若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
74
+ 5. 加载代码规范:\`cat {SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md 2>/dev/null\`
75
+ - 测试现状:\`cat {SPEC_ROOT}/docs/<project>/scan/TESTING.md 2>/dev/null\`(了解既有测试约定与覆盖范围,验收时对照)
76
+ - 技术债清单:\`cat {SPEC_ROOT}/docs/<project>/scan/CONCERNS.md 2>/dev/null\`(🔴/🟡 区域;本次变更若触碰须在 verify-result.md 标注)
73
77
  6. 标注每个文件的存在/不存在状态
74
78
 
75
79
  ### Execute Evidence 传递检查
@@ -83,9 +87,9 @@ export const definition = {
83
87
  - CLI 会 advisory 复核每个 cannot_verify 任务是否在 verify-result.md 体现(未体现仅 warn 不阻断归档;evidence 是否真满足由你诚实判定,CLI 不替你语义判定)
84
88
 
85
89
  ### 模块文档加载
86
- 8. 读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
90
+ 8. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过以下步骤)
87
91
  9. 根据 design.md 的文件变更清单匹配 _module-map.yaml 中的模块
88
- 10. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
92
+ 10. 读取匹配到的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`
89
93
  11. **检查模块索引可信度**:如果相关模块的 needs_review 为 true,提示"该模块索引可能不可信,需要回看模块卡片或源码"
90
94
 
91
95
  ### worktree 基线锚点(CLI 注入)
@@ -140,6 +144,9 @@ export const definition = {
140
144
  6. **模块文档一致性检查**:如果在"加载规范并锚定"步骤中加载了模块文档,检查实现是否符合模块文档描述的当前设计(特别关注接口签名、数据流、依赖关系)。不一致时**当场同步模块文档**(这是 verify 的收尾义务,不是可选项);module-impact.md「更新结果」表的文档同步项也须在本阶段回填 done/skipped——CLI 在 verify --done 时硬校验该表无 pending/待办死信行,未清即阻断完成
141
145
  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 不校验——是否真覆盖由你诚实判定)
142
146
 
147
+ ### 🩹 实现偏差 postmortem 提示(advisory,不强制——不影响 verify 结论判定)
148
+ 探针或 postcheck 检出**实现偏差**(不符合 design / 接口漂移 / 测试缺失 / 决策未闭环等)时,除按流程修复外,建议为该偏差补一条轻量 postmortem 记录进 QUICKLOG(走 quick 流程或既有条目的正文核对),根因块内按列表行写四子字段:\`- 现象:\`(偏差表现)、\`- 根因:\`(深层原因)、\`- 护栏:\`(防再犯措施)、\`- 证据:\`(可追溯路径——\`sillyspec agent-log --json\` 输出的本地会话日志 jsonl 路径、本变更 review.json、verify-result.md)。护栏结论经人工确认后归入 \`.sillyspec/knowledge/known-issues.md\`——走既有 knowledge 追加链路(同 quick 收尾先例:先入 knowledge/uncategorized.md,经知识整理确认后归类),不新建链路不新建命令。
149
+
143
150
  ### 输出
144
151
  探针报告 + 设计一致性检查结果 + 模块文档一致性检查结果 + 决策追踪矩阵(如有)`,
145
152
  outputHint: '设计一致性报告',
@@ -166,7 +173,7 @@ export const definition = {
166
173
  prompt: `运行代码质量扫描(测试实测统一由 CLI 对账执行,本步不重复手动跑全量)。
167
174
 
168
175
  ### 操作
169
- 1. 读取 \`.sillyspec/local.yaml\` 获取构建、测试和 lint 命令若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
176
+ 1. 读取 \`{SPEC_ROOT}/local.yaml\` 获取构建、测试和 lint 命令若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
170
177
  2. **不要手动重复跑 commands.test**——CLI 会在最终 --done 时统一执行一次(按变更命中模块子集),本步再跑 = 与 CLI 对账重复耗时(实测 198s×2)。如为提前发现实现问题,可对变更模块做**针对性快速冒烟**(可选,非必需):
171
178
  - Maven:\`mvn test -pl <变更模块> -am\`(仅编译变更模块及其依赖)
172
179
  - Gradle:\`./gradlew :<模块>:test\`
@@ -179,6 +186,16 @@ export const definition = {
179
186
  - **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test(同步,耗时可能较长);实测失败会直接阻断 verify 完成,谎报测试结果没有意义。commands.lint 同样会被 CLI 实测对账(advisory)
180
187
  - 冒烟测试非必需:全量/模块实测结果以 CLI 对账为准,本步跑的结果仅供你提前发现问题并写入验证报告
181
188
 
189
+ ### 检查选择指引(按变更影响面收窄,本地聚焦;FR-12)
190
+ 按本次变更的实际影响面选择检查组合,避免无差别全量:
191
+ - **行为类改动**(源码逻辑/数据结构/接口/调用关系/配置)→ 聚焦测试:只跑变更命中模块的子集(local.yaml 配 \`test_strategy: module\` + modules 映射),CLI --done 对账同口径收窄
192
+ - **文档/prompt 类改动**(*.md、docs/** 等)→ 文档检查(\`sillyspec docs check\` 口径),不跑代码测试
193
+ - **门禁/契约类改动**(gate/contract 相关文件、对外接口契约)→ 契约对账(\`sillyspec gate\` 口径)优先于跑测试
194
+ - **全量**仅在用户明确要求、或仓库级不可分变更(横切基础设施/全仓重命名等无法按模块拆分)时执行——本地聚焦,全量留给 CI 或明确要求
195
+
196
+ ### 测试策略推荐(CLI 注入;test_strategy: evidence-auto 时非空,其余策略为空)
197
+ {EVIDENCE_AUTO_RECOMMENDATION}
198
+
182
199
  ### 输出
183
200
  质量扫描结果 + 技术债务标记`,
184
201
  outputHint: '质量扫描结果 + 技术债务',
@@ -191,15 +208,15 @@ export const definition = {
191
208
  ### 操作
192
209
  1. 汇总以上所有检查结果
193
210
  2. **变更风险等级(change_risk_profile)由 CLI 自动判定与门控**:你无需自己扫描关键词。本步骤 --done 时,CLI 会用 detectChangeRisk 扫描 design.md / plan.md 自动判定等级(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical)并强制门控:integration-critical / deployment-critical 变更若结论为 PASS / PASS WITH NOTES 但缺少真实集成证据,CLI 直接阻断 verify 完成——谎报结论无效。
194
- - **判级是机械字面匹配、不认否定语境**:design 里写「本次不改动 daemon/session」仍命中关键词被误判高危级。
211
+ - **判级是机械字面匹配 + 同句否定抑制**:关键词命中位置前方同句有否定提示(不/未/无/避免…,如「本次不新增 daemon 协议」)时该次命中被抑制、不参与判级;「daemon 不稳定」「不同模块的 daemon」这类否定词不在关键词前方的仍正常命中。
195
212
  - **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
196
- - **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计。
213
+ - **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计;若有命中被否定语境抑制,同样写明被抑制关键词与理由(抑制可审计,不许用来静默降级)。
197
214
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
198
215
  - **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
199
216
 
200
- 3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`.sillyspec/changes/<change-name>/verify-result.md\`。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
217
+ 3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
201
218
  4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
202
- 5. **核对 module-impact.md**(若 changes/<change>/module-impact.md 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
219
+ 5. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
203
220
 
204
221
  ### verify-result.md 章节结构(骨架已含,占位替换即可)
205
222
 
@@ -210,7 +227,7 @@ export const definition = {
210
227
  ### 🧹 服务进程登记与自动回收(真实启动验证必读,坑 verify-service-process-leak)
211
228
  「真实启动一次」起的长驻服务(uvicorn/node server 等)**必须登记 PID,CLI 在 verify 收尾自动回收**——不登记的进程会挂死机器(实测 uvicorn 漏挂一天多):
212
229
  1. 后台启动服务并取 PID(PowerShell:\`Start-Process -PassThru\` 取 \`.Id\`;bash:\`cmd & echo $!\`)
213
- 2. 每行一个 PID 追加到 \`\${SPEC_ROOT}/.runtime/verify-services.pids\`(\`Add-Content\` / \`echo >>\`)
230
+ 2. 每行一个 PID 追加到 \`{SPEC_ROOT}/.runtime/verify-services-<change-name>.pids\`(\`Add-Content\` / \`echo >>\`;**按变更名分片**——多会话并发 verify 时各自的 --done 只回收自己的服务,互不误杀)
214
231
  3. verify \`--done\` 时 CLI 读该文件逐个 kill 并清空——你在 verify-result.md 的「生命周期终态断言」里写「PID 已登记,CLI 收尾回收」即可
215
232
  登记是硬要求:没有 PID 登记的「真实启动」证据在 deployment-critical 门控下视为不完整(进程无法证明被回收)。
216
233
 
package/src/sync.js CHANGED
@@ -415,6 +415,19 @@ export class SyncManager {
415
415
  return { synced: 0, errors: ['未指定变更名称'] };
416
416
  }
417
417
 
418
+ // 冲突降噪(坑 sync-conflict-banner-spam,2026-08-23 实证:push 冲突落 sync-conflict 文件后,
419
+ // 每步完成的 triggerSync 都会再 push 再 409 再打全幅双线横幅——「已卡死不会自愈」连环刷屏,
420
+ // 观感像致命错误。冲突文件本身就代表「待人工三选一的未决状态」,文件在 = 用户已被告知,
421
+ // 后续自动同步单行提示即可(可见但不吓人);resolve 后文件删除,推送自动恢复。fromResolve
422
+ // (keep-local 自动重推)不在此列——resolve 流程刚删冲突文件,它需要真实 push 判定平台态。
423
+ if (!fromResolve) {
424
+ const pendingConflict = this.readConflictFile(changeName);
425
+ if (pendingConflict) {
426
+ console.warn(`⚠️ [sync] 变更 ${changeName} 存在未决平台冲突,跳过自动推送。处理:sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort`);
427
+ return { synced: 0, errors: [`conflict pending: ${changeName}`], conflict: true, suppressed: true };
428
+ }
429
+ }
430
+
418
431
  // 检查变更目录是否存在(归档后目录已移走但 DB 仍有最终状态需推平台,serializeForSync 从
419
432
  // DB 读不依赖文件系统目录)。归档终态探测(坑 post-archive-sync-noise,2026-08-21 实证:
420
433
  // 归档尾声连打「目录不存在/变更不存在」warn,观感像出错):确认已归档(changes/archive/
@@ -545,6 +558,26 @@ export class SyncManager {
545
558
  continue;
546
559
  }
547
560
 
561
+ // push 侧内容一致自愈(坑 push-409-foreign-noise,2026-08-23 实证:他机/部署重推了内容
562
+ // 相同的进度——pull 侧已有 _progressContentEquals 自愈(坑 pull-deploy-noise-conflict),
563
+ // push 409 路径没有:外来噪声形态落真冲突 + 横幅 + 人工 resolve,而内容其实零分歧)。
564
+ // 409 回执的 platform_progress 与本次待推内容过同一比对(忽略时间戳列),一致 → 平台
565
+ // 「更新」只是噪声重推,本地无可丢失 → 跳过本次推送 + base_ts 推进到平台 ts(与
566
+ // resolve --keep-local 同语义,自动闭环),不落冲突文件。内容不同 → 真分歧,维持原判。
567
+ try {
568
+ if (platformProgress && this._progressContentEquals(progressData, platformProgress)) {
569
+ try {
570
+ const { ProgressManager } = await import('./progress.js');
571
+ const pm = new ProgressManager({ specDir: safePlatformSpecDir(this.cwd) });
572
+ pm._ensureDB(this.cwd).getDb().prepare(
573
+ 'UPDATE changes SET last_synced_platform_ts = MAX(?, COALESCE(last_synced_platform_ts, ?)) WHERE name = ?'
574
+ ).run(platformLastPushedAt || pushedAt, platformLastPushedAt || pushedAt, changeName);
575
+ } catch {}
576
+ console.warn(`⚠️ [sync] push 409 内容一致自愈: 平台 ts=${platformLastPushedAt} 为外来噪声重推(六表内容与本地一致),base_ts 已推进,不落冲突文件`);
577
+ return { synced: 1, errors: [], selfHealed: true, reason: 'push 409 平台内容与本地一致(外来噪声重推),base_ts 已推进' };
578
+ }
579
+ } catch { /* 比对失败维持原判(fail-closed 到真冲突分支) */ }
580
+
548
581
  // resolve --keep-local 的自动重推再撞 409:不落新冲突文件、不打「已卡死」横幅——
549
582
  // 原 conflict 已按用户裁决处理完(base_ts 已推进),此刻的 409 只是「平台在用户裁决期间
550
583
  // 又动了」;立即落新文件会破坏「keep-local 清冲突文件回 clean」的生命周期契约(冲突文件
@@ -561,7 +594,7 @@ export class SyncManager {
561
594
  console.warn('');
562
595
  console.warn('⚠️⚠️⚠️════════════════════════════════════════════════════');
563
596
  console.warn(`⚠️ 平台同步冲突:变更「${changeName}」推送被拒(base_ts 过期,平台已有更新)`);
564
- console.warn('⚠️ 该变更的自动同步已卡死,不会自愈——需人工 resolve 才能恢复:');
597
+ console.warn('⚠️ 该变更的自动同步已暂停,等待人工 resolve 恢复(期间不再重复刷本横幅,后续自动同步单行提示):');
565
598
  console.warn(`⚠️ 1. sillyspec platform status # 查看未决冲突`);
566
599
  console.warn(`⚠️ 2. sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort`);
567
600
  console.warn(`⚠️ 3. sillyspec platform sync --change ${changeName}`);
@@ -803,7 +836,12 @@ export class SyncManager {
803
836
  if (Array.isArray(v)) return v.map(strip);
804
837
  if (v && typeof v === 'object') {
805
838
  const out = {};
806
- for (const [k, val] of Object.entries(v).sort()) {
839
+ // sort 必须按 key 字符串比较(坑 content-equals-sort-typeerror,2026-08-23 实证:
840
+ // 默认比较器把 [key, value] 元素转字符串,serializeForSync 输出的 project 字段是
841
+ // null-prototype 对象(不可转原始值)→ sort 抛 TypeError 被下方 catch 吞成恒 false
842
+ // ——pull/push 两侧内容一致自愈形同虚设,部署噪声全部落真冲突人工 resolve)。
843
+ const entries = Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
844
+ for (const [k, val] of entries) {
807
845
  if (IGNORE_KEYS.has(k)) continue;
808
846
  out[k] = strip(val);
809
847
  }
@@ -1023,7 +1061,7 @@ export class SyncManager {
1023
1061
  console.warn('');
1024
1062
  console.warn('⚠️⚠️⚠️════════════════════════════════════════════════════');
1025
1063
  console.warn(`⚠️ 平台同步冲突:变更「${changeName}」pull 判定冲突(本地有未同步改动且平台已更新,base_ts 过期)`);
1026
- console.warn('⚠️ 该变更的自动同步已卡死,不会自愈——需人工 resolve 才能恢复:');
1064
+ console.warn('⚠️ 该变更的自动同步已暂停,等待人工 resolve 恢复(推送侧后续自动同步单行提示,不再重复刷本横幅):');
1027
1065
  console.warn(`⚠️ 1. sillyspec platform status # 查看未决冲突`);
1028
1066
  console.warn(`⚠️ 2. sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort`);
1029
1067
  console.warn(`⚠️ 3. sillyspec platform sync --change ${changeName}`);
@@ -679,7 +679,7 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
679
679
  try {
680
680
  let wtStatus = c.wtStatus.get(gitDir)
681
681
  if (wtStatus === undefined) {
682
- wtStatus = runGit(gitDir, ['status', '--porcelain'], { trim: false })
682
+ wtStatus = runGit(gitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
683
683
  c.wtStatus.set(gitDir, wtStatus)
684
684
  }
685
685
  if (wtStatus && wtStatus.trim().length > 0) {
@@ -1047,7 +1047,7 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
1047
1047
  const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
1048
1048
  ? meta.worktreePath
1049
1049
  : cwd
1050
- const wtStatus = runGit(wtGitDir, ['status', '--porcelain'], { trim: false })
1050
+ const wtStatus = runGit(wtGitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
1051
1051
  const wtFiles = parsePorcelainFiles(wtStatus)
1052
1052
  .map(p => String(p).replace(/\\/g, '/'))
1053
1053
  .filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
@@ -0,0 +1,21 @@
1
+ /**
2
+ * taskcard-placeholders.js — TaskCard 骨架占位符标记清单(叶子模块,无 import)。
3
+ *
4
+ * 坑 taskcard-placeholder-slip(2026-08-24 用户实证:task-03/04/06/07 留成空骨架直到人工
5
+ * 审计才发现——validatePlanFeasibility 只查字段存在性/非空,占位值全过)。标记是
6
+ * buildTaskcardSkeleton 生成的封闭集合,精确匹配零误伤;独立成叶子模块供骨架生成
7
+ * (taskcard.js)与 plan-postcheck 校验两侧同源 import——放 taskcard.js 会与
8
+ * plan-postcheck → taskcard → stages/plan → stages/index 形成 ESM 循环(TDZ 实证)。
9
+ *
10
+ * verify 字段的 `cd frontend && pnpm exec tsc --noEmit` 刻意不收——它可能是真实命令
11
+ * (前端项目 tsc 检查),拦了反而误伤;其余标记不可能出现在已填充卡里。
12
+ */
13
+ export const TASKCARD_PLACEHOLDERS = [
14
+ { field: 'requirement_ids', marker: 'FR-XX' },
15
+ { field: 'decision_ids', marker: 'D-XXX' },
16
+ { field: 'allowed_paths', marker: 'src/example/file.ts' },
17
+ { field: 'goal', marker: '一句话说明这个 task' },
18
+ { field: 'implementation', marker: '具体步骤 1' },
19
+ { field: 'acceptance', marker: '可验证的验收条件 1' },
20
+ { field: 'constraints', marker: '边界约束 1' },
21
+ ]
package/src/taskcard.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
14
14
  import { join } from 'path'
15
15
  import { git } from './git-helper.js'
16
+ import { nowWallClock } from './datetime.js'
16
17
  import { assertSafeChangeName, resolveSpecDir } from './run/shared.js'
17
18
  import { parseTaskNames, readTaskRegistryContent } from './stages/plan.js'
18
19
  import { parseTaskRegistry } from './stages/execute.js'
@@ -31,6 +32,11 @@ export function normalizeTaskId(raw) {
31
32
  return `task-${String(m[1]).padStart(2, '0')}`
32
33
  }
33
34
 
35
+ // 占位符标记清单移至叶子模块 taskcard-placeholders.js(本文件与 plan-postcheck 两侧同源;
36
+ // 留在此处会与 plan-postcheck → taskcard → stages/plan → stages/index 形成 ESM 循环 TDZ),
37
+ // 此处 re-export 保持既有 import 兼容(test/taskcard.test.mjs 等直接从本模块取)。
38
+ export { TASKCARD_PLACEHOLDERS } from './taskcard-placeholders.js'
39
+
34
40
  /**
35
41
  * YAML 标量安全序列化(坑 taskcard-title-backtick-yaml,2026-08-20 实证):plan checkbox 行名
36
42
  * 是自由文本,带反引号(如 "`-p` 参数支持")时裸插值进 frontmatter——`` ` `` 与 @ 是 YAML 保留
@@ -111,6 +117,8 @@ constraints:
111
117
  <!-- 骨架由 sillyspec taskcard 生成(LF 行尾 + frontmatter 已闭合 + 硬校验 9 字段齐全)。
112
118
  用 Edit tool 填充上方占位符(allowed_paths/goal/implementation/acceptance/verify/constraints 等),
113
119
  勿用 Write 整文件重写——会引入 CRLF 行尾/漏闭合 ---/漏字段回归。
120
+ ⚠️ plan --done 硬校验会拦截未替换的占位符(FR-XX / D-XXX / src/example/file.ts /
121
+ 一句话说明这个 task / 具体步骤 1 / 可验证的验收条件 1 / 边界约束 1)——占位符视同缺字段。
114
122
  可选字段按需插进上方 frontmatter(规则见 taskcard-rules):
115
123
  provides: 仅当本 task 给其他 task 提供接口/DTO/响应时填
116
124
  expects_from: 仅当本 task 消费其他 task 的契约时填
@@ -179,7 +187,8 @@ export function cmdTaskcard(changeName, opts = {}) {
179
187
  return 'unknown'
180
188
  }
181
189
  })()
182
- const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
190
+ // 本地墙钟(坑 taskcard-created-at-utc:toISOString 落 UTC,本地 09:39 写 01:39,子代理手工改两次)
191
+ const now = nowWallClock()
183
192
 
184
193
  const tasksDir = join(changeDir, 'tasks')
185
194
  mkdirSync(tasksDir, { recursive: true })