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
@@ -329,7 +329,62 @@ function validateCrossRepoNoOp(ctx, projectRoot, changeName) {
329
329
  * crossRepoValidated?: Array<{ repo: string, head: string }>
330
330
  * }}
331
331
  */
332
- export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null } = {}) {
332
+ /**
333
+ * 主仓级互斥锁(坑 main-apply-no-mutex → 泛化 main-repo-no-mutex,2026-08-23 二批实证:
334
+ * 除 apply 外,cleanup(worktree 注册表/分支删除)、archive 收尾(目录 rename + 共享 index
335
+ * 的 git add + marker 删除)同样直接改主仓共享状态且零互斥——两会话并发操作互踩。泛化为
336
+ * withMainRepoLock:同一把主仓锁保护所有「写主仓的破坏性短操作」(apply / cleanup / archive
337
+ * 收尾);verify 的长时间测试执行**不**上锁(读树为主,靠对账归属过滤与归因提示治误伤)。
338
+ *
339
+ * 锁粒度 = 主仓级(竞争资源是 main 工作区/.git 状态,非 change):O_EXCL 文件锁复用
340
+ * quicklog 的 withFileLock(stale 偷锁 + rename 防 TOCTOU),锁内容写
341
+ * {pid, changeName, purpose, startedAt} 供超时报错展示持有者。staleMs 10min(大 diff/deps
342
+ * 可慢);抢锁等待 60s 仍失败 → fail-closed 报错退出:写主仓操作并发互踩不可自动恢复,
343
+ * 宁可明确失败让人重试,不硬闯。只读路径(--check-only/status)不加锁。
344
+ */
345
+ export async function withMainRepoLock(projectRoot, changeName, purpose, fn, opts = {}) {
346
+ const { withFileLock } = await import('./quicklog.js');
347
+ const lockPath = join(projectRoot, '.sillyspec', '.runtime', 'main-repo.lock');
348
+ const holder = JSON.stringify({ pid: process.pid, changeName, purpose: purpose || 'unknown', startedAt: new Date().toISOString() });
349
+ const { staleMs = 10 * 60 * 1000, retryMs = 200 } = opts;
350
+ // 等待时长可被 env 覆盖(测试与紧急调参用;opts.timeoutMs 显式传入仍最高优先)
351
+ const envTimeout = parseInt(process.env.SILLYSPEC_MAIN_REPO_LOCK_TIMEOUT_MS || '', 10);
352
+ const defaultTimeout = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 60 * 1000;
353
+ const { timeoutMs = defaultTimeout } = opts;
354
+ // 进程退出兜底清锁:临界区内 process.exit(如 archiveChangeDirectory 的 guard exit(1))不会走
355
+ // withFileLock 的 finally——不挂此钩子锁会残留挡他者直到 staleMs(10min)被偷。正常路径
356
+ // finally 先释放并注销钩子,钩子只在进程真正退出时执行,无双删竞态。
357
+ const onExit = () => { try { unlinkSync(lockPath) } catch {} };
358
+ process.on('exit', onExit);
359
+ try {
360
+ return await withFileLock(lockPath, fn, {
361
+ content: holder,
362
+ staleMs,
363
+ timeoutMs,
364
+ retryMs,
365
+ });
366
+ } catch (e) {
367
+ if (/文件锁超时/.test(String(e.message))) {
368
+ let holderInfo = '';
369
+ try {
370
+ const raw = readFileSync(lockPath, 'utf8');
371
+ try {
372
+ const h = JSON.parse(raw);
373
+ holderInfo = `(持有者: pid=${h.pid}, change=${h.changeName}, 操作=${h.purpose}, 始于 ${h.startedAt})`;
374
+ } catch { holderInfo = `(持有者标识: ${raw.slice(0, 120)})`; }
375
+ } catch { /* 锁文件已被释放(竞态窗口)→ 无持有者信息 */ }
376
+ throw new Error(
377
+ `主仓互斥锁被占用${holderInfo}:另一会话正在对主仓执行写操作(apply/cleanup/归档会写主仓工作区与 git 状态,并发互踩会互相清文件)。` +
378
+ `等它完成后重试即可;若确认持有进程已崩溃退出,删除锁文件后重试:${lockPath}`
379
+ );
380
+ }
381
+ throw e;
382
+ } finally {
383
+ process.removeListener('exit', onExit);
384
+ }
385
+ }
386
+
387
+ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false } = {}) {
333
388
  const projectRoot = cwd || process.cwd();
334
389
  const wm = new WorktreeManager({ cwd: projectRoot });
335
390
  const meta = wm.getMeta(changeName);
@@ -344,6 +399,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
344
399
  errors: [],
345
400
  merged: false,
346
401
  crossRepoValidated: [],
402
+ skippedOverlapFiles: [],
347
403
  };
348
404
 
349
405
  // --- 0. 跨仓 task no-op 校验(task-05 / D-009) ---
@@ -464,6 +520,20 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
464
520
  if (wtBlob === mainBlob) continue;
465
521
  result.hashMismatchFiles.push(f);
466
522
  }
523
+ // 派生产物基线漂移 advisory(坑 derived-artifact-stale-baseline,2026-08-23 实证:多 agent
524
+ // 并发仓中 worktree 旧基线生成的 api-types 被 apply 落地,把并行变更已合入主仓的新枚举刷掉,
525
+ // 一次 build 红)。hashMismatchFiles 此前只在 rescue 分类 / merge 冲突时冒头——语义级覆盖
526
+ // (旧内容文本可合 → --3way 静默成功)零提示。纯 warning 不阻断(生成器类变更的产物是合法
527
+ // 交付,是否重跑生成命令由 agent 判断)。
528
+ {
529
+ const derivedRisk = result.hashMismatchFiles.filter(f => changedFiles.includes(f));
530
+ if (derivedRisk.length > 0) {
531
+ result.warnings = (result.warnings || []).concat([
532
+ `${derivedRisk.length} 个变更文件在 worktree 基线后主仓已有新提交(并行变更可能已合入):${derivedRisk.slice(0, 5).join(', ')}${derivedRisk.length > 5 ? ' 等' : ''}` +
533
+ `——若其中含生成产物(api-types/generated 等),本次 apply 可能以旧基线内容覆盖已合入的新内容;apply 后在新基线重跑生成命令(如 gen:types)再验证`
534
+ ]);
535
+ }
536
+ }
467
537
 
468
538
  // --- 4. 校验:变更文件 ⊆ 清单(无清单则跳过)---
469
539
  if (hasAllowList) {
@@ -482,8 +552,9 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
482
552
  // --- 4.6 显式 --merge:用户显式选择 git merge 兜底(主干已提交推进重叠时的三方合并) ---
483
553
  // 触发点从「4.5 baseline 漂移自动降级」改为「用户显式 --merge flag」(D-001 保留,触发方式变化)。
484
554
  // 用 --merge 时跳过未提交 dirty 拦截——merge 同样要求工作区相对干净,此处仅提示风险,真正失败由 applyByMerge 报告。
555
+ // keepConflicts:true——显式 --merge 冲突时保留现场供手工解决(不再直接 abort 丢上下文)。
485
556
  if (merge && !checkOnly) {
486
- return applyByMerge(result, changeName, projectRoot, wm);
557
+ return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: true });
487
558
  }
488
559
 
489
560
  // --- 4.5 校验:主工作区「未提交」脏文件是否与本次变更重叠(overlap-only 拦截)---
@@ -518,6 +589,29 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
518
589
  )].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
519
590
  const overlapDirty = dirtyFiles.filter(f => changedFiles.includes(f));
520
591
  if (overlapDirty.length > 0) {
592
+ // --skip-overlap(坑 apply-overlap-all-or-nothing,2026-08-23 实证:多 agent 并发仓主仓
593
+ // 常态有在途变更,overlap 整批拦截使 apply 基本不可用,rescue 手动 cp 又留混合状态):
594
+ // 显式 opt-in 剔除重叠文件、应用干净子集。重叠文件留在 worktree——step8 非 force
595
+ // cleanup 被 hasUnappliedChanges 护栏拦住(主仓工作区逐字节降噪层只剔除已应用文件),
596
+ // 待主仓干净后重新 apply(此时只剩剩余文件)或人工裁决。
597
+ if (skipOverlap && !checkOnly) {
598
+ const skipSet = new Set(overlapDirty);
599
+ result.skippedOverlapFiles = overlapDirty.slice();
600
+ changedFiles = changedFiles.filter(f => !skipSet.has(f));
601
+ result.changedFiles = changedFiles;
602
+ result.deletedFiles = deletedFiles.filter(f => !skipSet.has(f));
603
+ result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !skipSet.has(f));
604
+ if (changedFiles.length === 0) {
605
+ result.errors.push(
606
+ `--skip-overlap:本次 ${overlapDirty.length} 个变更文件全部与主仓未提交改动重叠,无可应用子集:\n ${overlapDirty.join('\n ')}\n` +
607
+ `请先提交/stash 主仓改动后重试(不带 --skip-overlap)。`
608
+ );
609
+ return result;
610
+ }
611
+ result.warnings = (result.warnings || []).concat([
612
+ `--skip-overlap:跳过 ${overlapDirty.length} 个与主仓未提交改动重叠的文件(留在 worktree,未应用):${overlapDirty.join(', ')}——待主仓提交/stash 后重新 apply 只应用剩余文件,或确认放弃后 cleanup --force`
613
+ ]);
614
+ } else {
521
615
  // 重叠拦截:只有与本次变更同文件的未提交改动才无法安全 apply(列重叠文件,非全部脏文件)
522
616
  const rescueDirty = computeRescueDirtyFiles(projectRoot);
523
617
  result.rescueCommands = generateRescueCommands({
@@ -531,6 +625,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
531
625
  result.errors.push(
532
626
  `主工作区以下未提交文件与本次 apply 的变更重叠,git apply 无法安全应用:\n ${overlapDirty.join('\n ')}\n` +
533
627
  `请先提交或暂存这些改动,再重新 apply:\n git add -A && git commit -m "..." 或 git stash\n` +
628
+ `或应用非重叠部分(重叠文件留在 worktree,主仓干净后重新 apply 只补剩余):\n sillyspec worktree apply ${changeName} --skip-overlap\n` +
534
629
  (result.rescueCommands.commands.length > 0 || result.rescueCommands.warnings.length > 0
535
630
  ? `或手动 rescue(旁路 git apply,逐文件 cp 安全子集;cp 后需手动 sillyspec worktree cleanup ${changeName}):\n` +
536
631
  result.rescueCommands.commands.map(c => ` ${c}`).join('\n') +
@@ -539,6 +634,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
539
634
  : '')
540
635
  );
541
636
  if (!checkOnly) return result; // checkOnly 收集不短路(一次报全);真实 apply 短路
637
+ }
542
638
  } else {
543
639
  // 无关脏文件放行(只校验重叠文件):--3way 若因 autocrlf 报 does not match index 会被
544
640
  // step7 catch 回滚(交集空前提下无损,见上注释),stash 后重试即可——不再硬挡 rescue 手动路径
@@ -559,6 +655,27 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
559
655
  // 如果脏文件和本次 apply 的文件有交集 → 报错
560
656
  const conflictDirty = mainDirtyFiles.filter(f => changedFiles.includes(f));
561
657
  if (conflictDirty.length > 0) {
658
+ // --skip-overlap 同款剔除(4.5 过滤后此处残余多为 staged 口径差集,防御性同处理)
659
+ if (skipOverlap && !checkOnly) {
660
+ const skipSet5a = new Set(conflictDirty);
661
+ for (const f of conflictDirty) {
662
+ if (!result.skippedOverlapFiles.includes(f)) result.skippedOverlapFiles.push(f);
663
+ }
664
+ changedFiles = changedFiles.filter(f => !skipSet5a.has(f));
665
+ result.changedFiles = changedFiles;
666
+ result.deletedFiles = deletedFiles.filter(f => !skipSet5a.has(f));
667
+ result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !skipSet5a.has(f));
668
+ if (changedFiles.length === 0) {
669
+ result.errors.push(
670
+ `--skip-overlap:变更文件全部与主仓未提交改动重叠,无可应用子集:\n ${conflictDirty.join('\n ')}\n` +
671
+ `请先提交/stash 主仓改动后重试。`
672
+ );
673
+ return result;
674
+ }
675
+ result.warnings = (result.warnings || []).concat([
676
+ `--skip-overlap(5a 口径):再剔除 ${conflictDirty.length} 个重叠文件:${conflictDirty.join(', ')}`
677
+ ]);
678
+ } else {
562
679
  const rescueDirty5a = computeRescueDirtyFiles(projectRoot);
563
680
  result.rescueCommands = generateRescueCommands({
564
681
  changedFiles, dirtyFiles: rescueDirty5a, hashMismatchFiles: result.hashMismatchFiles,
@@ -566,6 +683,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
566
683
  });
567
684
  result.errors.push(
568
685
  `主工作区有以下未 commit 的变更,会影响 apply:\n ${conflictDirty.join('\n ')}\n请先 commit 或 stash 这些变更。` +
686
+ `或应用非重叠部分:sillyspec worktree apply ${changeName} --skip-overlap` +
569
687
  (result.rescueCommands.commands.length > 0 || result.rescueCommands.warnings.length > 0
570
688
  ? `或手动 rescue(旁路 git apply,逐文件 cp 安全子集;cp 后需手动 sillyspec worktree cleanup ${changeName}):\n` +
571
689
  result.rescueCommands.commands.map(c => ` ${c}`).join('\n') +
@@ -574,6 +692,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
574
692
  : '')
575
693
  );
576
694
  if (!checkOnly) return result; // checkOnly 收集不短路(一次报全);真实 apply 短路
695
+ }
577
696
  }
578
697
  }
579
698
 
@@ -713,11 +832,37 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
713
832
  } catch { /* pathspec 落盘失败不影响 apply 结果 */ }
714
833
 
715
834
  // --- 8. 成功后自动 cleanup(失败不影响整体结果) ---
835
+ // --skip-overlap 时先查 hasUnappliedChanges:跳过文件未落主仓 → 保留 worktree(不触发
836
+ // cleanup 的拦截横幅吓人,主动判断 + 温和提示);全部已落地(无跳过残留)才照常清理。
837
+ // cleanup 返回值必须消费(坑 ghost-dir-junction-pierce,2026-08-23 实证:apply 后 partial
838
+ // 残留被静默丢弃,用户以为已干净、人工 rm -rf 时穿透 junction 删主仓 node_modules)。
839
+ const consumeCleanup = (cr) => {
840
+ if (cr && (cr.result === 'partial' || (Array.isArray(cr.residual) && cr.residual.length > 0))) {
841
+ result.warnings.push(
842
+ `⚠️ worktree 部分清理残留(${cr.result}):${(cr.residual || []).join('; ') || 'worktree 目录'}` +
843
+ `——Windows 勿直接 rm -rf(会穿透 node_modules junction 删主仓依赖):先 cmd /c rmdir "<worktree>\\node_modules" 解链再删目录,或跑 sillyspec worktree doctor --fix`
844
+ )
845
+ }
846
+ return cr
847
+ }
716
848
  try {
717
- wm.cleanup(changeName, { force: true });
849
+ if (skipOverlap) {
850
+ const unapplied = wm.hasUnappliedChanges(changeName);
851
+ if (unapplied.hasChanges) {
852
+ const pend = unapplied.changedFiles || [];
853
+ result.warnings.push(
854
+ `worktree 已保留(--skip-overlap 有未应用文件):${pend.length} 个文件仍在 worktree` +
855
+ `(${pend.slice(0, 5).join(', ')}${pend.length > 5 ? ' 等' : ''})——主仓提交/stash 后重新 sillyspec worktree apply ${changeName} 只应用剩余文件,或确认放弃后 sillyspec worktree cleanup ${changeName} --force`
856
+ );
857
+ } else {
858
+ consumeCleanup(wm.cleanup(changeName, { force: true }));
859
+ }
860
+ } else {
861
+ consumeCleanup(wm.cleanup(changeName, { force: true }));
862
+ }
718
863
  } catch (cleanupErr) {
719
864
  result.warnings = result.warnings || [];
720
- result.warnings.push(`cleanup 失败(不影响应用结果): ${cleanupErr.message}`);
865
+ result.warnings.push(`cleanup 失败(不影响应用结果;Windows 手动清理先解 junction 再删,勿 rm -rf): ${cleanupErr.message}`);
721
866
  }
722
867
 
723
868
  } catch (e) {
@@ -729,7 +874,8 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
729
874
  [`patch 生成超出进程缓冲区(ENOBUFS,超大 diff)——自动降级 git merge 路径应用`]
730
875
  );
731
876
  try {
732
- return applyByMerge(result, changeName, projectRoot, wm);
877
+ // keepConflicts:false——自动降级路径无人善后冲突状态,维持 abort 回滚干净态
878
+ return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: false });
733
879
  } catch (mergeErr) {
734
880
  result.errors.push(`ENOBUFS 降级 merge 也失败: ${mergeErr.message}`);
735
881
  return result;
@@ -966,7 +1112,14 @@ function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
966
1112
  * @param {object} wm - WorktreeManager 实例
967
1113
  * @returns {object} result(merged=true 表示走了 merge 降级)
968
1114
  */
969
- function applyByMerge(result, changeName, projectRoot, wm) {
1115
+ // opts.keepConflicts(坑 merge-conflict-abort-no-chance,2026-08-23 实证:--merge 遇冲突直接
1116
+ // abort 回滚,用户想手工解决只能自己重新 git merge——冲突现场与解决上下文全部丢失):
1117
+ // true(显式 --merge 路径)→ 冲突时保留 merge-in-progress 现场 + 完整解决指引(编辑冲突 →
1118
+ // git add + git commit;或 git merge --abort 放弃)。worktree/分支/meta 本就保留。
1119
+ // false(ENOBUFS 自动降级路径,默认)→ 维持 abort——自动降级无人善后冲突状态,回滚到
1120
+ // 干净态让用户显式重来更安全。
1121
+ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
1122
+ const { keepConflicts = false } = opts;
970
1123
  const meta = wm.getMeta(changeName);
971
1124
  const changedFiles = result.changedFiles || [];
972
1125
  // 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
@@ -978,12 +1131,36 @@ function applyByMerge(result, changeName, projectRoot, wm) {
978
1131
  try {
979
1132
  git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
980
1133
  } catch (e) {
981
- // merge 冲突:取冲突文件列表 + abort 回滚(不 cleanup,保留 worktree)
1134
+ // merge 失败两形态须区分(原实现混为一谈:dirty 拒绝启动也被当「冲突」报):
1135
+ // ① 未启动:主仓有会被合并覆盖的未提交改动,git 直接拒绝(无 MERGE_HEAD)——没有现场
1136
+ // 可保留也无需 abort,指引 commit/stash 或 --skip-overlap
1137
+ // ② 真冲突:merge 已启动留 MERGE_HEAD + 冲突标记——keepConflicts 时保留现场供手工解决
982
1138
  let conflictFiles = [];
983
1139
  try {
984
1140
  const cf = gitQuiet(projectRoot, ['diff', '--name-only', '--diff-filter=U']);
985
1141
  conflictFiles = cf ? cf.split('\n').filter(Boolean) : [];
986
1142
  } catch {}
1143
+ const mergeInProgress = gitQuiet(projectRoot, ['rev-parse', '-q', '--verify', 'MERGE_HEAD']) !== null;
1144
+ if (!mergeInProgress) {
1145
+ result.errors.push(
1146
+ `git merge ${branch} 未执行成功——主仓有会被合并覆盖的未提交改动,git 拒绝启动合并(无冲突现场)。\n` +
1147
+ `git 输出: ${String(e.message || '').split('\n').slice(0, 3).join(' | ').slice(0, 300)}\n` +
1148
+ `请先 commit/stash 主仓改动后重试,或用 --skip-overlap 应用非重叠子集。`
1149
+ );
1150
+ return result;
1151
+ }
1152
+ if (keepConflicts) {
1153
+ // 保留冲突现场(显式 --merge):主仓处于 merge-in-progress(MERGE_HEAD + unmerged index +
1154
+ // 冲突标记),用户可直接手工解决——解决前 git 操作受限由指引明示,sillyspec 主仓命令
1155
+ // (apply/cleanup 等)也会被 dirty/merge 状态挡住,属预期。
1156
+ result.errors.push(
1157
+ `git merge ${branch} 冲突,已在主仓保留冲突现场(未 abort)。冲突文件:\n` +
1158
+ (conflictFiles.length ? ` ${conflictFiles.join('\n ')}\n` : ` (未能获取冲突文件列表——git diff --name-only --diff-filter=U 查看)\n`) +
1159
+ `手工解决:编辑上述冲突文件 → git add <file> → git commit 完成合并(worktree/分支已保留,解决后无需重跑 apply)。\n` +
1160
+ `放弃合并:git merge --abort 回到合并前状态后重试。注意:解决前主仓处于 merge 中状态,其他 sillyspec 主仓命令会被阻挡。`
1161
+ );
1162
+ return result;
1163
+ }
987
1164
  try { gitQuiet(projectRoot, ['merge', '--abort']); } catch {}
988
1165
  result.errors.push(
989
1166
  `git merge ${branch} 冲突,请手动解决。冲突文件:\n` +
@@ -129,9 +129,33 @@ function tryLink(mainNodeModules, linkPath) {
129
129
  const resolvedTarget = realpathSync(mainNodeModules);
130
130
  if (resolved === resolvedTarget) return { ok: true, method: process.platform === 'win32' ? 'junction' : 'symlink' };
131
131
  } catch {}
132
+ // 真实目录 ≠ 已有依赖(坑 provision-preexisting-dir-fake-linked,2026-08-23 实证:doctor --fix
133
+ // 报 re-provisioned 但链接没建——worktree node_modules 是 install 半途中断残留的真实目录时,
134
+ // realpath 不等 main → 被 preexisting 短路标 ok,根快路径据此写 depsStatus=linked,一个链接
135
+ // 都没建且后验证 existsSync 对空/残目录照样过)。真实目录 → ok:false + preexistingDir 标记,
136
+ // 交调用方决策:根快路径降级 tryInstall 真重建(宁可重装不说谎);linkOneDir 视为本地安装
137
+ // 保留(installed 语义,合法状态)。指向别处的 link 仍是「他者安装」,尊重不 clobber。
138
+ try {
139
+ if (!lstatSync(linkPath).isSymbolicLink()) {
140
+ return { ok: false, preexistingDir: true, error: `node_modules 已存在且为真实目录(非链接,疑似 install 残留): ${linkPath}` };
141
+ }
142
+ } catch {}
132
143
  // 指向别处 → 不 clobber,视为已有依赖(installed 语义)
133
144
  return { ok: true, method: 'install', preexisting: true };
134
145
  }
146
+ // broken junction 清理(existsSync 对目标丢失的 junction/symlink 返回 false,但目录项占位会使
147
+ // mklink/ln 撞名失败 → 根:落 install 慢路径;子模块:linkOneDir failed。此坑在 _doctorReprovision
148
+ // 的解链段同样存在——它也用 existsSync 前置判断,broken junction 永远解不掉):lstat 是 link 而
149
+ // existsSync false → 删目录项后重建(rmdir 删 junction 不跟随 reparse,与 worktree.js 解链同款)。
150
+ try {
151
+ if (lstatSync(linkPath).isSymbolicLink()) {
152
+ if (process.platform === 'win32') {
153
+ execFileSync('cmd.exe', ['/c', 'rmdir', linkPath], { stdio: ['pipe', 'pipe', 'pipe'] });
154
+ } else {
155
+ unlinkSync(linkPath);
156
+ }
157
+ }
158
+ } catch { /* lstat 抛 = 目录项真不存在,直接建 */ }
135
159
  try {
136
160
  if (process.platform === 'win32') {
137
161
  // execFileSync 数组形式不经 shell:POSIX 双引号内 `/$() 会执行、cmd.exe 双引号内 %VAR% 仍展开,
@@ -292,6 +316,9 @@ function linkOneDir(wtDir, mainDir) {
292
316
  }
293
317
  const r = tryLink(mainNodeModules, join(wtDir, 'node_modules'));
294
318
  if (r.ok) return { status: 'linked', method: r.method };
319
+ // 真实目录 = 子模块本地安装过(合法状态,如 mismatch 后用户手动 pnpm install):保留为
320
+ // installed 真话,不假 linked 也不误报 failed(tryLink 的 preexistingDir 细分见其注释)
321
+ if (r.preexistingDir) return { status: 'installed', reason: '子模块 node_modules 为真实目录(本地安装),保留' };
295
322
  return { status: 'failed', error: r.error };
296
323
  }
297
324
 
@@ -443,6 +470,17 @@ export function provisionDeps(worktreePath, mainCwd, opts = {}) {
443
470
  const fmsg = failedModules.map(m => `${m.path}: ${m.error}`).join('; ');
444
471
  result.depsError = result.depsError ? `${result.depsError}; ${fmsg}` : fmsg;
445
472
  }
473
+ // mismatch 可见性(坑 modules-mismatch-invisible,2026-08-23 实证:mismatch/skipped 只写
474
+ // depsModules 而 src 内零消费——doctor 报 re-provisioned 成功、子模块实际无链接且无任何
475
+ // 提示)。mismatch(worktree 与主仓 lockfile 不一致,link 不适用)摘要进 depsError:可见、
476
+ // 落 meta、doctor msg 带出(_doctorReprovision 不再抹);但不降级 depsStatus——mismatch 是
477
+ // 配置分叉态不是供给失败,降级会经 deps gate 卡死 execute(正确动作是用户在 worktree 跑
478
+ // pnpm install 或对齐 lockfile, doctor --fix 的 force 重装路径也会覆盖此场景)。
479
+ const mismatchModules = moduleResults.filter(m => m.status === 'mismatch');
480
+ if (mismatchModules.length > 0) {
481
+ const mmsg = `子模块 lockfile 与主仓不一致(未链接,本地 install 或对齐 lockfile): ${mismatchModules.map(m => m.path).join('、')}`;
482
+ result.depsError = result.depsError ? `${result.depsError}; ${mmsg}` : mmsg;
483
+ }
446
484
  }
447
485
 
448
486
  return result;
@@ -492,6 +530,25 @@ export function checkDepsFreshness(meta, wtPath, mainCwd) {
492
530
  };
493
531
  }
494
532
 
533
+ // 2b. 子模块 missing(坑 modules-submodule-missing-no-selfheal):meta.depsModules 中 linked
534
+ // 状态的子模块逐一核验 wt/<mp>/node_modules 实存。provisionDeps 内的同款核验(坑
535
+ // modules-submodule-link-verify)只管供给时刻,此后 junction 被删(杀毒/手工误删/半途清理)
536
+ // 时本函数只查根(上方)→ 永远 fresh → doctor 不再报 → 漏链永不自愈。此处补 doctor 侧
537
+ // 闭环:缺失 → missing → doctor --fix 走 relinkFirst 重链(provisionDeps 非 force 路径
538
+ // 的 linkOneDir 对缺失目录直接重建 junction)。
539
+ if (Array.isArray(meta && meta.depsModules)) {
540
+ const missingMods = meta.depsModules.filter(m =>
541
+ m && m.status === 'linked' && typeof m.path === 'string'
542
+ && !existsSync(join(wtPath, m.path, 'node_modules')));
543
+ if (missingMods.length > 0) {
544
+ return {
545
+ status: 'missing',
546
+ detail: `${missingMods.length} 个 linked 子模块 node_modules 缺失: ${missingMods.map(m => m.path).join('、')}`,
547
+ wtHash, mainHash, metaLockHash,
548
+ };
549
+ }
550
+ }
551
+
495
552
  // 3. stale:worktree 自身 lockfile 与 meta 快照不一致(对齐 doctor 916-917 / ensure 404)
496
553
  if (metaLockHash && wtHash && wtHash !== metaLockHash) {
497
554
  return {