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/run/prompt.js CHANGED
@@ -23,6 +23,7 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
23
23
  import { stageRegistry } from '../stages/index.js'
24
24
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
25
25
  import { renderStageContract } from '../stage-contract-spec.js'
26
+ import { nowWallClock } from '../datetime.js'
26
27
  import { parseModuleMapSimple } from '../modules.js'
27
28
  import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
28
29
 
@@ -228,16 +229,18 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
228
229
  promptText = promptText.replace(/<git-user>/g, 'unknown')
229
230
  }
230
231
  }
231
- // 替换时间戳占位符
232
+ // 替换时间戳占位符(本地墙钟统一走 nowWallClock——坑 taskcard-created-at-utc:人读字段用
233
+ // UTC 会偏一个时区,子代理手工改)
232
234
  const now = new Date()
233
- const nowDatetime = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + ' ' + String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0') + ':' + String(now.getSeconds()).padStart(2,'0')
235
+ const nowDatetime = nowWallClock(now)
234
236
  const nowTimestamp = now.getFullYear() + String(now.getMonth()+1).padStart(2,'0') + String(now.getDate()).padStart(2,'0') + '-' + String(now.getHours()).padStart(2,'0') + String(now.getMinutes()).padStart(2,'0') + String(now.getSeconds()).padStart(2,'0')
235
237
  const nowDate = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
236
238
  promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
237
239
  promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
238
240
  promptText = promptText.replace(/<now-date>/g, nowDate)
239
- // <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)
240
- promptText = promptText.replace(/<now-iso-datetime>/g, now.toISOString().slice(0, 19).replace('T', ' '))
241
+ // <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)。
242
+ // 占位符名带 iso 是历史遗留——值已统一为本地墙钟(同 <now-datetime>),改名会破坏 stages 引用。
243
+ promptText = promptText.replace(/<now-iso-datetime>/g, nowWallClock(now))
241
244
  // <git-head-short>(scan frontmatter source_commit 用;CLI 代查,agent 勿自跑 rev-parse)
242
245
  if (promptText.includes('<git-head-short>')) {
243
246
  let headShort = 'unknown'
@@ -423,6 +426,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
423
426
  }
424
427
  }
425
428
 
429
+ // 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
430
+ // {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
431
+ // (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
432
+ // runtime JSON(与 KNOWLEDGE_HIT_REPORT 的 json 落盘同口径)。advisory 展示,不改变步骤流程;
433
+ // 全降级不抛(异常 → 单行说明)。
434
+ if (stageName === 'brainstorm' && promptText.includes('{DECISION_HITS}')) {
435
+ try {
436
+ const { matchKnowledge } = await import('../knowledge-match.js')
437
+ const decSpecBase = resolvePromptSpecBase(platformOpts, cwd)
438
+ const decKnowledgeDir = join(decSpecBase, 'knowledge')
439
+ // taskContext:changeName(brainstorm Step2 时 tasks.md 尚未生成,变更名是唯一稳定任务
440
+ // 语料;与 execute 分支的 changeName 基底同口径)
441
+ const decResult = matchKnowledge(decKnowledgeDir, changeName || '')
442
+ const rejectedHits = (decResult.decisionHits || []).filter(h => h.status === 'rejected')
443
+ let decInjected = ''
444
+ if (rejectedHits.length > 0) {
445
+ const lines = [
446
+ '⚠️ 否决决策提示(历史已否决,防复潮)——下列决策此前已被否决。看到否决理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据:'
447
+ ]
448
+ for (const h of rejectedHits) {
449
+ lines.push(`- ${h.id} ${h.title}(${h.file})`)
450
+ lines.push(` - 否决理由:${h.reason || '(未记录)'}`)
451
+ lines.push(` - 复潮条件:${h.revisitWhen || '(未记录)'}`)
452
+ }
453
+ decInjected = '\n\n' + lines.join('\n')
454
+ }
455
+ promptText = promptText.replace(/\{DECISION_HITS\}/g, decInjected)
456
+ // 注入结果同步落 runtime JSON(与既有 KNOWLEDGE_HIT_REPORT 落盘口径一致)
457
+ const decRuntimeDir = join(decSpecBase, '.runtime')
458
+ mkdirSync(decRuntimeDir, { recursive: true })
459
+ writeFileSync(join(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
460
+ } catch (e) {
461
+ promptText = promptText.replace(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
462
+ }
463
+ }
464
+
426
465
  // Knowledge hit report: execute 阶段注入匹配结果
427
466
  if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
428
467
  try {
@@ -458,53 +497,29 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
458
497
  // docs-debt 模块文档欠账事实注入(2026-08-15 docs-debt-inject D-006,债单第六节"CLI 算事实"):
459
498
  // Wave prompt {DOCS_DEBT} → computeDocsDebt(git diff × module-map)facts;空 facts 替换为空串
460
499
  // (无债零输出,无残留占位符)。changedFiles 口径(D-001/FR-007):worktree 根
461
- // (meta.json 所在)git status --porcelain + diff baselineCommit..HEAD 并集;无 worktree
462
- // (in-place/非 execute worktree 场景)退 cwd。全降级不抛(异常 → 单行说明)。
500
+ // (meta.json 所在)git status --porcelain + diff baselineCommit..HEAD 并集——公共实现已抽至
501
+ // docs-debt.collectExecuteChangedFiles(execute.js Wave 步渲染点同源复用,勿双写)。
502
+ // 全降级不抛(异常 → 单行说明)。
463
503
  if (stageName === 'execute' && promptText.includes('{DOCS_DEBT}')) {
464
504
  try {
465
- const { computeDocsDebt } = await import('../docs-debt.js')
466
505
  const debtSpecBase = resolvePromptSpecBase(platformOpts, cwd)
467
506
  const debtProjectName = dbProjectName || basename(cwd)
468
- // worktree 根解析:changeName → <specBase>/.runtime/worktrees/<changeName>/ 存在即锚
469
- let debtProjectRoot = cwd
470
- const wtRoot = join(debtSpecBase, '.runtime', 'worktrees', changeName || '')
471
- if (changeName && existsSync(wtRoot)) {
472
- debtProjectRoot = wtRoot
473
- }
474
- // changedFiles:已提交(baseline..HEAD)∪ 未提交(status porcelain,非未跟踪)
475
- const changed = new Set()
476
- const { safeGit } = await import('../git-helper.js')
477
- const statusRes = safeGit(debtProjectRoot, ['status', '--porcelain'], { trim: false })
478
- if (!statusRes.error && statusRes.value) {
479
- for (const line of statusRes.value.split('\n')) {
480
- if (!line.trim()) continue
481
- // "XY path" / rename "R old -> new" 取 new
482
- const pathPart = line.slice(3).split('->').pop().trim().replace(/"/g, '')
483
- if (pathPart && !pathPart.startsWith('.sillyspec/')) changed.add(pathPart.replace(/\\/g, '/'))
484
- }
485
- }
486
- const { readFileSync: rf } = await import('node:fs')
487
- const metaPath = join(wtRoot, 'meta.json')
488
- if (changeName && existsSync(metaPath)) {
489
- try {
490
- const meta = JSON.parse(rf(metaPath, 'utf8'))
491
- if (meta.baselineCommit) {
492
- const diffRes = safeGit(debtProjectRoot, ['diff', '--name-only', `${meta.baselineCommit}..HEAD`], { trim: false })
493
- if (!diffRes.error && diffRes.value) {
494
- for (const f of diffRes.value.split('\n')) {
495
- if (f.trim() && !f.trim().startsWith('.sillyspec/')) changed.add(f.trim().replace(/\\/g, '/'))
496
- }
497
- }
498
- }
499
- } catch { /* meta 读失败只用未提交集 */ }
500
- }
507
+ const { collectExecuteChangedFiles, computeDocsDebt, computeDecisionTouches, renderDecisionTouchFacts } = await import('../docs-debt.js')
508
+ const { root: debtProjectRoot, changedFiles } = collectExecuteChangedFiles({ specBase: debtSpecBase, changeName, cwd })
501
509
  const result = computeDocsDebt({
502
510
  projectRoot: debtProjectRoot,
503
511
  specBase: debtSpecBase,
504
512
  projectName: debtProjectName,
505
- changedFiles: [...changed],
513
+ changedFiles,
506
514
  })
507
- promptText = promptText.replace(/\{DOCS_DEBT\}/g, result.facts)
515
+ // 决策锚点触碰事实(2026-08-24-decision-touch-cli-drift task-01,W-A 次渲染点):重入/reset
516
+ // 场景 changedFiles 非空时呈现。advisory 追加渲染分支——既有 docs-debt facts 逐字不变,
517
+ // 无触碰 render 返回 ''(零输出);主渲染点在 execute.js buildWavePrompt(同源 facts 计算)。
518
+ let debtTouchFacts = ''
519
+ try {
520
+ debtTouchFacts = renderDecisionTouchFacts(computeDecisionTouches(changedFiles, join(debtSpecBase, 'knowledge')).touches)
521
+ } catch { /* 触碰计算降级为空——不影响欠账 facts 注入 */ }
522
+ promptText = promptText.replace(/\{DOCS_DEBT\}/g, [result.facts, debtTouchFacts].filter(Boolean).join('\n'))
508
523
  } catch (e) {
509
524
  promptText = promptText.replace(/\{DOCS_DEBT\}/g, `[docs-debt] 欠账计算异常(${e.message}),跳过`)
510
525
  }
@@ -677,6 +692,33 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
677
692
  promptText = promptText.split('{WORKTREE_BASELINE_INFO}').join(baselineInfo)
678
693
  }
679
694
 
695
+ // verify「运行测试和质量扫描」evidence-auto 推荐注入(task-12,FR-11 / D-005@v2):
696
+ // verify-postcheck 在 --done 事后解析 evidence-auto,agent 写 verify-result.md 时看不到
697
+ // 推荐组合、无从否决——故在 prompt 时点经 {EVIDENCE_AUTO_RECOMMENDATION} 把 task-11
698
+ // resolveTestStrategy 的 evidence_auto_recommendation.summary 渲染进 step prompt(含推荐
699
+ // 理由、降级注记与「可在 verify-result.md 否决并改跑全量」路径),供用户否决(FR-11)。
700
+ // 仅 test_strategy=evidence-auto 且 recommendation 非空时渲染;full/module/skip/未配置
701
+ // (recommendation=null)替换为空串零输出。fail-soft:读取/解析异常降级单行说明不抛,
702
+ // 绝不阻断 verify prompt 输出(对齐上方 {WORKTREE_BASELINE_INFO} 注入先例)。
703
+ if (stageName === 'verify' && promptText.includes('{EVIDENCE_AUTO_RECOMMENDATION}')) {
704
+ let eaInjected = ''
705
+ try {
706
+ const { resolveTestStrategy } = await import('../verify-postcheck.js')
707
+ const eaSpecBase = resolvePromptSpecBase(platformOpts, cwd)
708
+ // 信任边界:local.yaml 只读 specBase(平台 specRoot / 漂移锚定主仓 / cwd 本地),与
709
+ // runVerifyTestCheck 的 specBase 来源同口径,不读 agent 可写的 worktree 副本。
710
+ const eaYamlPath = join(eaSpecBase, 'local.yaml')
711
+ const eaYamlText = existsSync(eaYamlPath) ? readFileSync(eaYamlPath, 'utf8') : null
712
+ const eaChangeDir = changeName ? join(eaSpecBase, 'changes', changeName) : null
713
+ const resolution = resolveTestStrategy({ yamlText: eaYamlText, changeDir: eaChangeDir })
714
+ const rec = resolution.evidence_auto_recommendation
715
+ if (rec && rec.summary) eaInjected = rec.summary
716
+ } catch (e) {
717
+ eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
718
+ }
719
+ promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
720
+ }
721
+
680
722
  // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
681
723
  if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
682
724
  const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
@@ -68,6 +68,17 @@ export function printQuickAuditReview(review) {
68
68
  console.warn(` 它们归关联变更自己的流程管;若确系本会话需要改,用 --files 显式声明归属。`)
69
69
  }
70
70
 
71
+ // 他者会话声明豁免提示(坑 foreign-session-declared-false-block,独立于三态都打——放行可见
72
+ // 可审计,不静默,与上块同哲学):多 agent 并发时并行 quick 会话 --files 已声明的文件在本会话
73
+ // 窗口内出现——不属本会话,已退栈归该会话审计,本会话无需 --force-baseline。
74
+ if (Array.isArray(review.foreignSessionDeclared) && review.foreignSessionDeclared.length > 0) {
75
+ console.warn(`
76
+ 🔗 ${review.foreignSessionDeclared.length} 个文件已由其他 active quick 会话声明——已剔除出本会话审计,归该会话边界管:`)
77
+ for (const x of review.foreignSessionDeclared) console.warn(` - ${x.file} ← ${(x.sessions || []).join(', ')}`)
78
+ console.warn(` 并发下无需 --force-baseline:这些文件不属本会话。若确系本会话改动,恢复会话时 --files 追加声明归属(sillyspec run quick --files <file> --change <本会话ID>);`)
79
+ console.warn(` 他者会话若为崩溃残留(超 7 天未收尾),sillyspec run quick --cancel --change <他者会话ID> 清理后声明即失效。`)
80
+ }
81
+
71
82
  // D-8 文档欠账显性化(advisory,独立于 status 三态都打):改了源码没动文档 → 一行欠账标记。
72
83
  // 不阻断、不解锁、纯显性化——累积欠账可事后审计 QUICKLOG reasons 追溯。
73
84
  if (review.docSyncHint && review.docSyncHint.touchedSource > 0 && review.docSyncHint.docFiles.length === 0) {
package/src/run/shared.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
12
12
  */
13
13
  import { basename, join, resolve, dirname, sep } from 'node:path'
14
- import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
14
+ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync, realpathSync } from 'node:fs'
15
15
  import { fileURLToPath } from 'node:url'
16
16
  import os from 'node:os'
17
17
 
@@ -329,6 +329,55 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
329
329
  return join(resolveRuntimeRoot(platformOpts, localSpecBase), 'quick-sessions')
330
330
  }
331
331
 
332
+ // 他者会话声明豁免的僵尸阈值:超龄会话的声明不再作为豁免依据。与 doctor 幽灵清理
333
+ // GHOST_EMPTY_DIR_STALE_MS 同口径(7 天)——正常并发会话间隔在分钟/小时级,7 天未收尾的
334
+ // 会话按僵尸处理,防崩溃残留的 guard 永久「认领」文件侵蚀危险门。
335
+ const FOREIGN_SESSION_STALE_MS = 7 * 24 * 60 * 60 * 1000
336
+
337
+ /**
338
+ * 枚举其他 active quick 会话的显式文件声明(坑 foreign-session-declared-false-block)。
339
+ *
340
+ * 多 agent 并发时,并行 quick 会话在**本会话启动后**改的文件不在本会话 baseline →
341
+ * auditQuickCompletion 把它算进本会话窗口 → 命中危险清单(src/run/、package.json 等)或
342
+ * .sillyspec/ 判定即 blocked,只能 --force-baseline 无差别逃生。豁免口径 = 他者会话的
343
+ * **显式声明**(guard.allowedFiles,--files 传入)——他者的 baselineFiles 是「启动时已脏」
344
+ * 快照而非所有权声明,不作豁免依据(否则任何预存脏文件都能被洗白)。
345
+ *
346
+ * fail-open:目录不存在/损坏 guard.json/超龄僵尸/任何异常 → 跳过或整体返回 [],
347
+ * 采集失败回到无豁免现状(不放大豁免面)。
348
+ *
349
+ * @param {object} [platformOpts] 平台选项(resolveQuickSessionsDir 同参)
350
+ * @param {string} localSpecBase 本地 specBase(resolveQuickSessionsDir 同参)
351
+ * @param {string} currentSessionId 当前会话 sessionId(quick-<hex8>),枚举时排除
352
+ * @param {number} [nowMs] 当前时间戳(可注入,测试用)
353
+ * @returns {Array<{sessionId: string, files: string[]}>} 其他 active 会话的声明清单
354
+ */
355
+ export function collectOtherQuickSessionDeclarations(platformOpts, localSpecBase, currentSessionId, nowMs = Date.now()) {
356
+ const out = []
357
+ try {
358
+ const sessionsDir = resolveQuickSessionsDir(platformOpts, localSpecBase)
359
+ let sessionDirs = []
360
+ try {
361
+ sessionDirs = readdirSync(sessionsDir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name)
362
+ } catch { return out } // 目录不存在/不可读 → 无他者会话
363
+ for (const sessionName of sessionDirs) {
364
+ if (sessionName === currentSessionId) continue
365
+ const guardFile = join(sessionsDir, sessionName, 'guard.json')
366
+ let guard = null
367
+ try { guard = JSON.parse(readFileSync(guardFile, 'utf8')) } catch { continue } // 损坏/缺失跳过
368
+ // 僵尸防御:超龄会话(崩溃残留,--done/--cancel 都不会再走)的声明不再认领文件
369
+ if (guard.startedAt) {
370
+ const startedAtMs = Date.parse(guard.startedAt)
371
+ if (Number.isFinite(startedAtMs) && nowMs - startedAtMs > FOREIGN_SESSION_STALE_MS) continue
372
+ }
373
+ const files = (Array.isArray(guard.allowedFiles) ? guard.allowedFiles : []).filter(f => typeof f === 'string' && f)
374
+ if (files.length === 0) continue
375
+ out.push({ sessionId: guard.sessionId || sessionName, files })
376
+ }
377
+ } catch { /* fail-open:整体异常返回已采集部分 */ }
378
+ return out
379
+ }
380
+
332
381
  /**
333
382
  * 平台指针三写(单一数据源,runCommand scan 与 cmdInit 平台模式共用)。
334
383
  *
@@ -355,6 +404,15 @@ export const PLATFORM_MANAGED_FILENAME = '.sillyspec-platform-managed'
355
404
 
356
405
  export function writePlatformPointer(cwd, platformOpts, extra = {}) {
357
406
  if (!platformOpts || (!platformOpts.specRoot && !platformOpts.runtimeRoot)) return false
407
+ // 自指写入拦截(FR-4,变更 2026-08-23-repo-native-spec-backfill):specRoot 经 realpath
408
+ // 解析回本地 .sillyspec(repo-native junction 回环)时拒绝三写(主文件/指针/声明全不落盘)。
409
+ // 单点收口同时覆盖 runCommand 与 cmdInit(writeInitPlatformPointer) 两调用方——旧模板
410
+ // scan flag / init 平台模式显式传自指 specRoot 也不再重新投毒。runtimeRoot-only 组合
411
+ // 无 specRoot 可判(isSelfReferentialSpecRoot 对 null 恒 false),原语义不变。
412
+ if (isSelfReferentialSpecRoot(cwd, platformOpts.specRoot)) {
413
+ console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已跳过指针写入并按本地模式运行: ${platformOpts.specRoot}`)
414
+ return false
415
+ }
358
416
  // HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
359
417
  //(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
360
418
  // 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
@@ -427,6 +485,48 @@ export function checkPlatformManaged(cwd) {
427
485
  }
428
486
  }
429
487
 
488
+ /**
489
+ * 自指 specRoot 判定(repo-native junction 回环检测,变更 2026-08-23-repo-native-spec-backfill)。
490
+ *
491
+ * daemon repo-native 工作区把缓存目录(--spec-root 入参,~/.sillyhub/daemon/specs/{ws})以
492
+ * junction/symlink 指回源项目 <cwd>/.sillyspec——该 specRoot 经 realpath 解析后与本地
493
+ * .sillyspec 同物理目录,属"自指回环":不应让 CLI 进入平台模式(内置 sync / auto-pull
494
+ * 按本地模式语义运行,断链修复见 design.md Phase 2)。
495
+ *
496
+ * 双方 fs.realpathSync(native,junction/symlink 跨平台穿透)后严格相等 → true;
497
+ * specRoot 空 / 任一路径不存在 / realpath 抛错 → false(按外部目录处理,保守不干预)。
498
+ * 约束:cwd 必须为项目根(与指针查找 join(cwd,'.sillyspec-platform.json') 同基准)。
499
+ *
500
+ * @param {string} cwd - 项目根
501
+ * @param {string|null} [specRoot] - 平台 specRoot(可 null/undefined/空串)
502
+ * @returns {boolean} 是否自指回环
503
+ */
504
+ export function isSelfReferentialSpecRoot(cwd, specRoot) {
505
+ if (!specRoot || typeof specRoot !== 'string') return false
506
+ try {
507
+ return realpathSync(specRoot) === realpathSync(join(cwd, '.sillyspec'))
508
+ } catch {
509
+ return false
510
+ }
511
+ }
512
+
513
+ /**
514
+ * 平台模式判定单一数据源(自指免疫,变更 2026-08-23-repo-native-spec-backfill)。
515
+ *
516
+ * 语义:(specRoot || runtimeRoot) 存在且非自指 → 平台模式;自指回环(repo-native
517
+ * junction)按本地模式处理。本模块内平台模式门禁统一经本函数,裸表达式仅此一处
518
+ * (grep 验证:triggerSync/triggerPull/triggerPullActiveChange/checkApproval 四处收敛)。
519
+ * runtimeRoot 落在 specRoot 内,自指判定以 specRoot 为准即可覆盖;runtimeRoot-only
520
+ * 组合无 specRoot 可判,不触发自指豁免(保持平台模式)。
521
+ *
522
+ * @param {object} [platformOpts] - { specRoot?, runtimeRoot?, ... }(可 null/undefined)
523
+ * @param {string} cwd - 项目根(isSelfReferentialSpecRoot 同基准)
524
+ * @returns {boolean}
525
+ */
526
+ export function isPlatformMode(platformOpts, cwd) {
527
+ return Boolean((platformOpts?.specRoot || platformOpts?.runtimeRoot) && !isSelfReferentialSpecRoot(cwd, platformOpts?.specRoot))
528
+ }
529
+
430
530
  /**
431
531
  * 统一查找变更目录(与 progress.js 的变更检测逻辑一致)。
432
532
  */
@@ -483,8 +583,8 @@ async function raceWithAbort(op, timeoutMs = SYNC_TOTAL_TIMEOUT_MS) {
483
583
  export const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
484
584
 
485
585
  export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
486
- // 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
487
- if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
586
+ // 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync;自指回环(repo-native junction)按本地模式处理
587
+ if (isPlatformMode(platformOpts, cwd)) return
488
588
  try {
489
589
  // ql-20260818-011:quick 会话按设计无实体变更目录(progress.js initChange 同款
490
590
  // 跳过建目录),progress/四件套上行对它是孤儿数据;但 spec 树增量(QUICKLOG/
@@ -556,8 +656,8 @@ function _stampAutoPull(cwd) {
556
656
  * @param {object} [platformOpts] - 平台模式 opts(specRoot/runtimeRoot 存在则跳过,走平台自有链路)
557
657
  */
558
658
  export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {}) {
559
- // 平台模式(SillyHub)走自己的链路,跳过
560
- if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
659
+ // 平台模式(SillyHub)走自己的链路,跳过;自指回环(repo-native junction)按本地模式处理
660
+ if (isPlatformMode(platformOpts, cwd)) return
561
661
  try {
562
662
  const syncMod = await import('../sync.js')
563
663
  const sm = new syncMod.SyncManager(cwd)
@@ -579,7 +679,8 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
579
679
  * 供 index.js 在 stage 命令(run/--done/archive)与 approve 决策点注入一行调用。
580
680
  */
581
681
  export async function triggerPullActiveChange(cwd, platformOpts = {}) {
582
- if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
682
+ // 平台模式走自己的链路,跳过;自指回环(repo-native junction)按本地模式处理
683
+ if (isPlatformMode(platformOpts, cwd)) return
583
684
  // 先检查是否连接平台:未连接直接 return,避免 _ensureDB 在无 local.yaml 的 cwd 创建空 DB 污染
584
685
  let sm = null
585
686
  try {
@@ -645,8 +746,8 @@ export function warnApprovalUnknown(cwd, changeName, reason) {
645
746
  * 平台模式走自己的链路,跳过;否则 await import sync.js。
646
747
  * @returns {{ status: string, reason?: string } | null}
647
748
  */export async function checkApproval(cwd, changeName, platformOpts = {}) {
648
- // 平台模式不需要 CLI 内置审批检查
649
- if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
749
+ // 平台模式不需要 CLI 内置审批检查;自指回环(repo-native junction)按本地模式处理
750
+ if (isPlatformMode(platformOpts, cwd)) return null
650
751
  try {
651
752
  // shared.js 在 src/run/,sync.js 在 src/ → 退一层
652
753
  const syncMod = await import('../sync.js')
@@ -708,7 +809,7 @@ export function isQuickMetadata(p, linkedChanges = []) {
708
809
 
709
810
  /**
710
811
  * quick 完成审计:对比 baseline 与实际变更。
711
- * @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number, attributedFiles: string[], undeclaredFiles: string[] }}
812
+ * @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number, attributedFiles: string[], undeclaredFiles: string[], foreignSessionDeclared: Array<{file: string, sessions: string[]}> }}
712
813
  */
713
814
  /**
714
815
  * D-8 O-1 模块归属(2026-08-15 docs-signals-o12):quick 欠账 hint 从"改了 N 文件"升级为
@@ -818,7 +919,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
818
919
  // stagedTotal:当前所有非 quick 元数据的未提交条目(含前序 baseline 残留)。
819
920
  // 与 changedFiles(扣 baseline 后的本轮新增)区分,供审计文案同时展示「本轮新增 vs 累计暂存」,
820
921
  // 避免叠加 quick 会话时把前序会话未提交文件误读为「本会话只动了 N 个」。
821
- const result = { status: 'safe', reasons: [], changedFiles: [], newFiles: [], deletedFiles: [], baselineHit: [], stagedTotal: 0, attributedFiles: [], undeclaredFiles: [] }
922
+ const result = { status: 'safe', reasons: [], changedFiles: [], newFiles: [], deletedFiles: [], baselineHit: [], stagedTotal: 0, attributedFiles: [], undeclaredFiles: [], foreignSessionDeclared: [] }
822
923
 
823
924
  try {
824
925
  // safeGit 带 -c safe.directory,避免 linked worktree/容器异 uid/挂载点下裸 `git status` 抛错被
@@ -851,6 +952,38 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
851
952
  const f = normalizeGitPath(p)
852
953
  return baselineExact.has(f) || baselineDirs.some(d => f.startsWith(d))
853
954
  }
955
+ // 他者 active 会话声明索引(坑 foreign-session-declared-false-block,2026-08-23 实证:多 agent
956
+ // 并发时并行会话窗口内改的文件——尤其命中危险清单的(daemon/router.py 之于本仓是 src/run/*、
957
+ // package.json)或 .sillyspec/ 下的——不在本会话 baseline,被算进本会话窗口直接 blocked,
958
+ // 只能 --force-baseline 无差别逃生)。声明即归属的他向版本:他者会话显式声明(--files)的文件
959
+ // 退栈归该会话审计。本会话也声明同一文件(--files 重叠)时不退栈——同文件并发归本会话审计
960
+ // (sameFileHits advisory 管辖)。豁免面 = 他者显式声明,不含他者 baseline 快照(那是「启动时
961
+ // 已脏」非所有权声明,当豁免依据会洗白任何预存脏文件)。
962
+ const ownDeclaredNorm = new Set((allowedFiles || []).map(f => normalizeGitPath(f)))
963
+ const foreignDeclaredIndex = new Map()
964
+ for (const s of Array.isArray(guard.otherSessionsDeclared) ? guard.otherSessionsDeclared : []) {
965
+ if (!s || typeof s !== 'object') continue
966
+ for (const f of Array.isArray(s.files) ? s.files : []) {
967
+ const nf = normalizeGitPath(f)
968
+ if (!nf || ownDeclaredNorm.has(nf)) continue
969
+ if (!foreignDeclaredIndex.has(nf)) foreignDeclaredIndex.set(nf, [])
970
+ if (!foreignDeclaredIndex.get(nf).includes(s.sessionId)) foreignDeclaredIndex.get(nf).push(s.sessionId)
971
+ }
972
+ }
973
+ const findForeignDeclaredOwner = (p) => {
974
+ const f = normalizeGitPath(p)
975
+ if (f.endsWith('/')) {
976
+ // 折叠目录 token(porcelain 把全新未跟踪目录折成 `dir/`):目录下任一文件被他者声明 →
977
+ // 整目录归他者;但本会话在同目录下有声明时不退栈——目录内混有双方文件,留给各门判定
978
+ // (fail-closed:宁可误拦不可漏放,误拦有 --files 追加声明的明确出口)。
979
+ if ([...ownDeclaredNorm].some(x => x.startsWith(f))) return null
980
+ for (const [nf, owners] of foreignDeclaredIndex) {
981
+ if (nf.startsWith(f)) return owners
982
+ }
983
+ return null
984
+ }
985
+ return foreignDeclaredIndex.get(f) || null
986
+ }
854
987
  const DANGEROUS_PATTERNS = [
855
988
  'package.json',
856
989
  'package-lock.json',
@@ -891,6 +1024,15 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
891
1024
  // 预存脏文件:step1 baseline 已记录,非本次 quick 产生,跳过审计(含折叠目录前缀匹配)
892
1025
  if (isBaselineFile(file)) continue
893
1026
 
1027
+ // 他者 active 会话已声明 → 完全退栈归该会话审计(危险/删除/新增/越界/baseline 全部门跳过),
1028
+ // 收进 foreignSessionDeclared 供 printQuickAuditReview 软警告——放行可见可审计不静默
1029
+ // (与 linkedChangeLeftovers 同哲学)。stagedTotal 已在上方计数,累计暂存口径不受退栈影响。
1030
+ const foreignOwners = findForeignDeclaredOwner(file)
1031
+ if (foreignOwners) {
1032
+ result.foreignSessionDeclared.push({ file, sessions: foreignOwners })
1033
+ continue
1034
+ }
1035
+
894
1036
  result.changedFiles.push(file)
895
1037
  if (rawStatus[0] === 'D' || rawStatus[1] === 'D') result.deletedFiles.push(file)
896
1038
  if (status === '??') result.newFiles.push(file)
package/src/run.js CHANGED
@@ -15,8 +15,8 @@
15
15
  */
16
16
  // W6 Step8c: 主命令分发(runCommand)+ auto 模式 + ensureStageSteps 搬至 ./run/command.js
17
17
  export { runCommand, ensureStageSteps } from './run/command.js'
18
- // barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata 被 test 直接 import(契约保留)
19
- export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata } from './run/shared.js'
18
+ // barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata + collectOtherQuickSessionDeclarations 被 test 直接 import(契约保留)
19
+ export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata, collectOtherQuickSessionDeclarations } from './run/shared.js'
20
20
  // barrel re-export: applyRootPlaceholders 被 test/prompt-placeholders.test.mjs 直接 import(契约保留)
21
21
  export { applyRootPlaceholders } from './run/prompt.js'
22
22
  // barrel re-export: sanitizeProjectName + validateParsedProjects 被 test 直接 import(随 handleScan 搬走,契约保留)
@@ -10,6 +10,7 @@ import { join, basename } from 'path'
10
10
  import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
11
11
  import { validateScriptCommands } from './stages/cmd-existence.js'
12
12
  import { git } from './git-helper.js'
13
+ import { nowWallClock } from './datetime.js'
13
14
 
14
15
  const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
15
16
 
@@ -394,7 +395,7 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
394
395
  try {
395
396
  author = git(cwd, ['config', 'user.name']) || 'unknown'
396
397
  } catch { /* git 不可用 → unknown */ }
397
- const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
398
+ const now = nowWallClock()
398
399
 
399
400
  const fixed = []
400
401
  const skipped = []
@@ -410,26 +411,13 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
410
411
  } catch {
411
412
  continue
412
413
  }
413
- const header = content.slice(0, 512)
414
- const hasAuthor = /author\s*:/.test(header)
415
- const hasCreated = /created_at\s*:/.test(header)
416
- if (hasAuthor && hasCreated) {
414
+ const r = backfillFrontmatter(content, { author, createdAt: now })
415
+ if (!r.changed) {
417
416
  skipped.push(filePath)
418
417
  continue
419
418
  }
420
- const inserts = []
421
- if (!hasAuthor) inserts.push(`author: ${author}`)
422
- if (!hasCreated) inserts.push(`created_at: ${now}`)
423
- let newContent
424
- if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
425
- // 有 frontmatter:插到开标签后(CRLF 归一写回,与其它 CLI 写入口径一致)
426
- const normalized = content.replace(/\r\n?/g, '\n')
427
- newContent = normalized.replace(/^---\n/, `---\n${inserts.join('\n')}\n`)
428
- } else {
429
- newContent = `---\n${inserts.join('\n')}\n---\n\n` + content
430
- }
431
419
  try {
432
- writeFileSync(filePath, newContent)
420
+ writeFileSync(filePath, r.content)
433
421
  fixed.push(filePath)
434
422
  } catch { /* 只读文件等写失败 → 跳过不抛 */ }
435
423
  }
@@ -437,6 +425,31 @@ export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
437
425
  return { fixed, skipped }
438
426
  }
439
427
 
428
+ /**
429
+ * 通用 frontmatter 补齐(纯函数):首 512 字节缺 author:/created_at: 时补缺失键。
430
+ * 有 frontmatter(--- 开头)→ 插到开标签后(CRLF 归一 LF 写回);无 → 头部整块前置。
431
+ * fixScanDocHeaders 与 decisions.md 自动补(stage-contract.js ensureDecisionDocHeader)共用,
432
+ * 单一实现杜绝两处行尾/插入位置漂移。
433
+ * @param {string} content
434
+ * @param {{ author: string, createdAt: string }} p
435
+ * @returns {{ changed: boolean, content: string }}
436
+ */
437
+ export function backfillFrontmatter(content, { author, createdAt }) {
438
+ const header = String(content || '').slice(0, 512)
439
+ const hasAuthor = /author\s*:/.test(header)
440
+ const hasCreated = /created_at\s*:/.test(header)
441
+ if (hasAuthor && hasCreated) return { changed: false, content }
442
+ const inserts = []
443
+ if (!hasAuthor) inserts.push(`author: ${author}`)
444
+ if (!hasCreated) inserts.push(`created_at: ${createdAt}`)
445
+ if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
446
+ // 有 frontmatter:插到开标签后(CRLF 归一写回,与其它 CLI 写入口径一致)
447
+ const normalized = content.replace(/\r\n?/g, '\n')
448
+ return { changed: true, content: normalized.replace(/^---\n/, `---\n${inserts.join('\n')}\n`) }
449
+ }
450
+ return { changed: true, content: `---\n${inserts.join('\n')}\n---\n\n` + content.replace(/\r\n?/g, '\n') }
451
+ }
452
+
440
453
  /**
441
454
  * source_root 污染一键修复(2026-08-21 agent-手工产出审计第四批 C-3)。
442
455
  *
@@ -403,8 +403,9 @@ const PLAN_RULES = [
403
403
  messageVerify: '${id}: 缺少 verify 字段',
404
404
  messageConstraints: '${id}: 缺少 constraints 字段',
405
405
  messageDependsOnMissing: '${id}: depends_on 引用了不存在的 ${dep}',
406
+ messagePlaceholder: '${id}: task 卡仍是未填充的生成骨架(${fields} 里的占位符未替换)——占位符视同缺字段。用 Edit 填充 tasks/${id}.md 后重跑(勿删字段勿留 FR-XX/src/example/file.ts/具体步骤 等占位值)',
406
407
  },
407
- spec: 'task 卡片完整 TaskCard schema(可行性,只认 frontmatter 字段,均 error):YAML frontmatter 必备;frontmatter 需 id、title;allowed_paths 非空;frontmatter 需 goal、implementation、acceptance、verify、constraints 五字段;depends_on 引用的 task 必须存在。',
408
+ spec: 'task 卡片完整 TaskCard schema(可行性,只认 frontmatter 字段,均 error):YAML frontmatter 必备;frontmatter 需 id、title;allowed_paths 非空;frontmatter 需 goal、implementation、acceptance、verify、constraints 五字段;depends_on 引用的 task 必须存在;骨架占位符(FR-XX/D-XXX/src/example/file.ts/一句话说明这个 task/具体步骤 1/可验证的验收条件 1/边界约束 1)未替换视同缺字段。',
408
409
  failMessage: '${id}: task 卡片 frontmatter 缺少必要字段(id/title/allowed_paths/goal/implementation/acceptance/verify/constraints)',
409
410
  },
410
411
  ]