claude-code-session-manager 0.98.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 (431) 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-DCEgMTjH.js → DataModel-HdR0AxFq.js} +1 -1
  5. package/dist/assets/{History-Bfdoywlh.js → History-Dr4Jr_5z.js} +2 -2
  6. package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-NhrvH3JP.js} +3 -3
  7. package/dist/assets/{Library-7akfgy67.js → Library-Dv_EtIMr.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-DTKDWV_n.js} +1 -1
  9. package/dist/assets/{McpServers-D4lZtXof.js → McpServers-M034G9B3.js} +2 -2
  10. package/dist/assets/{Memory-CeI8G1w5.js → Memory-DzF1B4gx.js} +6 -6
  11. package/dist/assets/{Permissions-BGleLja4.js → Permissions-DrgbSGWl.js} +3 -3
  12. package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-Bd3ef35w.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-BFczzI1B.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
  14. package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-rimJem-Y.js} +1 -1
  15. package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
  17. package/dist/assets/Settings-Dc1F_sSg.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
  19. package/dist/assets/{Skills-BxZ9_fCR.js → Skills-DRXd18gu.js} +2 -2
  20. package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-ahy5-Fwe.js} +1 -1
  21. package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-C_tToDeL.js} +1 -1
  22. package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-DvdefS6K.js} +1 -1
  23. package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-6Jl5Njc6.js} +1 -1
  24. package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
  25. package/dist/assets/{index-Cd98Q1lD.js → index-vx8O73l8.js} +497 -502
  26. package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-D6iRguNV.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +25 -29
  29. package/plugins/CLAUDE.md +6 -6
  30. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -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 +121 -12
  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 +12 -25
  91. package/src/main/lib/workTypeLibrary.cjs +7 -1
  92. package/src/main/promptSessionEvents.cjs +37 -13
  93. package/src/main/scheduler.cjs +910 -198
  94. package/src/main/seedAgentPersonas.cjs +278 -23
  95. package/src/main/supervisor.cjs +4 -1
  96. package/src/main/templates/PRD_AUTHORING.md +126 -406
  97. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  98. package/src/preload/api.d.ts +41 -94
  99. package/src/preload/index.cjs +14 -9
  100. package/src/seed/agents/architect.md +1 -0
  101. package/src/seed/agents/dev-lead.md +17 -18
  102. package/src/seed/agents/project-home-builder.md +1 -0
  103. package/src/seed/agents/validator.md +10 -4
  104. package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
  105. package/dist/assets/Scheduler-C_phpeUS.js +0 -16
  106. package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
  107. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  108. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  109. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  110. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  111. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  112. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  113. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  114. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  115. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  116. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  117. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  118. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  119. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  120. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  121. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  122. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  123. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  124. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  125. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  126. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  127. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  128. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  129. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  130. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  131. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  132. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  133. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  134. package/src/main/__tests__/docEdit.test.cjs +0 -244
  135. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  136. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  137. package/src/main/__tests__/dod-report.test.cjs +0 -304
  138. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  139. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  140. package/src/main/__tests__/epicMint.test.cjs +0 -332
  141. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  142. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  143. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  144. package/src/main/__tests__/exchanges.test.cjs +0 -122
  145. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  146. package/src/main/__tests__/extractJson.test.cjs +0 -51
  147. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  148. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  149. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  150. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  151. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  152. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  153. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  154. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  155. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  156. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  157. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  158. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  159. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  160. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  161. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  162. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  163. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  164. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  165. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  166. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  167. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  168. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  169. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  170. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  171. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  172. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  173. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  174. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  175. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  176. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  177. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  178. package/src/main/__tests__/planValidator.test.cjs +0 -125
  179. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  180. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  181. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  182. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  183. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  184. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  185. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  186. package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
  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 -211
  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-mutate-reentrancy.test.cjs +0 -117
  282. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  283. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  284. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  285. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  286. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  287. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  288. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  289. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  290. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  291. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  292. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  293. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  294. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  295. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  296. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  297. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  298. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  299. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  300. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  301. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  302. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  303. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  304. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  305. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  306. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  307. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  308. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  309. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  310. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  311. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  312. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  313. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  314. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  315. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  316. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  317. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  318. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  319. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  320. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  321. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  322. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  323. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  324. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  325. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  326. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  327. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  328. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  329. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  330. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  331. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  332. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  333. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  334. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  335. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  336. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  337. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  338. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  339. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  340. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  341. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  342. package/src/main/bilkoHost.cjs +0 -314
  343. package/src/main/bilkoHostCore.cjs +0 -89
  344. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  345. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  346. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  347. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  348. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  349. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  350. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  351. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  352. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  353. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  354. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  355. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  356. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  357. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  358. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  359. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  360. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  361. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  362. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  363. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  364. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  365. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  366. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  367. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  368. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  369. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  370. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  371. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  372. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  373. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  374. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  375. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  376. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  377. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  378. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  379. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  380. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  381. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  382. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  383. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  384. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  385. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  386. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  387. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  388. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  389. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  390. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  391. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  392. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  393. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  394. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  395. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  396. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  397. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  398. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  399. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  400. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  401. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  402. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  403. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  404. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  405. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  406. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  407. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  408. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  409. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  410. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  411. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  412. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  413. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  414. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  415. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  416. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  417. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  418. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  419. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  420. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  421. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  422. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  423. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  424. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  425. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  426. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  427. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  428. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  429. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
  430. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  431. 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,