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
package/src/quicklog.js CHANGED
@@ -32,13 +32,15 @@ const sleep = (ms) => new Promise(r => setTimeout(r, ms))
32
32
  * 与 rename 之间),相比旧实现的必现双进已可忽略。
33
33
  * @param {string} lockPath 锁文件路径(建议与被保护文件同目录)
34
34
  * @param {() => (any|Promise<any>)} fn 临界区
35
- * @param {{staleMs?:number, timeoutMs?:number, retryMs?:number}} opts
35
+ * @param {{staleMs?:number, timeoutMs?:number, retryMs?:number, content?:string}} opts
36
+ * content:锁文件内容(默认 pid-randomId)。供超时报错时展示持有者语义(如 apply 锁写
37
+ * {pid, changeName})——仅展示用途,抢占/偷锁逻辑只比对内容一致性不解析语义。
36
38
  */
37
39
  export async function withFileLock(lockPath, fn, opts = {}) {
38
- const { staleMs = 30000, timeoutMs = 10000, retryMs = 50 } = opts
40
+ const { staleMs = 30000, timeoutMs = 10000, retryMs = 50, content = null } = opts
39
41
  mkdirSync(dirname(lockPath), { recursive: true })
40
42
  const start = Date.now()
41
- const myId = `${process.pid}-${randomBytes(6).toString('hex')}`
43
+ const myId = content || `${process.pid}-${randomBytes(6).toString('hex')}`
42
44
  let fd = null
43
45
  // 抢锁循环
44
46
  while (true) {
@@ -174,6 +176,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
174
176
  payload.timestamp = parts[1] || null
175
177
  payload.title = parts.slice(2).join('|').trim() || payload.title
176
178
  }
179
+ // 顶层标签白名单(^ 行首锚定):D-004@v1 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行)
180
+ // 因「- 」前缀不匹配本正则——它们是根因块正文内的列表行续行,不是新顶层标签,经 lastLabel 挂载
181
+ // 进 body_sections[根因](见下方 inFiles 复位注释)。顶层四字段边界解析不受影响(R-03)。
177
182
  const labelRe = /^(状态|关联变更|文件|审计|需求|根因|方案|结果)\s*[::]\s*(.*)$/
178
183
  const bulletRe = /^-\s+(.*)$/
179
184
  let inFiles = false
@@ -190,7 +195,9 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
190
195
  else if (label === '关联变更') { payload.linked_changes = value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean); inLinked = true; inFiles = false }
191
196
  else if (label === '文件') { if (value) payload.files.push(...value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean).map(p => ({ path: p, note: null }))); inFiles = true; inLinked = false }
192
197
  else if (label === '审计') { /* D-8 advisory 行:只进 raw_block 不进结构化段(body_sections 不扩 schema),并阻断续行误挂 */ lastLabel = null; inFiles = false; inLinked = false }
193
- else { payload.body_sections[label] = value; lastLabel = label }
198
+ // 进入 需求/根因/方案/结果 字段块须关闭 inFiles/inLinked 续行模式(task-07 / D-004@v1):否则根因块内
199
+ // 嵌套子字段列表行(- 现象:… 等)会命中下方「文件 bullet」分支被劫进 payload.files,从根因正文截断丢失。
200
+ else { payload.body_sections[label] = value; lastLabel = label; inFiles = false; inLinked = false }
194
201
  continue
195
202
  }
196
203
  if (bulletRe.test(stripped) && inFiles) {
@@ -332,6 +339,37 @@ export function parseFileNotes(raw) {
332
339
  .filter(Boolean)
333
340
  }
334
341
 
342
+ /**
343
+ * 校验 --file-notes 原文格式(坑 quick-file-notes-format-silent,2026-08-24 用户反馈二期②:
344
+ * 多文件分隔符是 `||` 双竖线,写错成 `|`/`,` 时整段被当成第一个文件的括注静默落盘,只能手工修
345
+ * QUICKLOG)。命令入口 fail-fast 用:每个 `||` 段必须是 `path::括注`(`::` 必填,括注可空);
346
+ * 括注内再出现 `::` 视为疑似漏写 `||` 分隔符。返回问题清单供调用方拼报错。
347
+ * @param {string} raw
348
+ * @returns {{ ok: boolean, problems: Array<{ seg: string, reason: string, hint: string }> }}
349
+ */
350
+ export function validateFileNotesFormat(raw) {
351
+ const problems = []
352
+ if (!raw) return { ok: true, problems }
353
+ for (const seg of String(raw).split('||')) {
354
+ const s = seg.trim()
355
+ if (!s) continue
356
+ const idx = s.indexOf('::')
357
+ if (idx === -1) {
358
+ problems.push({ seg: s, reason: '缺 `::` 路径/括注分隔符', hint: '格式为 path::括注;不要括注也要写 path::(括注可空)' })
359
+ continue
360
+ }
361
+ const path = s.slice(0, idx).trim()
362
+ if (!path) {
363
+ problems.push({ seg: s, reason: '`::` 前路径为空', hint: '格式为 path::括注' })
364
+ continue
365
+ }
366
+ if (s.slice(idx + 2).includes('::')) {
367
+ problems.push({ seg: s, reason: '括注内出现 `::`', hint: '疑似多文件漏写 `||` 分隔符(双竖线)——多文件写法:path1::括注1 || path2::括注2' })
368
+ }
369
+ }
370
+ return { ok: problems.length === 0, problems }
371
+ }
372
+
335
373
  // 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
336
374
  // 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
337
375
  const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
@@ -344,6 +382,8 @@ const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
344
382
  * @returns {{ ok: boolean, missing: string[] }} ok=true 通过;missing=缺失的字段名列表
345
383
  */
346
384
  export function validateQuickResult(text) {
385
+ // 子串包含判定:根因块含嵌套四子字段列表行(- 现象:… 等,task-07 / D-004@v1 合法形态)时,
386
+ // 顶层四标签仍须齐备(模板契约),嵌套行不影响判定(「- 」前缀不构成顶层标签,亦不缺字段)。
347
387
  const missing = RESULT_REQUIRED_LABELS.filter(label => !String(text || '').includes(label))
348
388
  return { ok: missing.length === 0, missing }
349
389
  }
@@ -486,6 +526,9 @@ async function checkTaskCheckbox(specBase, change, qlId) {
486
526
  // 前缀」或正则 split(/(?=需求:|根因:|方案:|结果:)/))因前导是「/|( 等非边界字符而跳过。严格失败
487
527
  // (如真实标签前导是「,」这类弱标点)退回宽松顺序扫描(上一标签之后首次出现)。缺标签返回 null → 落单行
488
528
  // 兜底(--done 契约校验仍会拦缺字段)。残余边界:正文引用标签且前导恰好是空白/句末标点时仍可能错位。
529
+ // task-07 / D-004@v1 声明:本切分只作用于「单行四字段压缩归一」路径,嵌套四子字段(- 现象:/- 根因:/
530
+ // - 护栏:/- 证据: 列表行)必须以多行列表行形态写入根因块正文——按序扫描先命中顶层「根因:」标签,
531
+ // 其后的嵌套「- 根因:」等字样落在根因字段正文内不构成新边界,无需改动三个边界函数即天然兼容(Grill C-15)。
489
532
  function isFieldBoundary(body, idx) {
490
533
  if (idx <= 0) return true
491
534
  const prev = body[idx - 1]
@@ -567,6 +610,9 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
567
610
  if (segs) body = segs.join('\n')
568
611
  }
569
612
  // 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
613
+ // 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行,D-004@v1 postmortem 形态)属
614
+ // 根因块正文:在此随多行结果逐行原样落盘(带「- 」前缀、保序含换行),不被字段边界扫描误切——
615
+ // 单行归一切分只认「- 」前缀之外的需求:/根因:/方案:/结果: 标签,嵌套行天然不参与边界判定。
570
616
  // 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
571
617
  const resultLines = /\r?\n/.test(body)
572
618
  ? body.split(/\r?\n/).filter(l => l.trim() !== '')
@@ -23,7 +23,7 @@ import { basename, join, resolve, dirname } from 'node:path'
23
23
  import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, PLATFORM_MANAGED_FILENAME } from './shared.js'
26
+ import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
@@ -319,19 +319,27 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
319
319
  try {
320
320
  const { readFileSync } = await import('fs')
321
321
  const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
322
- if (saved.specRoot) platformOpts.specRoot = saved.specRoot
323
- if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
324
- if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
325
- if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
326
- // 平台模式 fail-fast:文件存在但缺少 specRoot
327
- if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
328
- console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
329
- console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
330
- console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
331
- process.exit(2) // 环境错(平台文件损坏)→ exit 2
322
+ // 自指指针免疫(FR-4,变更 2026-08-23-repo-native-spec-backfill):saved.specRoot 经
323
+ // realpath 解析回本地 .sillyspec(repo-native junction 回环,旧模板投毒残留)→ 整体
324
+ // 忽略恢复(specRoot/runtimeRoot/workspaceId/scanRunId 全不回填,platformOpts 保持
325
+ // 无平台参数状态),按本地模式运行——内置 sync 走本地链路,specRoot 保持本地取值链。
326
+ if (isSelfReferentialSpecRoot(cwd, saved.specRoot)) {
327
+ console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}`)
328
+ } else {
329
+ if (saved.specRoot) platformOpts.specRoot = saved.specRoot
330
+ if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
331
+ if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
332
+ if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
333
+ // 平台模式 fail-fast:文件存在但缺少 specRoot
334
+ if (!platformOpts.specRoot && !platformOpts.runtimeRoot) {
335
+ console.error(`❌ 平台模式参数文件存在但缺少 specRoot/runtimeRoot: ${platformOptsFile}`)
336
+ console.error(' 可能原因:platform-scan.json 损坏或写入不完整')
337
+ console.error(' 解决:重新运行首次 scan 并传入 --spec-root')
338
+ process.exit(2) // 环境错(平台文件损坏)→ exit 2
339
+ }
340
+ // 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
341
+ specRoot = platformOpts.specRoot || specRoot
332
342
  }
333
- // 恢复成功:更新 specRoot(初始值可能是 cwd/.sillyspec,恢复后应为真实 specDir)
334
- specRoot = platformOpts.specRoot || specRoot
335
343
  } catch (e) {
336
344
  console.error(`❌ 平台模式参数文件读取失败: ${platformOptsFile}`)
337
345
  console.error(` 错误: ${e.message}`)
@@ -349,12 +357,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
349
357
  if (!platformOpts.specRoot && !platformOpts.runtimeRoot && !platformFileExists) {
350
358
  const decl = checkPlatformManaged(cwd)
351
359
  if (decl) {
352
- console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
353
- console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
354
- console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
355
- // exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
356
- // 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
357
- process.exit(1)
360
+ // 陈旧自指声明降级(FR-4,变更 2026-08-23-repo-native-spec-backfill):decl.specRoot 经
361
+ // realpath 解析回本地 .sillyspec(repo-native junction 投毒残留、指针已被清理)→ 视为
362
+ // 陈旧声明,warn 后按本地模式继续(不 exit 1)——声明描述的"平台接管"物理上指向本地
363
+ // .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
364
+ if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
365
+ console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
366
+ } else {
367
+ console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
368
+ console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
369
+ console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
370
+ // exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
371
+ // 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
372
+ process.exit(1)
373
+ }
358
374
  }
359
375
  }
360
376
  // 持久化 platformOpts
@@ -369,29 +385,6 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
369
385
  // let:下方 worktree 副本漂移守卫命中时锚回 wt.mainSpecBase(task-05/D-03)
370
386
  let specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
371
387
 
372
- // ── 本地 agent 会话日志登记 + 上报(平台会话展示用,best-effort)──
373
- // 平台/daemon 模式下 SillyHub 只能看到 CLI 阶段信息,看不到本地 agent 的实际执行日志。
374
- // 在此探测当前 agent 环境(Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG
375
- // 覆盖),把日志本地路径登记进 <runtimeRoot>/agent-session-log.json(留底)并立即
376
- // REST 上报平台(POST /api/agent-logs,与进度上报同风格;协议见
377
- // docs/platform-agent-log-protocol.md)。探测不到不发不写;任何失败只 warn 一行,
378
- // 绝不阻断 run 主流程。
379
- try {
380
- const { recordAgentLogInvocation } = await import('../agent-session-log.js')
381
- const r = await recordAgentLogInvocation({
382
- cwd,
383
- invokedCwd,
384
- platformOpts,
385
- specBase,
386
- // 只记 flag 名不记值(--output 等 flag 值是 agent 工作文本,不进产物)
387
- command: [stageName, ...flags.filter(t => typeof t === 'string' && t.startsWith('--'))].join(' '),
388
- })
389
- if (r && r.isNew && r.latestLogPath) {
390
- const pushNote = r.pushed === true ? '已上报平台' : r.pushed === false ? '上报失败(本地已留底)' : '本地留底'
391
- console.log(`📄 本地 agent 日志已登记(${pushNote}): ${r.latestLogPath}`)
392
- }
393
- } catch { /* best-effort:登记失败不影响 run 主流程 */ }
394
-
395
388
  // 漂移提醒:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
396
389
  // 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
397
390
  // 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)。仅提醒不阻断。
@@ -424,7 +417,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
424
417
  // ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
425
418
  // 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
426
419
  // 后续每次 run 直接跳过,避免重复检查 + 红叉噪声(此清理不阻塞流程、不动真实资产)。
427
- if (platformOpts.specRoot) {
420
+ // 自指守卫(FR-4,变更 2026-08-23-repo-native-spec-backfill):显式自指 --spec-root
421
+ // (repo-native junction 回环)下 cwd/.sillyspec 与 specRoot 同物理目录——它就是平台
422
+ // 规范目录本体,绝非"旧版本残留";跳过整个清理决策(含 marker 写入,防后续真平台
423
+ // 接入被误标记已清理)。否则 rmSync 会删掉 repo-native 唯一真理源并留下悬空链接。
424
+ if (platformOpts.specRoot && !isSelfReferentialSpecRoot(cwd, platformOpts.specRoot)) {
428
425
  const legacyDir = join(cwd, '.sillyspec')
429
426
  const cleanedMarker = join(cwd, '.sillyspec-platform-cleaned')
430
427
  if (!existsSync(cleanedMarker)) {
@@ -492,6 +489,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
492
489
  const inputValue = getFlagValue('--input')
493
490
  if (inputValue !== null) inputText = inputValue
494
491
 
492
+ // quick 位置参数 → 任务描述(与 auto 模式建议的 `sillyspec run quick "需求"` 用法一致)。
493
+ // 旧行为静默忽略裸 token(QUICKLOG 标题回退占位);现显式消费——跳过值类 flag 的值
494
+ // (--files src/a.js 的 src/a.js 不是位置参数),取第一个真位置 token。配合下方
495
+ // 「新会话必须带描述」启动门(坑 quick-no-input-placeholder-title)。
496
+ if (stageName === 'quick' && inputText === null) {
497
+ for (let i = 0; i < flags.length; i++) {
498
+ const t = flags[i]
499
+ if (typeof t !== 'string') continue
500
+ if (t.startsWith('--')) { if (VALUE_FLAGS.has(t)) i++; continue }
501
+ inputText = t
502
+ break
503
+ }
504
+ }
505
+
495
506
  // 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
496
507
  // 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
497
508
  // null = 未指定(走持久化/交互/兼容回退);[] = 显式 none(不关联);[...] = 显式列表
@@ -546,6 +557,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
546
557
  const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
547
558
  let quickSessionId = null
548
559
  let quickFallbackUsed = false // Q7: --done 未带 --change 时 fallback 读 current-quick-run-id 命中(区别于显式 --change quick-<hex>)
560
+ let quickSidFresh = false // 本轮刚生成全新 sessionId(非 --change 精确恢复 / 非 fallback)——供下方「新会话必须带 --input」门判定
549
561
  if (stageName === 'quick') {
550
562
  // 1373-1376 已把 quick 的 --change 值清进 linkedChanges、changeName 置 null。
551
563
  // 此处回看 --change 原始值:若恰好是单个 quick-<8hex> → 识别为本会话 sessionId(精确恢复),
@@ -574,6 +586,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
574
586
  }
575
587
  if (!quickSessionId) {
576
588
  quickSessionId = 'quick-' + randomUUID().slice(0, 8)
589
+ quickSidFresh = true
577
590
  }
578
591
  changeName = quickSessionId
579
592
  } else {
@@ -582,6 +595,43 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
582
595
  }
583
596
  }
584
597
 
598
+ // ── 本地 agent 会话日志登记 + 上报(平台会话展示用,best-effort)──
599
+ // 平台/daemon 模式下 SillyHub 只能看到 CLI 阶段信息,看不到本地 agent 的实际执行日志。
600
+ // 在此探测当前 agent 环境(Claude Code / Codex / ZCode transcript / SILLYSPEC_AGENT_LOG
601
+ // 覆盖),把日志本地路径登记进 <runtimeRoot>/agent-session-log.json(留底)并立即
602
+ // REST 上报平台(POST /api/agent-logs,与进度上报同风格;协议见
603
+ // docs/platform-agent-log-protocol.md)。探测不到不发不写;任何失败只 warn 一行,
604
+ // 绝不阻断 run 主流程。
605
+ // 位置(2026-08-23-agent-activity-sessions task-01):必须在上方 changeName/quickSessionId
606
+ // 解析完成之后——上报携带会话化上下文 context:entry 级 changeKey/quickId(平台按
607
+ // (workspace, harness, entry.ctx) 聚合建会话;quick 会话 quickId 优先且与 changeKey 互斥)
608
+ // + body 级 hubSessionId(daemon 派发平台会话时注入 env SILLYHUB_SESSION_ID,非空才带)。
609
+ // 两段解析均在顶层作用域,此处统一算一次 context 两值皆可达。
610
+ try {
611
+ const isQuickSession = stageName === 'quick' && Boolean(quickSessionId)
612
+ const hubSessionIdEnv = typeof process.env.SILLYHUB_SESSION_ID === 'string' ? process.env.SILLYHUB_SESSION_ID.trim() : ''
613
+ const agentLogContext = {
614
+ hubSessionId: hubSessionIdEnv || null,
615
+ // quick 会话 → quickId(quick-<8hex> 完整原样,不剥前缀);普通场景 → changeKey(--change 值,无则 null)
616
+ changeKey: isQuickSession ? null : (changeName || null),
617
+ quickId: isQuickSession ? quickSessionId : null,
618
+ }
619
+ const { recordAgentLogInvocation } = await import('../agent-session-log.js')
620
+ const r = await recordAgentLogInvocation({
621
+ cwd,
622
+ invokedCwd,
623
+ platformOpts,
624
+ specBase,
625
+ context: agentLogContext,
626
+ // 只记 flag 名不记值(--output 等 flag 值是 agent 工作文本,不进产物)
627
+ command: [stageName, ...flags.filter(t => typeof t === 'string' && t.startsWith('--'))].join(' '),
628
+ })
629
+ if (r && r.isNew && r.latestLogPath) {
630
+ const pushNote = r.pushed === true ? '已上报平台' : r.pushed === false ? '上报失败(本地已留底)' : '本地留底'
631
+ console.log(`📄 本地 agent 日志已登记(${pushNote}): ${r.latestLogPath}`)
632
+ }
633
+ } catch { /* best-effort:登记失败不影响 run 主流程 */ }
634
+
585
635
  // quick 关联变更存在性守卫(坑 quick-change-phantom-linked):quick 的 --change/--linked-changes
586
636
  // 被解析为关联变更后静默接受不存在的名字(quick 不建变更,链接幻影名必是笔误/语义误用——想给
587
637
  // 会话起名却传了 --change),后果是 QUICKLOG 挂悬空关联污染 change 关联图谱 + --done 时 sessionId
@@ -620,10 +670,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
620
670
  // 解析 --file-notes "path::括注 || path::括注"(quick 专用:QUICKLOG「文件:」行落盘多行括注)。
621
671
  // 旁路注入 quicklog.js(per-process setter),不经 complete 收尾路径透传(避碰多 agent 并发改的收尾热点)。
622
672
  // completeQuicklogEntry 读后即清;不传 → '' → 回填审计到的实际文件单行(向后兼容)。
673
+ // 格式 fail-fast(坑 quick-file-notes-format-silent,2026-08-24):分隔符写错(`|`/`,` 等)时
674
+ // 整段会被静默挤进第一个文件的括注、QUICKLOG 落盘后只能手工修——解析处直接拦。
623
675
  let quickFileNotes = ''
624
676
  const fileNotesValue = getFlagValue('--file-notes')
625
677
  if (fileNotesValue !== null) {
626
678
  quickFileNotes = fileNotesValue
679
+ const { validateFileNotesFormat } = await import('../quicklog.js')
680
+ const fv = validateFileNotesFormat(quickFileNotes)
681
+ if (!fv.ok) {
682
+ console.error(`❌ --file-notes 格式不对(${fv.problems.length} 处),已拒绝执行(旧行为会静默挤错进 QUICKLOG,只能手工修):`)
683
+ for (const p of fv.problems.slice(0, 5)) {
684
+ console.error(` 「${p.seg.slice(0, 80)}」${p.reason}——${p.hint}`)
685
+ }
686
+ if (fv.problems.length > 5) console.error(` …等共 ${fv.problems.length} 处`)
687
+ console.error(' 正确格式: --file-notes "path1::括注1 || path2::括注2"(`||` 双竖线分隔多文件,`::` 分隔路径与括注)')
688
+ process.exit(2) // 用法错(--file-notes 格式非法)→ exit 2
689
+ }
627
690
  }
628
691
  setQuickFileNotes(quickFileNotes)
629
692
 
@@ -693,6 +756,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
693
756
  process.exit(0)
694
757
  }
695
758
 
759
+ // 坑 quick-no-input-placeholder-title(2026-08-24 用户反馈二期①):全新 quick 会话(上方
760
+ // 刚生成新 sessionId)不带 --input 且无 --linked-changes 可取标题 → 拒绝启动(exit 2)。
761
+ // 旧行为只 warn 提示「建议放弃重启」,占位标题「(quick 任务)」已落盘、只能 reset 重来——
762
+ // 语义标题要拖到最终 --done 才回填,平台「快速修复」列表全程隐藏。此刻零沉没成本(progress
763
+ // 行/QUICKLOG/guard 均未创建),直接拒掉最便宜。放在 --help 短路之后(help 查询不该被拦)、
764
+ // 任何会话副作用之前。精确恢复(--change quick-<hex>)与 done-like(--done/--reset/--cancel/
765
+ // --status/--skip/--reopen)不受限;--linked-changes 启动可从关联变更 proposal/design 提取标题。
766
+ if (stageName === 'quick' && quickSidFresh
767
+ && !(isDone || isStatus || isSkip || isReset || isReopen || isCancel)
768
+ && !inputText && !(Array.isArray(linkedChanges) && linkedChanges.length > 0)) {
769
+ console.error('❌ 新 quick 会话必须带 --input "<一句话任务描述>"(或 --linked-changes <a,b> 从关联变更取标题)。')
770
+ console.error(' 不带描述启动会落「(quick 任务)」占位标题:平台「快速修复」列表隐藏占位条目、语义标题要拖到最终 --done 才回填,长任务全程不可见。')
771
+ console.error(' 用法: sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
772
+ process.exit(2) // 用法错(新会话无任务描述)→ exit 2
773
+ }
774
+
696
775
  const isAuxiliary = auxiliaryStages.includes(stageName)
697
776
  // scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
698
777
 
@@ -25,7 +25,7 @@
25
25
  import { basename, join, resolve, relative, isAbsolute } from 'node:path'
26
26
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
27
27
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
28
- import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
28
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations } from './shared.js'
29
29
  import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
30
30
  import { stageRegistry } from '../stages/index.js'
31
31
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
@@ -431,7 +431,11 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
431
431
  console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
432
432
  return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
433
433
  }
434
- const archivedDir = await archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts)
434
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):archiveChangeDirectory 改主仓共享状态(目录
435
+ // rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
436
+ // 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
437
+ const { withMainRepoLock } = await import('../worktree-apply.js')
438
+ const archivedDir = await withMainRepoLock(cwd, changeName, 'archive-finalize', () => archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts))
435
439
  if (archivedDir && existsSync(archivedDir)) {
436
440
  // 内存快照同步(坑 archive-progress-show-stale,2026-08-21 实证):archiveChangeDirectory 内
437
441
  // unregisterChange 已在 DB 写 current_stage='archive'(终态一致化),但本进程 progress 是命令
@@ -863,6 +867,11 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
863
867
  // (平台模式 resolveSpecDir(cwd) 会 miss specRoot 静默丢信号,plan 审查 gap-5)
864
868
  specBase,
865
869
  projectName: progress?.project || null,
870
+ // 他者 active 会话声明索引(坑 foreign-session-declared-false-block):多 agent 并发时
871
+ // 并行会话 --files 声明的文件退栈归该会话审计,不再误拦本会话。与上方 guard 读取同源
872
+ // (resolveQuickSessionsDir 同参,平台模式 runtimeRoot 对齐);fail-open:采集失败 → 空
873
+ // → 回到无豁免现状。
874
+ otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, specBase, changeName),
866
875
  }
867
876
  review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
868
877
  printQuickAuditReview(review)
@@ -913,8 +922,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
913
922
  const isLastStep = currentIdx === steps.length - 1
914
923
  if (isLastStep && !outputText) {
915
924
  console.error('\n❌ quick 最后一步 --done 必须带 --output(四字段结果模板)。')
916
- console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),照抄模板:')
917
- console.error(' sillyspec run quick --done --change <changeName> --output "需求:… 根因:… 方案:… 结果:…"')
925
+ console.error(' --output 是 QUICKLOG「结果:」归档的唯一来源。补全后重跑 --done(不丢进度),推荐四参数形式(CLI 自动合成,无嵌套冒号事故面):')
926
+ console.error(' sillyspec run quick --done --change <changeName> --req "一句话语义化短标题" --cause "为什么这样改" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"')
927
+ console.error(' 兼容旧形式:--output "需求:… 根因:… 方案:… 结果:…"')
918
928
  steps[currentIdx].status = 'pending'
919
929
  steps[currentIdx].completedAt = null
920
930
  process.exit(1)
@@ -924,7 +934,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
924
934
  if (!resultCheck.ok) {
925
935
  console.error('\n' + getRule('quick.result-labels').failMessage.replaceAll('${missing}', resultCheck.missing.join('、')))
926
936
  console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
927
- console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
937
+ console.error(` 补全后重跑 --done(不丢进度)。推荐四参数形式(CLI 自动合成结构化模板,避免旧形式嵌套全角冒号被拆分判定缺字段):`)
938
+ console.error(` sillyspec run quick --done --change <changeName> --req "一句话语义化短标题(即 QUICKLOG 条目标题)" --cause "为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"`)
939
+ console.error(` 或照抄旧形式模板:`)
928
940
  console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
929
941
  console.error(` 根因:为什么这样改(纯新增/样式则写「无,纯新增/纯样式」)`)
930
942
  console.error(` 方案:怎么改的`)
@@ -1331,22 +1343,39 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
1331
1343
  } else if (meta.mode === 'native-worktree') {
1332
1344
  console.log('🔗 Worktree: kept (外部隔离环境)');
1333
1345
  } else {
1334
- // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)
1335
- const check = wm.hasUnappliedChanges(changeName);
1336
- if (check.hasChanges) {
1337
- console.log(`🔗 Worktree: pending apply (${check.changedFiles.length} 个未应用变更)`);
1338
- console.log(` 下一步: sillyspec worktree apply ${changeName}`);
1339
- } else {
1340
- const cleanResult = wm.cleanup(changeName);
1341
- console.log(`🔗 Worktree: ${cleanResult.result}`);
1342
- if (cleanResult.residual?.length > 0) {
1343
- console.warn(` ⚠️ 清理残留: ${cleanResult.residual.join('; ')}`);
1344
- console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
1345
- } else if (cleanResult.details?.length > 0) {
1346
- for (const d of cleanResult.details) {
1347
- if (d.startsWith('⚠️')) console.log(` ${d}`);
1346
+ // in-place 模式不再短路:cleanup 现在能安全处理 in-place(只清 meta,不碰主工作区)。
1347
+ // 主仓互斥锁(坑 main-repo-no-mutex 二批):检查+清理与并行会话的 apply/cleanup 互斥
1348
+ // (防 TOCTOU:检查时他者正在 apply → 判定漂移)。best-effort:锁超时不阻断 execute
1349
+ // 完成,降级为保留 worktree + 手动清理指引。
1350
+ try {
1351
+ const { withMainRepoLock } = await import('../worktree-apply.js')
1352
+ const cleanResult = await withMainRepoLock(cwd, changeName, 'execute-cleanup', () => {
1353
+ const check = wm.hasUnappliedChanges(changeName);
1354
+ if (check.hasChanges) {
1355
+ return { kept: true, check }
1356
+ }
1357
+ return { kept: false, result: wm.cleanup(changeName) }
1358
+ })
1359
+ if (cleanResult.kept) {
1360
+ console.log(`🔗 Worktree: pending apply (${cleanResult.check.changedFiles.length} 个未应用变更)`);
1361
+ console.log(` 下一步: sillyspec worktree apply ${changeName}`);
1362
+ } else {
1363
+ console.log(`🔗 Worktree: ${cleanResult.result.result}`);
1364
+ if (cleanResult.result.residual?.length > 0) {
1365
+ console.warn(` ⚠️ 清理残留: ${cleanResult.result.residual.join('; ')}`);
1366
+ console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`);
1367
+ } else if (cleanResult.result.details?.length > 0) {
1368
+ for (const d of cleanResult.result.details) {
1369
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
1370
+ }
1348
1371
  }
1349
1372
  }
1373
+ } catch (lockErr) {
1374
+ if (/互斥锁被占用/.test(String(lockErr.message))) {
1375
+ console.log(`🔗 Worktree 自动清理跳过(主仓互斥锁被他者会话持有)——稍后手动: sillyspec worktree cleanup ${changeName}`);
1376
+ } else {
1377
+ console.warn(`⚠️ worktree 清理异常(不阻断): ${lockErr.message}`);
1378
+ }
1350
1379
  }
1351
1380
  }
1352
1381
  } catch (e) {
@@ -996,8 +996,11 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
996
996
  // requiresWait 语义前置(坑 archive-step3-wait-answer-hint-late):--continue --answer 只是中继回答,
997
997
  // 本步会回到待执行(回答后还需执行动作再 --done 收尾);不想两段式可用 --done --answer 一步完成。
998
998
  // 提示落在标记 --wait 的此刻,而非等 agent 撞 --done 报错才知道。
999
- if (stepDef.requiresWait === true || currentStep.requiresWait === true) {
1000
- console.log(` 注:本步为 requiresWait 步骤——--continue --answer 后本步回到待执行,完成动作后需再 --done 收尾`)
999
+ // conditionalWait(坑 archive-subconfirm-redundant 后 archive step3 降级软门)+ repeatableWait 的
1000
+ // --wait 同样回 pending(continueStep shouldReturnToCurrentStep),提示同样适用。
1001
+ if (stepDef.requiresWait === true || currentStep.requiresWait === true
1002
+ || stepDef.conditionalWait === true || currentStep.conditionalWait === true) {
1003
+ console.log(` 注:本步为需回执的等待步骤(requiresWait/conditionalWait)——--continue --answer 后本步回到待执行,完成动作后需再 --done 收尾`)
1001
1004
  console.log(` 或一步完成:sillyspec run ${stageName} --done --answer "你的选择"${changeName ? ` --change ${changeName}` : ''} --output "你的摘要"`)
1002
1005
  }
1003
1006
  }
package/src/run/gates.js CHANGED
@@ -463,6 +463,16 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
463
463
  */
464
464
  export async function runStageCompletionGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, ctx = null }) {
465
465
  const projectName = progress.project || basename(cwd)
466
+ // decisions.md header 自动补齐(坑 decisions-header-late-warning,2026-08-24 用户反馈二期):
467
+ // brainstorm step8 旧模板自带无 frontmatter 的 decisions 样例,存量变更照抄必缺 author/created_at,
468
+ // 拖到平台同步/后续环节才提示。gate 前幂等补机械字段(先例:autoCheckPlanFromReviews 同层写
469
+ // tasks.md),validator 读到的即已补齐版——补的是 CLI 可自证的机械字段,不替 agent 产出语义内容。
470
+ if (['brainstorm', 'plan', 'verify'].includes(stageName) && changeName) {
471
+ try {
472
+ const { ensureDecisionDocHeader } = await import('../stage-contract.js')
473
+ ensureDecisionDocHeader(resolveChangeDir(cwd, changeName, platformOpts?.specRoot))
474
+ } catch { /* 补齐失败不阻断 gate */ }
475
+ }
466
476
  const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot })
467
477
  if (contractResult.errors.length > 0) {
468
478
  console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
@@ -498,6 +508,37 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
498
508
  printVerifyTestCheck(testCheck)
499
509
  if (testCheck.status === 'failed') {
500
510
  console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
511
+ // 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):实测跑在主仓共享工作区,并行会话
512
+ // 在途 WIP 物理在场——他者文件命中声明集时提示复验归因(EADDRINUSE 鉴别同款风格),不静默背锅。
513
+ try {
514
+ const { collectForeignDeclaredFiles } = await import('../verify-postcheck.js')
515
+ const { gitQuiet } = await import('../git-helper.js')
516
+ const foreignMap = collectForeignDeclaredFiles(cwd, changeName)
517
+ if (foreignMap.size > 0) {
518
+ const dirty = (gitQuiet(cwd, ['diff', '--name-only', 'HEAD']) || '')
519
+ .split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
520
+ const hitForeign = dirty.filter(f => foreignMap.has(f))
521
+ if (hitForeign.length > 0) {
522
+ console.error(` ℹ️ 归因提示:主仓检出 ${hitForeign.length} 个并行会话声明的在途文件(${hitForeign.slice(0, 5).join(', ')}${hitForeign.length > 5 ? ' 等' : ''})——实测失败可能混入他者 WIP 而非本变更问题。待其提交/收尾后复验,仍失败才是本变更的。`)
523
+ }
524
+ }
525
+ // 第二判据(坑 derived-artifact-stale-baseline):本变更 apply 的文件(apply-pathspec 落盘)
526
+ // 与主仓近期提交重叠——本变更旧基线的生成产物可能覆盖了已合入内容,先在新基线重跑生成命令再复验
527
+ try {
528
+ const pathspecFile = join(resolveRuntimeRoot(platformOpts, specBase), `apply-pathspec-${changeName}.txt`)
529
+ if (changeName && existsSync(pathspecFile)) {
530
+ const ownFiles = new Set(readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(f => f.replace(/\\/g, '/')))
531
+ if (ownFiles.size > 0) {
532
+ const recent = (gitQuiet(cwd, ['log', '-n', '10', '--name-only', '--pretty=format:']) || '')
533
+ .split('\n').map(l => l.trim().replace(/\\/g, '/')).filter(Boolean)
534
+ const overlap = [...new Set(recent)].filter(f => ownFiles.has(f))
535
+ if (overlap.length > 0) {
536
+ console.error(` ℹ️ 归因提示②:本变更有 ${overlap.length} 个文件与主仓最近 10 条提交重叠(${overlap.slice(0, 5).join(', ')}${overlap.length > 5 ? ' 等' : ''})——若为生成产物(api-types 等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令(如 gen:types)再复验。`)
537
+ }
538
+ }
539
+ }
540
+ } catch { /* 判据②失败不影响主流程 */ }
541
+ } catch { /* 归因提示失败不影响阻断语义 */ }
501
542
  console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
502
543
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
503
544
  }
@@ -543,29 +584,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
543
584
  }
544
585
  }
545
586
  }
546
- // ── verify 服务进程回收(坑 verify-service-process-leak,2026-08-21 实证:真实启动验证
547
- // 起的 uvicorn 漏挂一天多)。verify prompt 要求长驻服务 PID 登记 .runtime/verify-services.pids,
548
- // 此处收尾逐个 kill + 清文件。best-effort:ESRCH(已退出)静默,kill 失败 warn 不阻断 verify。
587
+ // ── verify 服务进程回收(坑 verify-service-process-leak;坑 verify-pids-cross-session-kill
588
+ // 见 reapVerifyServices 注释)──
549
589
  try {
550
- const servicesPath = join(resolveRuntimeRoot(platformOpts, specBase), 'verify-services.pids')
551
- if (existsSync(servicesPath)) {
552
- const pids = readFileSync(servicesPath, 'utf8').split('\n').map(l => l.trim()).filter(l => /^\d+$/.test(l))
553
- const reaped = []; const failed = []
554
- for (const pid of pids) {
555
- try { process.kill(parseInt(pid, 10), 'SIGTERM'); reaped.push(pid) }
556
- catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.message})`) }
557
- }
558
- try { unlinkSync(servicesPath) } catch {}
559
- if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个(PID: ${reaped.join(', ')})`)
560
- // CLI 回执(坑 verify-literal-evidence-mismatch):回收即真实启动的结构化证据——
561
- // checkIntegrationEvidence(stage-contract)读回执注入匹配,agent 措辞差异不再误拦
562
- try {
563
- writeFileSync(join(dirname(servicesPath), 'verify-services.receipt.json'), JSON.stringify({
564
- change: changeName || null, reapedPidCount: reaped.length, reapedAt: new Date().toISOString(),
565
- }, null, 2) + '\n')
566
- } catch {}
567
- if (failed.length > 0) console.warn(`⚠️ 服务进程回收失败 ${failed.length} 个:${failed.join(', ')} — 手动处理:taskkill /PID <pid> /F(Windows)/ kill -9 <pid>`)
568
- }
590
+ reapVerifyServices(platformOpts, specBase, changeName)
569
591
  } catch (e) { console.warn(`⚠️ verify 服务进程回收异常(不阻断): ${e.message}`) }
570
592
 
571
593
  console.log('\n✅ 验证通过,下一步:sillyspec run archive')
@@ -1027,3 +1049,46 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
1027
1049
  return null
1028
1050
  }
1029
1051
 
1052
+
1053
+ /**
1054
+ * verify 服务进程回收(verify --done 收尾调用)。
1055
+ *
1056
+ * 坑 verify-service-process-leak(2026-08-21):真实启动验证起的长驻服务(uvicorn 等)漏挂
1057
+ * 一天多——verify prompt 要求 PID 按变更分片登记 .runtime/verify-services-<change>.pids。
1058
+ * 坑 verify-pids-cross-session-kill(2026-08-23):原单文件无归属,A 会话 --done 会把 B 正在
1059
+ * 收集 Runtime Evidence 的服务一并杀掉——分片后各回收各的;旧单文件兼容回收(升级过渡期)。
1060
+ * 回执同样按变更分片(原单份被并行会话后写覆盖),checkIntegrationEvidence(stage-contract)
1061
+ * 读分片名注入证据匹配。best-effort:ESRCH(已退出)静默,kill 失败 warn 不阻断 verify。
1062
+ *
1063
+ * @returns {{ reaped: number, receiptPath: string|null }}
1064
+ */
1065
+ export function reapVerifyServices(platformOpts, specBase, changeName) {
1066
+ const runtimeDir = resolveRuntimeRoot(platformOpts, specBase)
1067
+ const reapFile = (path, label) => {
1068
+ if (!existsSync(path)) return null
1069
+ const pids = readFileSync(path, 'utf8').split('\n').map(l => l.trim()).filter(l => /^\d+$/.test(l))
1070
+ const reaped = []; const failed = []
1071
+ for (const pid of pids) {
1072
+ try { process.kill(parseInt(pid, 10), 'SIGTERM'); reaped.push(pid) }
1073
+ catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.message})`) }
1074
+ }
1075
+ try { unlinkSync(path) } catch {}
1076
+ if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个${label}(PID: ${reaped.join(', ')})`)
1077
+ return { reaped: reaped.length, failed }
1078
+ }
1079
+ const own = reapFile(join(runtimeDir, `verify-services-${changeName}.pids`), '')
1080
+ const legacy = reapFile(join(runtimeDir, 'verify-services.pids'), '(旧格式单文件,兼容回收)')
1081
+ const reapedTotal = (own?.reaped || 0) + (legacy?.reaped || 0)
1082
+ const failedAll = [...(own?.failed || []), ...(legacy?.failed || [])]
1083
+ let receiptPath = null
1084
+ if (reapedTotal > 0) {
1085
+ try {
1086
+ receiptPath = join(runtimeDir, `verify-services-${changeName}.receipt.json`)
1087
+ writeFileSync(receiptPath, JSON.stringify({
1088
+ change: changeName || null, reapedPidCount: reapedTotal, reapedAt: new Date().toISOString(),
1089
+ }, null, 2) + '\n')
1090
+ } catch { receiptPath = null }
1091
+ }
1092
+ if (failedAll.length > 0) console.warn(`⚠️ 服务进程回收失败 ${failedAll.length} 个:${failedAll.join(', ')} — 手动处理:taskkill /PID <pid> /F(Windows)/ kill -9 <pid>`)
1093
+ return { reaped: reapedTotal, receiptPath }
1094
+ }