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
@@ -21,6 +21,7 @@
21
21
  * 纯 --init(无 --draft)路径零变化;无 sidecar → 门禁 not-applicable(存量变更零红)。
22
22
  */
23
23
  import { existsSync, readFileSync } from 'node:fs'
24
+ import { runtimeStatePath } from './runtime-paths.js'
24
25
  import { join } from 'node:path'
25
26
  import { writeAtomicSync } from './fs-atomic.js'
26
27
  // 三件套原语消费方化(R7 切片三 task-04):标记格式/指纹/三态判定/重锚的机械半边在本模块,
@@ -33,7 +34,7 @@ const AMEND_CMD = 'sillyspec verify-probes --change <变更名> --amend-draft'
33
34
 
34
35
  /** sidecar 路径(.runtime 下,与质量扫描记录/探针回执同族) */
35
36
  export function verifyDraftSidecarPath(runtimeRoot, changeName) {
36
- return join(runtimeRoot, `verify-draft-${changeName}.json`)
37
+ return runtimeStatePath(runtimeRoot, `verify-draft-${changeName}.json`)
37
38
  }
38
39
 
39
40
  /**
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { execSync, spawnSync } from 'child_process'
26
+ import { runtimeStatePath } from './runtime-paths.js'
26
27
  import { createHash } from 'crypto'
27
28
  import { IR_STRICT_SINCE } from './constants.js'
28
29
  import { gitQuiet } from './git-helper.js'
@@ -236,7 +237,7 @@ function recordVerifyLintTally({ specBase, result }) {
236
237
  try {
237
238
  const runtimeDir = join(specBase, '.runtime')
238
239
  mkdirSync(runtimeDir, { recursive: true })
239
- const tallyPath = join(runtimeDir, 'verify-lint-tally.json')
240
+ const tallyPath = runtimeStatePath(runtimeDir, 'verify-lint-tally.json')
240
241
  let tally = { totalRuns: 0, failedRuns: 0, history: [] }
241
242
  try {
242
243
  tally = { ...tally, ...JSON.parse(readFileSync(tallyPath, 'utf8')) }
@@ -1424,7 +1425,7 @@ function readTestRerunConfig(specBase) {
1424
1425
  }
1425
1426
 
1426
1427
  function testRerunLedgerPath(specBase, changeName) {
1427
- return join(specBase, '.runtime', `test-rerun-${changeName || 'change'}.json`)
1428
+ return runtimeStatePath(join(specBase, '.runtime'), `test-rerun-${changeName || 'change'}.json`)
1428
1429
  }
1429
1430
 
1430
1431
  /**
@@ -1861,7 +1862,7 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
1861
1862
  if (!ctx || typeof ctx.repos !== 'object' || ctx.repos === null) {
1862
1863
  try {
1863
1864
  const rtRoot = resolveRuntimeRoot({ runtimeRoot: opts.runtimeRoot, specDriftAnchor: opts.specDriftAnchor }, opts.specBase || join(cwd, '.sillyspec'))
1864
- const pathspecFile = join(rtRoot, `apply-pathspec-${changeName}.txt`)
1865
+ const pathspecFile = runtimeStatePath(rtRoot, `apply-pathspec-${changeName}.txt`)
1865
1866
  if (existsSync(pathspecFile)) {
1866
1867
  const declared = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean)
1867
1868
  .map(p => p.replace(/\\/g, '/'))
@@ -2821,6 +2822,31 @@ function inferFullTestCommand(projectRoot) {
2821
2822
  * 相关回归)。组卷改配额制:每组优先面整跑,普通 import 依赖只填剩余席位(可为 0)——帽
2822
2823
  * 只界普通依赖尾。批尺寸上界护栏由 TEST_TIMEOUT_MS 兜底(超时=失败 fail-closed 不变)。
2823
2824
  */
2825
+ /**
2826
+ * 批命令长度分块(2026-10-15 平台侧实证,跨仓事故驱动最小修复):Windows cmd 执行器
2827
+ * 命令行上限 ~8191 字符——大依赖面(广被引用的前端文件变更 → 依赖测试 100+ 个)拼单条
2828
+ * vitest/node --test 命令必炸「命令行太长」exit 1,且失败批进增量账本自我循环恒败
2829
+ * (实测:137 文件批反复重跑复败,verify 门禁无法收口)。按「命令前缀+文件参数拼接
2830
+ * 总长」分块:单块保持原名(零回归),多块命名 <base>:N(N 从 1 起);每块独立计账。
2831
+ */
2832
+ function chunkByCommandLength(prefix, files, maxLen = 6000) {
2833
+ const chunks = []
2834
+ let cur = []
2835
+ let curLen = prefix.length
2836
+ for (const f of files) {
2837
+ const add = f.length + 1
2838
+ if (cur.length > 0 && curLen + add > maxLen) {
2839
+ chunks.push(cur)
2840
+ cur = []
2841
+ curLen = prefix.length
2842
+ }
2843
+ cur.push(f)
2844
+ curLen += add
2845
+ }
2846
+ if (cur.length > 0) chunks.push(cur)
2847
+ return chunks
2848
+ }
2849
+
2824
2850
  function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, priorityFiles = [] }) {
2825
2851
  const norm = (p2) => String(p2).replace(/\\/g, '/')
2826
2852
  const changedSet = new Set((changedFiles || []).map(norm))
@@ -2930,10 +2956,11 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
2930
2956
  const jsProjectRun = jsProject.filter(f => !E2E_DIR_RE.test(_norm(f)))
2931
2957
  const e2eDroppedFiles = jsProject.filter(f => E2E_DIR_RE.test(_norm(f)))
2932
2958
  let jsxRunner = null
2959
+ let jsxRunnerFromHits = false
2933
2960
  for (const h of hits || []) {
2934
2961
  const cmd = String(h.test || '')
2935
2962
  const m = /^((?:cd\s+[^&|;]+&&\s*)*[^&|;]*?(?:vitest|jest))(?=\s|$)/.exec(cmd) || /(?:^|&&|;|\|\|)\s*([^&|;]*(?:vitest|jest)(?:\s|$))/.exec(cmd)
2936
- if (m) { jsxRunner = m[1].trim(); break }
2963
+ if (m) { jsxRunner = m[1].trim(); jsxRunnerFromHits = true; break }
2937
2964
  }
2938
2965
  if (!jsxRunner && cwd && jsProjectRun.length > 0) {
2939
2966
  const inferred = inferJsxRunner(cwd, jsProjectRun[0])
@@ -2958,24 +2985,59 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
2958
2985
  gi++
2959
2986
  }
2960
2987
  }
2961
- if (jsNative.length > 0) batches.push({ name: 'deps(auto-js)', short: 'js', command: `node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout ${jsNative.join(' ')}`, count: jsNative.length, prioCount: jsNative.filter(f => prio(f) === 0).length, dropped: jsTest.length - jsRun.length, prioDropped: jsQ.prioDropped, tap: true, files: jsNative })
2988
+ if (jsNative.length > 0) {
2989
+ const jsPrefix = 'node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout '
2990
+ const jsChunks = chunkByCommandLength(jsPrefix, jsNative)
2991
+ jsChunks.forEach((chunk, i) => {
2992
+ const isMulti = jsChunks.length > 1
2993
+ batches.push({ name: isMulti ? `deps(auto-js:${i + 1}/${jsChunks.length})` : 'deps(auto-js)', short: 'js', command: jsPrefix + chunk.join(' '), count: chunk.length, prioCount: chunk.filter(f => prio(f) === 0).length, dropped: i === 0 ? (jsTest.length - jsRun.length) : 0, prioDropped: i === 0 ? jsQ.prioDropped : 0, tap: true, files: chunk })
2994
+ })
2995
+ }
2962
2996
  if (jsMeta.length > 0) batches.push({ name: 'deps(auto-js-meta-skip)', short: 'js-meta-skip', command: null, count: jsMeta.length, dropped: 0, skip: true, files: jsMeta, reason: '套件编排器(run-tests.mjs=npm test 组卷入口)非单测文件——内嵌执行=递归全量套件(嵌套 runner 污染假败);全量门由收口实测/CI 的 npm test 承担' })
2963
2997
 
2964
2998
  if (nonTestFiles.length > 0) batches.push({ name: 'deps(auto-nontest-skip)', short: 'nontest-skip', command: null, count: nonTestFiles.length, dropped: 0, skip: true, files: nonTestFiles, reason: `非测试形态文件 ${nonTestFiles.length} 个不进单测执行批(node --test 语义「文件即测试」/ vitest No test suite found / pytest 0 collected exit 5——直跑必假败;坑 verify-dynamic-subset-node-test-ts-source):${nonTestFiles.slice(0, 3).join('、')}${nonTestFiles.length > 3 ? ' 等' : ''}——FR 绑定面脏数据可 sillyspec tests --unbind --tests <路径> 清行` })
2965
2999
  if (jsProjectRun.length > 0) {
2966
- if (jsxRunner) {
2967
- const isVitest = /vitest/.test(jsxRunner)
2968
- // jsx 批的 cd 重定基(评审 MEDIUM 清偿):cdDir 只源 pyRunner——jsxRunner 自带 cd 前缀
2969
- // (cd frontend && pnpm exec vitest)时文件未重定基产双前缀错路径;独立提取 jsxDir。
2970
- const jsxDir = /(?:^|\s)cd\s+(\S+)\s*&&/.exec(jsxRunner)?.[1]
2971
- const rebaseJsx = (f) => (jsxDir && f.startsWith(jsxDir + '/')) ? f.slice(jsxDir.length + 1) : f
2972
- // R20(2026-09-27-thin-display-fix 实证,用户授权修复):Windows cmd 执行器对含
2973
- // 括号/方括号/空格的路径(src/app/(dashboard)/workspaces/[id]/...)不加引号会以
2974
- // 「此时不应有 <。」语法错误炸整批——文件参数含特殊字符时逐个 wrap 双引号。
2975
- const quoteWinPath = (f) => (/[()\[\]\s&^%,;!]/.test(f) ? `"${f}"` : f)
2976
- batches.push({ name: 'deps(auto-jsx)', short: 'jsx', command: `${jsxRunner} ${isVitest ? 'run ' : ''}${jsProjectRun.map(rebaseJsx).map(quoteWinPath).join(' ')}`, count: jsProjectRun.length, dropped: 0, files: jsProjectRun })
2977
- } else {
2978
- batches.push({ name: 'deps(auto-jsx-skip)', short: 'jsx-skip', command: null, count: jsProjectRun.length, dropped: 0, skip: true, files: jsProjectRun, reason: `JSX 测试文件(tsx/jsx,${jsProjectRun.length} 个)node 原生不可跑且项目结构无 vitest/jest 运行器可推断——转项目运行器执行并如实披露,不制造恒败段(R18-SF-full 残差段 11 连败的坑)` })
3000
+ // R20(2026-09-27-thin-display-fix 实证,用户授权修复):Windows cmd 执行器对含
3001
+ // 括号/方括号/空格的路径(src/app/(dashboard)/workspaces/[id]/...)不加引号会以
3002
+ // 「此时不应有 <。」语法错误炸整批——文件参数含特殊字符时逐个 wrap 双引号。
3003
+ const quoteWinPath = (f) => (/[()\[\]\s&^%,;!]/.test(f) ? `"${f}"` : f)
3004
+ // jsx 批按包根分组(2026-10-15-zcode-engine-appserver verify 实证,跨仓事故驱动修复):
3005
+ // 混合包根(frontend/ 的 tsx 测试 + sillyhub-daemon/ 的 ts 测试同批)共享单一 runner
3006
+ // 前缀时,非本根路径在 runner 的 cwd 下「No test files found」exit 1 恒败(分块修复
3007
+ // 后暴露:第二块全 daemon 路径仍跑 cd frontend)。与 pyGroups 同款:每文件按最近
3008
+ // package.json 推断自身 runner/根;显式命中命令(hits)保持整批单组旧语义。
3009
+ const jsxGroups = new Map() // key: 组命令前缀
3010
+ const jsxUngrouped = []
3011
+ for (const f of jsProjectRun) {
3012
+ // 命中命令(hits)→ 整批单组旧语义;全局推断(jsProjectRun[0])仅兜底,逐文件按自身包根推断优先。
3013
+ let command = null
3014
+ let dirKey = null
3015
+ if (!jsxRunnerFromHits && cwd) {
3016
+ const inferred = inferJsxRunner(cwd, f)
3017
+ if (inferred) { command = inferred.command; dirKey = inferred.dir || null }
3018
+ }
3019
+ if (!command) command = jsxRunner
3020
+ if (!command) { jsxUngrouped.push(f); continue }
3021
+ if (!jsxGroups.has(command)) jsxGroups.set(command, { files: [], dirKey })
3022
+ jsxGroups.get(command).files.push(f)
3023
+ }
3024
+ const multiGroup = jsxGroups.size > 1
3025
+ for (const [command, grp] of jsxGroups) {
3026
+ const isVitest = /vitest/.test(command)
3027
+ const jsxPrefix = `${command} ${isVitest ? 'run ' : ''}`
3028
+ const grpDir = grp.dirKey || /(?:^|\s)cd\s+(\S+)\s*&&/.exec(command)?.[1] || null
3029
+ const rebaseJsx = (f) => (grpDir && f.startsWith(grpDir + '/')) ? f.slice(grpDir.length + 1) : f
3030
+ const jsxArgs = grp.files.map(rebaseJsx).map(quoteWinPath)
3031
+ const jsxChunks = chunkByCommandLength(jsxPrefix, jsxArgs)
3032
+ const baseName = multiGroup ? `deps(auto-jsx:${grp.dirKey || 'root'})` : 'deps(auto-jsx)'
3033
+ jsxChunks.forEach((chunk, i) => {
3034
+ const isMulti = jsxChunks.length > 1
3035
+ batches.push({ name: isMulti ? `${baseName}:${i + 1}/${jsxChunks.length}` : baseName, short: 'jsx', command: jsxPrefix + chunk.join(' '), count: chunk.length, dropped: 0, files: chunk })
3036
+ })
3037
+ }
3038
+ if (jsxUngrouped.length > 0) {
3039
+ // 无运行器可推断的文件(含 !jsxRunner && !cwd 全量形态):skip 批如实披露(原整批 skip 语义按文件收窄)。
3040
+ batches.push({ name: multiGroup || jsxGroups.size > 0 ? 'deps(auto-jsx-skip:unresolved)' : 'deps(auto-jsx-skip)', short: 'jsx-skip', command: null, count: jsxUngrouped.length, dropped: 0, skip: true, files: jsxUngrouped, reason: `JSX 测试文件(tsx/jsx,${jsxUngrouped.length} 个)node 原生不可跑且各自最近 package.json 无 vitest/jest 运行器可推断——转项目运行器执行并如实披露,不制造恒败段(R18-SF-full 残差段 11 连败的坑)` })
2979
3041
  }
2980
3042
  }
2981
3043
  if (e2eDroppedFiles.length > 0) {
@@ -3042,6 +3104,26 @@ function traceRepoRoots({ specBase, cwd }) {
3042
3104
  }
3043
3105
 
3044
3106
  /** 残差解析:active·非 superseded·锚命中的 trace 行 → { anchors, rows, files, dangling, perFile } */
3107
+ /**
3108
+ * 主仓侧候选解析根(worktree 感知;2026-10-15-zcode-engine-appserver verify 实证,跨仓
3109
+ * 事故驱动最小修复):变更在隔离 worktree 内实现时,新增测试文件在主仓根不可见——
3110
+ * 悬空判定对主仓文件按「主仓根 ∪ 变更 worktree 根」多根解析(任一存在即非悬空);
3111
+ * 残差执行根同款(文件在 worktree 存在则从 worktree 根跑)。meta 缺失/不可读 → 仅主仓。
3112
+ */
3113
+ function traceMainRoots({ specBase, changeName, cwd }) {
3114
+ const roots = [cwd]
3115
+ try {
3116
+ const metaPath = join(specBase, '.runtime', 'worktrees', changeName, 'meta.json')
3117
+ if (existsSync(metaPath)) {
3118
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
3119
+ if (meta && typeof meta.worktreePath === 'string' && meta.worktreePath && existsSync(meta.worktreePath) && meta.worktreePath !== cwd) {
3120
+ roots.push(meta.worktreePath)
3121
+ }
3122
+ }
3123
+ } catch { /* meta 不可读 → 仅主仓(零回归) */ }
3124
+ return roots
3125
+ }
3126
+
3045
3127
  export function resolveTraceResidual({ specBase, changeName, cwd }) {
3046
3128
  const anchors = resolveVerifyAnchorSet({ specBase, changeName })
3047
3129
  if (anchors.length === 0) return { anchors, rows: [], files: [], dangling: [], perFile: [] }
@@ -3050,19 +3132,25 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
3050
3132
  // tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——文件面/悬空判定走剥锚。
3051
3133
  // FR-06(2026-10-09):文件按行 repo 归属配对(perFile),悬空判定/残差执行按行根解析。
3052
3134
  const repoRoots = traceRepoRoots({ specBase, cwd })
3135
+ const mainRoots = traceMainRoots({ specBase, changeName, cwd })
3053
3136
  const seen = new Set()
3054
3137
  const perFile = []
3055
3138
  for (const r of rows) {
3056
3139
  const repoKey = r.repo && r.repo !== 'main' ? r.repo : null
3057
3140
  for (const t of (r.tests || [])) {
3058
3141
  const f = testAnchorFile(t)
3059
- const key = (repoKey ? repoKey + '' : '') + f
3142
+ const key = (repoKey ? repoKey + '\u0000' : '') + f
3060
3143
  if (!seen.has(key)) { seen.add(key); perFile.push({ repo: repoKey, file: f }) }
3061
3144
  }
3062
3145
  }
3063
3146
  const files = [...new Set(perFile.map(x => x.file))].sort()
3147
+ // 主仓文件多根解析:解析到 worktree 根则记录 resolvedRoot(残差执行同款消费)。
3148
+ for (const x of perFile) {
3149
+ if (x.repo) { x.resolvedRoot = repoRoots.get(x.repo) || cwd; continue }
3150
+ x.resolvedRoot = mainRoots.find(root => existsSync(resolve(root, x.file))) || cwd
3151
+ }
3064
3152
  const dangling = perFile
3065
- .filter(x => !existsSync(resolve((x.repo && repoRoots.get(x.repo)) || cwd, x.file)))
3153
+ .filter(x => !existsSync(resolve(x.resolvedRoot, x.file)))
3066
3154
  .map(x => x.repo ? `${x.repo}:${x.file}` : x.file)
3067
3155
  return { anchors, rows, files, dangling, perFile }
3068
3156
  }
@@ -3141,8 +3229,9 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
3141
3229
  for (const x of (tr.perFile || [])) {
3142
3230
  if (provable.has(String(x.file).replace(/\\/g, '/'))) continue
3143
3231
  const _rk = x.repo || null
3144
- const _root = (_rk && _repoRoots.get(_rk)) || cwd
3145
- const _gk = _rk + '' + _root
3232
+ // worktree 感知(同悬空判定口径):主仓行按 resolvedRoot(主仓或变更 worktree 根)取执行根。
3233
+ const _root = (_rk && _repoRoots.get(_rk)) || x.resolvedRoot || cwd
3234
+ const _gk = _rk + '\u0000' + _root
3146
3235
  if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
3147
3236
  _gmap.get(_gk).files.push(x.file)
3148
3237
  }
@@ -3284,8 +3373,20 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
3284
3373
  // not ok 以测试文件为套件名(not ok N - test/b.test.mjs),用判账行集匹配批内文件名把
3285
3374
  // 「批失败」收窄到「文件失败」;归因不出(非 TAP/行集缺文件名)保守全记批文件。
3286
3375
  failedFiles: (() => {
3376
+ // 基建失败甄别(坑 verify-incremental-rerun-infra-batch-growth,2026-10-15 两变更实证:
3377
+ // Windows argv 超限/runner 启动失败类批失败无任何测试级失败行,重跑不可能转绿——计入
3378
+ // 前轮失败批只会让增量面随轮膨胀(141→159 自锁)。判据:输出无 not ok/FAILED/ERROR/
3379
+ // AssertionError 任何测试级痕迹,且 reason 命中基建关键词(命令行太长/E2BIG/spawn/
3380
+ // Cannot find module/No test files found)——此类批不进失败批账本,loud 留痕供人工。
3381
+ const INFRA_REASON_RE = /命令行太长|E2BIG|spawn|Cannot find module|No test files found|ENAMETOOLONG/i
3382
+ const TEST_FAILURE_RE = /not ok \d|FAILED|ERROR|AssertionError|assert/i
3287
3383
  const out = new Set()
3288
3384
  for (const r of perModule.filter(x => x.status === 'failed')) {
3385
+ const text = String(r.outputFull || r.outputTail || '')
3386
+ if (!TEST_FAILURE_RE.test(text) && INFRA_REASON_RE.test(String(r.reason || '') + text)) {
3387
+ console.warn(`⚠️ 批 ${r.name} 判定基建失败(${String(r.reason || 'runner 层失败').slice(0, 80)})——不进增量失败批账本(重跑不可能转绿),修复基建后整轮重测`)
3388
+ continue
3389
+ }
3289
3390
  for (const f of attributeFailedFiles(r)) out.add(f)
3290
3391
  }
3291
3392
  return [...out].sort()
@@ -3680,7 +3781,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
3680
3781
  if (!absPath || !existsSync(absPath)) return { regressed: false, prevMtime: null }
3681
3782
  const st = statSync(absPath)
3682
3783
  const hash = createHash('sha256').update(readFileSync(absPath)).digest('hex')
3683
- const hwmPath = join(specBase, '.runtime', `verify-result-hwm-${changeName}.json`)
3784
+ const hwmPath = runtimeStatePath(join(specBase, '.runtime'), `verify-result-hwm-${changeName}.json`)
3684
3785
  let prev = null
3685
3786
  try { prev = JSON.parse(readFileSync(hwmPath, 'utf8')) } catch { prev = null }
3686
3787
  const regressed = !!(prev && typeof prev.hash === 'string' &&
@@ -3690,7 +3791,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
3690
3791
  if (!prev || st.mtimeMs >= (prev.mtimeMs || 0)) {
3691
3792
  try {
3692
3793
  mkdirSync(join(specBase, '.runtime'), { recursive: true })
3693
- writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: st.mtimeMs, ts: Date.now() }) + '\n')
3794
+ writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: Math.round(st.mtimeMs), ts: Date.now() }) + '\n')
3694
3795
  } catch { /* hwm 落盘失败 → 下次当首次(fail-open) */ }
3695
3796
  }
3696
3797
  return { regressed, prevMtime: prev && prev.mtimeMs ? prev.mtimeMs : null }
@@ -4207,7 +4308,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
4207
4308
  }
4208
4309
  } catch { /* 窗口源失败静默省略 */ }
4209
4310
  // B3 apply-pathspec 兜底(存在则并入,读取失败静默忽略——前两源不受影响)
4210
- const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
4311
+ const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`)
4211
4312
  if (existsSync(pathspecFile)) {
4212
4313
  try {
4213
4314
  const lines = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(normalizeReconcilePath)
@@ -4604,7 +4705,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
4604
4705
  const rt = runtimeRoot || join(sb, '.runtime')
4605
4706
 
4606
4707
  // —— 声明侧:开跑定价落档(task-03 产物,tier/components/reasons/transitions)——
4607
- const tierPath = join(rt, `ceremony-tier-${changeName}.json`)
4708
+ const tierPath = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
4608
4709
  let tierFile = null
4609
4710
  try { tierFile = JSON.parse(readFileSync(tierPath, 'utf8')) } catch { /* 缺失/坏 JSON → 下方跳过 */ }
4610
4711
  if (tierFile == null || typeof tierFile !== 'object' || Array.isArray(tierFile)) {
@@ -4701,7 +4802,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
4701
4802
  recomputedReasons: [`双跑低报自动追赶(事实面 tier=${verdict.factTier},证据面 seed ${actual.files.length} 文件)`],
4702
4803
  frictionCounts: {},
4703
4804
  })
4704
- const tierPathForReprice = join(rt, `ceremony-tier-${changeName}.json`)
4805
+ const tierPathForReprice = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
4705
4806
  const { writeAtomicSync } = await import('./fs-atomic.js')
4706
4807
  writeAtomicSync(tierPathForReprice, JSON.stringify(caught, null, 2) + '\n')
4707
4808
  result.violation.message += `;档位文件已自动按事实面追赶重定价(${String(declaredTier)} → ${caught.tier})——修复低报根因后重跑 --done 即对账通过,勿手动改档(ql-013 死锁修复)`