claude-code-session-manager 0.97.0 → 0.100.0

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 (430) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.cjs +16 -4
  3. package/bin/node-floor.cjs +41 -0
  4. package/dist/assets/{DataModel-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
  5. package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
  6. package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
  7. package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
  9. package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
  10. package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
  11. package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
  12. package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
  14. package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
  15. package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
  17. package/dist/assets/Settings-Dc1F_sSg.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
  19. package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
  20. package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
  21. package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
  22. package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
  23. package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
  24. package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
  25. package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
  26. package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +26 -29
  29. package/plugins/CLAUDE.md +6 -6
  30. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -19
  31. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  32. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  33. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  34. package/scripts/README.md +3 -11
  35. package/scripts/audit-ops-hygiene.cjs +3 -3
  36. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  37. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  38. package/scripts/mint-epic.cjs +2 -3
  39. package/scripts/scheduler-mcp-server.cjs +36 -7
  40. package/src/main/agentLibrary.cjs +22 -2
  41. package/src/main/build-info.json +4 -4
  42. package/src/main/config.cjs +41 -38
  43. package/src/main/docEdit.cjs +4 -1
  44. package/src/main/files.cjs +2 -5
  45. package/src/main/health.cjs +1 -1
  46. package/src/main/index.cjs +23 -10
  47. package/src/main/ipcSchemas.cjs +28 -35
  48. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  49. package/src/main/lib/atomicFs.cjs +116 -0
  50. package/src/main/lib/branchSweep.cjs +13 -12
  51. package/src/main/lib/buildTarget.cjs +3 -3
  52. package/src/main/lib/claudeCliCaps.cjs +129 -0
  53. package/src/main/lib/credentials.cjs +2 -4
  54. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  55. package/src/main/lib/cwdClassify.cjs +15 -3
  56. package/src/main/lib/definitionOfDone.cjs +207 -125
  57. package/src/main/lib/dodDrainHook.cjs +11 -5
  58. package/src/main/lib/epicMint.cjs +2 -4
  59. package/src/main/lib/epicStatusMirror.cjs +8 -10
  60. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  61. package/src/main/lib/gateAuthority.cjs +96 -0
  62. package/src/main/lib/gitExec.cjs +69 -0
  63. package/src/main/lib/gitWorktree.cjs +656 -92
  64. package/src/main/lib/instanceLock.cjs +5 -8
  65. package/src/main/lib/launchFailure.cjs +2 -3
  66. package/src/main/lib/macroLibrary.cjs +386 -0
  67. package/src/main/lib/mcpToolCatalog.cjs +26 -17
  68. package/src/main/lib/needsReviewLedger.cjs +1 -1
  69. package/src/main/lib/opsErrorLog.cjs +11 -1
  70. package/src/main/lib/opsOwnership.cjs +0 -5
  71. package/src/main/lib/pidAlive.cjs +32 -0
  72. package/src/main/lib/prdCreate.cjs +153 -19
  73. package/src/main/lib/prdDisposition.cjs +2 -2
  74. package/src/main/lib/prdGateFiles.cjs +284 -0
  75. package/src/main/lib/prdLocations.cjs +61 -0
  76. package/src/main/lib/prdMigration.cjs +2 -3
  77. package/src/main/lib/prdSizing.cjs +2 -1
  78. package/src/main/lib/promptSessionSchema.cjs +10 -2
  79. package/src/main/lib/rcaReport.cjs +4 -6
  80. package/src/main/lib/reaperHelpers.cjs +8 -1
  81. package/src/main/lib/reservationExpiry.cjs +6 -1
  82. package/src/main/lib/reviewNotice.cjs +263 -0
  83. package/src/main/lib/runClaudeP.cjs +4 -1
  84. package/src/main/lib/schedulerPaths.cjs +22 -2
  85. package/src/main/lib/sessionSlots.cjs +2 -4
  86. package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
  87. package/src/main/lib/timeoutShim.cjs +123 -0
  88. package/src/main/lib/timeoutShimScript.cjs +275 -0
  89. package/src/main/lib/upgradeDrain.cjs +4 -10
  90. package/src/main/lib/watchdogHelpers.cjs +33 -30
  91. package/src/main/lib/workTypeLibrary.cjs +7 -1
  92. package/src/main/promptSessionEvents.cjs +37 -13
  93. package/src/main/queueOps.cjs +17 -6
  94. package/src/main/scheduler.cjs +975 -203
  95. package/src/main/seedAgentPersonas.cjs +278 -23
  96. package/src/main/supervisor.cjs +4 -1
  97. package/src/main/templates/PRD_AUTHORING.md +126 -406
  98. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  99. package/src/preload/api.d.ts +41 -94
  100. package/src/preload/index.cjs +14 -9
  101. package/src/seed/agents/architect.md +1 -0
  102. package/src/seed/agents/dev-lead.md +17 -18
  103. package/src/seed/agents/project-home-builder.md +1 -0
  104. package/src/seed/agents/validator.md +10 -4
  105. package/dist/assets/HostBilko-nESrTRIg.js +0 -1
  106. package/dist/assets/Scheduler-BVYTeh39.js +0 -16
  107. package/dist/assets/Settings-DIhGgdfs.js +0 -3
  108. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  109. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  110. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  111. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  112. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  113. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  114. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  115. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  116. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  117. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  118. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  119. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  120. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  121. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  122. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  123. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  124. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  125. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  126. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  127. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  128. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  129. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  130. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  131. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  132. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  133. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  134. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  135. package/src/main/__tests__/docEdit.test.cjs +0 -244
  136. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  137. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  138. package/src/main/__tests__/dod-report.test.cjs +0 -304
  139. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  140. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  141. package/src/main/__tests__/epicMint.test.cjs +0 -332
  142. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  143. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  144. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  145. package/src/main/__tests__/exchanges.test.cjs +0 -122
  146. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  147. package/src/main/__tests__/extractJson.test.cjs +0 -51
  148. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  149. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  150. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  151. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  152. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  153. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  154. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  155. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  156. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  157. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  158. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  159. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  160. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  161. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  162. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  163. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  164. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  165. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  166. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  167. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  168. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  169. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  170. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  171. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  172. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  173. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  174. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  175. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  176. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  177. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  178. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  179. package/src/main/__tests__/planValidator.test.cjs +0 -125
  180. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  181. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  182. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  183. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  184. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  185. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  186. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  187. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  188. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  189. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  190. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  191. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  192. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  193. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  194. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  195. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  196. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  197. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  198. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  199. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  200. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  201. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  202. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  203. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  204. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  205. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  206. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  207. package/src/main/__tests__/projectPages.test.cjs +0 -137
  208. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  209. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  210. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  211. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  212. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  213. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  214. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  215. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  216. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  217. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  218. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  219. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  220. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  221. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -154
  222. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  223. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  224. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  225. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  226. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  227. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  228. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  229. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  230. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  231. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  232. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  233. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  234. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  235. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  236. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  237. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  238. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  239. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  240. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  241. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  242. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  243. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  244. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  245. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  246. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  247. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  248. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  249. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  250. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  251. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  252. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  253. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  254. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  255. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  256. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  257. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  258. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  259. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  260. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  261. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  262. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  263. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  264. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  265. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  266. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  267. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  268. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  269. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  270. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  271. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  272. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  273. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  274. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  275. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  276. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  277. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  278. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  279. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  280. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  281. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  282. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  283. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  284. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  285. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  286. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  287. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  288. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  289. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  290. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  291. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  292. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  293. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  294. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  296. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  297. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  298. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  299. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  300. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  301. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  302. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  303. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  304. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  305. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  306. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  307. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  308. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  309. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  310. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  311. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  312. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  313. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  314. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  315. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  316. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  317. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  318. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  319. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  320. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  321. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  322. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  323. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  324. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  325. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  326. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  327. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  328. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  329. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  330. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  331. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  332. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  333. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  334. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  335. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  336. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  337. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  338. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  339. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  340. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  341. package/src/main/bilkoHost.cjs +0 -314
  342. package/src/main/bilkoHostCore.cjs +0 -89
  343. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  344. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  345. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  346. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  347. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  348. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  349. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  350. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  351. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  352. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  353. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  354. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  355. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  356. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  357. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  358. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  359. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  360. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  361. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  362. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  363. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  364. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  365. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  366. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  367. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  368. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  369. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  370. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  371. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  372. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  373. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  374. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  375. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  376. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  377. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  378. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  379. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  380. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  381. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  382. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  383. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  384. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  385. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  386. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  387. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  388. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  389. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  390. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  391. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  392. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  393. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  394. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  395. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  396. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  397. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  398. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  399. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  400. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  401. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  402. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  403. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  404. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  405. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  406. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  407. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  408. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  409. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  410. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  411. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  412. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  413. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  414. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  415. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  416. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  417. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  418. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  419. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  420. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  421. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  422. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  423. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  424. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  425. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  426. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  427. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  428. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -375
  429. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  430. package/src/main/lib/kgExchangePairing.cjs +0 -75
@@ -53,9 +53,9 @@ const fsp = require('node:fs/promises');
53
53
  const path = require('node:path');
54
54
  const os = require('node:os');
55
55
  const crypto = require('node:crypto');
56
- const { execFile } = require('node:child_process');
57
56
  const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
58
57
  const schedulerPaths = require('./schedulerPaths.cjs');
58
+ const sharedGitExec = require('./gitExec.cjs');
59
59
 
60
60
  // Per-kind configuration. Roots are kept OUTSIDE any project's own tree
61
61
  // (os.tmpdir(), not `<cwd>/.git/...`) so a managed worktree never shows up in
@@ -112,6 +112,27 @@ function configFor(kind) {
112
112
  return cfg;
113
113
  }
114
114
 
115
+ // Per-Epic integration branch prefix (checkout-free landing — see
116
+ // integrateOntoRef/ensureLandBranch below). Not a `kind` in KIND_CONFIG's
117
+ // sense (no worktree root, no cap) — it never backs a `git worktree add`
118
+ // checkout — but it IS one of our own managed prefixes for every place that
119
+ // decides "is this branch ours", so it's declared once, here, alongside the
120
+ // job/epic prefixes it sits next to.
121
+ const LAND_BRANCH_PREFIX = 'sm-land/';
122
+
123
+ /** True when `b` is one of OUR OWN managed branches — a job/epic worktree
124
+ * branch (KIND_CONFIG's `branchPrefix`) or an `sm-land/*` integration
125
+ * branch. Hoisted out of `integrateBranch` so every caller that needs to
126
+ * tell "ours" from "a real project branch" (integrateBranch's baseBranch
127
+ * fallback, ensureLandBranch's baseBranch fallback) shares one definition
128
+ * instead of drifting apart. */
129
+ function isOwnManagedBranch(b) {
130
+ return typeof b === 'string' && b.length > 0 && (
131
+ Object.values(KIND_CONFIG).some((c) => b.startsWith(c.branchPrefix))
132
+ || b.startsWith(LAND_BRANCH_PREFIX)
133
+ );
134
+ }
135
+
115
136
  function worktreeRootFor(kind) {
116
137
  return configFor(kind).root;
117
138
  }
@@ -206,22 +227,12 @@ const worktreeCapLockChain = { job: Promise.resolve(), epic: Promise.resolve() }
206
227
  const observedWorktreeCountCache = { job: { count: null, at: 0 }, epic: { count: null, at: 0 } };
207
228
  const OBSERVED_WORKTREE_COUNT_TTL_MS = 2000;
208
229
 
209
- function execGit(args, { cwd, timeout = 20_000 } = {}) {
210
- return new Promise((resolve, reject) => {
211
- execFile('git', args, { cwd, timeout, windowsHide: true, encoding: 'utf8' }, (err, stdout, stderr) => {
212
- if (err) {
213
- err.stderrText = stderr;
214
- // Some git subcommands (e.g. `diff --no-index`) exit non-zero to mean
215
- // "found a difference", not "failed" — stdout still carries the real
216
- // result in that case, so callers that need it can recover it off
217
- // the rejected error rather than losing it.
218
- err.stdoutText = stdout;
219
- reject(err);
220
- return;
221
- }
222
- resolve(stdout || '');
223
- });
224
- });
230
+ // Thin re-shape over the shared gitExec.execGit (cwd-first signature) —
231
+ // kept so the ~40 call sites below (args-first, `{ cwd, timeout }`) don't
232
+ // all need touching. Semantics (timeout/maxBuffer/env, execFile with an
233
+ // argv array, stderrText/stdoutText on error) now live in gitExec.cjs.
234
+ function execGit(args, { cwd, timeout = 20_000, maxBuffer, env } = {}) {
235
+ return sharedGitExec.execGit(cwd, args, { timeout, maxBuffer, env });
225
236
  }
226
237
 
227
238
  // Unquote a single git porcelain v1 path token. Git wraps a path in double
@@ -784,7 +795,10 @@ async function reserveWorktreeSlot(kind) {
784
795
  /**
785
796
  * Create a linked worktree on a fresh branch checked out from the main
786
797
  * tree's current HEAD. Returns `{ ok: true, dir, branch, baseCwd,
787
- * carriedPaths }` on success, or `{ ok: false, reason }` — the reason is
798
+ * carriedPaths, baseBranch }` on success (`baseBranch` is `cwd`'s branch at
799
+ * creation time — `null` on a detached HEAD — so integration later targets
800
+ * the branch the checkout actually started from, not whatever `cwd` sits on
801
+ * by the time the job finishes), or `{ ok: false, reason }` — the reason is
788
802
  * always a short, human-readable string meant to be logged verbatim so a
789
803
  * fallback to running in place is never silent.
790
804
  *
@@ -805,8 +819,22 @@ async function reserveWorktreeSlot(kind) {
805
819
  * failure, git error) is a normal, expected outcome for a project that
806
820
  * hasn't opted into — or currently can't support — isolation, not an
807
821
  * exceptional one.
822
+ *
823
+ * `fromRef` (optional) forks the checkout from that ref instead of HEAD —
824
+ * used by the checkout-free `sm-land/<epicId>` integration path, where a
825
+ * job's worktree should start from the shared land branch rather than
826
+ * whatever the human's own checkout happens to be on. Only used when it
827
+ * actually resolves to a commit; an unresolvable `fromRef` silently falls
828
+ * back to HEAD, same as omitting it. `baseBranch` on the result always stays
829
+ * the human checkout's OWN branch (never the land ref) — integration still
830
+ * needs to know what the human was really on, `forkedFrom` carries the ref
831
+ * the worktree content itself came from. The base-tree WIP carry-over
832
+ * (`captureAndCarryBaseDiff`) is skipped whenever `fromRef` resolved: the
833
+ * human's dirty diff is relative to their OWN tree, not `fromRef`'s, so
834
+ * applying it here would silently misplace or conflict with content that has
835
+ * nothing to do with it.
808
836
  */
809
- async function createWorktree({ kind, cwd, key }) {
837
+ async function createWorktree({ kind, cwd, key, fromRef }) {
810
838
  configFor(kind); // throws on an unknown kind before anything else runs
811
839
  if (!cwd || typeof cwd !== 'string') return { ok: false, reason: 'no cwd provided' };
812
840
  if (!key || typeof key !== 'string') return { ok: false, reason: 'no key provided' };
@@ -814,8 +842,21 @@ async function createWorktree({ kind, cwd, key }) {
814
842
 
815
843
  if (!(await isGitRepo(cwd))) return { ok: false, reason: 'not a git repository' };
816
844
 
845
+ const baseBranch = await getCurrentBranch(cwd);
817
846
  const baseWasClean = await isBaseTreeClean(cwd);
818
847
 
848
+ // Resolved to a commit SHA — never the bare ref/branch NAME — before it's
849
+ // handed to `git worktree add` below: a name could keep moving (or, for a
850
+ // '-'-prefixed name, be read as an option rather than a positional arg)
851
+ // between this resolution and the actual checkout; a SHA pins exactly the
852
+ // commit this call decided on. `forkedFrom` on the result still carries
853
+ // the original NAME (never the SHA) — that's the human-readable "what ref
854
+ // did this fork from" a caller wants to log/display.
855
+ let resolvedFromRefSha = null;
856
+ if (typeof fromRef === 'string' && fromRef) {
857
+ resolvedFromRefSha = await tryResolveCommit(cwd, `${fromRef}^{commit}`);
858
+ }
859
+
819
860
  // Cap check + slot reservation as a single atomic step, per kind — see
820
861
  // reserveWorktreeSlot's header. Released again below on any failure path
821
862
  // so a failed create never permanently shrinks capacity.
@@ -824,6 +865,7 @@ async function createWorktree({ kind, cwd, key }) {
824
865
 
825
866
  const dir = worktreeDirFor(kind, cwd, key);
826
867
  const branch = branchNameFor(kind, key);
868
+ const worktreeSource = resolvedFromRefSha || 'HEAD';
827
869
  try {
828
870
  await fsp.mkdir(path.dirname(dir), { recursive: true });
829
871
  // Defensive: a same-key leftover from a prior crashed run (same slug/id
@@ -831,14 +873,14 @@ async function createWorktree({ kind, cwd, key }) {
831
873
  // with `git worktree add`'s own branch/path checks.
832
874
  await removeWorktreeDir(cwd, dir);
833
875
  try { await execGit(['branch', '-D', branch], { cwd, timeout: 10_000 }); } catch { /* didn't exist */ }
834
- await execGit(['worktree', 'add', '-b', branch, dir, 'HEAD'], { cwd, timeout: 30_000 });
876
+ await execGit(['worktree', 'add', '-b', branch, dir, worktreeSource], { cwd, timeout: 30_000 });
835
877
  } catch (e) {
836
878
  activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
837
879
  return { ok: false, reason: `git worktree add failed: ${(e && (e.stderrText || e.message)) || e}` };
838
880
  }
839
881
 
840
882
  let carriedPaths = [];
841
- if (!baseWasClean) {
883
+ if (!resolvedFromRefSha && !baseWasClean) {
842
884
  // Routed through module.exports (not the bare local function) so tests
843
885
  // can substitute a failing carry-over without needing a real git-apply
844
886
  // conflict fixture — see gitWorktree.test.cjs's carry-over-failure case.
@@ -853,7 +895,9 @@ async function createWorktree({ kind, cwd, key }) {
853
895
  }
854
896
 
855
897
  registeredCheckouts.set(dir, { kind, branch });
856
- return { ok: true, dir, branch, baseCwd: cwd, carriedPaths };
898
+ const result = { ok: true, dir, branch, baseCwd: cwd, carriedPaths, baseBranch };
899
+ if (resolvedFromRefSha) result.forkedFrom = fromRef;
900
+ return result;
857
901
  }
858
902
 
859
903
  /**
@@ -1154,6 +1198,40 @@ async function getCurrentBranch(cwd) {
1154
1198
  }
1155
1199
  }
1156
1200
 
1201
+ /**
1202
+ * True when `branch`'s ONLY committed changes (relative to `cwd`'s HEAD)
1203
+ * touch paths in `carriedPaths` (the human's carried-over base-tree WIP from
1204
+ * `createWorktree`) AND every one of those changed paths' committed blob on
1205
+ * `branch` is byte-identical to what's still sitting dirty in `cwd` right
1206
+ * now — i.e. the branch committed exactly the carried WIP and nothing more.
1207
+ * Extracted out of `integrateBranch` (identical logic, same content-verify
1208
+ * safety reasoning documented there) so a checkout-free Epic land path can
1209
+ * reuse the same classification before ever attempting a land.
1210
+ * Never throws — any read failure (including no `carriedPaths`) is reported
1211
+ * as `false`, the safer default that falls through to a real integration
1212
+ * attempt rather than silently dropping a commit.
1213
+ */
1214
+ async function isCarriedWipOnly({ cwd, branch, carriedPaths }) {
1215
+ if (!Array.isArray(carriedPaths) || !carriedPaths.length) return false;
1216
+ try {
1217
+ let mergeBase = '';
1218
+ try {
1219
+ mergeBase = (await execGit(['merge-base', 'HEAD', branch], { cwd, timeout: 10_000 })).trim();
1220
+ } catch {
1221
+ mergeBase = '';
1222
+ }
1223
+ const changedOut = await execGit(['diff', `${mergeBase || 'HEAD'}..${branch}`, '--name-only'], { cwd, timeout: 10_000 });
1224
+ const changed = changedOut.split('\n').map((l) => l.trim()).filter(Boolean);
1225
+ if (changed.length && changed.every((p) => carriedPaths.includes(p))) {
1226
+ const allIdentical = await pathsIdenticalToBranch({ cwd, branch, paths: changed });
1227
+ if (allIdentical) return true;
1228
+ }
1229
+ return false;
1230
+ } catch {
1231
+ return false;
1232
+ }
1233
+ }
1234
+
1157
1235
  /**
1158
1236
  * Integrate a branch back into `cwd`'s current HEAD — fast-forward when
1159
1237
  * possible, a real merge commit when the main tree advanced underneath (a
@@ -1174,26 +1252,143 @@ async function getCurrentBranch(cwd) {
1174
1252
  * the base tree still holding that same path dirty.
1175
1253
  *
1176
1254
  * Refuses outright — before touching any other git state — when `cwd`'s
1177
- * HEAD is not on the repo's own default branch (resolved via
1178
- * `resolveDefaultBranch`, never hardcoded to `main`) or is detached. A
1179
- * stray checkout onto some other branch (e.g. a leftover `sm-job/*` branch
1180
- * from a prior run) would otherwise make every subsequent merge-base/merge
1181
- * silently target the WRONG tree, producing merge "conflicts" that are
1182
- * really just wrong-base artifacts (RCA: starry-night-ships, 2026-09-12).
1183
- * This refusal never checks out, resets, or switches anything — it only
1184
- * reports; a human fixes the checkout.
1255
+ * HEAD is not on the expected target branch or is detached. The expected
1256
+ * target is `baseBranch` (the branch `cwd` was actually on when the
1257
+ * worktree was created, per `createWorktree`) when it is a non-empty string
1258
+ * that doesn't start with one of OUR OWN managed branch prefixes (KIND_CONFIG's
1259
+ * `branchPrefix`, i.e. `sm-job/`/`sm-epic/` — read from the same config
1260
+ * createWorktree/branchNameFor use, so a prefix rename can never drift the two
1261
+ * apart) — a leftover managed-branch value is never a legitimate integration
1262
+ * target, so it falls back the same as no baseBranch at all); otherwise it
1263
+ * falls back to `resolveDefaultBranch(cwd)` (never hardcoded to `main`) —
1264
+ * today's pre-PRD behavior for callers that don't pass `baseBranch` (e.g.
1265
+ * `branchSweep.cjs`'s orphan sweep, which has no job row to read it from).
1266
+ * A checkout that has moved off the expected target since the worktree was
1267
+ * created (e.g. a leftover `sm-job/*` branch from a prior run) would
1268
+ * otherwise make every subsequent merge-base/merge silently target the
1269
+ * WRONG tree, producing merge "conflicts" that are really just wrong-base
1270
+ * artifacts (RCA: starry-night-ships, 2026-09-12) — still guarded here
1271
+ * because an `sm-*` baseBranch is rejected as an expected target, and a HEAD
1272
+ * that moved after the worktree forked is refused regardless of what
1273
+ * baseBranch was recorded. This refusal never checks out, resets, or
1274
+ * switches anything — it only reports (`failureKind: 'stray_checkout'`); a
1275
+ * human fixes the checkout. Pass `allowRefLanding: true` to land onto
1276
+ * `expectedBranch`'s ref instead of refusing (still never touches any
1277
+ * checkout) — only `performMechanicalRecovery` opts into this; every other
1278
+ * caller keeps today's refusal.
1279
+ *
1280
+ * The ref-landing path refuses the same way — reports, never lands — in
1281
+ * three more cases: `cwd`'s HEAD is detached (a plain detached checkout and
1282
+ * a mid-rebase one both read back as `null` from `getCurrentBranch`, and the
1283
+ * two can't be told apart, so both are refused); `expectedBranch` is being
1284
+ * rebased in ANY worktree of this repo, not only checked out in one (the
1285
+ * rebase scan inside `checkTargetRefNotCheckedOut`) — a later `git rebase
1286
+ * --abort`/`--continue` there would otherwise undo or outrun the ref move
1287
+ * this path just made; or `branch`'s only committed changes touch
1288
+ * `carriedPaths` — judged by path membership alone here, unlike the
1289
+ * in-checkout path's `isCarriedWipOnly`, because `cwd` is on some other
1290
+ * branch and has no working tree this path could trust to content-verify
1291
+ * against.
1185
1292
  */
1186
- async function integrateBranch({ cwd, branch, key, kind, carriedPaths }) {
1293
+ async function integrateBranch({ cwd, branch, key, kind, carriedPaths, baseBranch, allowRefLanding = false }) {
1187
1294
  if (!cwd || !branch) return { ok: false, reason: 'missing cwd/branch' };
1188
1295
 
1189
- const defaultBranch = await resolveDefaultBranch(cwd);
1296
+ const expectedBranch = (typeof baseBranch === 'string' && baseBranch && !isOwnManagedBranch(baseBranch))
1297
+ ? baseBranch
1298
+ : await resolveDefaultBranch(cwd);
1299
+ // Hoisted above the stray-checkout check: both the ref-landing path below
1300
+ // and the normal in-checkout merge path further down land with this exact
1301
+ // text, and must never drift apart on it.
1302
+ const mergeMessage = kind === 'epic'
1303
+ ? `merge epic ${key || branch}`
1304
+ // Preserves the exact job-kind message the pre-generalization jobWorktree.cjs
1305
+ // used, so a job's merge-commit text never changes under this refactor.
1306
+ : `merge scheduler job ${key || branch}`;
1190
1307
  const currentBranch = await getCurrentBranch(cwd);
1191
- if (currentBranch !== defaultBranch) {
1192
- const actual = currentBranch === null ? 'detached HEAD' : currentBranch;
1193
- return {
1308
+ if (currentBranch !== expectedBranch) {
1309
+ const actualBranch = currentBranch === null ? 'detached HEAD' : currentBranch;
1310
+ const strayCheckoutFailure = {
1194
1311
  ok: false,
1195
- reason: `refusing to integrate: HEAD is on "${actual}", not the repo's default branch "${defaultBranch}" — a stray checkout must be fixed before integration can proceed`,
1312
+ failureKind: 'stray_checkout',
1313
+ expectedBranch,
1314
+ actualBranch,
1315
+ reason: `refusing to integrate: HEAD is on "${actualBranch}", not the expected base branch "${expectedBranch}" — a stray checkout must be fixed before integration can proceed`,
1196
1316
  };
1317
+ if (!allowRefLanding || process.env.SM_REF_LANDING_DISABLE === '1') {
1318
+ return strayCheckoutFailure;
1319
+ }
1320
+ // A detached HEAD is indistinguishable from a mid-rebase one — both read
1321
+ // back as `null` from getCurrentBranch — and a ref move under a paused
1322
+ // rebase gets undone the moment that rebase ends (`--abort`) or outrun
1323
+ // (`--continue`). We cannot tell the two apart, so neither is landed.
1324
+ if (currentBranch === null) {
1325
+ return strayCheckoutFailure;
1326
+ }
1327
+ // The checkout is busy on some OTHER branch (a human, or a sibling job's
1328
+ // own worktree-free promotion) — land straight onto `expectedBranch`'s
1329
+ // ref instead of refusing outright. Never touches `cwd`'s working tree,
1330
+ // index, or HEAD: `integrateOntoRef` is a pure object-database CAS. Only
1331
+ // `performMechanicalRecovery` opts into this today (`allowRefLanding:
1332
+ // true`) — the first-pass integrate (scheduler.cjs's `finalizeJobWorktree`)
1333
+ // must keep refusing here, because it finds the landed commit by
1334
+ // watching HEAD move, which a ref-only land never does.
1335
+ let refBranchHead;
1336
+ try {
1337
+ refBranchHead = (await execGit(['rev-parse', branch], { cwd, timeout: 10_000 })).trim();
1338
+ } catch {
1339
+ return strayCheckoutFailure;
1340
+ }
1341
+ let refMergeBase = '';
1342
+ try {
1343
+ refMergeBase = (await execGit(['merge-base', expectedBranch, branch], { cwd, timeout: 10_000 })).trim();
1344
+ } catch {
1345
+ refMergeBase = '';
1346
+ }
1347
+ if (refMergeBase && refMergeBase === refBranchHead) {
1348
+ return { ok: true, integrated: false, reason: 'branch has no new commits' };
1349
+ }
1350
+ // Carried-WIP shortcut, path membership only: `cwd` is on `actualBranch`,
1351
+ // not `expectedBranch`, so there is no trustworthy working tree here to
1352
+ // content-verify against (isCarriedWipOnly's approach, used by the
1353
+ // in-checkout path below). Refusing instead of reporting a false
1354
+ // "carried-wip-only" matters because the caller (performMechanicalRecovery)
1355
+ // treats that reason as safe to delete the job branch — a wrong guess
1356
+ // would discard real work. A branch that changed carried paths AND other
1357
+ // paths still lands below, same as the in-checkout path.
1358
+ if (Array.isArray(carriedPaths) && carriedPaths.length) {
1359
+ try {
1360
+ const changedOut = await execGit(['diff', `${refMergeBase || expectedBranch}..${branch}`, '--name-only'], { cwd, timeout: 10_000 });
1361
+ const changed = changedOut.split('\n').map((l) => l.trim()).filter(Boolean);
1362
+ if (changed.length && changed.every((p) => carriedPaths.includes(p))) {
1363
+ return strayCheckoutFailure;
1364
+ }
1365
+ } catch {
1366
+ // Diff failed — fall through to the normal ref landing below, same
1367
+ // as every other "never throws" rule in this function.
1368
+ }
1369
+ }
1370
+ const refResult = await integrateOntoRef({
1371
+ cwd, targetRef: `refs/heads/${expectedBranch}`, branch, message: mergeMessage,
1372
+ });
1373
+ if (refResult.ok) {
1374
+ // Spread refResult first so a fast-forward keeps `fastForward: true`
1375
+ // and a merge commit keeps `mergeCommit: true` — this used to
1376
+ // hard-code `fastForward: false` on every success, misreporting a
1377
+ // plain fast-forward as a merge.
1378
+ return { ...refResult, viaRef: true, sha: refResult.sha || refBranchHead };
1379
+ }
1380
+ if (refResult.failureKind === 'target_checked_out') {
1381
+ return strayCheckoutFailure;
1382
+ }
1383
+ if (refResult.failureKind === 'content_conflict') {
1384
+ return {
1385
+ ok: false,
1386
+ reason: `merge failed (likely a real content conflict): ${refResult.reason}`,
1387
+ failureKind: 'content_conflict',
1388
+ conflictedPaths: refResult.conflictedPaths || [],
1389
+ };
1390
+ }
1391
+ return strayCheckoutFailure;
1197
1392
  }
1198
1393
 
1199
1394
  let branchHead;
@@ -1212,34 +1407,17 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths }) {
1212
1407
  return { ok: true, integrated: false, reason: 'branch has no new commits' };
1213
1408
  }
1214
1409
 
1215
- if (Array.isArray(carriedPaths) && carriedPaths.length) {
1216
- try {
1217
- const changedOut = await execGit(['diff', `${mergeBase || 'HEAD'}..${branch}`, '--name-only'], { cwd, timeout: 10_000 });
1218
- const changed = changedOut.split('\n').map((l) => l.trim()).filter(Boolean);
1219
- if (changed.length && changed.every((p) => carriedPaths.includes(p))) {
1220
- // Path membership alone is NOT enough: a job that legitimately edits
1221
- // the SAME file the base tree had carried-in WIP on (e.g. this
1222
- // repo's own scheduler.cjs churns queue.json/active-index.json while
1223
- // jobs run) would otherwise have its real commit misclassified as
1224
- // "just the carried WIP" and dropped — the caller treats
1225
- // `integrated: false` as safe-to-delete-the-branch, so that commit
1226
- // would be gone for good, worse than the ordinary merge-conflict
1227
- // path (branch kept, flagged for manual recovery) this shortcut is
1228
- // supposed to be a safe subset of. Content-verify: only when every
1229
- // changed path's committed blob on `branch` is byte-identical to
1230
- // what's STILL sitting dirty in `cwd` right now proves the job
1231
- // committed exactly the carried WIP and nothing more. Any mismatch
1232
- // (including a read failure — fail toward the safer default) falls
1233
- // through to the normal merge attempt below instead of skipping.
1234
- const allIdentical = await pathsIdenticalToBranch({ cwd, branch, paths: changed });
1235
- if (allIdentical) {
1236
- return { ok: true, integrated: false, reason: 'carried-wip-only' };
1237
- }
1238
- }
1239
- } catch {
1240
- // Best-effort classification only — if the diff can't be read, fall
1241
- // through to the normal integration attempt below.
1242
- }
1410
+ // Path membership alone is NOT enough: a job that legitimately edits the
1411
+ // SAME file the base tree had carried-in WIP on (e.g. this repo's own
1412
+ // scheduler.cjs churns queue.json/active-index.json while jobs run) would
1413
+ // otherwise have its real commit misclassified as "just the carried WIP"
1414
+ // and dropped — the caller treats `integrated: false` as
1415
+ // safe-to-delete-the-branch, so that commit would be gone for good, worse
1416
+ // than the ordinary merge-conflict path (branch kept, flagged for manual
1417
+ // recovery) this shortcut is supposed to be a safe subset of. See
1418
+ // `isCarriedWipOnly` above for the content-verify logic.
1419
+ if (await isCarriedWipOnly({ cwd, branch, carriedPaths })) {
1420
+ return { ok: true, integrated: false, reason: 'carried-wip-only' };
1243
1421
  }
1244
1422
 
1245
1423
  try {
@@ -1249,11 +1427,6 @@ async function integrateBranch({ cwd, branch, key, kind, carriedPaths }) {
1249
1427
  // Main tree advanced since the worktree branched (a sibling job/Epic
1250
1428
  // merged first) — a real merge commit still lands the branch's commit(s).
1251
1429
  }
1252
- const mergeMessage = kind === 'epic'
1253
- ? `merge epic ${key || branch}`
1254
- // Preserves the exact job-kind message the pre-generalization jobWorktree.cjs
1255
- // used, so a job's merge-commit text never changes under this refactor.
1256
- : `merge scheduler job ${key || branch}`;
1257
1430
  try {
1258
1431
  await execGit(['merge', '--no-ff', '--no-edit', '-m', mergeMessage, branch], { cwd, timeout: 30_000 });
1259
1432
  return { ok: true, integrated: true, mergeCommit: true };
@@ -1330,10 +1503,14 @@ async function cleanupWorktree({ kind, cwd, dir, branch, keepBranch }) {
1330
1503
  try { await execGit(['branch', '-D', branch], { cwd, timeout: 10_000 }); } catch { /* already gone */ }
1331
1504
  }
1332
1505
  try { await execGit(['worktree', 'prune'], { cwd, timeout: 10_000 }); } catch { /* best effort */ }
1333
- // A checkout the expiry pass already released must not be counted down twice.
1506
+ // A checkout the expiry pass already released must not be counted down
1507
+ // twice, and neither must a dir-less call (e.g. mechanical recovery's
1508
+ // branch-only cleanup after the worktree dir was already freed by the
1509
+ // job's own earlier finalize) — no `dir` means this call never held a
1510
+ // counted slot to begin with.
1334
1511
  const alreadyExpired = dir ? expiredCheckoutDirs.delete(dir) : false;
1335
1512
  if (dir) registeredCheckouts.delete(dir);
1336
- if (!alreadyExpired) activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
1513
+ if (dir && !alreadyExpired) activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
1337
1514
  }
1338
1515
 
1339
1516
  /**
@@ -1475,20 +1652,11 @@ async function isBranchMergedIntoHead(cwd, branch) {
1475
1652
  }
1476
1653
  }
1477
1654
 
1478
- /** Parse `git worktree list --porcelain` into `[{ worktree, branch }]`. */
1479
- function parseWorktreeListPorcelain(text) {
1480
- const entries = [];
1481
- let cur = null;
1482
- for (const line of String(text || '').split('\n')) {
1483
- if (line.startsWith('worktree ')) {
1484
- cur = { worktree: line.slice('worktree '.length).trim(), branch: null };
1485
- entries.push(cur);
1486
- } else if (line.startsWith('branch ') && cur) {
1487
- cur.branch = line.slice('branch '.length).trim().replace(/^refs\/heads\//, '');
1488
- }
1489
- }
1490
- return entries;
1491
- }
1655
+ // Alias — the real implementation now lives in gitExec.cjs
1656
+ // (parseWorktreePorcelain) so it can be shared below both this module and
1657
+ // scheduler.cjs in the require graph. Name kept for the many existing
1658
+ // `gitWorktree.parseWorktreeListPorcelain` call sites/tests.
1659
+ const parseWorktreeListPorcelain = sharedGitExec.parseWorktreePorcelain;
1492
1660
 
1493
1661
  /** Strips a kind's branch prefix off a branch name, returning the bare
1494
1662
  * key (slug/epicId), or null if the branch doesn't carry that prefix. */
@@ -1650,7 +1818,10 @@ async function reconcileWorktreesOnBoot(cwds, opts = {}) {
1650
1818
  if (isLive && key && (await isLive(key, entry))) continue;
1651
1819
  await removeWorktreeDir(cwd, entry.worktree);
1652
1820
  checkoutsRemoved++;
1653
- if (entry.branch) {
1821
+ // sm-land/* is the per-Epic integration branch, never a throwaway
1822
+ // worktree branch — even if something else checked it out onto a
1823
+ // worktree under this kind's root, this sweep must never delete it.
1824
+ if (entry.branch && !entry.branch.startsWith(LAND_BRANCH_PREFIX)) {
1654
1825
  try { await execGit(['branch', '-D', entry.branch], { cwd, timeout: 10_000 }); } catch { /* already gone */ }
1655
1826
  }
1656
1827
  }
@@ -1674,13 +1845,399 @@ async function reconcileWorktreesOnBoot(cwds, opts = {}) {
1674
1845
  return totals;
1675
1846
  }
1676
1847
 
1848
+ // ──────────────────────────────────────────── checkout-free integration (integrateOntoRef / ensureLandBranch)
1849
+
1850
+ /** `git rev-parse --verify --quiet <spec>`, trimmed — null (never throws) on
1851
+ * any failure, e.g. `spec` doesn't resolve to a commit. */
1852
+ async function tryResolveCommit(cwd, spec) {
1853
+ try {
1854
+ const out = await execGit(['rev-parse', '--verify', '--quiet', spec], { cwd, timeout: 10_000 });
1855
+ return out.trim() || null;
1856
+ } catch {
1857
+ return null;
1858
+ }
1859
+ }
1860
+
1861
+ /**
1862
+ * Runs `git merge-tree --write-tree --name-only --no-messages <targetRef>
1863
+ * <branch>` and classifies the result — never throws. Confirmed against a
1864
+ * real repo (git 2.39.5): a clean merge exits 0 with the resulting tree OID
1865
+ * as the only stdout line; a content conflict exits 1 with the tree OID on
1866
+ * line 1 followed by one conflicted path per line; anything else (including
1867
+ * exit 129 / an "unknown option" stderr, git < 2.38's signature for not
1868
+ * supporting `--write-tree` at all) is a hard failure.
1869
+ */
1870
+ async function runMergeTreeWriteTree({ cwd, oldSha, branchSha }) {
1871
+ try {
1872
+ // Resolved SHAs, never ref/branch NAMES: (1) keeps the tree content this
1873
+ // computes and the parents `git commit-tree` later stamps always in sync
1874
+ // with the exact objects decided on above — a name could resolve to a
1875
+ // DIFFERENT object by the time this runs if something else moved it
1876
+ // between the earlier resolution and this call; (2) a branch/ref name
1877
+ // starting with '-' (e.g. an epicId-derived branch) would otherwise be
1878
+ // read as an option by `git merge-tree`, not a positional argument.
1879
+ const out = await execGit(
1880
+ ['merge-tree', '--write-tree', '--name-only', '--no-messages', oldSha, branchSha],
1881
+ { cwd, timeout: 30_000 },
1882
+ );
1883
+ return { ok: true, treeOid: (out.split('\n')[0] || '').trim() };
1884
+ } catch (e) {
1885
+ const stderrText = (e && e.stderrText) || '';
1886
+ const stdoutText = (e && e.stdoutText) || '';
1887
+ if (e && e.code === 129 || /unknown option/i.test(stderrText)) {
1888
+ return {
1889
+ ok: false,
1890
+ failureKind: 'git_too_old',
1891
+ reason: `git merge-tree --write-tree is unsupported by this git (need >= 2.38): ${stderrText || e.message}`,
1892
+ };
1893
+ }
1894
+ if (e && e.code === 1) {
1895
+ const lines = stdoutText.split('\n').map((l) => l.trim());
1896
+ const conflictedPaths = Array.from(new Set(lines.slice(1).filter(Boolean))).sort();
1897
+ return {
1898
+ ok: false,
1899
+ failureKind: 'content_conflict',
1900
+ conflictedPaths,
1901
+ reason: `merge-tree conflict on: ${conflictedPaths.join(', ')}`,
1902
+ };
1903
+ }
1904
+ return { ok: false, reason: `git merge-tree --write-tree failed: ${stderrText || e.message}` };
1905
+ }
1906
+ }
1907
+
1908
+ /**
1909
+ * One computation attempt for `integrateOntoRef` — resolves both tips fresh,
1910
+ * decides ff vs merge-commit vs no-op, and lands the result via a
1911
+ * compare-and-swap `git update-ref`. Returns `{ retry: true }` (never `ok`)
1912
+ * when the CAS lost a race against a concurrent ref update — the caller
1913
+ * recomputes from scratch, since a stale `oldSha` invalidates everything
1914
+ * derived from it (the ff-vs-merge decision, the merge-tree/commit-tree
1915
+ * inputs). Every other return is terminal: a real `{ ok: true/false, ... }`.
1916
+ */
1917
+ async function attemptIntegrateOntoRef({ cwd, targetRef, branch, message }) {
1918
+ const branchSha = await tryResolveCommit(cwd, `${branch}^{commit}`);
1919
+ if (!branchSha) return { ok: false, reason: `branch "${branch}" not found` };
1920
+
1921
+ // Resolve the ref itself (any object type) separately from its `^{commit}`
1922
+ // form: a ref that exists but doesn't resolve to a commit (points at a
1923
+ // blob/tree, or a corrupt/annotated-non-commit ref) must be an explicit
1924
+ // failure, never silently treated as "doesn't exist yet" — that would run
1925
+ // the create-only CAS below and stomp the ref's existing (non-commit)
1926
+ // value instead of refusing.
1927
+ const targetRefExists = await tryResolveCommit(cwd, targetRef);
1928
+ const oldSha = await tryResolveCommit(cwd, `${targetRef}^{commit}`);
1929
+ if (targetRefExists && !oldSha) {
1930
+ return { ok: false, reason: `targetRef "${targetRef}" exists but does not resolve to a commit` };
1931
+ }
1932
+
1933
+ if (oldSha === branchSha) {
1934
+ return { ok: true, integrated: false, reason: 'branch has no new commits' };
1935
+ }
1936
+
1937
+ // Retries only when `targetRef` actually moved since `oldSha` was read
1938
+ // (a real CAS race a fresh attempt can resolve); otherwise the update-ref
1939
+ // failure is real (bad ref name, permissions, disk) and reported as such
1940
+ // rather than silently retried forever.
1941
+ async function updateRefOrClassify(args) {
1942
+ try {
1943
+ await execGit(['update-ref', ...args], { cwd, timeout: 10_000 });
1944
+ return { ok: true };
1945
+ } catch (e) {
1946
+ const stderrText = (e && (e.stderrText || e.message)) || String(e);
1947
+ const nowSha = await tryResolveCommit(cwd, `${targetRef}^{commit}`);
1948
+ if (nowSha !== oldSha) return { retry: true };
1949
+ return { ok: false, failureKind: 'update_ref_failed', reason: stderrText };
1950
+ }
1951
+ }
1952
+
1953
+ if (!oldSha) {
1954
+ // targetRef doesn't exist yet — a create-only CAS is the correct
1955
+ // fast-forward: everything on branch is new relative to an empty ref.
1956
+ const outcome = await updateRefOrClassify([targetRef, branchSha, '']);
1957
+ if (outcome.retry) return { retry: true };
1958
+ if (!outcome.ok) return outcome;
1959
+ return { ok: true, integrated: true, fastForward: true, sha: branchSha };
1960
+ }
1961
+
1962
+ let isAncestor;
1963
+ try {
1964
+ await execGit(['merge-base', '--is-ancestor', oldSha, branchSha], { cwd, timeout: 10_000 });
1965
+ isAncestor = true;
1966
+ } catch (e) {
1967
+ if (e && e.code === 1) {
1968
+ isAncestor = false;
1969
+ } else {
1970
+ return { ok: false, reason: `merge-base --is-ancestor failed: ${(e && (e.stderrText || e.message)) || e}` };
1971
+ }
1972
+ }
1973
+
1974
+ if (isAncestor) {
1975
+ const outcome = await updateRefOrClassify([targetRef, branchSha, oldSha]);
1976
+ if (outcome.retry) return { retry: true };
1977
+ if (!outcome.ok) return outcome;
1978
+ return { ok: true, integrated: true, fastForward: true, sha: branchSha };
1979
+ }
1980
+
1981
+ const mergeTreeResult = await runMergeTreeWriteTree({ cwd, oldSha, branchSha });
1982
+ if (!mergeTreeResult.ok) return mergeTreeResult;
1983
+
1984
+ const commitTreeArgs = ['commit-tree', mergeTreeResult.treeOid, '-p', oldSha, '-p', branchSha, '-m', message];
1985
+ let newSha;
1986
+ try {
1987
+ newSha = (await execGit(commitTreeArgs, { cwd, timeout: 15_000 })).trim();
1988
+ } catch (e) {
1989
+ const stderrText = (e && (e.stderrText || e.message)) || String(e);
1990
+ if (!/Please tell me who you are/i.test(stderrText)) {
1991
+ return { ok: false, reason: `git commit-tree failed: ${stderrText}` };
1992
+ }
1993
+ try {
1994
+ newSha = (await execGit(
1995
+ ['-c', 'user.name=Session Manager Scheduler', '-c', 'user.email=scheduler@session-manager.local', ...commitTreeArgs],
1996
+ { cwd, timeout: 15_000 },
1997
+ )).trim();
1998
+ } catch (e2) {
1999
+ return { ok: false, reason: `git commit-tree failed: ${(e2 && (e2.stderrText || e2.message)) || e2}` };
2000
+ }
2001
+ }
2002
+
2003
+ const outcome = await updateRefOrClassify([targetRef, newSha, oldSha]);
2004
+ if (outcome.retry) return { retry: true };
2005
+ if (!outcome.ok) return outcome;
2006
+ return { ok: true, integrated: true, mergeCommit: true, sha: newSha };
2007
+ }
2008
+
2009
+ /**
2010
+ * Lands `branch` onto `targetRef` WITHOUT touching any working tree, index,
2011
+ * or HEAD — no `git checkout`, no `git merge`, nothing in any human's or
2012
+ * job's own checkout ever moves. Fast-forwards via a compare-and-swap `git
2013
+ * update-ref` when `targetRef` is an ancestor of `branch`; otherwise builds a
2014
+ * merge commit entirely in the object database (`git merge-tree --write-tree`
2015
+ * + `git commit-tree`) and lands IT via the same CAS. This is the foundation
2016
+ * for a per-Epic `sm-land/<epicId>` integration branch that scheduler jobs
2017
+ * can land onto regardless of what the human's own checkout is doing.
2018
+ *
2019
+ * Refuses up front (`failureKind: 'target_checked_out'`, nothing touched) if
2020
+ * `targetRef` is checked out in ANY worktree of this repo — updating a
2021
+ * checked-out branch's ref out from under `git worktree list` would desync
2022
+ * that working tree's HEAD from its own index, which no compare-and-swap can
2023
+ * detect or prevent (a worktree's HEAD isn't a ref-race participant here).
2024
+ *
2025
+ * A CAS loss (the ref moved between our read and our write — a sibling
2026
+ * caller landed first) retries the WHOLE computation, up to 3 attempts total,
2027
+ * before giving up with `failureKind: 'ref_race'`.
2028
+ */
2029
+ /**
2030
+ * Normalizes `targetRef` to a full `refs/heads/...` name: a short name like
2031
+ * `sm-land/x` becomes `refs/heads/sm-land/x`; a value already under
2032
+ * `refs/heads/` passes through unchanged; anything else that already starts
2033
+ * with `refs/` (e.g. `refs/tags/x`, `refs/remotes/x`) is rejected — it is not
2034
+ * a local branch and prefixing `refs/heads/` onto it would silently target
2035
+ * the wrong ref. Returns null on any non-normalizable input (including a
2036
+ * non-string/empty value). Pure — never touches git or disk.
2037
+ */
2038
+ function normalizeTargetRef(targetRef) {
2039
+ if (typeof targetRef !== 'string' || !targetRef) return null;
2040
+ if (targetRef.startsWith('refs/heads/')) return targetRef;
2041
+ if (targetRef.startsWith('refs/')) return null;
2042
+ return `refs/heads/${targetRef}`;
2043
+ }
2044
+
2045
+ /**
2046
+ * Reads `<adminDir>/rebase-merge/head-name` or `.../rebase-apply/head-name`
2047
+ * (git uses exactly one of the two backends per paused rebase) and returns
2048
+ * its trimmed content — the `refs/heads/...` ref that rebase will update
2049
+ * when it finishes — or `null` when neither file exists (no rebase paused in
2050
+ * this admin dir). ENOENT/ENOTDIR on a read means "not that backend," not a
2051
+ * failure; any other error (permissions, I/O) propagates so the caller can
2052
+ * fail closed instead of silently treating an unreadable admin dir as
2053
+ * rebase-free.
2054
+ */
2055
+ async function pausedRebaseHeadRef(adminDir) {
2056
+ for (const rel of ['rebase-merge/head-name', 'rebase-apply/head-name']) {
2057
+ try {
2058
+ return (await fsp.readFile(path.join(adminDir, rel), 'utf8')).trim();
2059
+ } catch (e) {
2060
+ if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) continue;
2061
+ throw e;
2062
+ }
2063
+ }
2064
+ return null;
2065
+ }
2066
+
2067
+ /**
2068
+ * Fetches `git worktree list --porcelain` fresh and refuses when
2069
+ * `targetRef` (already normalized to `refs/heads/...`) is checked out in any
2070
+ * worktree — see integrateOntoRef's header. Split out so each retry
2071
+ * iteration in integrateOntoRef re-checks this from scratch: a sibling
2072
+ * process could check `targetRef` out into a worktree in between two of our
2073
+ * own retry attempts, and a guard that only ran once before the retry loop
2074
+ * would miss that.
2075
+ *
2076
+ * Also refuses when `targetRef` is mid-rebase in any worktree of this repo,
2077
+ * checked out there or not: a paused rebase detaches HEAD, so `git worktree
2078
+ * list`'s `branch` line goes missing for the SAME worktree this guard most
2079
+ * needs to catch — the check above would otherwise pass and the ref would
2080
+ * move, only for `git rebase --abort` to reset it back (losing the landed
2081
+ * commit) or `--continue` to leave it stale. Reads each worktree's own admin
2082
+ * dir directly instead of running git inside it, because a worktree's
2083
+ * checkout directory can be gone while its admin dir under the shared
2084
+ * `.git` still holds the paused rebase's state. Fails closed: any read
2085
+ * error here is also a refusal, never a silent pass.
2086
+ */
2087
+ async function checkTargetRefNotCheckedOut({ cwd, targetRef }) {
2088
+ let worktreeListOut;
2089
+ try {
2090
+ worktreeListOut = await execGit(['worktree', 'list', '--porcelain'], { cwd, timeout: 15_000 });
2091
+ } catch (e) {
2092
+ return { ok: false, reason: `git worktree list failed: ${(e && (e.stderrText || e.message)) || e}` };
2093
+ }
2094
+ // NOT parseWorktreeListPorcelain here on purpose — that helper strips the
2095
+ // `refs/heads/` prefix, but targetRef (below) is always compared in full
2096
+ // `refs/heads/...` form (normalizeTargetRef), so this needs the raw ref.
2097
+ const checkedOutRefs = new Set(
2098
+ worktreeListOut.split('\n').filter((l) => l.startsWith('branch ')).map((l) => l.slice('branch '.length).trim()),
2099
+ );
2100
+ if (checkedOutRefs.has(targetRef)) {
2101
+ return {
2102
+ ok: false,
2103
+ failureKind: 'target_checked_out',
2104
+ reason: `${targetRef} is checked out in a worktree — updating it directly would desync that working tree`,
2105
+ };
2106
+ }
2107
+
2108
+ let gitCommonDirOut;
2109
+ try {
2110
+ gitCommonDirOut = await execGit(['rev-parse', '--git-common-dir'], { cwd, timeout: 10_000 });
2111
+ } catch (e) {
2112
+ return { ok: false, reason: `git rev-parse --git-common-dir failed: ${(e && (e.stderrText || e.message)) || e}` };
2113
+ }
2114
+ const sharedGitDir = path.resolve(cwd, gitCommonDirOut.trim());
2115
+ const worktreesDir = path.join(sharedGitDir, 'worktrees');
2116
+ const adminDirs = [sharedGitDir];
2117
+ try {
2118
+ for (const entry of await fsp.readdir(worktreesDir)) adminDirs.push(path.join(worktreesDir, entry));
2119
+ } catch (e) {
2120
+ if (!(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR'))) {
2121
+ return { ok: false, reason: `reading ${worktreesDir} failed: ${(e && e.message) || e}` };
2122
+ }
2123
+ }
2124
+ for (const adminDir of adminDirs) {
2125
+ let pausedRef;
2126
+ try {
2127
+ pausedRef = await pausedRebaseHeadRef(adminDir);
2128
+ } catch (e) {
2129
+ return { ok: false, reason: `reading rebase state under ${adminDir} failed: ${(e && e.message) || e}` };
2130
+ }
2131
+ if (pausedRef && pausedRef === targetRef) {
2132
+ return {
2133
+ ok: false,
2134
+ failureKind: 'target_checked_out',
2135
+ reason: `${targetRef} is being rebased in a worktree — moving it now would be undone when that rebase ends`,
2136
+ };
2137
+ }
2138
+ }
2139
+
2140
+ return { ok: true };
2141
+ }
2142
+
2143
+ async function integrateOntoRef({ cwd, targetRef, branch, message }) {
2144
+ if (!cwd || typeof cwd !== 'string') return { ok: false, reason: 'no cwd provided' };
2145
+ if (!targetRef || typeof targetRef !== 'string') return { ok: false, reason: 'no targetRef provided' };
2146
+ if (!branch || typeof branch !== 'string') return { ok: false, reason: 'no branch provided' };
2147
+ if (!(await isGitRepo(cwd))) return { ok: false, reason: 'not a git repository' };
2148
+
2149
+ const normalizedTargetRef = normalizeTargetRef(targetRef);
2150
+ if (!normalizedTargetRef) {
2151
+ return { ok: false, reason: `targetRef "${targetRef}" is not a refs/heads/... branch ref (or a short name that normalizes to one)` };
2152
+ }
2153
+ try {
2154
+ await execGit(['check-ref-format', normalizedTargetRef], { cwd, timeout: 10_000 });
2155
+ } catch (e) {
2156
+ return { ok: false, reason: `targetRef "${targetRef}" failed check-ref-format: ${(e && (e.stderrText || e.message)) || e}` };
2157
+ }
2158
+
2159
+ const mergeMessage = (typeof message === 'string' && message) || `integrate ${branch} onto ${normalizedTargetRef}`;
2160
+ for (let attempt = 0; attempt < 3; attempt += 1) {
2161
+ const guard = await checkTargetRefNotCheckedOut({ cwd, targetRef: normalizedTargetRef });
2162
+ if (!guard.ok) return guard;
2163
+ const result = await attemptIntegrateOntoRef({ cwd, targetRef: normalizedTargetRef, branch, message: mergeMessage });
2164
+ if (!result.retry) return result;
2165
+ }
2166
+ return { ok: false, failureKind: 'ref_race' };
2167
+ }
2168
+
2169
+ /**
2170
+ * Validates `epicId` is ALREADY a safe branch-name component and prefixes it
2171
+ * with `sm-land/` — the single definition of an Epic's land-branch name, so
2172
+ * `ensureLandBranch` and any future caller never drift apart on how an
2173
+ * epicId becomes a branch name.
2174
+ *
2175
+ * Returns null (never a silently-mangled name) when `epicId` is not a
2176
+ * string, is empty, contains any character outside `[A-Za-z0-9._-]`,
2177
+ * contains `..`, ends with `.lock`, or starts with `.` or `-` — the same
2178
+ * shapes `git check-ref-format`/a shell arg parser would choke on or
2179
+ * misinterpret. A caller that gets null must treat the epicId as rejected,
2180
+ * not retry with a stripped-down version of it: silently stripping invalid
2181
+ * characters would let two different epicIds collide on the same land
2182
+ * branch (e.g. `"a/b"` and `"ab"` both sanitizing to `"ab"`).
2183
+ */
2184
+ function landBranchNameFor(epicId) {
2185
+ if (typeof epicId !== 'string' || !epicId) return null;
2186
+ if (!/^[A-Za-z0-9._-]+$/.test(epicId)) return null;
2187
+ if (epicId.includes('..')) return null;
2188
+ if (epicId.endsWith('.lock')) return null;
2189
+ if (epicId.startsWith('.') || epicId.startsWith('-')) return null;
2190
+ return `${LAND_BRANCH_PREFIX}${epicId}`;
2191
+ }
2192
+
2193
+ /**
2194
+ * Ensures the per-Epic integration branch `sm-land/<epicId>` exists, creating
2195
+ * it (via a create-only CAS `git update-ref`, never `git checkout`/`git
2196
+ * branch -f`) at the tip of `baseBranch` when absent. `baseBranch` falls back
2197
+ * to `resolveDefaultBranch(cwd)` when it's falsy or one of our OWN managed
2198
+ * branches (`isOwnManagedBranch` — an `sm-job/*`/`sm-epic/*`/`sm-land/*` value
2199
+ * is never a legitimate base, same reasoning `integrateBranch` already
2200
+ * applies to its own baseBranch fallback).
2201
+ *
2202
+ * Idempotent: a second call for the same epicId is a no-op (`created:
2203
+ * false`), including the race where a concurrent caller created it between
2204
+ * our existence check and our own create-only CAS.
2205
+ */
2206
+ async function ensureLandBranch({ cwd, epicId, baseBranch }) {
2207
+ if (!cwd || typeof cwd !== 'string') return { ok: false, reason: 'no cwd provided' };
2208
+
2209
+ const branch = landBranchNameFor(epicId);
2210
+ if (!branch) return { ok: false, reason: `epicId "${epicId}" is not a safe branch-name component` };
2211
+ const ref = `refs/heads/${branch}`;
2212
+
2213
+ const existingSha = await tryResolveCommit(cwd, `${ref}^{commit}`);
2214
+ if (existingSha) return { ok: true, ref, branch, created: false };
2215
+
2216
+ const base = (typeof baseBranch === 'string' && baseBranch && !isOwnManagedBranch(baseBranch))
2217
+ ? baseBranch
2218
+ : await resolveDefaultBranch(cwd);
2219
+ const baseSha = await tryResolveCommit(cwd, `${base}^{commit}`);
2220
+ if (!baseSha) return { ok: false, reason: `base branch "${base}" not found` };
2221
+
2222
+ try {
2223
+ await execGit(['update-ref', ref, baseSha, ''], { cwd, timeout: 10_000 });
2224
+ return { ok: true, ref, branch, created: true };
2225
+ } catch (e) {
2226
+ // A concurrent caller may have created it between our check above and
2227
+ // this create-only CAS — re-check before treating it as a real failure.
2228
+ const nowSha = await tryResolveCommit(cwd, `${ref}^{commit}`);
2229
+ if (nowSha) return { ok: true, ref, branch, created: false };
2230
+ return { ok: false, reason: `update-ref create failed: ${(e && (e.stderrText || e.message)) || e}` };
2231
+ }
2232
+ }
2233
+
1677
2234
  // ──────────────────────────────────────────── kind-scoped convenience wrappers
1678
2235
 
1679
- async function createJobWorktree({ cwd, slug }) {
1680
- return createWorktree({ kind: 'job', cwd, key: slug });
2236
+ async function createJobWorktree({ cwd, slug, fromRef }) {
2237
+ return createWorktree({ kind: 'job', cwd, key: slug, fromRef });
1681
2238
  }
1682
- async function integrateJobBranch({ cwd, branch, slug, carriedPaths }) {
1683
- return integrateBranch({ kind: 'job', cwd, branch, key: slug, carriedPaths });
2239
+ async function integrateJobBranch({ cwd, branch, slug, carriedPaths, baseBranch, allowRefLanding }) {
2240
+ return integrateBranch({ kind: 'job', cwd, branch, key: slug, carriedPaths, baseBranch, allowRefLanding });
1684
2241
  }
1685
2242
  async function cleanupJobWorktree({ cwd, dir, branch, keepBranch }) {
1686
2243
  return cleanupWorktree({ kind: 'job', cwd, dir, branch, keepBranch });
@@ -1695,8 +2252,8 @@ async function salvageJobDirtyDelta({ cwd, paths, outFile }) {
1695
2252
  async function createEpicWorktree({ cwd, epicId }) {
1696
2253
  return createWorktree({ kind: 'epic', cwd, key: epicId });
1697
2254
  }
1698
- async function integrateEpicBranch({ cwd, branch, epicId }) {
1699
- return integrateBranch({ kind: 'epic', cwd, branch, key: epicId });
2255
+ async function integrateEpicBranch({ cwd, branch, epicId, baseBranch }) {
2256
+ return integrateBranch({ kind: 'epic', cwd, branch, key: epicId, baseBranch });
1700
2257
  }
1701
2258
  async function cleanupEpicWorktree({ cwd, dir, branch, keepBranch }) {
1702
2259
  return cleanupWorktree({ kind: 'epic', cwd, dir, branch, keepBranch });
@@ -1719,6 +2276,7 @@ module.exports = {
1719
2276
  pathsIdenticalToBranch,
1720
2277
  resolveDefaultBranch,
1721
2278
  getCurrentBranch,
2279
+ isCarriedWipOnly,
1722
2280
  integrateBranch,
1723
2281
  cleanupWorktree,
1724
2282
  salvageWorktreeDiff,
@@ -1734,6 +2292,12 @@ module.exports = {
1734
2292
  reserveWorktreeSlot,
1735
2293
  expireDeadWorktreeRegistrations,
1736
2294
  reclaimTerminalJobOrphans,
2295
+ // Checkout-free integration primitives — land a branch onto a ref without
2296
+ // touching any working tree/index/HEAD (see integrateOntoRef's header).
2297
+ isOwnManagedBranch,
2298
+ integrateOntoRef,
2299
+ landBranchNameFor,
2300
+ ensureLandBranch,
1737
2301
  // Job-kind convenience wrappers — same call shape jobWorktree.cjs has
1738
2302
  // always exposed.
1739
2303
  createJobWorktree,