claude-code-session-manager 0.110.0 → 0.112.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/dist/assets/{AgentLibrary-Cm6MrOnF.js → AgentLibrary-RwPvEgqm.js} +2 -2
  3. package/dist/assets/{Button-CGVncJ6a.js → Button-36j3PzYv.js} +1 -1
  4. package/dist/assets/{DataModel-C3hlMoa8.js → DataModel-DUhANo4l.js} +1 -1
  5. package/dist/assets/{History-CTyfGNKn.js → History-BeZzNjmv.js} +2 -2
  6. package/dist/assets/{Hooks-BXxCBibx.js → Hooks-iQ8845Hp.js} +3 -3
  7. package/dist/assets/{Library-aCs7zxAg.js → Library-DPE1h-1L.js} +6 -6
  8. package/dist/assets/{ListDetail-B9Hqk1_f.js → ListDetail-PStOuQAo.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-Djfgp32k.js → MarkdownEditor-DPw-N2-m.js} +1 -1
  10. package/dist/assets/{McpServers-8HQC6QgR.js → McpServers-CUyRTNep.js} +2 -2
  11. package/dist/assets/{Memory-c4SVE3l2.js → Memory-bZ2xjBGy.js} +3 -3
  12. package/dist/assets/{Panel-BEQTwE4z.js → Panel-_VZ_uXNg.js} +1 -1
  13. package/dist/assets/{Permissions-BaYzgZtc.js → Permissions-PPRmfNbK.js} +3 -3
  14. package/dist/assets/{Plugins-DnXsVau7.js → Plugins-BFcvfvlx.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-CT2vWps1.js → ProvenanceBadge-B1WUknak.js} +1 -1
  16. package/dist/assets/RemoteMachines-D5V0lzgW.js +3 -0
  17. package/dist/assets/{SaveBar-DwcHt3iR.js → SaveBar-Dw64FVdM.js} +1 -1
  18. package/dist/assets/{Scheduler-D6Gcuo0M.js → Scheduler-C8s_pJ3I.js} +9 -9
  19. package/dist/assets/{ScopeSwitcher-CgvWDc9Y.js → ScopeSwitcher-D-nKDMAE.js} +1 -1
  20. package/dist/assets/{Settings-CJoogVAB.js → Settings-CQSrR4mO.js} +4 -4
  21. package/dist/assets/{SkillReferenceGraph-4fq01kxq.js → SkillReferenceGraph-BbZFMHim.js} +1 -1
  22. package/dist/assets/{Skills-BU4ZX4h4.js → Skills-Bzyrh6EQ.js} +1 -1
  23. package/dist/assets/SystemPrompt-DP6i3Dx0.js +1 -0
  24. package/dist/assets/TagLibrary-iip0C6Bl.js +1 -0
  25. package/dist/assets/{TiptapBody-DVyBsfsT.js → TiptapBody-Cr-hxX8L.js} +1 -1
  26. package/dist/assets/Toggle-7WpPzcZH.js +1 -0
  27. package/dist/assets/{ViewTabs-BLHjKUv-.js → ViewTabs-9lQCRwWJ.js} +1 -1
  28. package/dist/assets/{historyTotals-BXDd3EzP.js → historyTotals-BrFxw5J1.js} +1 -1
  29. package/dist/assets/{index-CuiU3I-8.js → index-CTBpf-26.js} +250 -249
  30. package/dist/assets/{settingsSchema-DD4bc5cT.js → settingsSchema-Dh5Kl7VO.js} +1 -1
  31. package/dist/assets/{useAgentsDirWatch-DMQ_XZtr.js → useAgentsDirWatch-CMpx0d04.js} +1 -1
  32. package/dist/assets/{useRemotePeers-C9NfteSD.js → useRemotePeers-CI2axd-L.js} +1 -1
  33. package/dist/index.html +1 -1
  34. package/package.json +3 -2
  35. package/plugins/CLAUDE.md +5 -8
  36. package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -6
  37. package/plugins/session-manager-dev/skills/builder/1-classify-and-bump/SKILL.md +2 -2
  38. package/plugins/session-manager-dev/skills/builder/SKILL.md +2 -3
  39. package/plugins/session-manager-dev/skills/develop/SKILL.md +1 -1
  40. package/plugins/session-manager-dev/skills/develop/standards.md +3 -3
  41. package/scripts/README.md +3 -3
  42. package/scripts/audit-ops-hygiene.cjs +4 -4
  43. package/scripts/hooks/guard-destructive-git.cjs +4 -4
  44. package/scripts/hooks/guard-inline-implementation.cjs +1 -1
  45. package/scripts/hooks/guard-prd-writes.cjs +2 -2
  46. package/scripts/hooks/lib/guard-destructive-git-policy.cjs +1 -1
  47. package/scripts/mint-epic.cjs +2 -2
  48. package/scripts/scheduler-mcp-server.cjs +4 -4
  49. package/src/main/agentLibrary.cjs +5 -5
  50. package/src/main/agentsIpc.cjs +76 -0
  51. package/src/main/build-info.json +4 -4
  52. package/src/main/chatRunner.cjs +12 -12
  53. package/src/main/config.cjs +2 -2
  54. package/src/main/crashDiagnostics.cjs +2 -2
  55. package/src/main/docEdit.cjs +5 -5
  56. package/src/main/files.cjs +2 -2
  57. package/src/main/git.cjs +1 -1
  58. package/src/main/health.cjs +20 -18
  59. package/src/main/heapSnapshot.cjs +1 -1
  60. package/src/main/historyAggregator.cjs +2 -2
  61. package/src/main/index.cjs +44 -132
  62. package/src/main/ipcSchemas.cjs +10 -10
  63. package/src/main/lib/CLAUDE.md +48 -0
  64. package/src/main/lib/activeSessions.cjs +1 -1
  65. package/src/main/lib/classifyPromptTicket.cjs +2 -2
  66. package/src/main/lib/{agentModelResolve.cjs → claude/agentModelResolve.cjs} +7 -7
  67. package/src/main/lib/{claudeBin.cjs → claude/claudeBin.cjs} +4 -4
  68. package/src/main/lib/{effectiveModelInfo.cjs → claude/effectiveModelInfo.cjs} +9 -9
  69. package/src/main/lib/{modelCatalog.cjs → claude/modelCatalog.cjs} +3 -3
  70. package/src/main/lib/{rateLimitDetect.cjs → claude/rateLimitDetect.cjs} +1 -1
  71. package/src/main/lib/{rateLimitWindow.cjs → claude/rateLimitWindow.cjs} +1 -1
  72. package/src/main/lib/{runClaudeP.cjs → claude/runClaudeP.cjs} +1 -1
  73. package/src/main/lib/credentials.cjs +3 -3
  74. package/src/main/lib/cwdClassify.cjs +1 -1
  75. package/src/main/lib/{activeIndexMerge.cjs → epic/activeIndexMerge.cjs} +3 -3
  76. package/src/main/lib/{activeIndexRebuild.cjs → epic/activeIndexRebuild.cjs} +1 -1
  77. package/src/main/lib/{delegationReadiness.cjs → epic/delegationReadiness.cjs} +21 -29
  78. package/src/main/lib/{epicContextDigest.cjs → epic/epicContextDigest.cjs} +1 -1
  79. package/src/main/lib/{epicDelegationStats.cjs → epic/epicDelegationStats.cjs} +2 -2
  80. package/src/main/lib/{epicMint.cjs → epic/epicMint.cjs} +11 -11
  81. package/src/main/lib/{epicSpawnPlan.cjs → epic/epicSpawnPlan.cjs} +3 -3
  82. package/src/main/lib/{epicStatusMirror.cjs → epic/epicStatusMirror.cjs} +2 -2
  83. package/src/main/lib/{epicTranscriptDiagnostic.cjs → epic/epicTranscriptDiagnostic.cjs} +2 -2
  84. package/src/main/lib/{epicTranscriptPath.cjs → epic/epicTranscriptPath.cjs} +2 -2
  85. package/src/main/lib/{epicValidationHook.cjs → epic/epicValidationHook.cjs} +3 -3
  86. package/src/main/lib/{epicWorktreeBoot.cjs → epic/epicWorktreeBoot.cjs} +1 -1
  87. package/src/main/lib/{epicWorktreeMerge.cjs → epic/epicWorktreeMerge.cjs} +3 -3
  88. package/src/main/lib/{epicWorktreeMint.cjs → epic/epicWorktreeMint.cjs} +3 -3
  89. package/src/main/lib/{epicWorktreeProjectConfig.cjs → epic/epicWorktreeProjectConfig.cjs} +4 -4
  90. package/src/main/lib/{promptSessionsCreateEpic.cjs → epic/promptSessionsCreateEpic.cjs} +4 -4
  91. package/src/main/lib/{crossProjectFeedback.cjs → feedback-remote/crossProjectFeedback.cjs} +7 -7
  92. package/src/main/lib/{customerFeedbackClient.cjs → feedback-remote/customerFeedbackClient.cjs} +3 -3
  93. package/src/main/lib/{customerFeedbackInbox.cjs → feedback-remote/customerFeedbackInbox.cjs} +2 -2
  94. package/src/main/lib/{customerFeedbackIpc.cjs → feedback-remote/customerFeedbackIpc.cjs} +1 -1
  95. package/src/main/lib/{localAdminHttp.cjs → feedback-remote/localAdminHttp.cjs} +2 -2
  96. package/src/main/lib/{macroAdminRoutes.cjs → feedback-remote/macroAdminRoutes.cjs} +1 -1
  97. package/src/main/lib/{projectHomeAdminRoutes.cjs → feedback-remote/projectHomeAdminRoutes.cjs} +4 -4
  98. package/src/main/lib/{relayClientAuth.cjs → feedback-remote/relayClientAuth.cjs} +2 -2
  99. package/src/main/lib/{remoteClientSessions.cjs → feedback-remote/remoteClientSessions.cjs} +1 -1
  100. package/src/main/lib/{gitWorktree.cjs → git/gitWorktree.cjs} +7 -7
  101. package/src/main/lib/historyRollup.cjs +1 -1
  102. package/src/main/lib/importReferences.cjs +1 -1
  103. package/src/main/lib/machineProfile.cjs +1 -1
  104. package/src/main/lib/openExternalApp.cjs +1 -1
  105. package/src/main/lib/opsOwnership.cjs +3 -3
  106. package/src/main/lib/{agentPersonaSchema.cjs → persona-macro/agentPersonaSchema.cjs} +2 -2
  107. package/src/main/lib/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +2 -2
  108. package/src/main/lib/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
  109. package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
  110. package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.cjs} +11 -0
  111. package/src/main/lib/{prdAdminRoutes.cjs → prd/prdAdminRoutes.cjs} +7 -7
  112. package/src/main/lib/{prdAgentType.cjs → prd/prdAgentType.cjs} +3 -3
  113. package/src/main/lib/{prdCreate.cjs → prd/prdCreate.cjs} +12 -12
  114. package/src/main/lib/{prdDeclaredPaths.cjs → prd/prdDeclaredPaths.cjs} +1 -1
  115. package/src/main/lib/{prdGateFiles.cjs → prd/prdGateFiles.cjs} +1 -1
  116. package/src/main/lib/{prdLocations.cjs → prd/prdLocations.cjs} +2 -2
  117. package/src/main/lib/{prdMigration.cjs → prd/prdMigration.cjs} +3 -3
  118. package/src/main/lib/prereqs.cjs +2 -2
  119. package/src/main/lib/{instanceLock.cjs → process/instanceLock.cjs} +4 -11
  120. package/src/main/lib/{pidAlive.cjs → process/pidAlive.cjs} +1 -1
  121. package/src/main/lib/{procName.cjs → process/procName.cjs} +2 -2
  122. package/src/main/lib/{timeoutShim.cjs → process/timeoutShim.cjs} +1 -1
  123. package/src/main/lib/projectRootResolve.cjs +1 -1
  124. package/src/main/lib/promptSessionSchema.cjs +1 -1
  125. package/src/main/lib/{definitionOfDone.cjs → scheduler/definitionOfDone.cjs} +6 -12
  126. package/src/main/lib/{fixChainDepth.cjs → scheduler/fixChainDepth.cjs} +1 -1
  127. package/src/main/lib/{jobDirtFilter.cjs → scheduler/jobDirtFilter.cjs} +1 -1
  128. package/src/main/lib/{jobSupervisorRecord.cjs → scheduler/jobSupervisorRecord.cjs} +1 -1
  129. package/src/main/lib/{launchFailure.cjs → scheduler/launchFailure.cjs} +2 -2
  130. package/src/main/lib/{queueStore.cjs → scheduler/queueStore.cjs} +6 -6
  131. package/src/main/lib/{quietMachineLease.cjs → scheduler/quietMachineLease.cjs} +1 -1
  132. package/src/main/lib/{rcaReport.cjs → scheduler/rcaReport.cjs} +4 -4
  133. package/src/main/lib/{reaperHelpers.cjs → scheduler/reaperHelpers.cjs} +3 -3
  134. package/src/main/lib/{reservationExpiry.cjs → scheduler/reservationExpiry.cjs} +4 -11
  135. package/src/main/lib/{reviewNotice.cjs → scheduler/reviewNotice.cjs} +1 -1
  136. package/src/main/lib/{scheduleJobTransitions.cjs → scheduler/scheduleJobTransitions.cjs} +3 -3
  137. package/src/main/lib/{schedulerBatch.cjs → scheduler/schedulerBatch.cjs} +2 -2
  138. package/src/main/lib/{schedulerConfig.cjs → scheduler/schedulerConfig.cjs} +1 -1
  139. package/src/main/lib/{schedulerRuntimeState.cjs → scheduler/schedulerRuntimeState.cjs} +1 -1
  140. package/src/main/lib/{sessionSlots.cjs → scheduler/sessionSlots.cjs} +2 -2
  141. package/src/main/lib/{upgradeDrain.cjs → scheduler/upgradeDrain.cjs} +8 -11
  142. package/src/main/lib/{watchdogHelpers.cjs → scheduler/watchdogHelpers.cjs} +5 -19
  143. package/src/main/lib/{auditLog.cjs → telemetry/auditLog.cjs} +1 -1
  144. package/src/main/lib/{opsErrorLog.cjs → telemetry/opsErrorLog.cjs} +4 -4
  145. package/src/main/lib/{telemetryBacklog.cjs → telemetry/telemetryBacklog.cjs} +4 -4
  146. package/src/main/lib/{telemetryBoot.cjs → telemetry/telemetryBoot.cjs} +1 -1
  147. package/src/main/lib/{telemetryClient.cjs → telemetry/telemetryClient.cjs} +4 -4
  148. package/src/main/lib/{telemetrySettings.cjs → telemetry/telemetrySettings.cjs} +1 -1
  149. package/src/main/logs.cjs +1 -1
  150. package/src/main/mcpStatus.cjs +2 -2
  151. package/src/main/memoryAggregate.cjs +3 -3
  152. package/src/main/memoryTool.cjs +1 -1
  153. package/src/main/otel.cjs +2 -2
  154. package/src/main/pluginInstall.cjs +29 -3
  155. package/src/main/projectBrief.cjs +6 -6
  156. package/src/main/projectPages.cjs +1 -1
  157. package/src/main/promptSessionEvents.cjs +2 -2
  158. package/src/main/pty.cjs +7 -7
  159. package/src/main/queueOps.cjs +6 -6
  160. package/src/main/remoteClient.cjs +4 -4
  161. package/src/main/remoteClientIpc.cjs +1 -1
  162. package/src/main/runVerify.cjs +1 -1
  163. package/src/main/scheduler/CLAUDE.md +26 -0
  164. package/src/main/scheduler/budget.cjs +576 -0
  165. package/src/main/scheduler/gitTreeGuard.cjs +672 -0
  166. package/src/main/scheduler/prdParser.cjs +4 -4
  167. package/src/main/scheduler/prdPaths.cjs +487 -0
  168. package/src/main/scheduler/reviewLadder.cjs +1011 -0
  169. package/src/main/scheduler/starvation.cjs +390 -0
  170. package/src/main/scheduler.cjs +289 -3311
  171. package/src/main/seedAgentPersonas.cjs +5 -5
  172. package/src/main/seedDevPlugin.cjs +70 -3
  173. package/src/main/seedSchedulerMcp.cjs +2 -2
  174. package/src/main/supervisor.cjs +5 -5
  175. package/src/main/telemetryIpc.cjs +51 -0
  176. package/src/main/transcripts.cjs +3 -3
  177. package/src/main/updaterIpc.cjs +1 -1
  178. package/src/main/usage.cjs +1 -1
  179. package/src/main/watchers.cjs +1 -1
  180. package/src/main/webRemote.cjs +7 -7
  181. package/src/preload/api.d.ts +8 -8
  182. package/src/preload/index.cjs +3 -3
  183. package/src/seed/agents/demo-video-builder.md +62 -19
  184. package/dist/assets/RemoteMachines-DHJZfdJC.js +0 -3
  185. package/dist/assets/SystemPrompt-BCEyMSEq.js +0 -1
  186. package/dist/assets/TagLibrary-m3It8Oal.js +0 -1
  187. package/dist/assets/Toggle-In_AExPV.js +0 -1
  188. package/plugins/session-manager-dev/skills/blog-for-project-feature/0-select/SKILL.md +0 -39
  189. package/plugins/session-manager-dev/skills/blog-for-project-feature/1-research/SKILL.md +0 -37
  190. package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +0 -64
  191. package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +0 -52
  192. package/plugins/session-manager-dev/skills/blog-for-project-feature/4-draft-copy/SKILL.md +0 -37
  193. package/plugins/session-manager-dev/skills/blog-for-project-feature/5-illustrate/SKILL.md +0 -50
  194. package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +0 -51
  195. package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +0 -54
  196. package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +0 -144
  197. package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +0 -90
  198. package/plugins/session-manager-dev/skills/discover-features/SKILL.md +0 -95
  199. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +0 -205
  200. package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +0 -163
  201. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +0 -83
  202. package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +0 -38
  203. package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +0 -64
  204. package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +0 -71
  205. package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +0 -39
  206. package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +0 -75
  207. package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +0 -39
  208. package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +0 -57
  209. package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +0 -40
  210. package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +0 -33
  211. package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +0 -40
  212. package/plugins/session-manager-dev/skills/issue-address/SKILL.md +0 -120
  213. package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +0 -58
  214. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +0 -147
  215. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +0 -137
  216. package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +0 -41
  217. package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +0 -56
  218. package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +0 -49
  219. package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +0 -57
  220. package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +0 -60
  221. package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +0 -124
  222. package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +0 -67
  223. package/plugins/session-manager-dev/skills/project-status/SKILL.md +0 -243
  224. package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +0 -98
  225. package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +0 -146
  226. /package/src/main/lib/{agentEffortResolve.cjs → claude/agentEffortResolve.cjs} +0 -0
  227. /package/src/main/lib/{claudeCliCaps.cjs → claude/claudeCliCaps.cjs} +0 -0
  228. /package/src/main/lib/{usageCircuit.cjs → claude/usageCircuit.cjs} +0 -0
  229. /package/src/main/lib/{epicSpawnCwd.cjs → epic/epicSpawnCwd.cjs} +0 -0
  230. /package/src/main/lib/{customerFeedbackAdminRoutes.cjs → feedback-remote/customerFeedbackAdminRoutes.cjs} +0 -0
  231. /package/src/main/lib/{remoteE2eCrypto.cjs → feedback-remote/remoteE2eCrypto.cjs} +0 -0
  232. /package/src/main/lib/{branchSweep.cjs → git/branchSweep.cjs} +0 -0
  233. /package/src/main/lib/{gitExec.cjs → git/gitExec.cjs} +0 -0
  234. /package/src/main/lib/{jobWorktree.cjs → git/jobWorktree.cjs} +0 -0
  235. /package/src/main/lib/{jobWorktreeBootLive.cjs → git/jobWorktreeBootLive.cjs} +0 -0
  236. /package/src/main/lib/{jobWorktreeTerminalOrphanLive.cjs → git/jobWorktreeTerminalOrphanLive.cjs} +0 -0
  237. /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
  238. /package/src/main/lib/{depSlugResolve.cjs → prd/depSlugResolve.cjs} +0 -0
  239. /package/src/main/lib/{planValidator.cjs → prd/planValidator.cjs} +0 -0
  240. /package/src/main/lib/{prdAuthoringSeed.cjs → prd/prdAuthoringSeed.cjs} +0 -0
  241. /package/src/main/lib/{prdDisposition.cjs → prd/prdDisposition.cjs} +0 -0
  242. /package/src/main/lib/{prdFrontmatter.cjs → prd/prdFrontmatter.cjs} +0 -0
  243. /package/src/main/lib/{prdSizing.cjs → prd/prdSizing.cjs} +0 -0
  244. /package/src/main/lib/{workTypeLibrary.cjs → prd/workTypeLibrary.cjs} +0 -0
  245. /package/src/main/lib/{childWithLog.cjs → process/childWithLog.cjs} +0 -0
  246. /package/src/main/lib/{cleanEnv.cjs → process/cleanEnv.cjs} +0 -0
  247. /package/src/main/lib/{defaultShell.cjs → process/defaultShell.cjs} +0 -0
  248. /package/src/main/lib/{killTree.cjs → process/killTree.cjs} +0 -0
  249. /package/src/main/lib/{loginShellPath.cjs → process/loginShellPath.cjs} +0 -0
  250. /package/src/main/lib/{procIdentity.cjs → process/procIdentity.cjs} +0 -0
  251. /package/src/main/lib/{singleInstanceGuard.cjs → process/singleInstanceGuard.cjs} +0 -0
  252. /package/src/main/lib/{smProcNames.cjs → process/smProcNames.cjs} +0 -0
  253. /package/src/main/lib/{timeoutShimScript.cjs → process/timeoutShimScript.cjs} +0 -0
  254. /package/src/main/lib/{winSpawn.cjs → process/winSpawn.cjs} +0 -0
  255. /package/src/main/lib/{adoptedRunSupervisor.cjs → scheduler/adoptedRunSupervisor.cjs} +0 -0
  256. /package/src/main/lib/{dispatchLoop.cjs → scheduler/dispatchLoop.cjs} +0 -0
  257. /package/src/main/lib/{dodDrainHook.cjs → scheduler/dodDrainHook.cjs} +0 -0
  258. /package/src/main/lib/{fixPlanSlug.cjs → scheduler/fixPlanSlug.cjs} +0 -0
  259. /package/src/main/lib/{gateAuthority.cjs → scheduler/gateAuthority.cjs} +0 -0
  260. /package/src/main/lib/{landedSinceRun.cjs → scheduler/landedSinceRun.cjs} +0 -0
  261. /package/src/main/lib/{loadGate.cjs → scheduler/loadGate.cjs} +0 -0
  262. /package/src/main/lib/{needsReviewLedger.cjs → scheduler/needsReviewLedger.cjs} +0 -0
  263. /package/src/main/lib/{queueHealth.cjs → scheduler/queueHealth.cjs} +0 -0
  264. /package/src/main/lib/{queueHistory.cjs → scheduler/queueHistory.cjs} +0 -0
  265. /package/src/main/lib/{runLogRetention.cjs → scheduler/runLogRetention.cjs} +0 -0
  266. /package/src/main/lib/{scheduleJobSchema.cjs → scheduler/scheduleJobSchema.cjs} +0 -0
  267. /package/src/main/lib/{schedulerPaths.cjs → scheduler/schedulerPaths.cjs} +0 -0
  268. /package/src/main/lib/{terminalRunOutcome.cjs → scheduler/terminalRunOutcome.cjs} +0 -0
  269. /package/src/main/lib/{validationSentinels.cjs → scheduler/validationSentinels.cjs} +0 -0
  270. /package/src/main/lib/{crashTelemetry.cjs → telemetry/crashTelemetry.cjs} +0 -0
  271. /package/src/main/lib/{otlpTraceExporter.cjs → telemetry/otlpTraceExporter.cjs} +0 -0
  272. /package/src/main/lib/{telemetryConsent.cjs → telemetry/telemetryConsent.cjs} +0 -0
  273. /package/src/main/lib/{telemetryCounters.cjs → telemetry/telemetryCounters.cjs} +0 -0
  274. /package/src/main/lib/{atomicFs.cjs → util/atomicFs.cjs} +0 -0
  275. /package/src/main/lib/{encodeCwd.cjs → util/encodeCwd.cjs} +0 -0
  276. /package/src/main/lib/{expandHome.cjs → util/expandHome.cjs} +0 -0
  277. /package/src/main/lib/{extractJson.cjs → util/extractJson.cjs} +0 -0
  278. /package/src/main/lib/{fileTail.cjs → util/fileTail.cjs} +0 -0
  279. /package/src/main/lib/{headTailBuffer.cjs → util/headTailBuffer.cjs} +0 -0
  280. /package/src/main/lib/{kebabCase.cjs → util/kebabCase.cjs} +0 -0
  281. /package/src/main/lib/{loopDelay.cjs → util/loopDelay.cjs} +0 -0
  282. /package/src/main/lib/{lruCache.cjs → util/lruCache.cjs} +0 -0
  283. /package/src/main/lib/{withTimeout.cjs → util/withTimeout.cjs} +0 -0
@@ -0,0 +1,672 @@
1
+ /**
2
+ * scheduler/gitTreeGuard.cjs — git status/stash/HEAD helpers, the shared-tree
3
+ * guard, commit-guard/attribution and finalize-drop predicates. Extracted
4
+ * verbatim from scheduler.cjs; internal calls go through module.exports (spy seam).
5
+ */
6
+ 'use strict';
7
+
8
+ const fsp = require('node:fs/promises');
9
+ const path = require('node:path');
10
+ const { execFile, execFileSync } = require('node:child_process');
11
+ const gitWorktree = require('../lib/git/gitWorktree.cjs');
12
+ const sharedGitExec = require('../lib/git/gitExec.cjs');
13
+ const { resolveProjectRoot } = require('../lib/opsOwnership.cjs');
14
+
15
+ // Unquote a single git porcelain v1 path token. Defined once in
16
+ // gitWorktree.cjs (which this file already requires — the reverse would be
17
+ // circular, since gitWorktree.cjs's own salvageDirtyDelta needs the exact
18
+ // same unquoting) and reused here rather than re-implemented, so the two
19
+ // porcelain consumers in this codebase can never drift apart.
20
+ const { unquotePorcelainPath } = gitWorktree;
21
+
22
+ // Split a rename/copy porcelain path field ("old -> new") into its two real
23
+ // paths. Each side is independently quoted per unquotePorcelainPath's rule —
24
+ // only the side that needs escaping is wrapped in quotes, the literal " -> "
25
+ // arrow between them never is. Returns null when no " -> " separator is
26
+ // found (a malformed/unexpected line) so the caller can fall back to treating
27
+ // the whole field as one opaque path rather than guessing.
28
+ function splitRenamePorcelainField(field) {
29
+ const arrow = ' -> ';
30
+ let head;
31
+ let rest;
32
+ if (field[0] === '"') {
33
+ let end = -1;
34
+ for (let i = 1; i < field.length; i += 1) {
35
+ if (field[i] === '\\') { i += 1; continue; }
36
+ if (field[i] === '"') { end = i; break; }
37
+ }
38
+ if (end === -1) return null;
39
+ head = field.slice(0, end + 1);
40
+ rest = field.slice(end + 1);
41
+ } else {
42
+ const idx = field.indexOf(arrow);
43
+ if (idx === -1) return null;
44
+ // An unquoted path containing a literal " -> " substring (git only
45
+ // quotes for a quote/backslash/control-byte/non-ASCII byte — a plain
46
+ // ASCII arrow inside a filename is never quoted) makes the true
47
+ // old/new boundary genuinely ambiguous from this text alone: the first
48
+ // occurrence could be the real separator, or it could be sitting
49
+ // inside the old path with the real separator later in the field.
50
+ // Guessing wrong silently corrupts oldPath/path for downstream
51
+ // fs.existsSync/Set-membership checks, which is worse than the
52
+ // existing "malformed line" fallback below — so more than one
53
+ // occurrence falls back to treating the whole field as one opaque
54
+ // path, same as any other line this function can't confidently parse.
55
+ if (field.indexOf(arrow, idx + arrow.length) !== -1) return null;
56
+ head = field.slice(0, idx);
57
+ rest = field.slice(idx);
58
+ }
59
+ if (!rest.startsWith(arrow)) return null;
60
+ return { oldPath: unquotePorcelainPath(head), path: unquotePorcelainPath(rest.slice(arrow.length)) };
61
+ }
62
+
63
+ // Parse `git status --porcelain` output into `{ code, path }` entries (plus
64
+ // `oldPath` for a rename/copy). Pure + exported for unit testing. Each
65
+ // porcelain line is "XY<space>PATH"; a staged rename/copy line is
66
+ // "XY<space>OLD -> NEW" instead — X (index status) is 'R' or 'C' — and NEW is
67
+ // the path git will report in any later `git status` call, so callers that
68
+ // key off `.path` (dirtyAfter membership, pathsCommittedDuringRun membership,
69
+ // fs.existsSync) must compare against NEW, never the fused "OLD -> NEW"
70
+ // string. `oldPath` is retained on the entry for callers that need the
71
+ // original path too. `code` is the raw 2-char status (e.g. '??' for
72
+ // untracked) — callers that need to distinguish "untracked" from
73
+ // "tracked-but-modified" (the shared-tree guard's revert-vs-now-ignored
74
+ // split) read it off the entry instead of re-deriving it later.
75
+ function parsePorcelainEntries(stdout) {
76
+ return String(stdout || '')
77
+ .split('\n')
78
+ .filter((l) => l.length > 0)
79
+ .map((l) => {
80
+ const code = l.slice(0, 2);
81
+ const field = l.slice(3);
82
+ if (code.includes('R') || code.includes('C')) {
83
+ const split = module.exports.splitRenamePorcelainField(field);
84
+ if (split) return { code, path: split.path, oldPath: split.oldPath };
85
+ }
86
+ return { code, path: unquotePorcelainPath(field) };
87
+ })
88
+ .filter((e) => e.path);
89
+ }
90
+
91
+ // Parse `git status --porcelain` output into a list of changed paths. Pure +
92
+ // exported for unit testing.
93
+ function parsePorcelain(stdout) {
94
+ return module.exports.parsePorcelainEntries(stdout).map((e) => e.path);
95
+ }
96
+
97
+ // Same as uncommittedChanges but keeps each path's porcelain status code —
98
+ // the shared-tree guard's baseline needs this to tell "was untracked" apart
99
+ // from "was tracked-and-modified" (see evaluateSharedTreeGuard). Returns null
100
+ // when the guard does not apply (cwd is not a git work tree, git is missing,
101
+ // or the call errors); never throws — a guard failure must not fail an
102
+ // otherwise-successful job.
103
+ function uncommittedChangesWithStatus(cwd) {
104
+ return new Promise((resolve) => {
105
+ if (!cwd) { resolve(null); return; }
106
+ execFile(
107
+ 'git',
108
+ ['-C', cwd, 'status', '--porcelain'],
109
+ { timeout: 10_000, windowsHide: true },
110
+ (err, stdout) => {
111
+ if (err) { resolve(null); return; }
112
+ resolve(module.exports.parsePorcelainEntries(stdout));
113
+ },
114
+ );
115
+ });
116
+ }
117
+
118
+ // Return the list of uncommitted paths in cwd, or null under the same
119
+ // conditions as uncommittedChangesWithStatus (never throws). Kept as a thin
120
+ // path-only projection of that call rather than its own execFile, so a future
121
+ // fix to the git invocation (timeout, error handling) can't land in one and
122
+ // silently miss the other.
123
+ function uncommittedChanges(cwd) {
124
+ return module.exports.uncommittedChangesWithStatus(cwd).then((entries) => (
125
+ entries === null ? null : entries.map((e) => e.path)
126
+ ));
127
+ }
128
+
129
+ // Return the current HEAD commit sha in cwd, or null on any error. Used by the
130
+ // commit-guard to detect whether the job self-committed during its run (HEAD
131
+ // moved) — in which case leftover working-tree dirt is presumptively from a
132
+ // concurrent external writer, not the job's unsaved deliverable. Never throws.
133
+ function gitHead(cwd) {
134
+ return new Promise((resolve) => {
135
+ if (!cwd) { resolve(null); return; }
136
+ execFile(
137
+ 'git',
138
+ ['-C', cwd, 'rev-parse', 'HEAD'],
139
+ { timeout: 10_000, windowsHide: true },
140
+ (err, stdout) => { resolve(err ? null : String(stdout || '').trim() || null); },
141
+ );
142
+ });
143
+ }
144
+
145
+ // Return the current `git stash list` entries in cwd as raw lines
146
+ // "<hash> <ref> <subject>" (hash is stable even as ref indices shift when a
147
+ // new entry is pushed on top), or null when the guard does not apply (cwd is
148
+ // not a git work tree, git is missing, or the call errors). Never throws.
149
+ function stashList(cwd) {
150
+ return new Promise((resolve) => {
151
+ if (!cwd) { resolve(null); return; }
152
+ execFile(
153
+ 'git',
154
+ ['-C', cwd, 'stash', 'list', '--format=%H %gd %gs'],
155
+ { timeout: 10_000, windowsHide: true },
156
+ (err, stdout) => {
157
+ if (err) { resolve(null); return; }
158
+ resolve(String(stdout || '').split('\n').filter(Boolean));
159
+ },
160
+ );
161
+ });
162
+ }
163
+
164
+ // Parse one `stashList()` line into { hash, ref, subject }. Pure, exported
165
+ // for unit testing. Returns null for a malformed line.
166
+ function parseStashLine(line) {
167
+ const m = /^(\S+)\s+(\S+)\s+(.*)$/.exec(String(line || ''));
168
+ return m ? { hash: m[1], ref: m[2], subject: m[3] } : null;
169
+ }
170
+
171
+ // Paths touched by any commit landed in cwd strictly between headBefore and
172
+ // headAfter. Returns [] when no commit landed (headBefore === headAfter, or
173
+ // either is missing) — used by the shared-tree guard below to tell a path
174
+ // the job legitimately committed apart from a path that just silently went
175
+ // quiet with nothing to explain it. Never throws.
176
+ function pathsChangedSince(cwd, headBefore, headAfter) {
177
+ return new Promise((resolve) => {
178
+ if (!cwd || !headBefore || !headAfter || headBefore === headAfter) { resolve([]); return; }
179
+ execFile(
180
+ 'git',
181
+ ['-C', cwd, 'diff', '--name-only', `${headBefore}..${headAfter}`],
182
+ { timeout: 10_000, windowsHide: true },
183
+ (err, stdout) => { resolve(err ? [] : String(stdout || '').split('\n').filter(Boolean)); },
184
+ );
185
+ });
186
+ }
187
+
188
+ // Restore ONE specific stash ref (never a blanket pop of "whatever is on
189
+ // top") into cwd: apply, then drop only on a clean apply. On conflict the
190
+ // entry is left in place — never dropped, never forced — so the operator's
191
+ // own `git stash pop`/`apply` still works afterward. Never throws.
192
+ function restoreSpecificStash(cwd, ref) {
193
+ return new Promise((resolve) => {
194
+ execFile('git', ['-C', cwd, 'stash', 'apply', ref], { timeout: 10_000, windowsHide: true }, (applyErr, _stdout, applyStderr) => {
195
+ if (applyErr) {
196
+ resolve({ ok: false, error: String(applyStderr || applyErr.message || applyErr).trim().split('\n')[0] });
197
+ return;
198
+ }
199
+ execFile('git', ['-C', cwd, 'stash', 'drop', ref], { timeout: 10_000, windowsHide: true }, () => {
200
+ resolve({ ok: true });
201
+ });
202
+ });
203
+ });
204
+ }
205
+
206
+ // Diff a before/after `stashList()` pair plus a before/after dirty-path pair
207
+ // to find what an in-place job silently discarded from a tree it shares with
208
+ // something else (Incident: social-signals-trader 2026-09-01, a blanket
209
+ // `git stash` reverted a live operator config edit with no error anywhere).
210
+ // Two independent signals, either of which means the job discarded state it
211
+ // did not create:
212
+ // - newStashes: a stash entry now present that wasn't in the baseline —
213
+ // the job ran `git stash` itself.
214
+ // - reverted: a path that was dirty in the baseline, is clean now, and was
215
+ // not touched by any commit landed during the run — the job reset/
216
+ // checked-out over pre-existing uncommitted work without stashing it.
217
+ //
218
+ // A THIRD outcome is not a revert at all: an untracked path can drop out of
219
+ // `git status` because the run committed a `.gitignore` change that now
220
+ // matches it — the file is untouched on disk, just no longer visible to git
221
+ // (Incident: 2026-09-12, PRD 1181 added a bare `logs/` ignore pattern, eleven
222
+ // untracked `session-manager-operations/logs/*` paths vanished from status,
223
+ // and an otherwise-perfect run was parked in needs_review for a human who had
224
+ // nothing to decide). `dirtyBefore` entries therefore carry each path's
225
+ // porcelain status code (`{ code, path }`, from parsePorcelainEntries) so this
226
+ // function can tell "was untracked" apart from "was tracked-and-modified":
227
+ // - a TRACKED path (any code other than '??') leaving the dirty set always
228
+ // means its content was restored to HEAD — still `reverted`, even though
229
+ // the file still exists on disk, because for a tracked file "exists" is
230
+ // not the question; "matches what the human left uncommitted" is.
231
+ // - an UNTRACKED path ('??') leaving the dirty set is `reverted` only if it
232
+ // no longer exists on disk; if it still exists, it merely became ignored
233
+ // and is reported separately as `nowIgnored`.
234
+ // Plain path strings are still accepted in `dirtyBefore` for callers that
235
+ // have no status code (e.g. the stash-detection pass, which always passes an
236
+ // empty array) — an entry with no `code` is treated as tracked, matching the
237
+ // old behavior exactly.
238
+ //
239
+ // Pure/no I/O — status-code parsing and on-disk existence checks both happen
240
+ // at the call site (checkSharedTreeGuard); this function never stats the
241
+ // filesystem. Exported for unit testing.
242
+ function evaluateSharedTreeGuard({ stashBefore, stashAfter, dirtyBefore, dirtyAfter, pathsCommittedDuringRun, existsAfter }) {
243
+ const beforeHashes = new Set((stashBefore || []).map((l) => module.exports.parseStashLine(l)?.hash).filter(Boolean));
244
+ const newStashes = (stashAfter || [])
245
+ .map(parseStashLine)
246
+ .filter((e) => e && !beforeHashes.has(e.hash));
247
+ const dirtyAfterSet = new Set(dirtyAfter || []);
248
+ const committedSet = new Set(pathsCommittedDuringRun || []);
249
+ const existsSet = new Set(existsAfter || []);
250
+ const reverted = [];
251
+ const nowIgnored = [];
252
+ for (const entry of dirtyBefore || []) {
253
+ const p = typeof entry === 'string' ? entry : entry.path;
254
+ const code = typeof entry === 'string' ? undefined : entry.code;
255
+ if (dirtyAfterSet.has(p) || committedSet.has(p)) continue;
256
+ if (code === '??' && existsSet.has(p)) {
257
+ nowIgnored.push(p);
258
+ } else {
259
+ reverted.push(p);
260
+ }
261
+ }
262
+ return { newStashes, reverted, nowIgnored };
263
+ }
264
+
265
+ // Post-run shared-tree guard for an IN-PLACE job (worktree.ok === false —
266
+ // callers must gate on that; a worktree-isolated run's git state can never
267
+ // leak into guardCwd, so there is nothing here to check). Best-effort: never
268
+ // throws, never changes the job's exit code. Restores exactly one
269
+ // executor-created stash (never guesses when there are 2+); reports anything
270
+ // it can't safely resolve on the returned object so the caller can surface it
271
+ // on the job row instead of finishing silently green.
272
+ async function checkSharedTreeGuard({ cwd, stashBaseline, dirtyBaseline, headBefore, slug, landedCommit }) {
273
+ try {
274
+ const [stashAfter, headAfter] = await Promise.all([
275
+ module.exports.stashList(cwd),
276
+ module.exports.gitHead(cwd),
277
+ ]);
278
+ const pathsCommittedDuringRun = await module.exports.pathsChangedSince(cwd, headBefore, headAfter);
279
+ // First pass: which stashes are new. Decided before charging anything
280
+ // against dirtyBaseline — a path this run's own stash covers must not be
281
+ // judged "reverted" using dirty state captured before the restore below
282
+ // has had a chance to bring it back.
283
+ const { newStashes } = module.exports.evaluateSharedTreeGuard({
284
+ stashBefore: stashBaseline,
285
+ stashAfter,
286
+ dirtyBefore: [],
287
+ dirtyAfter: [],
288
+ pathsCommittedDuringRun,
289
+ });
290
+
291
+ const result = {};
292
+ if (newStashes.length === 1) {
293
+ const [entry] = newStashes;
294
+ const restore = await module.exports.restoreSpecificStash(cwd, entry.ref);
295
+ if (restore.ok) {
296
+ result.restoredStash = entry.ref;
297
+ console.log(`[scheduler] ${slug}: restored a stash the job created in the shared tree (${entry.ref})`);
298
+ } else {
299
+ result.restoreFailed = `${entry.ref}: ${restore.error || 'apply failed'}`;
300
+ console.error(`[scheduler] ${slug}: shared-tree guard could not restore ${entry.ref}: ${restore.error}`);
301
+ }
302
+ } else if (newStashes.length > 1) {
303
+ result.ambiguousStashes = newStashes.map((e) => e.ref);
304
+ console.error(`[scheduler] ${slug}: shared-tree guard found ${newStashes.length} stashes the job created — ambiguous, not auto-restoring (${result.ambiguousStashes.join(', ')})`);
305
+ }
306
+
307
+ // Second pass: recompute "reverted" against the tree's dirty state AFTER
308
+ // any restore attempt above, so a path that came back via a successfully
309
+ // restored stash is not ALSO reported as an unexplained revert (it was
310
+ // explained — by the stash this guard just restored).
311
+ const dirtyAfter = await module.exports.uncommittedChanges(cwd);
312
+ // Existence check for the "now ignored, not reverted" split (2026-09-12
313
+ // incident) — only untracked baseline entries need it; a tracked path
314
+ // leaving the dirty set is always a revert regardless of disk state (see
315
+ // evaluateSharedTreeGuard). Scoped to entries carrying a status code —
316
+ // plain path strings (no code) fall back to the old always-reverted path.
317
+ const untrackedBaselinePaths = (dirtyBaseline || [])
318
+ .filter((e) => e && typeof e === 'object' && e.code === '??')
319
+ .map((e) => e.path);
320
+ // A large untracked baseline (the 2026-09-12 incident's shared tree had
321
+ // ~240 such paths) makes this a lot of stat calls — fs.promises.access
322
+ // run concurrently instead of fs.existsSync run synchronously one at a
323
+ // time keeps this off the event loop instead of blocking every other
324
+ // in-flight scheduler/IPC task for the duration.
325
+ const existsChecks = await Promise.all(
326
+ untrackedBaselinePaths.map((p) => fsp.access(path.join(cwd, p)).then(() => true, () => false)),
327
+ );
328
+ const existsAfter = untrackedBaselinePaths.filter((_, i) => existsChecks[i]);
329
+ const { reverted, nowIgnored } = module.exports.evaluateSharedTreeGuard({
330
+ stashBefore: stashBaseline,
331
+ stashAfter,
332
+ dirtyBefore: dirtyBaseline,
333
+ dirtyAfter,
334
+ pathsCommittedDuringRun,
335
+ existsAfter,
336
+ });
337
+ // Ground truth outranks the baseline diff (2026-09-18, 1229-fo-03): the
338
+ // dirty baseline is invalidated by ANY later writer (a human commit that
339
+ // sweeps the same paths), so it can't prove a revert on its own. The
340
+ // job's own landedCommit still being an ancestor of HEAD proves its work
341
+ // was not discarded — anchored to that sha, not to the baseline.
342
+ const workSurvives = reverted.length > 0
343
+ && await module.exports.landedCommitIsAncestorOfHead(cwd, landedCommit);
344
+ if (workSurvives) {
345
+ console.log(`[scheduler] ${slug}: shared-tree guard: ${reverted.length} baseline path(s) went clean but landed commit ${String(landedCommit).slice(0, 7)} is still an ancestor of HEAD — not a revert`);
346
+ } else if (reverted.length) {
347
+ result.reverted = reverted;
348
+ console.error(`[scheduler] ${slug}: shared-tree guard: ${reverted.length} path(s) reverted in the shared tree with no commit to explain it (${reverted.slice(0, 3).join(', ')})`);
349
+ }
350
+ if (nowIgnored.length) {
351
+ result.nowIgnored = nowIgnored;
352
+ console.log(`[scheduler] ${slug}: shared-tree guard: ${nowIgnored.length} path(s) no longer shown by git status but still present on disk — likely a new ignore rule, not a revert (${nowIgnored.slice(0, 3).join(', ')})`);
353
+ }
354
+ return (result.restoredStash || result.restoreFailed || result.ambiguousStashes || result.reverted || result.nowIgnored) ? result : null;
355
+ } catch (e) {
356
+ console.error(`[scheduler] ${slug}: shared-tree guard error`, e);
357
+ return null;
358
+ }
359
+ }
360
+
361
+ // True when cwd is inside a git repository. Used to keep a non-git cwd (e.g.
362
+ // a scratch dir like /tmp) from ever being handed to an investigation's
363
+ // fix-plan as its cwd — the commit guard, worktree isolation, and
364
+ // runVerify's PASS+commit sentinel override all read git state from job.cwd,
365
+ // so a fix plan rooted at a non-repo path gets parked in needs_review even
366
+ // when its commit really landed (Incident: 99-fix-e2e-needs-review-test,
367
+ // 2026-08-07 — green, committed 6ecbd40, PASS sentinel, still needs_review).
368
+ // Bounded and never throws.
369
+ function isGitRepoSync(cwd) {
370
+ if (!cwd) return false;
371
+ try {
372
+ execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
373
+ timeout: 10_000,
374
+ windowsHide: true,
375
+ stdio: ['ignore', 'pipe', 'pipe'],
376
+ });
377
+ return true;
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
382
+
383
+ // Best-effort `git fetch --all --prune` in cwd, bounded and never throwing.
384
+ // A PRD that does its work in a separate `git worktree add` checkout (per
385
+ // standards.md's own recommended pattern for shared repos) commits and pushes
386
+ // from THAT worktree, then removes it — the commit never touches job.cwd's
387
+ // own refs, so job.cwd's remote-tracking branches can be stale relative to
388
+ // what was actually pushed. Refreshing them here is what lets the git log
389
+ // --all scan below see a worktree-pushed commit. Resolves once fetch settles
390
+ // (or times out / errors) — callers don't need the result, just the refresh.
391
+ function fetchAllRefs(cwd) {
392
+ return new Promise((resolve) => {
393
+ if (!cwd) { resolve(); return; }
394
+ execFile(
395
+ 'git',
396
+ ['-C', cwd, 'fetch', '--all', '--prune'],
397
+ { timeout: 20_000, windowsHide: true },
398
+ () => resolve(),
399
+ );
400
+ });
401
+ }
402
+
403
+ // Returns true if ≥1 commit landed on any ref (branch, remote-tracking branch,
404
+ // or tag) in cwd between startedAt and finishedAt (with 60s slack) — not just
405
+ // the currently checked-out branch. Used both by the self-heal pass and by the
406
+ // live commit-guard's fallback (see computeCommittedDuringRun) to derive
407
+ // committedDuringRun from the recorded run window. Fetches remotes first (see
408
+ // fetchAllRefs) so a commit pushed from a separate worktree checkout that was
409
+ // since removed is still visible via job.cwd's remote-tracking refs. Never
410
+ // throws; git-unavailable → false (no override, job stays as-is).
411
+ async function committedInWindow(cwd, startedAt, finishedAt) {
412
+ if (!cwd || !startedAt) return false;
413
+ await module.exports.fetchAllRefs(cwd);
414
+ return new Promise((resolve) => {
415
+ const until = finishedAt
416
+ ? new Date(Date.parse(finishedAt) + 60_000).toISOString()
417
+ : new Date().toISOString();
418
+ execFile(
419
+ 'git',
420
+ ['-C', cwd, 'log', '--all', '--format=%H', `--since=${startedAt}`, `--until=${until}`],
421
+ { timeout: 10_000, windowsHide: true },
422
+ (err, stdout) => { resolve(!err && String(stdout || '').trim().length > 0); },
423
+ );
424
+ });
425
+ }
426
+
427
+ // Live commit-guard: cheap HEAD-diff fast path, falling back to the
428
+ // git-log --all scan when HEAD didn't move on the starting branch. A PRD that
429
+ // checks out other branches, commits real work on each, then checks its
430
+ // starting branch back out before exit leaves HEAD unchanged even though
431
+ // commits landed — the fallback catches that case. Never throws.
432
+ //
433
+ // Both signals are known to race against ref/object visibility at the exact
434
+ // moment of process exit — a job that commits in a throwaway linked worktree
435
+ // and removes it before exiting can have committedInWindow() return false
436
+ // even though the commit is real and already pushed (confirmed incidents:
437
+ // pass-no-commit-worktree-commit-invisible-at-exit, RCA 770-pr269). When both
438
+ // signals say "no commit", wait a short bounded delay and retry once before
439
+ // giving up — a replayed identical call moments later reliably finds it.
440
+ const COMMIT_GUARD_RETRY_DELAY_MS = 2000;
441
+
442
+ async function computeCommittedDuringRun(cwd, headBefore, headAfter, startedAt, untilIso) {
443
+ if (headBefore && headAfter && headBefore !== headAfter) return true;
444
+ // Call via module.exports (not the bare local binding) so tests can
445
+ // vi.spyOn(gitTreeGuard, 'committedInWindow') to drive the retry deterministically.
446
+ if (await module.exports.committedInWindow(cwd, startedAt, untilIso)) return true;
447
+ await new Promise((resolve) => { setTimeout(resolve, COMMIT_GUARD_RETRY_DELAY_MS); });
448
+ return module.exports.committedInWindow(cwd, startedAt, untilIso);
449
+ }
450
+
451
+ /**
452
+ * Which commit does this run get credit for? A worktree-isolated run shares the
453
+ * main checkout with concurrent jobs, so a HEAD move there may be someone else's
454
+ * integration — credit comes only from this run's own integration result. An
455
+ * in-place run keeps the HEAD-delta / time-window behaviour.
456
+ */
457
+ function resolveRunCommitAttribution({
458
+ ranInWorktree, integratedSha, headBefore, headAtExit, committedInWindow,
459
+ rangeCommits, transcriptCommitShas, siblingOverlap,
460
+ }) {
461
+ if (ranInWorktree) {
462
+ const landedCommit = integratedSha || null;
463
+ return { landedCommit, committedDuringRun: Boolean(landedCommit) };
464
+ }
465
+ const shas = Array.isArray(transcriptCommitShas) ? transcriptCommitShas.filter(Boolean) : [];
466
+ if (shas.length === 0 && !siblingOverlap) {
467
+ const headMoved = Boolean(headBefore && headAtExit && headBefore !== headAtExit);
468
+ return {
469
+ landedCommit: headMoved ? headAtExit : null,
470
+ committedDuringRun: headMoved || Boolean(committedInWindow),
471
+ };
472
+ }
473
+ // Shared tree with possible siblings (or transcript evidence): credit only a
474
+ // commit in the range that this run's own transcript reported.
475
+ const own = (Array.isArray(rangeCommits) ? rangeCommits : [])
476
+ .find((c) => shas.some((sha) => c.startsWith(sha)));
477
+ const landedCommit = own || null;
478
+ return { landedCommit, committedDuringRun: Boolean(landedCommit) };
479
+ }
480
+
481
+ /** Full shas in `before..after` (newest first); [] on any error. Never throws. */
482
+ function gitRangeCommits(cwd, before, after) {
483
+ return new Promise((resolve) => {
484
+ if (!cwd || !before || !after || before === after) { resolve([]); return; }
485
+ execFile(
486
+ 'git', ['-C', cwd, 'rev-list', `${before}..${after}`],
487
+ { timeout: 10_000, windowsHide: true },
488
+ (err, out) => {
489
+ if (err) { resolve([]); return; }
490
+ resolve(String(out || '').split('\n').map((l) => l.trim()).filter(Boolean));
491
+ },
492
+ );
493
+ });
494
+ }
495
+
496
+ /**
497
+ * Read-only check: is a job's `sm-job/<slug>` worktree branch already fully
498
+ * integrated into `cwd`'s current HEAD — i.e. every commit on the branch is
499
+ * an ancestor of (or equal to) HEAD? Mirrors gitWorktree.cjs's own
500
+ * integrateBranch() no-op detection (`mergeBase === branchHead`) exactly, but
501
+ * deliberately never calls integrateBranch itself: this is used by
502
+ * reapDeadRunningJobs to PROVE a dead job's work already landed, never to
503
+ * perform the landing — attempting a real merge from an audit check is a
504
+ * mutating action with its own conflict/timing risk, and "land the still-
505
+ * stranded branch" is explicitly a separate, later decision (mechanical
506
+ * recovery, or a human), not something a reap pass should do on its own
507
+ * initiative. See reapDeadRunningJobs's own header comment for why the
508
+ * conservative failure direction here is needs_review, never a silent merge.
509
+ *
510
+ * Returns:
511
+ * - true — branch exists and is fully contained in HEAD (already
512
+ * integrated by a prior pass, or a genuine no-op branch that never
513
+ * diverged from its base — both cases legitimately "landed").
514
+ * - false — branch exists and still holds commits not in HEAD (a real
515
+ * stranded deliverable, e.g. the PRD 1118 shape).
516
+ * - null — branch does not exist at all (never a worktree run — an
517
+ * in-place run, or SM_JOB_WORKTREE_DISABLE), or the git calls errored.
518
+ * Never throws.
519
+ */
520
+ function isBranchAlreadyIntegrated(cwd, branch) {
521
+ return new Promise((resolve) => {
522
+ if (!cwd || !branch) { resolve(null); return; }
523
+ execFile(
524
+ 'git', ['-C', cwd, 'rev-parse', '--verify', branch],
525
+ { timeout: 10_000, windowsHide: true },
526
+ (err, branchHeadOut) => {
527
+ if (err) { resolve(null); return; } // branch doesn't exist — not a worktree run
528
+ const branchHead = String(branchHeadOut || '').trim();
529
+ execFile(
530
+ 'git', ['-C', cwd, 'merge-base', 'HEAD', branch],
531
+ { timeout: 10_000, windowsHide: true },
532
+ (mbErr, mbOut) => {
533
+ const mergeBase = mbErr ? '' : String(mbOut || '').trim();
534
+ resolve(mergeBase !== '' && mergeBase === branchHead);
535
+ },
536
+ );
537
+ },
538
+ );
539
+ });
540
+ }
541
+
542
+ /**
543
+ * Override for a SIGTERM'd (143) run when a commit landed in its window.
544
+ * Exit 143 alone doesn't prove the deliverable is missing — the 776/779
545
+ * incidents (2026-07-30) both died on a headless-incompatible interactive
546
+ * step (xvfb Electron screenshot / playwright electron.launch) AFTER their
547
+ * commit had already landed, and sat `failed` forever with a real deliverable
548
+ * on disk. A landed commit doesn't prove every AC line passed though (the
549
+ * step it died on is still unverified), so this routes to `needs_review`,
550
+ * never silently promotes to `completed`. Returns null for every other exit
551
+ * code, or for a 143 with no commit found in window — those fall through to
552
+ * the existing `failed` path unchanged. Pure/no I/O: the commit-window scan
553
+ * (committedInWindow) happens at the call site, not here.
554
+ */
555
+ function classifySigtermWithCommit(exitCode, commitFoundInWindow) {
556
+ if (exitCode !== 143 || !commitFoundInWindow) return null;
557
+ return {
558
+ status: 'needs_review',
559
+ reason: 'SIGTERM after a commit landed — verify AC before treating as done',
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Pure decision for spawnJob's finalize mutate (2026-09-06 incident): given
565
+ * the row this run is trying to finalize (or its absence) and this run's own
566
+ * identity, decide whether the finalize must be DROPPED instead of applied.
567
+ * Never mutates anything itself — callers apply `stampLandedCommit` and emit
568
+ * the audit event/log line.
569
+ *
570
+ * - row missing entirely (i2 === -1 upstream): always drop, nothing to
571
+ * stamp — there is no row left to carry the fact.
572
+ * - row present but not 'running': drop the STATUS change (a cancelled row,
573
+ * PRD 1024, must never be re-legalized to completed/needs_review by a
574
+ * stale exit handler) but still let a genuinely-landed commit be stamped
575
+ * as a fact — never a transition — when this run owns the row's current
576
+ * runId, or the row doesn't already carry a newer one of its own.
577
+ * - row present and 'running': not a drop; caller proceeds to finalize.
578
+ */
579
+ function evaluateFinalizeDrop({ rowExists, rowStatus, rowRunId, rowLandedCommit, runId, landedCommit }) {
580
+ if (!rowExists) {
581
+ return { drop: true, reason: 'row-missing', stampLandedCommit: null };
582
+ }
583
+ if (rowStatus !== 'running') {
584
+ const stampLandedCommit = landedCommit && (rowRunId === runId || !rowLandedCommit) ? landedCommit : null;
585
+ return { drop: true, reason: 'row-not-running', stampLandedCommit };
586
+ }
587
+ return { drop: false, reason: null, stampLandedCommit: null };
588
+ }
589
+
590
+ /**
591
+ * Pure decision for spawnJob's dispatch mutate: given a PENDING row about to
592
+ * be dispatched and the newest terminalRunOutcome sidecar result for its
593
+ * slug, decide whether this dispatch should be SKIPPED because a prior run
594
+ * already completed this exact slug — the gap left by reconcile()'s own
595
+ * anti-resurrection guard, which only ever sees a slug BEFORE it first lands
596
+ * in s.jobs (2026-09-06 incident: an existing pending row was re-dispatched
597
+ * three times against already-shipped work).
598
+ *
599
+ * Compares the sidecar's finishedAt against THIS row's own last pending
600
+ * transition (or queuedAt, when the row has never been reset) rather than
601
+ * merely "a completed sidecar exists somewhere" — so a deliberate human
602
+ * re-queue of the same slug in a LATER episode (whose queuedAt/pending
603
+ * transition postdates the earlier completion) still runs.
604
+ */
605
+ function evaluateDispatchSidecarReconcile({ rowStatus, rowRunId, statusHistory, queuedAt, outcome }) {
606
+ if (rowStatus !== 'pending') return { skip: false };
607
+ if (!outcome || outcome.status !== 'completed') return { skip: false };
608
+ if (outcome.runId === rowRunId) return { skip: false };
609
+ const lastPendingEntry = Array.isArray(statusHistory)
610
+ ? [...statusHistory].reverse().find((h) => h.to === 'pending')
611
+ : null;
612
+ const lastPendingAt = lastPendingEntry?.at ?? queuedAt ?? null;
613
+ if (lastPendingAt && (!outcome.finishedAt || outcome.finishedAt < lastPendingAt)) {
614
+ return { skip: false };
615
+ }
616
+ return { skip: true, runId: outcome.runId, finishedAt: outcome.finishedAt };
617
+ }
618
+
619
+ /**
620
+ * Pure detector for mutate()'s write-path regression check: transitionJob
621
+ * only ever APPENDS to statusHistory (capped at STATUS_HISTORY_CAP, never
622
+ * shrunk below it once reached) — so a running->pending change whose
623
+ * statusHistory got SHORTER is not a real transition, it's evidence this
624
+ * row's prior state was lost (e.g. a finalize mutate working off a stale
625
+ * in-memory snapshot).
626
+ */
627
+ function isQueueRowRegression({ statusBefore, statusAfter, historyLenBefore, historyLenAfter }) {
628
+ return statusBefore === 'running' && statusAfter === 'pending' && historyLenAfter < historyLenBefore;
629
+ }
630
+
631
+ /**
632
+ * True when `sha` is a non-empty commit that is an ancestor of (or equal to)
633
+ * HEAD in the repo at `cwd` (`git merge-base --is-ancestor`). Bounded, never
634
+ * throws: an empty sha, an unknown sha, or any git failure is `false`, so the
635
+ * caller's safe default is "cannot prove the work survived".
636
+ */
637
+ async function landedCommitIsAncestorOfHead(cwd, sha) {
638
+ if (!sha || typeof sha !== 'string' || !cwd) return false;
639
+ try {
640
+ await sharedGitExec.execGit(resolveProjectRoot(cwd), ['merge-base', '--is-ancestor', sha, 'HEAD'], { timeout: 10_000 });
641
+ return true;
642
+ } catch {
643
+ return false;
644
+ }
645
+ }
646
+
647
+ module.exports = {
648
+ splitRenamePorcelainField,
649
+ parsePorcelainEntries,
650
+ parsePorcelain,
651
+ uncommittedChangesWithStatus,
652
+ uncommittedChanges,
653
+ gitHead,
654
+ stashList,
655
+ parseStashLine,
656
+ pathsChangedSince,
657
+ restoreSpecificStash,
658
+ evaluateSharedTreeGuard,
659
+ checkSharedTreeGuard,
660
+ isGitRepoSync,
661
+ fetchAllRefs,
662
+ committedInWindow,
663
+ computeCommittedDuringRun,
664
+ resolveRunCommitAttribution,
665
+ gitRangeCommits,
666
+ isBranchAlreadyIntegrated,
667
+ classifySigtermWithCommit,
668
+ evaluateFinalizeDrop,
669
+ evaluateDispatchSidecarReconcile,
670
+ isQueueRowRegression,
671
+ landedCommitIsAncestorOfHead,
672
+ };