claude-code-session-manager 0.97.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.cjs +16 -4
- package/bin/node-floor.cjs +41 -0
- package/dist/assets/{DataModel-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +26 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -19
- package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
- package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
- package/scripts/README.md +3 -11
- package/scripts/audit-ops-hygiene.cjs +3 -3
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -3
- package/scripts/scheduler-mcp-server.cjs +36 -7
- package/src/main/agentLibrary.cjs +22 -2
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +41 -38
- package/src/main/docEdit.cjs +4 -1
- package/src/main/files.cjs +2 -5
- package/src/main/health.cjs +1 -1
- package/src/main/index.cjs +23 -10
- package/src/main/ipcSchemas.cjs +28 -35
- package/src/main/lib/agentPersonaSchema.cjs +13 -2
- package/src/main/lib/atomicFs.cjs +116 -0
- package/src/main/lib/branchSweep.cjs +13 -12
- package/src/main/lib/buildTarget.cjs +3 -3
- package/src/main/lib/claudeCliCaps.cjs +129 -0
- package/src/main/lib/credentials.cjs +2 -4
- package/src/main/lib/crossProjectFeedback.cjs +3 -3
- package/src/main/lib/cwdClassify.cjs +15 -3
- package/src/main/lib/definitionOfDone.cjs +207 -125
- package/src/main/lib/dodDrainHook.cjs +11 -5
- package/src/main/lib/epicMint.cjs +2 -4
- package/src/main/lib/epicStatusMirror.cjs +8 -10
- package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
- package/src/main/lib/gateAuthority.cjs +96 -0
- package/src/main/lib/gitExec.cjs +69 -0
- package/src/main/lib/gitWorktree.cjs +656 -92
- package/src/main/lib/instanceLock.cjs +5 -8
- package/src/main/lib/launchFailure.cjs +2 -3
- package/src/main/lib/macroLibrary.cjs +386 -0
- package/src/main/lib/mcpToolCatalog.cjs +26 -17
- package/src/main/lib/needsReviewLedger.cjs +1 -1
- package/src/main/lib/opsErrorLog.cjs +11 -1
- package/src/main/lib/opsOwnership.cjs +0 -5
- package/src/main/lib/pidAlive.cjs +32 -0
- package/src/main/lib/prdCreate.cjs +153 -19
- package/src/main/lib/prdDisposition.cjs +2 -2
- package/src/main/lib/prdGateFiles.cjs +284 -0
- package/src/main/lib/prdLocations.cjs +61 -0
- package/src/main/lib/prdMigration.cjs +2 -3
- package/src/main/lib/prdSizing.cjs +2 -1
- package/src/main/lib/promptSessionSchema.cjs +10 -2
- package/src/main/lib/rcaReport.cjs +4 -6
- package/src/main/lib/reaperHelpers.cjs +8 -1
- package/src/main/lib/reservationExpiry.cjs +6 -1
- package/src/main/lib/reviewNotice.cjs +263 -0
- package/src/main/lib/runClaudeP.cjs +4 -1
- package/src/main/lib/schedulerPaths.cjs +22 -2
- package/src/main/lib/sessionSlots.cjs +2 -4
- package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
- package/src/main/lib/timeoutShim.cjs +123 -0
- package/src/main/lib/timeoutShimScript.cjs +275 -0
- package/src/main/lib/upgradeDrain.cjs +4 -10
- package/src/main/lib/watchdogHelpers.cjs +33 -30
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/queueOps.cjs +17 -6
- package/src/main/scheduler.cjs +975 -203
- package/src/main/seedAgentPersonas.cjs +278 -23
- package/src/main/supervisor.cjs +4 -1
- package/src/main/templates/PRD_AUTHORING.md +126 -406
- package/src/preload/__tests__/preload-surface.test.cjs +68 -0
- package/src/preload/api.d.ts +41 -94
- package/src/preload/index.cjs +14 -9
- package/src/seed/agents/architect.md +1 -0
- package/src/seed/agents/dev-lead.md +17 -18
- package/src/seed/agents/project-home-builder.md +1 -0
- package/src/seed/agents/validator.md +10 -4
- package/dist/assets/HostBilko-nESrTRIg.js +0 -1
- package/dist/assets/Scheduler-BVYTeh39.js +0 -16
- package/dist/assets/Settings-DIhGgdfs.js +0 -3
- package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
- package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
- package/src/main/__tests__/agentLibrary.test.cjs +0 -276
- package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
- package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
- package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
- package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
- package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
- package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
- package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
- package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
- package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
- package/src/main/__tests__/chat-queue.test.cjs +0 -97
- package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
- package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
- package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
- package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
- package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
- package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
- package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
- package/src/main/__tests__/docEdit.test.cjs +0 -244
- package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
- package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
- package/src/main/__tests__/dod-report.test.cjs +0 -304
- package/src/main/__tests__/dod-reverify.test.cjs +0 -285
- package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
- package/src/main/__tests__/epicMint.test.cjs +0 -332
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
- package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
- package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
- package/src/main/__tests__/exchanges.test.cjs +0 -122
- package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
- package/src/main/__tests__/extractJson.test.cjs +0 -51
- package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
- package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
- package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
- package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
- package/src/main/__tests__/health-credentials.test.cjs +0 -81
- package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
- package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
- package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
- package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
- package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
- package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
- package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
- package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
- package/src/main/__tests__/historyDashboard.test.cjs +0 -163
- package/src/main/__tests__/historyRollup.test.cjs +0 -333
- package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
- package/src/main/__tests__/kg-augment.test.cjs +0 -195
- package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
- package/src/main/__tests__/machineProfile.test.cjs +0 -152
- package/src/main/__tests__/mcpStatus.test.cjs +0 -61
- package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
- package/src/main/__tests__/memoryStale.test.cjs +0 -88
- package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
- package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
- package/src/main/__tests__/personaMerge.test.cjs +0 -169
- package/src/main/__tests__/planValidator.test.cjs +0 -125
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
- package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
- package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
- package/src/main/__tests__/prdAgentType.test.cjs +0 -103
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
- package/src/main/__tests__/prdCreate.test.cjs +0 -979
- package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
- package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
- package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
- package/src/main/__tests__/prdLocations.test.cjs +0 -195
- package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
- package/src/main/__tests__/prdMigration.test.cjs +0 -349
- package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
- package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
- package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
- package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
- package/src/main/__tests__/prdSizing.test.cjs +0 -106
- package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
- package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
- package/src/main/__tests__/proc-role-env.test.cjs +0 -125
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
- package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
- package/src/main/__tests__/projectPages.test.cjs +0 -137
- package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
- package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
- package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
- package/src/main/__tests__/pty-write-result.test.cjs +0 -46
- package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
- package/src/main/__tests__/queueHistory.test.cjs +0 -355
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
- package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
- package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -154
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
- package/src/main/__tests__/rcaReport.test.cjs +0 -266
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
- package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
- package/src/main/__tests__/runLogRetention.test.cjs +0 -489
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
- package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
- package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
- package/src/main/__tests__/runVerify.test.cjs +0 -1784
- package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
- package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
- package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
- package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
- package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
- package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
- package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
- package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
- package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
- package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
- package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
- package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
- package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
- package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
- package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
- package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
- package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
- package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
- package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
- package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
- package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
- package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
- package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
- package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
- package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
- package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
- package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
- package/src/main/__tests__/seedStatus.test.cjs +0 -100
- package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
- package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
- package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
- package/src/main/__tests__/telemetryContract.test.cjs +0 -930
- package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
- package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
- package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
- package/src/main/__tests__/validationSentinels.test.cjs +0 -84
- package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
- package/src/main/bilkoHost.cjs +0 -314
- package/src/main/bilkoHostCore.cjs +0 -89
- package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
- package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
- package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
- package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
- package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
- package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
- package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
- package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
- package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
- package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
- package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
- package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
- package/src/main/lib/__tests__/gateFixtures.json +0 -20
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
- package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
- package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
- package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
- package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
- package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
- package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
- package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
- package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
- package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
- package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
- package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
- package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
- package/src/main/lib/__tests__/procName.test.cjs +0 -92
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
- package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
- package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
- package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
- package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
- package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
- package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
- package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -375
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- 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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
1178
|
-
* `
|
|
1179
|
-
*
|
|
1180
|
-
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
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
|
|
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 !==
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1308
|
+
if (currentBranch !== expectedBranch) {
|
|
1309
|
+
const actualBranch = currentBranch === null ? 'detached HEAD' : currentBranch;
|
|
1310
|
+
const strayCheckoutFailure = {
|
|
1194
1311
|
ok: false,
|
|
1195
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
|
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
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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,
|