claude-code-session-manager 0.98.0 → 0.101.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 (441) 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-CXaH2GED.js +1 -0
  5. package/dist/assets/{History-Bfdoywlh.js → History-Cerii-HA.js} +2 -2
  6. package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-aXMb5LDy.js} +3 -3
  7. package/dist/assets/{Library-7akfgy67.js → Library-WrJFKEOJ.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-D1BMCXS7.js} +1 -1
  9. package/dist/assets/{McpServers-D4lZtXof.js → McpServers-Cf0davLd.js} +2 -2
  10. package/dist/assets/{Memory-CeI8G1w5.js → Memory-Dn6NHWPe.js} +6 -6
  11. package/dist/assets/Permissions-CTVuFPjg.js +3 -0
  12. package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-C2TyF8Mn.js} +2 -2
  13. package/dist/assets/ProvenanceBadge-F_jwHSwE.js +1 -0
  14. package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-DA1KVJST.js} +1 -1
  15. package/dist/assets/Scheduler-Bm6Q_hlu.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-BygZlJca.js} +1 -1
  17. package/dist/assets/Settings-BYCiv5KV.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-CcLnl9np.js} +1 -1
  19. package/dist/assets/{Skills-BxZ9_fCR.js → Skills-ChffsdTD.js} +2 -2
  20. package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-dNkfHE_M.js} +1 -1
  21. package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-jVlEuB0m.js} +1 -1
  22. package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-6_cdwcP0.js} +1 -1
  23. package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-CVXqGpAP.js} +1 -1
  24. package/dist/assets/ViewTabs-DO1YVbpc.js +1 -0
  25. package/dist/assets/{index-CKH5Uxik.css → index-D89EUizB.css} +1 -1
  26. package/dist/assets/{index-Cd98Q1lD.js → index-DbCQPWWY.js} +747 -752
  27. package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-Wrvn-Yz1.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +25 -29
  30. package/plugins/CLAUDE.md +6 -6
  31. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
  32. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  34. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  35. package/scripts/README.md +3 -11
  36. package/scripts/audit-ops-hygiene.cjs +3 -3
  37. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  38. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  39. package/scripts/mint-epic.cjs +2 -3
  40. package/scripts/scheduler-mcp-server.cjs +64 -7
  41. package/src/main/agentLibrary.cjs +22 -2
  42. package/src/main/build-info.json +4 -4
  43. package/src/main/chatRunner.cjs +4 -3
  44. package/src/main/config.cjs +41 -38
  45. package/src/main/docEdit.cjs +4 -1
  46. package/src/main/files.cjs +2 -5
  47. package/src/main/health.cjs +1 -1
  48. package/src/main/index.cjs +40 -11
  49. package/src/main/ipcSchemas.cjs +35 -34
  50. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  51. package/src/main/lib/atomicFs.cjs +116 -0
  52. package/src/main/lib/branchSweep.cjs +13 -12
  53. package/src/main/lib/buildTarget.cjs +3 -3
  54. package/src/main/lib/builtinMacros.cjs +39 -0
  55. package/src/main/lib/claudeCliCaps.cjs +129 -0
  56. package/src/main/lib/credentials.cjs +2 -4
  57. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  58. package/src/main/lib/cwdClassify.cjs +15 -3
  59. package/src/main/lib/definitionOfDone.cjs +207 -125
  60. package/src/main/lib/dodDrainHook.cjs +11 -5
  61. package/src/main/lib/epicMint.cjs +2 -4
  62. package/src/main/lib/epicStatusMirror.cjs +8 -10
  63. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  64. package/src/main/lib/gateAuthority.cjs +96 -0
  65. package/src/main/lib/gitExec.cjs +69 -0
  66. package/src/main/lib/gitWorktree.cjs +656 -92
  67. package/src/main/lib/instanceLock.cjs +5 -8
  68. package/src/main/lib/launchFailure.cjs +2 -3
  69. package/src/main/lib/macroLibrary.cjs +480 -0
  70. package/src/main/lib/mcpToolCatalog.cjs +49 -17
  71. package/src/main/lib/needsReviewLedger.cjs +1 -1
  72. package/src/main/lib/opsErrorLog.cjs +11 -1
  73. package/src/main/lib/opsOwnership.cjs +5 -9
  74. package/src/main/lib/pidAlive.cjs +32 -0
  75. package/src/main/lib/prdCreate.cjs +121 -12
  76. package/src/main/lib/prdDisposition.cjs +2 -2
  77. package/src/main/lib/prdGateFiles.cjs +284 -0
  78. package/src/main/lib/prdLocations.cjs +61 -0
  79. package/src/main/lib/prdMigration.cjs +2 -3
  80. package/src/main/lib/prdSizing.cjs +2 -1
  81. package/src/main/lib/projectHomeAdminRoutes.cjs +136 -21
  82. package/src/main/lib/promptSessionSchema.cjs +10 -2
  83. package/src/main/lib/rcaReport.cjs +4 -6
  84. package/src/main/lib/reaperHelpers.cjs +8 -1
  85. package/src/main/lib/reservationExpiry.cjs +6 -1
  86. package/src/main/lib/reviewNotice.cjs +263 -0
  87. package/src/main/lib/runClaudeP.cjs +4 -1
  88. package/src/main/lib/schedulerPaths.cjs +22 -2
  89. package/src/main/lib/sessionSlots.cjs +2 -4
  90. package/src/main/lib/shippedPersonaSeeds.cjs +226 -0
  91. package/src/main/lib/smfileHeaders.cjs +66 -0
  92. package/src/main/lib/timeoutShim.cjs +123 -0
  93. package/src/main/lib/timeoutShimScript.cjs +275 -0
  94. package/src/main/lib/upgradeDrain.cjs +4 -10
  95. package/src/main/lib/watchdogHelpers.cjs +12 -25
  96. package/src/main/lib/workTypeLibrary.cjs +7 -1
  97. package/src/main/projectPages.cjs +43 -13
  98. package/src/main/promptSessionEvents.cjs +37 -13
  99. package/src/main/scheduler.cjs +910 -198
  100. package/src/main/seedAgentPersonas.cjs +279 -24
  101. package/src/main/supervisor.cjs +4 -1
  102. package/src/main/templates/PRD_AUTHORING.md +126 -406
  103. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  104. package/src/preload/api.d.ts +53 -94
  105. package/src/preload/index.cjs +14 -9
  106. package/src/seed/agents/architect.md +2 -0
  107. package/src/seed/agents/demo-video-builder.md +84 -0
  108. package/src/seed/agents/dev-lead.md +18 -18
  109. package/src/seed/agents/project-home-builder.md +66 -35
  110. package/src/seed/agents/validator.md +10 -4
  111. package/dist/assets/DataModel-DCEgMTjH.js +0 -1
  112. package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
  113. package/dist/assets/Permissions-BGleLja4.js +0 -3
  114. package/dist/assets/ProvenanceBadge-BFczzI1B.js +0 -1
  115. package/dist/assets/Scheduler-C_phpeUS.js +0 -16
  116. package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
  117. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  118. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  119. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  120. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  121. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  122. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  123. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  124. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  125. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  126. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  127. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  128. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  129. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  130. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  131. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  132. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  133. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  134. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  135. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  136. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  137. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  138. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  139. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  140. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  141. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  142. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  143. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  144. package/src/main/__tests__/docEdit.test.cjs +0 -244
  145. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  146. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  147. package/src/main/__tests__/dod-report.test.cjs +0 -304
  148. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  149. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  150. package/src/main/__tests__/epicMint.test.cjs +0 -332
  151. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  152. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  153. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  154. package/src/main/__tests__/exchanges.test.cjs +0 -122
  155. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  156. package/src/main/__tests__/extractJson.test.cjs +0 -51
  157. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  158. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  159. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  160. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  161. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  162. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  163. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  164. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  165. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  166. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  167. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  168. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  169. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  170. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  171. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  172. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  173. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  174. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  175. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  176. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  177. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  178. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  179. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  180. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  181. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  182. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  183. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  184. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  185. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  186. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  187. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  188. package/src/main/__tests__/planValidator.test.cjs +0 -125
  189. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  190. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  191. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  192. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  193. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  194. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  195. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  196. package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
  197. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  198. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  199. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  200. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  201. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  202. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  203. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  204. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  205. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  206. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  207. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  208. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  209. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  210. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  211. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  212. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  213. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  214. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  215. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  216. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  217. package/src/main/__tests__/projectPages.test.cjs +0 -137
  218. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  219. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  220. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  221. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  222. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  223. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  224. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  225. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  226. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  227. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  228. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  229. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  230. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  231. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
  232. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  233. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  234. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  235. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  236. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  237. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  238. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  239. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  240. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  241. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  242. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  243. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  244. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  245. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  246. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  247. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  248. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  249. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  250. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  251. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  252. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  253. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  254. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  255. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  256. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  257. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  258. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  259. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  260. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  261. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  262. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  263. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  264. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  265. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  266. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  267. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  268. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  269. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  270. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  271. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  272. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  273. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  274. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  275. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  276. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  277. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  278. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  279. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  280. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  281. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  282. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  283. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  284. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  285. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  286. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  287. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  288. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  289. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  290. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  291. package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
  292. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  293. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  294. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  296. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  297. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  298. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  299. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  300. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  301. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  302. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  303. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  304. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  305. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  306. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  307. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  308. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  309. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  310. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  311. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  312. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  313. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  314. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  315. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  316. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  317. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  318. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  319. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  320. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  321. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  322. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  323. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  324. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  325. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  326. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  327. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  328. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  329. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  330. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  331. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  332. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  333. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  334. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  335. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  336. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  337. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  338. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  339. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  340. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  341. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  342. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  343. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  344. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  345. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  346. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  347. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  348. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  349. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  350. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  351. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  352. package/src/main/bilkoHost.cjs +0 -314
  353. package/src/main/bilkoHostCore.cjs +0 -89
  354. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  355. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  356. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  357. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  358. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  359. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  360. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  361. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  362. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  363. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  364. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  365. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  366. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  367. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  368. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  369. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  370. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  371. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  372. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  373. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  374. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  375. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  376. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  377. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  378. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  379. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  380. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  381. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  382. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  383. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  384. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  385. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  386. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  387. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  388. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  389. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  390. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  391. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  392. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  393. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  394. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  395. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  396. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  397. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  398. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  399. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  400. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  401. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  402. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  403. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  404. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  405. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  406. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  407. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  408. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  409. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  410. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  411. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  412. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  413. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  414. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  415. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  416. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  417. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  418. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  419. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  420. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  421. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  422. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  423. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  424. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  425. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  426. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  427. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  428. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  429. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  430. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  431. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  432. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  433. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  434. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  435. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  436. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  437. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  438. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  439. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
  440. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  441. 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,