sillyspec 3.32.10 → 3.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  2. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  3. package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
  4. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  6. package/.github/workflows/ci.yml +66 -0
  7. package/.husky/pre-push +7 -6
  8. package/AGENTS.md +1 -1
  9. package/assets/command-cards/flow.md +1 -1
  10. package/eval/eval.config.json +3 -3
  11. package/eval/lib/config.mjs +6 -3
  12. package/eval/lib/util.mjs +16 -3
  13. package/eval/lib/workdir.mjs +4 -1
  14. package/eval/results/eval.db +0 -0
  15. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
  16. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
  17. package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
  18. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
  19. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
  20. package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
  21. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
  22. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
  23. package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
  24. package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
  25. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
  26. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
  27. package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
  28. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
  29. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
  30. package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
  31. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
  32. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
  33. package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
  34. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
  35. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
  36. package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
  37. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
  38. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
  39. package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
  40. package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
  41. package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
  42. package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
  43. package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
  44. package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
  45. package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
  46. package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  47. package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
  48. package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
  49. package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
  50. package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
  51. package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
  52. package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
  53. package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
  54. package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
  55. package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
  56. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
  57. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
  58. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
  59. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
  60. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
  61. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
  62. package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
  63. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
  64. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
  65. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
  66. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
  67. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
  68. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
  69. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
  70. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
  71. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
  72. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
  73. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
  74. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
  75. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
  76. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
  77. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
  78. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
  79. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
  80. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
  81. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
  82. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
  83. package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
  84. package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
  85. package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
  86. package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
  87. package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
  88. package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
  89. package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
  90. package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
  91. package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
  92. package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
  93. package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  94. package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
  95. package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
  96. package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
  97. package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
  98. package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
  99. package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
  100. package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
  101. package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
  102. package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
  103. package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
  104. package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
  105. package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
  106. package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
  107. package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
  108. package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
  109. package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
  110. package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
  111. package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
  112. package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
  113. package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
  114. package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
  115. package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
  116. package/eval/tasks/004-phase-stats/task.json +6 -0
  117. package/eval/tasks/004-phase-stats/verify.mjs +38 -0
  118. package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
  119. package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
  120. package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
  121. package/eval/tasks/005-orders-export/task.json +6 -0
  122. package/eval/tasks/005-orders-export/verify.mjs +28 -0
  123. package/package.json +1 -1
  124. package/quicklog/tmp-fix-review.mjs +21 -0
  125. package/src/archive-delta.js +4 -3
  126. package/src/ceremony-tier.js +2 -1
  127. package/src/change-delete.js +6 -5
  128. package/src/contract-matrix.js +2 -1
  129. package/src/cross-placement.js +5 -1
  130. package/src/db.js +8 -2
  131. package/src/decision-distill.js +29 -3
  132. package/src/decision-redomain.js +148 -0
  133. package/src/deleted-ledger.js +2 -1
  134. package/src/design-facts.js +3 -3
  135. package/src/docs-debt.js +3 -3
  136. package/src/doctor-diagnostics.js +8 -3
  137. package/src/flow-draft.js +2 -1
  138. package/src/flow-parity.js +10 -2
  139. package/src/flow.js +112 -63
  140. package/src/forensics.js +236 -0
  141. package/src/fr-index.js +2 -1
  142. package/src/friction-ledger.js +2 -1
  143. package/src/friction-tally.js +8 -4
  144. package/src/handoff.js +2 -1
  145. package/src/hooks/worktree-guard.js +5 -5
  146. package/src/index.js +75 -8
  147. package/src/knowledge-graph.js +3 -4
  148. package/src/knowledge-hits.js +2 -1
  149. package/src/knowledge-vector.js +2 -1
  150. package/src/module-impact.js +2 -5
  151. package/src/modules.js +31 -0
  152. package/src/progress/consistency-doctor.js +5 -2
  153. package/src/progress/stage-machine.js +6 -2
  154. package/src/progress.js +19 -8
  155. package/src/quicklog.js +194 -9
  156. package/src/review-dispatch.js +4 -3
  157. package/src/review-tier.js +2 -1
  158. package/src/route-hindsight.js +4 -3
  159. package/src/run/bg-sync.js +10 -3
  160. package/src/run/command.js +13 -11
  161. package/src/run/complete-handlers.js +40 -37
  162. package/src/run/complete.js +12 -8
  163. package/src/run/gate-snapshot-ledger.js +2 -1
  164. package/src/run/gate-snapshot.js +57 -11
  165. package/src/run/gates.js +38 -21
  166. package/src/run/lint-ownership.js +37 -0
  167. package/src/run/prompt.js +46 -30
  168. package/src/run/scan-profile.js +2 -1
  169. package/src/run/shared.js +109 -5
  170. package/src/run/stage.js +5 -4
  171. package/src/run/test-ledger.js +2 -1
  172. package/src/run/verify-quality-scan.js +21 -6
  173. package/src/runtime-paths.js +189 -0
  174. package/src/scan-refresh.js +4 -3
  175. package/src/scope-audit.js +7 -6
  176. package/src/sentinel-assertions.js +37 -0
  177. package/src/spec-sync.js +118 -23
  178. package/src/stage-contract.js +24 -2
  179. package/src/stage-review.js +5 -3
  180. package/src/stages/brainstorm.js +72 -21
  181. package/src/stages/plan-postcheck.js +13 -2
  182. package/src/stages/plan.js +19 -0
  183. package/src/sync-noise.js +3 -2
  184. package/src/sync.js +71 -15
  185. package/src/task-done.js +2 -1
  186. package/src/task-review.js +7 -5
  187. package/src/task-tick.js +23 -1
  188. package/src/test-bindings.js +32 -3
  189. package/src/ui-visual.js +119 -4
  190. package/src/validate-artifacts.js +2 -1
  191. package/src/verify-draft.js +2 -1
  192. package/src/verify-postcheck.js +128 -27
  193. package/src/verify-probes.js +157 -52
  194. package/src/watcher.js +15 -9
  195. package/src/worktree-apply.js +28 -3
  196. package/src/write-audit.js +2 -1
  197. package/templates/agents-instruction.md +1 -1
@@ -17,12 +17,13 @@
17
17
  * - node: join(path) + existsSync/readdirSync/readFileSync/mkdirSync/writeFileSync/appendFileSync/statSync(fs)
18
18
  */
19
19
  import { join, isAbsolute, resolve } from 'node:path'
20
+ import { runtimeStatePath } from '../runtime-paths.js'
20
21
  import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
21
22
  import { writeAtomicSync } from '../fs-atomic.js'
22
23
  import { gitQuiet } from '../git-helper.js'
23
24
  import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
24
25
  import { withFileLock } from '../quicklog.js'
25
- import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
26
+ import { triggerSync, triggerSyncTerminalStrict, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
26
27
  import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
27
28
 
28
29
  // 退役自动道守门(2026-10-14-brainstorm-auto-retire):brainstorm-auto 4 步表已随自动道删除。
@@ -828,7 +829,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
828
829
 
829
830
  // gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
830
831
  pm._write(cwd, progress, changeName)
831
- triggerSync(cwd, changeName, platformOpts, { completion: true })
832
+ // 终态强校验(2026-10-17-archive-half-state-location-stuck-active):状态机驱动——仅
833
+ // DB status 已达 archived/deleted(archive 阶段完成即此态)走内联强校验,其余阶段
834
+ // 原样委托 triggerSync(后台化不变,非终态零行为变化)。见 run/shared.js 契约注释。
835
+ await triggerSyncTerminalStrict(cwd, changeName, platformOpts, { completion: true })
832
836
 
833
837
  const total = steps.length
834
838
  console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
@@ -843,7 +847,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
843
847
  let stageLedger = null
844
848
  let crossStage = false
845
849
  try {
846
- const ledgerPath = join(specBase, '.runtime', `stage-session-ledger-${changeName}.json`)
850
+ const ledgerPath = runtimeStatePath(join(specBase, '.runtime'), `stage-session-ledger-${changeName}.json`)
847
851
  try { stageLedger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首阶段 */ }
848
852
  const r = updateStageSessionLedger(stageLedger, process.env.SILLYSPEC_SESSION_ID, stageName)
849
853
  stageLedger = r.ledger
@@ -1018,7 +1022,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
1018
1022
  if (stageName === 'execute' && changeName && defSteps && defSteps[currentIdx] && defSteps[nextPendingIdx] &&
1019
1023
  /Wave \d+ 执行/.test(defSteps[currentIdx].name || '') && /Wave \d+ 执行/.test(defSteps[nextPendingIdx].name || '')) {
1020
1024
  try {
1021
- const ledgerPath = join(specBase, '.runtime', `wave-session-ledger-${changeName}.json`)
1025
+ const ledgerPath = runtimeStatePath(join(specBase, '.runtime'), `wave-session-ledger-${changeName}.json`)
1022
1026
  let ledger = null
1023
1027
  try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首 Wave */ }
1024
1028
  const { ledger: nextLedger, crossWaveAdvisory } = updateWaveSessionLedger(ledger, process.env.SILLYSPEC_SESSION_ID, defSteps[currentIdx].name)
@@ -1141,7 +1145,7 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
1141
1145
  snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
1142
1146
  } catch {
1143
1147
  try {
1144
- snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
1148
+ snapshot = JSON.parse(readFileSync(runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
1145
1149
  } catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
1146
1150
  }
1147
1151
  }
@@ -1559,7 +1563,7 @@ export async function autoTickTasksFromCommitSubjects({ changeName, cwd, specBas
1559
1563
  try {
1560
1564
  const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
1561
1565
  if (!existsSync(_tasksMdPath)) return { ticked: 0, skippedNoAnchor: 0 }
1562
- const _runIdFile = join(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
1566
+ const _runIdFile = runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
1563
1567
  let _tickLog = ''
1564
1568
  try {
1565
1569
  if (existsSync(_runIdFile)) {
@@ -1640,7 +1644,7 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
1640
1644
  // plan.md Wave 引用行无 checkbox,无处可勾)。tasks.md 存在性作前置。
1641
1645
  const tasksMdPath = join(changeDir, 'tasks.md')
1642
1646
  const runtimeRoot = resolveRuntimeRoot(platformOpts, specBaseLc)
1643
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1647
+ const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
1644
1648
  if (!existsSync(tasksMdPath) || !existsSync(runIdFile)) {
1645
1649
  return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
1646
1650
  }
@@ -1798,7 +1802,7 @@ export async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd,
1798
1802
  // 平台模式 runtimeRoot / worktree 漂移 specDriftAnchor 不透传时 marker 恒读不到
1799
1803
  // → 每个 task 进 blockedTasks → 批量完成静默失效
1800
1804
  const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
1801
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1805
+ const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
1802
1806
  // ── 缺席草稿自动补齐(坑 task-review-retired-archive-mismatch,2026-10-15 归档门口实证)──
1803
1807
  // Task Review 层退役(2026-09-26)后 execute 批量完成不再产 per-task review.json,但本函数
1804
1808
  // 下方复核循环与 archive step2 完成度判定(真相源=review.json verdict)仍以它在场为前提——
@@ -16,6 +16,7 @@
16
16
  * 异常面:全部 fail-open 吞掉(账本/回收任一异常零阻断,退现状残留)。
17
17
  */
18
18
  import { execFileSync } from 'node:child_process'
19
+ import { runtimeStatePath } from '../runtime-paths.js'
19
20
  import { existsSync, mkdirSync, readFileSync } from 'node:fs'
20
21
  import { basename, dirname, join, resolve, sep } from 'node:path'
21
22
  import { tmpdir } from 'node:os'
@@ -28,7 +29,7 @@ const STALE_HOURS_DEFAULT = 24
28
29
 
29
30
  /** 账本文件路径(runtime 域:.sillyspec/.runtime/active-gate-snapshots.json) */
30
31
  export function gateSnapshotLedgerPath(runtimeRoot) {
31
- return join(runtimeRoot, LEDGER_FILE)
32
+ return runtimeStatePath(runtimeRoot, LEDGER_FILE)
32
33
  }
33
34
 
34
35
  /** 读账本(损坏/缺失/非数组 → 空数组,fail-open) */
@@ -494,7 +494,12 @@ export function applyGateSnapshotCopy(cwd, snapshotRoot) {
494
494
  * @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
495
495
  * @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
496
496
  */
497
- export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null, runtimeRoot = null }) {
497
+ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null, runtimeRoot = null, failRef = null }) {
498
+ // failRef 可选出参(坑 snapshot-fallback-reason-blind,2026-10-16 下游实证):所有失败分支
499
+ // 把结构化原因写进 failRef.reason——调用方(verify --done 实测门 / noAI 质量扫描)消费它
500
+ // 替换硬编码笼统文案,降级原因可直读(pnpm 布局拒建 ≠ 文件集为空 ≠ junction 失败)。
501
+ // 返回契约不变(失败恒 null);不传 failRef 行为与既有逐字节一致。
502
+ const setFail = (reason) => { if (failRef && typeof failRef === 'object') failRef.reason = reason }
498
503
  let snapshotRoot = null
499
504
  try {
500
505
  // 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
@@ -519,6 +524,7 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
519
524
  // 与其让沙箱报无关错误逼 advisory,不如布局命中即作废快照回退主仓(宁可主仓口径)。
520
525
  const layoutHit = detectSymlinkStoreLayout(cwd)
521
526
  if (layoutHit) {
527
+ setFail(`symlink-store 布局拒建(${layoutHit}):node_modules 符号链接 store 经 junction 跨根失效,实测必假败——设计性回退主仓实测`)
522
528
  console.warn(`⚠️ 门禁快照对 ${layoutHit} 布局不可靠(node_modules 符号链接 store 经 junction 跨根失效,实测必假败)——跳过快照回退主仓实测;主仓失败再做污染归属鉴定`)
523
529
  return null
524
530
  }
@@ -625,6 +631,7 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
625
631
  // 回退主仓现行为(主仓口径可能被并行脏文件污染,但环境真实;两害取轻 + 回退原因可见)。
626
632
  const envMissing = envDirsLinked(cwd, snapshotRoot)
627
633
  if (envMissing.length > 0) {
634
+ setFail(`环境目录链接不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测`)
628
635
  console.warn(`⚠️ 门禁快照环境不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测(宁可主仓口径不可假沙箱硬挂;回退后失败先做污染归属鉴定)`)
629
636
  // 与正常 cleanup/失败 catch 同一清理体:双清确认才销账(该早退曾残留旧式 --quiet
630
637
  // 清理与无重试 rmSync——独立审查 P1-1 根因未除尽的漏网面)
@@ -723,7 +730,10 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
723
730
  return { snapshotRoot, cleanup, overlaid, pinnedCacheLinks }
724
731
  } catch (e) {
725
732
  // 基建失败(worktree add 拒绝/磁盘满/…):尽力清理后回退主仓;销账同走双清确认
726
- // (D-004@v2:清不掉的条目保留待回收——它不是悬空条目,是待回收记录)
733
+ // (D-004@v2:清不掉的条目保留待回收——它不是悬空条目,是待回收记录)。
734
+ // 异常细节不再静默(坑 snapshot-fallback-reason-blind):failRef 带原因 + warn 落日志。
735
+ setFail(`快照基建异常: ${e && e.message ? e.message : e}`)
736
+ try { console.warn(`⚠️ 门禁快照基建失败(回退主仓实测): ${e && e.message ? e.message : e}`) } catch { /* 输出失败不影响回退 */ }
727
737
  if (snapshotRoot) {
728
738
  try {
729
739
  const { dirRemoved, worktreeCleaned } = cleanupSnapshot({ snapshotRoot, cwd })
@@ -870,7 +880,8 @@ export function printSnapshotFailureHint(snapInfo, { offEnv = 'SILLYSPEC_VERIFY_
870
880
  * @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
871
881
  * @returns {Promise<{snapshotRoot: string, cleanup: () => void, overlaid: number, changeFileCount: number}|null>}
872
882
  */
873
- export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {} }) {
883
+ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {}, failRef = null }) {
884
+ const setFail = (reason) => { if (failRef && typeof failRef === 'object') failRef.reason = reason }
874
885
  try {
875
886
  const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
876
887
  const { resolveRuntimeRoot } = await import('./shared.js')
@@ -879,10 +890,19 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
879
890
  let verifyRuntimeRoot = null
880
891
  try { verifyRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase) } catch { verifyRuntimeRoot = null }
881
892
  const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
882
- let changeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
893
+ // 变更文件集为空三分成因区分(坑 snapshot-fallback-reason-blind):null=git 不可用/解析
894
+ // 失败;[]=HEAD 无 diff;他者声明过滤清空在下方单独计——三者处置同为回退主仓,但排查
895
+ // 出路完全不同,笼统「变更文件集为空」曾把 pnpm 布局拒建也误标成它。
896
+ const rawChangeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
883
897
  specBase, includeWorkingTree: true,
884
- }) || []
885
- if (changeFiles.length === 0) return null // 无变更文件集 → 无从定向,回退主仓
898
+ })
899
+ let changeFiles = rawChangeFiles || []
900
+ if (changeFiles.length === 0) {
901
+ setFail(rawChangeFiles === null
902
+ ? '变更文件集为空(git 不可用/解析失败——非 git 仓或 git 异常,无从定向 overlay)'
903
+ : '变更文件集为空(HEAD 无 diff——改动可能已全部提交,无可隔离面)')
904
+ return null // 无变更文件集 → 无从定向,回退主仓
905
+ }
886
906
  // 他者声明归属过滤(坑 verify-reconcile-foreign-wip 同款):in-place 模式 working-tree
887
907
  // 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
888
908
  // 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
@@ -891,7 +911,10 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
891
911
  if (foreign.length > 0) {
892
912
  const foreignSet = new Set(foreign.map(x => x.file))
893
913
  changeFiles = changeFiles.filter(f => !foreignSet.has(f))
894
- if (changeFiles.length === 0) return null
914
+ if (changeFiles.length === 0) {
915
+ setFail(`变更文件集为空(${foreign.length} 个 diff 文件全部被他者声明过滤——并行会话声明面覆盖了本变更全部在途文件)`)
916
+ return null
917
+ }
895
918
  }
896
919
  } catch { /* 过滤失败退全量(fail-closed:宁可多 overlay 不漏本变更文件) */ }
897
920
 
@@ -920,8 +943,12 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
920
943
  }
921
944
  } catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
922
945
 
923
- const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase, runtimeRoot: verifyRuntimeRoot })
924
- if (!snap) return null
946
+ const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase, runtimeRoot: verifyRuntimeRoot, failRef })
947
+ if (!snap) {
948
+ // createGateSnapshot 已把具体原因写进同一 failRef;无 reason(旧形态兜底)补笼统口径
949
+ if (failRef && typeof failRef === 'object' && !failRef.reason) setFail('快照基建不可用(createGateSnapshot 返回 null,无更细原因)')
950
+ return null
951
+ }
925
952
 
926
953
  // 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
927
954
  // 在 .sillyspec(gitignore)不进 HEAD,从主仓 specBase 复制 changes/<name>/**
@@ -940,7 +967,26 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
940
967
  } catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
941
968
 
942
969
  return { ...snap, changeFileCount: changeFiles.length, sourceRoot }
943
- } catch {
944
- return null // 任一链路异常 → 回退主仓现行为
970
+ } catch (e) {
971
+ // 任一链路异常 → 回退主仓现行为。异常细节不再静默(坑 snapshot-fallback-reason-blind):
972
+ // failRef 带原因 + warn 落日志,排查不再靠 stdout 考古。
973
+ setFail(`快照链路异常: ${e && e.message ? e.message : e}`)
974
+ try { console.warn(`⚠️ 门禁快照链路异常(回退主仓实测): ${e && e.stack ? e.stack.split('\n')[0] : e}`) } catch { /* 输出失败不影响回退 */ }
975
+ return null
945
976
  }
946
977
  }
978
+
979
+ /**
980
+ * 快照口径观测字段(坑 snapshot-fallback-reason-blind,FR-04 落盘面):--done 实测门把
981
+ * used_snapshot 与降级原因经 runVerifyTestCheck 的 observability 通道带进 test-result.json
982
+ * extra(对齐 2026-10-09 复用观测先例)。快照在场时记录随快照销毁(无可降级事实);
983
+ * 回退主仓时记录恰好落在主仓 .runtime——降级证据存活在主仓,与用途一致。
984
+ * @param {boolean} used 快照是否真实在场
985
+ * @param {{ reason?: string }|null} failRef createVerifyGateSnapshot 的出参(可 null)
986
+ * @returns {{ used_snapshot: boolean, snapshot_fallback_reason?: string }}
987
+ */
988
+ export function snapshotObservability(used, failRef) {
989
+ if (used) return { used_snapshot: true }
990
+ const reason = failRef && failRef.reason ? String(failRef.reason) : 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)'
991
+ return { used_snapshot: false, snapshot_fallback_reason: reason }
992
+ }
package/src/run/gates.js CHANGED
@@ -28,6 +28,7 @@
28
28
  * 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
29
29
  */
30
30
  import { basename, join, relative, dirname } from 'node:path'
31
+ import { runtimeStatePath } from '../runtime-paths.js'
31
32
  import { createHash } from 'node:crypto'
32
33
  import { execFileSync } from 'node:child_process'
33
34
  import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
@@ -224,7 +225,7 @@ export function warnMissingUiPrototype(stageName, changeName, currentStepName, s
224
225
  const design = readFileSync(designPath, 'utf8')
225
226
  const feExts = [...new Set((design.match(/\.(vue|svelte|astro|tsx|jsx|css|scss|sass|less|styl|html)\b/gi) || []).map(s => s.replace('.', '').toLowerCase()))]
226
227
  if (feExts.length === 0) return
227
- console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入)`)
228
+ console.warn(` ⚠️ design.md 命中前端文件类型(${feExts.join('/')})但变更目录无 prototype-*.html——轻微样式改动可忽略;布局/交互流程级变化应补原型(sillyspec run brainstorm --reopen 回到「分段展示设计」,或直接让 agent 补生成;已有可复跑 HTML 原型则登记复用:复制进变更目录并命名 prototype-<名称>.html,execute 按此命名注入;本仓形态未在 local.yaml ui_prototype 登记的,收口前补登记——form: real-code 或 single-html+豁免 ruling)`)
228
229
  }
229
230
 
230
231
  /**
@@ -444,8 +445,8 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
444
445
  if (worktreeGone && changeName) {
445
446
  try {
446
447
  const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
447
- const legitimatelyEnded = existsSync(join(runtimeRoot, `apply-pathspec-${changeName}.txt`))
448
- || existsSync(join(runtimeRoot, `execute-cleanup-${changeName}.json`))
448
+ const legitimatelyEnded = existsSync(runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`))
449
+ || existsSync(runtimeStatePath(runtimeRoot, `execute-cleanup-${changeName}.json`))
449
450
  if (legitimatelyEnded) {
450
451
  console.log('ℹ️ worktree 已在 apply/execute 收尾时正当清理(凭据在 .runtime),deps 门放行')
451
452
  return true
@@ -549,7 +550,7 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
549
550
 
550
551
  /** 档位状态文件路径(runtimeRoot 下 per-change;命名对齐 apply-pathspec-<change>.txt 先例) */
551
552
  function ceremonyTierFilePath(runtimeRoot, changeName) {
552
- return join(runtimeRoot, `ceremony-tier-${changeName}.json`)
553
+ return runtimeStatePath(runtimeRoot, `ceremony-tier-${changeName}.json`)
553
554
  }
554
555
 
555
556
  /**
@@ -1100,17 +1101,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1100
1101
  // 变更文档随快照(test_strategy 消费)。并行会话在途文件物理不进快照、不参与本门判定。
1101
1102
  // 快照基建失败 → 回退主仓现行为(失败路径沿用 renderVerifyTestAttribution 归因提示)。
1102
1103
  let verifyGateSnap = null
1104
+ let snapshotFailRef = null // createVerifyGateSnapshot 出参:结构化失败原因(FR-04,替代硬编码笼统文案)
1103
1105
  if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
1104
1106
  try {
1105
1107
  const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
1106
- verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
1108
+ snapshotFailRef = {}
1109
+ verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts, failRef: snapshotFailRef })
1107
1110
  if (verifyGateSnap) {
1108
1111
  console.log(`🧪 Verify 实测隔离快照(根:${verifyGateSnap.snapshotRoot}):HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
1109
1112
  } else {
1110
- // FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账
1113
+ // FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账;
1114
+ // FR-04(2026-10-16-fix-downstream-tool-defects):reason 消费 failRef 结构化原因
1115
+ // (pnpm 布局拒建 / 文件集为空三分成因 / junction 失败 / 基建异常详情),不再笼统
1111
1116
  try {
1112
1117
  const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
1113
- if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'verify --done 实测门', reason: 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)' })
1118
+ const fbReason = snapshotFailRef && snapshotFailRef.reason
1119
+ ? `createVerifyGateSnapshot 返回 null:${snapshotFailRef.reason}`
1120
+ : 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)'
1121
+ if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'verify --done 实测门', reason: fbReason })
1114
1122
  } catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
1115
1123
  }
1116
1124
  } catch (e) {
@@ -1120,6 +1128,22 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1120
1128
  } catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
1121
1129
  }
1122
1130
  }
1131
+ // FR-04 降级事实落盘:used_snapshot 与失败原因随 test-result.json extra 持久化(快照在场
1132
+ // 时记录随快照销毁;回退主仓时恰好落在主仓 .runtime——降级证据存活面与用途一致)。
1133
+ // 评审 P3-2 整改:OFF 开关是主动关闸,不得记成「基建故障/文件集为空」的笼统回退原因。
1134
+ let snapObsFields
1135
+ if (process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
1136
+ snapObsFields = { used_snapshot: false, snapshot_fallback_reason: 'SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF=1 主动关闸(快照未尝试,非基建故障)' }
1137
+ } else if (verifyGateSnap) {
1138
+ snapObsFields = { used_snapshot: true }
1139
+ } else {
1140
+ try {
1141
+ const { snapshotObservability } = await import('./gate-snapshot.js')
1142
+ snapObsFields = snapshotObservability(false, snapshotFailRef)
1143
+ } catch {
1144
+ snapObsFields = { used_snapshot: false, snapshot_fallback_reason: snapshotFailRef && snapshotFailRef.reason ? String(snapshotFailRef.reason) : 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)' }
1145
+ }
1146
+ }
1123
1147
  const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
1124
1148
  const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
1125
1149
  let testCheck
@@ -1220,7 +1244,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1220
1244
  `质量扫描: ${reusableScan ? '命中(上层未消费)' : '指纹失配/无记录'}`,
1221
1245
  `green-cache: ${greenHit ? '命中(上层未消费)' : '指纹失配/过期/无记录'}`,
1222
1246
  ].join(';')
1223
- testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, observability: { fingerprint: _obsFingerprint, reuse_decision: { layer: 'real-run', hit: false, reason: _missReason } }, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
1247
+ testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, observability: { fingerprint: _obsFingerprint, reuse_decision: { layer: 'real-run', hit: false, reason: _missReason }, ...snapObsFields }, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
1224
1248
  printVerifyTestCheck(testCheck)
1225
1249
  // 自动绑定补全(full-autopilot-parity,R21 thin 迁移):测试已跑、test-result.json 已落盘,
1226
1250
  // verify --done 收口时从测试结果自动补全 requirements.md 的空绑定槽——与 thin 同逻辑(逐行扫描)。
@@ -1397,18 +1421,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1397
1421
  // 快照含基线债文件时恒败,agent 被逼范围外清偿或逃生口)。失败输出提及文件 × 本变更
1398
1422
  // 文件集零交集 → 存量债 advisory 降档放行(债非本变更引入,硬拦只是制造范围外功);
1399
1423
  // 有交集 / 输出无可识别路径(unattributable,纯配置错误类)→ 维持硬拦(保守)。──
1400
- let lintOwnership = null
1401
- try {
1402
- const { triageLintOwnership, resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
1403
- const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
1404
- .map(f => String(f).replace(/\\/g, '/')).filter(f => !f.startsWith('.sillyspec/'))
1405
- lintOwnership = triageLintOwnership({ failureFiles: lintCheck.failureFiles || [], changeFiles: lintScopeFiles })
1406
- } catch { /* 鉴定链异常维持硬拦(保守) */ }
1407
- if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
1408
- console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 完成:')
1409
- console.warn(` 存量债文件:${(lintCheck.failureFiles || []).join('、') || '(见上方输出)'}`)
1410
- console.warn(' 处置建议:单独 quick 机械清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
1411
- } else {
1424
+ // 2026-10-15-lint-triage-path-unify:判定与文案收敛到 ./lint-ownership.js(与 noAI 质量
1425
+ // 扫描路径同源——原两路径分叉见消费仓坑档案 verify-lint-triage-path-divergence.md)。
1426
+ const { lintOwnershipDowngrade } = await import('./lint-ownership.js')
1427
+ const lintAdvisoryDowngraded = await lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase })
1428
+ if (!lintAdvisoryDowngraded) {
1412
1429
  console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
1413
1430
  if (usedSnapInfo) {
1414
1431
  try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
@@ -1432,7 +1449,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1432
1449
  }
1433
1450
  console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
1434
1451
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
1435
- } // else(pre-existing 降档分支)结束——维持硬拦分支的 rollback 语义不变
1452
+ } // 硬拦分支结束(降档放行时直接落到后续 parity/删除探针——rollback 语义不变)
1436
1453
  }
1437
1454
  // 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
1438
1455
  // 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
@@ -0,0 +1,37 @@
1
+ /**
2
+ * lint 归属鉴定降档(共享 helper)——2026-10-15-lint-triage-path-unify。
3
+ *
4
+ * 背景(消费仓坑档案 docs/sillyspec/verify-lint-triage-path-divergence.md,2026-10-15
5
+ * -dsh-engine-acp verify 实证):降档判定原先只实现在 gates.js 的 verify --done 拦截面;
6
+ * verify step6「noAI 质量扫描」(verify-quality-scan.js)只做 shouldBlockVerifyLint 二值
7
+ * 判定直接 throw——同一失败两路径行为分叉(step6 throw / --done 降档)。本模块把判定与
8
+ * 提示文案收敛为单一来源,两路径同接线。
9
+ *
10
+ * 语义(R4-S-F 实证口径,与 gates.js 原内联实现等价):
11
+ * - pre-existing(失败提及文件 × 本变更文件集零交集)→ 打印降档 warning,返回 true(放行);
12
+ * - owned(有交集)/ unattributable(输出无可识别路径)/ 鉴定链异常 → 返回 false
13
+ * (维持硬拦,保守——无法排除本变更责任时宁可拦不可漏)。
14
+ *
15
+ * @param {{lintCheck?: {failureFiles?: string[]}, cwd: string, changeName: string, specBase: string, importer?: () => Promise<any>}} input
16
+ * importer 可注入(测试用);缺省动态加载 ../verify-postcheck.js(triageLintOwnership +
17
+ * resolveVerifyChangedFiles——运行时 import 避免模块加载环)。
18
+ * @returns {Promise<boolean>} true=降档放行(advisory),false=维持硬拦。
19
+ */
20
+ export async function lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase, importer }) {
21
+ const load = importer || (async () => import('../verify-postcheck.js'))
22
+ let lintOwnership = null
23
+ try {
24
+ const { triageLintOwnership, resolveVerifyChangedFiles } = await load()
25
+ const lintScopeFiles = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
26
+ .map((f) => String(f).replace(/\\/g, '/')).filter((f) => !f.startsWith('.sillyspec/'))
27
+ lintOwnership = triageLintOwnership({ failureFiles: (lintCheck && lintCheck.failureFiles) || [], changeFiles: lintScopeFiles })
28
+ } catch { /* 鉴定链异常维持硬拦(保守) */ }
29
+ if (lintOwnership && lintOwnership.verdict === 'pre-existing') {
30
+ const failureFiles = (lintCheck && lintCheck.failureFiles) || []
31
+ console.warn('\n⚠️ Verify lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本变更文件集零交集)——降档 advisory 不阻断 verify 推进:')
32
+ console.warn(` 存量债文件:${failureFiles.join('、') || '(见上方输出)'}`)
33
+ console.warn(' 处置建议:单独清偿(顺手清债解锁全仓 lint 门),或本变更确认无关时设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。债随基线存在,与本变更交付无关。')
34
+ return true
35
+ }
36
+ return false
37
+ }
package/src/run/prompt.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
21
  import { basename, dirname, join } from 'node:path'
22
+ import { runtimeStatePath } from '../runtime-paths.js'
22
23
  import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rmSync } from 'node:fs'
23
24
  import { createHash } from 'node:crypto'
24
25
  import jsYaml from 'js-yaml'
@@ -68,6 +69,7 @@ const VOLATILE_PLACEHOLDER_RES = [
68
69
  /<now-datetime>/g, /<now-timestamp>/g, /<now-date>/g, /<now-iso-datetime>/g, /<git-head-short>/g, /<quicklog-id>/g,
69
70
  /{SCAN_STALENESS}/g, /{SCAN_FACTS}/g, /{QUICK_CONTEXT_DIGEST}/g, /{DECISION_HITS}/g, /{UI_VISUAL_GUIDANCE}/g,
70
71
  /{KNOWLEDGE_HIT_REPORT}/g, /{DOCS_DEBT}/g, /{MODULE_RESOLVE_TABLE}/g, /{REVIEW_MATERIALS}/g,
72
+ /{REVIEW_TIER}/g, /{REVIEW_TIER_REASON}/g, /{REVIEW_JSON_CONTRACT}/g,
71
73
  /{EXECUTE_RUN_ID}/g, /{STAGE_REVIEW_RUN_ID}/g, /{PROGRESS_SNAPSHOT}/g, /{TASK_COMPLETION_REPORT}/g,
72
74
  /{WORKTREE_META}/g, /{WORKTREE_BASELINE_INFO}/g, /{TASKS_CHECKBOX}/g, /{GIT_DIRTY}/g,
73
75
  /{SCOPE_AUDIT_TABLE}/g, /{FR_INDEX_DIGEST}/g, /{HANDOVER_SUMMARY}/g, /{PREFLIGHT_FAILURES}/g,
@@ -867,7 +869,7 @@ export async function renderPreflightFailures({ stageName, stepName, cwd, specBa
867
869
  export function shouldInjectFullContext({ changeName, stageName, runtimeRoot } = {}) {
868
870
  try {
869
871
  if (!runtimeRoot || !changeName || !stageName) return { full: true }
870
- const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
872
+ const ledgerPath = runtimeStatePath(runtimeRoot, `prompt-inject-${changeName}.json`)
871
873
  if (!existsSync(ledgerPath)) return { full: true }
872
874
  const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'))
873
875
  const rec = (ledger && ledger.stages && typeof ledger.stages === 'object') ? ledger.stages[stageName] : null
@@ -905,7 +907,7 @@ function renderPromptInjectSummaryLine({ firstStep, digest, specBase, projectNam
905
907
  * @param {{ runtimeRoot: string, changeName: string, stageName: string, firstStep: number, digest: string }} args
906
908
  */
907
909
  async function recordPromptInjectLedger({ runtimeRoot, changeName, stageName, firstStep, digest }) {
908
- const ledgerPath = join(runtimeRoot, `prompt-inject-${changeName}.json`)
910
+ const ledgerPath = runtimeStatePath(runtimeRoot, `prompt-inject-${changeName}.json`)
909
911
  await withFileLock(ledgerPath + '.lock', () => {
910
912
  let ledger = null
911
913
  try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { ledger = null }
@@ -1044,6 +1046,18 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1044
1046
  promptText = promptText.split(token).join(v)
1045
1047
  return promptText
1046
1048
  }
1049
+ // 裸 split/join 注入点的收集通道(2026-10-16-step-guide-dyn-completeness):语义与
1050
+ // substituteSplitJoin 相同,供不以占位符正则批量替换的站点使用。
1051
+ // ⚠️ 纪律:凡向 promptText 注入运行期值的站点必须走本组三个助手之一——裸 `.split().join()`
1052
+ // 会让值漏进复入短输出的「动态注入段」附录(值不落盘、guide 掩码后无回读通道)。
1053
+ // 2026-10-15 消费仓实证:评审六值(tier/材料包/契约等)漏收,主代理被迫读引擎源码逆推档位、
1054
+ // 手建材料包、手拼 review.json 路径写错一层前缀再纠正——收集器注释「漏收才是坑」的兑现。
1055
+ const substituteRaw = (token, value) => {
1056
+ const v = String(value)
1057
+ dynSegments.push(v)
1058
+ promptText = promptText.split(token).join(v)
1059
+ return promptText
1060
+ }
1047
1061
  // 替换 prompt 中的占位符
1048
1062
  if (projectName && promptText.includes('<project>')) {
1049
1063
  substitute(/<project>/g, projectName, true)
@@ -1415,7 +1429,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1415
1429
  // 注入结果同步落 runtime JSON(与既有 KNOWLEDGE_HIT_REPORT 落盘口径一致)
1416
1430
  const decRuntimeDir = join(decSpecBase, '.runtime')
1417
1431
  mkdirSync(decRuntimeDir, { recursive: true })
1418
- writeAtomicSync(join(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
1432
+ writeAtomicSync(runtimeStatePath(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
1419
1433
  } catch (e) {
1420
1434
  substitute(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
1421
1435
  }
@@ -1427,7 +1441,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1427
1441
  // 替换空串零输出;fail-soft 异常单行说明,不留残留占位符。
1428
1442
  if (stageName === 'brainstorm' && promptText.includes('{UI_VISUAL_GUIDANCE}')) {
1429
1443
  try {
1430
- const { detectUiTouch, buildUiGuidanceLines } = await import('../ui-visual.js')
1444
+ const { detectUiTouch, buildUiGuidanceLines, extractQuotedPaths } = await import('../ui-visual.js')
1431
1445
  const uiSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1432
1446
  let corpus = changeName || ''
1433
1447
  for (const name of ['proposal.md', 'requirements.md']) {
@@ -1435,7 +1449,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1435
1449
  corpus += '\n' + readFileSync(join(uiSpecBase, 'changes', changeName, name), 'utf8')
1436
1450
  } catch { /* 缺席跳过 */ }
1437
1451
  }
1438
- substitute(/\{UI_VISUAL_GUIDANCE\}/g, detectUiTouch(corpus) ? '\n' + buildUiGuidanceLines().join('\n') + '\n' : '')
1452
+ // ui_prototype 形态注入(2026-10-15-ui-proto-form-config):语料路径参与子仓 override
1453
+ // 匹配(proposal/requirements 反引号路径粗提);无路径=仓级兜底。
1454
+ const guidancePaths = extractQuotedPaths(corpus)
1455
+ substitute(/\{UI_VISUAL_GUIDANCE\}/g, detectUiTouch(corpus) ? '\n' + buildUiGuidanceLines(uiSpecBase, guidancePaths).join('\n') + '\n' : '')
1439
1456
  } catch (e) {
1440
1457
  substitute(/\{UI_VISUAL_GUIDANCE\}/g, `[ui-guidance] UI 须知注入异常(${e.message}),跳过`)
1441
1458
  }
@@ -1479,7 +1496,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1479
1496
  )
1480
1497
  // 写入 runtime JSON(既有机制照旧,新旧遥测共存)
1481
1498
  mkdirSync(runtimeDir, { recursive: true })
1482
- writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
1499
+ writeAtomicSync(runtimeStatePath(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
1483
1500
  } catch (e) {
1484
1501
  substitute(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
1485
1502
  }
@@ -1536,7 +1553,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1536
1553
  let runId = ''
1537
1554
  const execSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1538
1555
  const runtimeRoot = resolveRuntimeRoot(platformOpts, execSpecBase)
1539
- const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1556
+ const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
1540
1557
  try {
1541
1558
  if (existsSync(runIdFile)) {
1542
1559
  const c = readFileSync(runIdFile, 'utf8').trim()
@@ -1676,22 +1693,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1676
1693
  reviewMaterialsMd = await assembleStageReviewMaterials({ stage: matStage, cwd, changeName, specBase: tierSpecBase })
1677
1694
  } catch { /* 组装 best-effort:失败零注入 */ }
1678
1695
  }
1679
- promptText = promptText
1680
- .split('{REVIEW_TIER}').join(ceremonyInjection.tierValue + ceremonyInjection.menuMd)
1681
- .split('{REVIEW_TIER_REASON}').join(tier.reason)
1682
- .split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
1683
- .split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
1684
- .split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
1685
- .split('{REVIEW_MATERIALS}').join(reviewMaterialsMd) // 三阶段材料包槽(CLI 组装注入,Gap 1 收口——此前恒空串靠主代理手组,机械保证断一半;主代理点名半边由模板补位指引接手)
1696
+ // 全部走 substituteRaw(收集进 dynSegments——复入附录含评审六值;2026-10-16-step-guide-dyn-completeness)
1697
+ substituteRaw('{REVIEW_TIER}', ceremonyInjection.tierValue + ceremonyInjection.menuMd)
1698
+ substituteRaw('{REVIEW_TIER_REASON}', tier.reason)
1699
+ substituteRaw('{STAGE_REVIEW_RUN_ID}', reviewRunId)
1700
+ substituteRaw('{REVIEW_JSON_CONTRACT}', reviewContractMd)
1701
+ substituteRaw('{PRIOR_REVIEW_FACTS}', priorFactsMd)
1702
+ substituteRaw('{REVIEW_MATERIALS}', reviewMaterialsMd) // 三阶段材料包槽(CLI 组装注入,Gap 1 收口——此前恒空串靠主代理手组,机械保证断一半;主代理点名半边由模板补位指引接手)
1686
1703
  } catch (e) {
1687
- // 降级 self,避免 prompt 残留占位符
1688
- promptText = promptText
1689
- .split('{REVIEW_TIER}').join('self')
1690
- .split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
1691
- .split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
1692
- .split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
1693
- .split('{PRIOR_REVIEW_FACTS}').join('')
1694
- .split('{REVIEW_MATERIALS}').join('') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
1704
+ // 降级 self,避免 prompt 残留占位符(降级值同样收编进 dynSegments——附录如实呈现降级态)
1705
+ substituteRaw('{REVIEW_TIER}', 'self')
1706
+ substituteRaw('{REVIEW_TIER_REASON}', '分级异常降级 self: ' + e.message)
1707
+ substituteRaw('{STAGE_REVIEW_RUN_ID}', 'review-unknown')
1708
+ substituteRaw('{REVIEW_JSON_CONTRACT}', '(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
1709
+ substituteRaw('{PRIOR_REVIEW_FACTS}', '')
1710
+ substituteRaw('{REVIEW_MATERIALS}', '') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
1695
1711
  }
1696
1712
  }
1697
1713
 
@@ -1721,7 +1737,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1721
1737
  } catch (e) {
1722
1738
  metaInfo = '(注入失败:' + e.message + '——运行 sillyspec worktree meta 自查,meta.json 不存在则停止并报错)'
1723
1739
  }
1724
- promptText = promptText.split('{WORKTREE_META}').join(metaInfo)
1740
+ substituteRaw('{WORKTREE_META}', metaInfo)
1725
1741
  }
1726
1742
 
1727
1743
  // archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
@@ -1738,9 +1754,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1738
1754
  const summary = tcrChangeDir
1739
1755
  ? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
1740
1756
  : null
1741
- promptText = promptText.split('{TASK_COMPLETION_REPORT}').join(summary ? summary.report : '(无法计算完成度:变更目录缺失)')
1757
+ substituteRaw('{TASK_COMPLETION_REPORT}', summary ? summary.report : '(无法计算完成度:变更目录缺失)')
1742
1758
  } catch (e) {
1743
- promptText = promptText.split('{TASK_COMPLETION_REPORT}').join('(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
1759
+ substituteRaw('{TASK_COMPLETION_REPORT}', '(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
1744
1760
  }
1745
1761
  }
1746
1762
 
@@ -1781,7 +1797,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1781
1797
  } catch (e) {
1782
1798
  baselineInfo = '(worktree 基线注入失败:' + e.message + '——diff 对账前先 git branch -a 查 sillyspec/<change> 分支并用 git merge-base <main> <branch> 自查基点,勿用主仓 HEAD 当基点)'
1783
1799
  }
1784
- promptText = promptText.split('{WORKTREE_BASELINE_INFO}').join(baselineInfo)
1800
+ substituteRaw('{WORKTREE_BASELINE_INFO}', baselineInfo)
1785
1801
  }
1786
1802
 
1787
1803
  // verify「运行测试和质量扫描」evidence-auto 推荐注入(task-12,FR-11 / D-005@v2):
@@ -1822,7 +1838,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1822
1838
  } catch (e) {
1823
1839
  eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
1824
1840
  }
1825
- promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
1841
+ substituteRaw('{EVIDENCE_AUTO_RECOMMENDATION}', eaInjected)
1826
1842
  }
1827
1843
 
1828
1844
  // {ARCHIVE_IMPACT_AUDIT}(2026-09-09 ql-20260909-004):archive extract-module-impact 步的
@@ -1842,7 +1858,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1842
1858
  } catch (e) {
1843
1859
  auditBlock = '(三重核对机械预填失败:' + (e && e.message ? e.message : e) + '——回退手跑 git diff --name-only 比对)'
1844
1860
  }
1845
- promptText = promptText.split('{ARCHIVE_IMPACT_AUDIT}').join(auditBlock)
1861
+ substituteRaw('{ARCHIVE_IMPACT_AUDIT}', auditBlock)
1846
1862
  }
1847
1863
 
1848
1864
  // {SCOPE_AUDIT_TABLE}(2026-09-10-change-scope-audit task-05,FR-03):archive「确认归档」步的
@@ -1865,7 +1881,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1865
1881
  } catch (e) {
1866
1882
  scopeTable = '(变更范围对账表注入失败:' + (e && e.message ? e.message : e) + '——回退手跑 sillyspec scope-audit --change <name> 查看后再确认归档,注入失败不阻断归档)'
1867
1883
  }
1868
- promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
1884
+ substituteRaw('{SCOPE_AUDIT_TABLE}', scopeTable)
1869
1885
  }
1870
1886
 
1871
1887
  // {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
@@ -1901,7 +1917,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1901
1917
  } catch (e) {
1902
1918
  handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
1903
1919
  }
1904
- promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
1920
+ substituteRaw('{HANDOVER_SUMMARY}', handoverBlock)
1905
1921
  }
1906
1922
 
1907
1923
  // 注入模块上下文(brainstorm/plan/execute 阶段 + quick 首步「理解任务」——刀①:quick
@@ -9,6 +9,7 @@
9
9
  * - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
10
10
  */
11
11
  import { join, basename, extname } from 'node:path'
12
+ import { runtimeStatePath } from '../runtime-paths.js'
12
13
  import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs'
13
14
  import { safeGit, resolveRuntimeRoot } from './shared.js'
14
15
  import { SCAN_REQUIRED_DOCS } from '../constants.js'
@@ -332,7 +333,7 @@ export async function executeScanResumeCheck(cwd, platformOpts) {
332
333
  // .runtime/last-delta.json(归档/delta 补跑写入的最近变更快照),14 天窗口内提示本轮
333
334
  // scan 优先核对其涉及模块——advisory 不阻断不改变步骤结构;缺失/过期/解析失败静默。
334
335
  try {
335
- const sidecarPath = join(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
336
+ const sidecarPath = runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
336
337
  if (existsSync(sidecarPath)) {
337
338
  const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
338
339
  const ageMs = Date.now() - Date.parse(sidecar.updatedAt || '')