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.
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/assets/{AgentLibrary-Cm6MrOnF.js → AgentLibrary-RwPvEgqm.js} +2 -2
- package/dist/assets/{Button-CGVncJ6a.js → Button-36j3PzYv.js} +1 -1
- package/dist/assets/{DataModel-C3hlMoa8.js → DataModel-DUhANo4l.js} +1 -1
- package/dist/assets/{History-CTyfGNKn.js → History-BeZzNjmv.js} +2 -2
- package/dist/assets/{Hooks-BXxCBibx.js → Hooks-iQ8845Hp.js} +3 -3
- package/dist/assets/{Library-aCs7zxAg.js → Library-DPE1h-1L.js} +6 -6
- package/dist/assets/{ListDetail-B9Hqk1_f.js → ListDetail-PStOuQAo.js} +1 -1
- package/dist/assets/{MarkdownEditor-Djfgp32k.js → MarkdownEditor-DPw-N2-m.js} +1 -1
- package/dist/assets/{McpServers-8HQC6QgR.js → McpServers-CUyRTNep.js} +2 -2
- package/dist/assets/{Memory-c4SVE3l2.js → Memory-bZ2xjBGy.js} +3 -3
- package/dist/assets/{Panel-BEQTwE4z.js → Panel-_VZ_uXNg.js} +1 -1
- package/dist/assets/{Permissions-BaYzgZtc.js → Permissions-PPRmfNbK.js} +3 -3
- package/dist/assets/{Plugins-DnXsVau7.js → Plugins-BFcvfvlx.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CT2vWps1.js → ProvenanceBadge-B1WUknak.js} +1 -1
- package/dist/assets/RemoteMachines-D5V0lzgW.js +3 -0
- package/dist/assets/{SaveBar-DwcHt3iR.js → SaveBar-Dw64FVdM.js} +1 -1
- package/dist/assets/{Scheduler-D6Gcuo0M.js → Scheduler-C8s_pJ3I.js} +9 -9
- package/dist/assets/{ScopeSwitcher-CgvWDc9Y.js → ScopeSwitcher-D-nKDMAE.js} +1 -1
- package/dist/assets/{Settings-CJoogVAB.js → Settings-CQSrR4mO.js} +4 -4
- package/dist/assets/{SkillReferenceGraph-4fq01kxq.js → SkillReferenceGraph-BbZFMHim.js} +1 -1
- package/dist/assets/{Skills-BU4ZX4h4.js → Skills-Bzyrh6EQ.js} +1 -1
- package/dist/assets/SystemPrompt-DP6i3Dx0.js +1 -0
- package/dist/assets/TagLibrary-iip0C6Bl.js +1 -0
- package/dist/assets/{TiptapBody-DVyBsfsT.js → TiptapBody-Cr-hxX8L.js} +1 -1
- package/dist/assets/Toggle-7WpPzcZH.js +1 -0
- package/dist/assets/{ViewTabs-BLHjKUv-.js → ViewTabs-9lQCRwWJ.js} +1 -1
- package/dist/assets/{historyTotals-BXDd3EzP.js → historyTotals-BrFxw5J1.js} +1 -1
- package/dist/assets/{index-CuiU3I-8.js → index-CTBpf-26.js} +250 -249
- package/dist/assets/{settingsSchema-DD4bc5cT.js → settingsSchema-Dh5Kl7VO.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-DMQ_XZtr.js → useAgentsDirWatch-CMpx0d04.js} +1 -1
- package/dist/assets/{useRemotePeers-C9NfteSD.js → useRemotePeers-CI2axd-L.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +3 -2
- package/plugins/CLAUDE.md +5 -8
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -6
- package/plugins/session-manager-dev/skills/builder/1-classify-and-bump/SKILL.md +2 -2
- package/plugins/session-manager-dev/skills/builder/SKILL.md +2 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/develop/standards.md +3 -3
- package/scripts/README.md +3 -3
- package/scripts/audit-ops-hygiene.cjs +4 -4
- package/scripts/hooks/guard-destructive-git.cjs +4 -4
- package/scripts/hooks/guard-inline-implementation.cjs +1 -1
- package/scripts/hooks/guard-prd-writes.cjs +2 -2
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -2
- package/scripts/scheduler-mcp-server.cjs +4 -4
- package/src/main/agentLibrary.cjs +5 -5
- package/src/main/agentsIpc.cjs +76 -0
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +12 -12
- package/src/main/config.cjs +2 -2
- package/src/main/crashDiagnostics.cjs +2 -2
- package/src/main/docEdit.cjs +5 -5
- package/src/main/files.cjs +2 -2
- package/src/main/git.cjs +1 -1
- package/src/main/health.cjs +20 -18
- package/src/main/heapSnapshot.cjs +1 -1
- package/src/main/historyAggregator.cjs +2 -2
- package/src/main/index.cjs +44 -132
- package/src/main/ipcSchemas.cjs +10 -10
- package/src/main/lib/CLAUDE.md +48 -0
- package/src/main/lib/activeSessions.cjs +1 -1
- package/src/main/lib/classifyPromptTicket.cjs +2 -2
- package/src/main/lib/{agentModelResolve.cjs → claude/agentModelResolve.cjs} +7 -7
- package/src/main/lib/{claudeBin.cjs → claude/claudeBin.cjs} +4 -4
- package/src/main/lib/{effectiveModelInfo.cjs → claude/effectiveModelInfo.cjs} +9 -9
- package/src/main/lib/{modelCatalog.cjs → claude/modelCatalog.cjs} +3 -3
- package/src/main/lib/{rateLimitDetect.cjs → claude/rateLimitDetect.cjs} +1 -1
- package/src/main/lib/{rateLimitWindow.cjs → claude/rateLimitWindow.cjs} +1 -1
- package/src/main/lib/{runClaudeP.cjs → claude/runClaudeP.cjs} +1 -1
- package/src/main/lib/credentials.cjs +3 -3
- package/src/main/lib/cwdClassify.cjs +1 -1
- package/src/main/lib/{activeIndexMerge.cjs → epic/activeIndexMerge.cjs} +3 -3
- package/src/main/lib/{activeIndexRebuild.cjs → epic/activeIndexRebuild.cjs} +1 -1
- package/src/main/lib/{delegationReadiness.cjs → epic/delegationReadiness.cjs} +21 -29
- package/src/main/lib/{epicContextDigest.cjs → epic/epicContextDigest.cjs} +1 -1
- package/src/main/lib/{epicDelegationStats.cjs → epic/epicDelegationStats.cjs} +2 -2
- package/src/main/lib/{epicMint.cjs → epic/epicMint.cjs} +11 -11
- package/src/main/lib/{epicSpawnPlan.cjs → epic/epicSpawnPlan.cjs} +3 -3
- package/src/main/lib/{epicStatusMirror.cjs → epic/epicStatusMirror.cjs} +2 -2
- package/src/main/lib/{epicTranscriptDiagnostic.cjs → epic/epicTranscriptDiagnostic.cjs} +2 -2
- package/src/main/lib/{epicTranscriptPath.cjs → epic/epicTranscriptPath.cjs} +2 -2
- package/src/main/lib/{epicValidationHook.cjs → epic/epicValidationHook.cjs} +3 -3
- package/src/main/lib/{epicWorktreeBoot.cjs → epic/epicWorktreeBoot.cjs} +1 -1
- package/src/main/lib/{epicWorktreeMerge.cjs → epic/epicWorktreeMerge.cjs} +3 -3
- package/src/main/lib/{epicWorktreeMint.cjs → epic/epicWorktreeMint.cjs} +3 -3
- package/src/main/lib/{epicWorktreeProjectConfig.cjs → epic/epicWorktreeProjectConfig.cjs} +4 -4
- package/src/main/lib/{promptSessionsCreateEpic.cjs → epic/promptSessionsCreateEpic.cjs} +4 -4
- package/src/main/lib/{crossProjectFeedback.cjs → feedback-remote/crossProjectFeedback.cjs} +7 -7
- package/src/main/lib/{customerFeedbackClient.cjs → feedback-remote/customerFeedbackClient.cjs} +3 -3
- package/src/main/lib/{customerFeedbackInbox.cjs → feedback-remote/customerFeedbackInbox.cjs} +2 -2
- package/src/main/lib/{customerFeedbackIpc.cjs → feedback-remote/customerFeedbackIpc.cjs} +1 -1
- package/src/main/lib/{localAdminHttp.cjs → feedback-remote/localAdminHttp.cjs} +2 -2
- package/src/main/lib/{macroAdminRoutes.cjs → feedback-remote/macroAdminRoutes.cjs} +1 -1
- package/src/main/lib/{projectHomeAdminRoutes.cjs → feedback-remote/projectHomeAdminRoutes.cjs} +4 -4
- package/src/main/lib/{relayClientAuth.cjs → feedback-remote/relayClientAuth.cjs} +2 -2
- package/src/main/lib/{remoteClientSessions.cjs → feedback-remote/remoteClientSessions.cjs} +1 -1
- package/src/main/lib/{gitWorktree.cjs → git/gitWorktree.cjs} +7 -7
- package/src/main/lib/historyRollup.cjs +1 -1
- package/src/main/lib/importReferences.cjs +1 -1
- package/src/main/lib/machineProfile.cjs +1 -1
- package/src/main/lib/openExternalApp.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +3 -3
- package/src/main/lib/{agentPersonaSchema.cjs → persona-macro/agentPersonaSchema.cjs} +2 -2
- package/src/main/lib/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +2 -2
- package/src/main/lib/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
- package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
- package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.cjs} +11 -0
- package/src/main/lib/{prdAdminRoutes.cjs → prd/prdAdminRoutes.cjs} +7 -7
- package/src/main/lib/{prdAgentType.cjs → prd/prdAgentType.cjs} +3 -3
- package/src/main/lib/{prdCreate.cjs → prd/prdCreate.cjs} +12 -12
- package/src/main/lib/{prdDeclaredPaths.cjs → prd/prdDeclaredPaths.cjs} +1 -1
- package/src/main/lib/{prdGateFiles.cjs → prd/prdGateFiles.cjs} +1 -1
- package/src/main/lib/{prdLocations.cjs → prd/prdLocations.cjs} +2 -2
- package/src/main/lib/{prdMigration.cjs → prd/prdMigration.cjs} +3 -3
- package/src/main/lib/prereqs.cjs +2 -2
- package/src/main/lib/{instanceLock.cjs → process/instanceLock.cjs} +4 -11
- package/src/main/lib/{pidAlive.cjs → process/pidAlive.cjs} +1 -1
- package/src/main/lib/{procName.cjs → process/procName.cjs} +2 -2
- package/src/main/lib/{timeoutShim.cjs → process/timeoutShim.cjs} +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/promptSessionSchema.cjs +1 -1
- package/src/main/lib/{definitionOfDone.cjs → scheduler/definitionOfDone.cjs} +6 -12
- package/src/main/lib/{fixChainDepth.cjs → scheduler/fixChainDepth.cjs} +1 -1
- package/src/main/lib/{jobDirtFilter.cjs → scheduler/jobDirtFilter.cjs} +1 -1
- package/src/main/lib/{jobSupervisorRecord.cjs → scheduler/jobSupervisorRecord.cjs} +1 -1
- package/src/main/lib/{launchFailure.cjs → scheduler/launchFailure.cjs} +2 -2
- package/src/main/lib/{queueStore.cjs → scheduler/queueStore.cjs} +6 -6
- package/src/main/lib/{quietMachineLease.cjs → scheduler/quietMachineLease.cjs} +1 -1
- package/src/main/lib/{rcaReport.cjs → scheduler/rcaReport.cjs} +4 -4
- package/src/main/lib/{reaperHelpers.cjs → scheduler/reaperHelpers.cjs} +3 -3
- package/src/main/lib/{reservationExpiry.cjs → scheduler/reservationExpiry.cjs} +4 -11
- package/src/main/lib/{reviewNotice.cjs → scheduler/reviewNotice.cjs} +1 -1
- package/src/main/lib/{scheduleJobTransitions.cjs → scheduler/scheduleJobTransitions.cjs} +3 -3
- package/src/main/lib/{schedulerBatch.cjs → scheduler/schedulerBatch.cjs} +2 -2
- package/src/main/lib/{schedulerConfig.cjs → scheduler/schedulerConfig.cjs} +1 -1
- package/src/main/lib/{schedulerRuntimeState.cjs → scheduler/schedulerRuntimeState.cjs} +1 -1
- package/src/main/lib/{sessionSlots.cjs → scheduler/sessionSlots.cjs} +2 -2
- package/src/main/lib/{upgradeDrain.cjs → scheduler/upgradeDrain.cjs} +8 -11
- package/src/main/lib/{watchdogHelpers.cjs → scheduler/watchdogHelpers.cjs} +5 -19
- package/src/main/lib/{auditLog.cjs → telemetry/auditLog.cjs} +1 -1
- package/src/main/lib/{opsErrorLog.cjs → telemetry/opsErrorLog.cjs} +4 -4
- package/src/main/lib/{telemetryBacklog.cjs → telemetry/telemetryBacklog.cjs} +4 -4
- package/src/main/lib/{telemetryBoot.cjs → telemetry/telemetryBoot.cjs} +1 -1
- package/src/main/lib/{telemetryClient.cjs → telemetry/telemetryClient.cjs} +4 -4
- package/src/main/lib/{telemetrySettings.cjs → telemetry/telemetrySettings.cjs} +1 -1
- package/src/main/logs.cjs +1 -1
- package/src/main/mcpStatus.cjs +2 -2
- package/src/main/memoryAggregate.cjs +3 -3
- package/src/main/memoryTool.cjs +1 -1
- package/src/main/otel.cjs +2 -2
- package/src/main/pluginInstall.cjs +29 -3
- package/src/main/projectBrief.cjs +6 -6
- package/src/main/projectPages.cjs +1 -1
- package/src/main/promptSessionEvents.cjs +2 -2
- package/src/main/pty.cjs +7 -7
- package/src/main/queueOps.cjs +6 -6
- package/src/main/remoteClient.cjs +4 -4
- package/src/main/remoteClientIpc.cjs +1 -1
- package/src/main/runVerify.cjs +1 -1
- package/src/main/scheduler/CLAUDE.md +26 -0
- package/src/main/scheduler/budget.cjs +576 -0
- package/src/main/scheduler/gitTreeGuard.cjs +672 -0
- package/src/main/scheduler/prdParser.cjs +4 -4
- package/src/main/scheduler/prdPaths.cjs +487 -0
- package/src/main/scheduler/reviewLadder.cjs +1011 -0
- package/src/main/scheduler/starvation.cjs +390 -0
- package/src/main/scheduler.cjs +289 -3311
- package/src/main/seedAgentPersonas.cjs +5 -5
- package/src/main/seedDevPlugin.cjs +70 -3
- package/src/main/seedSchedulerMcp.cjs +2 -2
- package/src/main/supervisor.cjs +5 -5
- package/src/main/telemetryIpc.cjs +51 -0
- package/src/main/transcripts.cjs +3 -3
- package/src/main/updaterIpc.cjs +1 -1
- package/src/main/usage.cjs +1 -1
- package/src/main/watchers.cjs +1 -1
- package/src/main/webRemote.cjs +7 -7
- package/src/preload/api.d.ts +8 -8
- package/src/preload/index.cjs +3 -3
- package/src/seed/agents/demo-video-builder.md +62 -19
- package/dist/assets/RemoteMachines-DHJZfdJC.js +0 -3
- package/dist/assets/SystemPrompt-BCEyMSEq.js +0 -1
- package/dist/assets/TagLibrary-m3It8Oal.js +0 -1
- package/dist/assets/Toggle-In_AExPV.js +0 -1
- package/plugins/session-manager-dev/skills/blog-for-project-feature/0-select/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/blog-for-project-feature/1-research/SKILL.md +0 -37
- package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +0 -64
- package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +0 -52
- package/plugins/session-manager-dev/skills/blog-for-project-feature/4-draft-copy/SKILL.md +0 -37
- package/plugins/session-manager-dev/skills/blog-for-project-feature/5-illustrate/SKILL.md +0 -50
- package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +0 -51
- package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +0 -54
- package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +0 -144
- package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +0 -90
- package/plugins/session-manager-dev/skills/discover-features/SKILL.md +0 -95
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +0 -205
- package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +0 -163
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +0 -83
- package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +0 -38
- package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +0 -64
- package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +0 -71
- package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +0 -75
- package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +0 -39
- package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +0 -57
- package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +0 -40
- package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +0 -33
- package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +0 -40
- package/plugins/session-manager-dev/skills/issue-address/SKILL.md +0 -120
- package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +0 -58
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +0 -147
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +0 -137
- package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +0 -41
- package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +0 -56
- package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +0 -49
- package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +0 -57
- package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +0 -60
- package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +0 -124
- package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +0 -67
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +0 -243
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +0 -98
- package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +0 -146
- /package/src/main/lib/{agentEffortResolve.cjs → claude/agentEffortResolve.cjs} +0 -0
- /package/src/main/lib/{claudeCliCaps.cjs → claude/claudeCliCaps.cjs} +0 -0
- /package/src/main/lib/{usageCircuit.cjs → claude/usageCircuit.cjs} +0 -0
- /package/src/main/lib/{epicSpawnCwd.cjs → epic/epicSpawnCwd.cjs} +0 -0
- /package/src/main/lib/{customerFeedbackAdminRoutes.cjs → feedback-remote/customerFeedbackAdminRoutes.cjs} +0 -0
- /package/src/main/lib/{remoteE2eCrypto.cjs → feedback-remote/remoteE2eCrypto.cjs} +0 -0
- /package/src/main/lib/{branchSweep.cjs → git/branchSweep.cjs} +0 -0
- /package/src/main/lib/{gitExec.cjs → git/gitExec.cjs} +0 -0
- /package/src/main/lib/{jobWorktree.cjs → git/jobWorktree.cjs} +0 -0
- /package/src/main/lib/{jobWorktreeBootLive.cjs → git/jobWorktreeBootLive.cjs} +0 -0
- /package/src/main/lib/{jobWorktreeTerminalOrphanLive.cjs → git/jobWorktreeTerminalOrphanLive.cjs} +0 -0
- /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
- /package/src/main/lib/{depSlugResolve.cjs → prd/depSlugResolve.cjs} +0 -0
- /package/src/main/lib/{planValidator.cjs → prd/planValidator.cjs} +0 -0
- /package/src/main/lib/{prdAuthoringSeed.cjs → prd/prdAuthoringSeed.cjs} +0 -0
- /package/src/main/lib/{prdDisposition.cjs → prd/prdDisposition.cjs} +0 -0
- /package/src/main/lib/{prdFrontmatter.cjs → prd/prdFrontmatter.cjs} +0 -0
- /package/src/main/lib/{prdSizing.cjs → prd/prdSizing.cjs} +0 -0
- /package/src/main/lib/{workTypeLibrary.cjs → prd/workTypeLibrary.cjs} +0 -0
- /package/src/main/lib/{childWithLog.cjs → process/childWithLog.cjs} +0 -0
- /package/src/main/lib/{cleanEnv.cjs → process/cleanEnv.cjs} +0 -0
- /package/src/main/lib/{defaultShell.cjs → process/defaultShell.cjs} +0 -0
- /package/src/main/lib/{killTree.cjs → process/killTree.cjs} +0 -0
- /package/src/main/lib/{loginShellPath.cjs → process/loginShellPath.cjs} +0 -0
- /package/src/main/lib/{procIdentity.cjs → process/procIdentity.cjs} +0 -0
- /package/src/main/lib/{singleInstanceGuard.cjs → process/singleInstanceGuard.cjs} +0 -0
- /package/src/main/lib/{smProcNames.cjs → process/smProcNames.cjs} +0 -0
- /package/src/main/lib/{timeoutShimScript.cjs → process/timeoutShimScript.cjs} +0 -0
- /package/src/main/lib/{winSpawn.cjs → process/winSpawn.cjs} +0 -0
- /package/src/main/lib/{adoptedRunSupervisor.cjs → scheduler/adoptedRunSupervisor.cjs} +0 -0
- /package/src/main/lib/{dispatchLoop.cjs → scheduler/dispatchLoop.cjs} +0 -0
- /package/src/main/lib/{dodDrainHook.cjs → scheduler/dodDrainHook.cjs} +0 -0
- /package/src/main/lib/{fixPlanSlug.cjs → scheduler/fixPlanSlug.cjs} +0 -0
- /package/src/main/lib/{gateAuthority.cjs → scheduler/gateAuthority.cjs} +0 -0
- /package/src/main/lib/{landedSinceRun.cjs → scheduler/landedSinceRun.cjs} +0 -0
- /package/src/main/lib/{loadGate.cjs → scheduler/loadGate.cjs} +0 -0
- /package/src/main/lib/{needsReviewLedger.cjs → scheduler/needsReviewLedger.cjs} +0 -0
- /package/src/main/lib/{queueHealth.cjs → scheduler/queueHealth.cjs} +0 -0
- /package/src/main/lib/{queueHistory.cjs → scheduler/queueHistory.cjs} +0 -0
- /package/src/main/lib/{runLogRetention.cjs → scheduler/runLogRetention.cjs} +0 -0
- /package/src/main/lib/{scheduleJobSchema.cjs → scheduler/scheduleJobSchema.cjs} +0 -0
- /package/src/main/lib/{schedulerPaths.cjs → scheduler/schedulerPaths.cjs} +0 -0
- /package/src/main/lib/{terminalRunOutcome.cjs → scheduler/terminalRunOutcome.cjs} +0 -0
- /package/src/main/lib/{validationSentinels.cjs → scheduler/validationSentinels.cjs} +0 -0
- /package/src/main/lib/{crashTelemetry.cjs → telemetry/crashTelemetry.cjs} +0 -0
- /package/src/main/lib/{otlpTraceExporter.cjs → telemetry/otlpTraceExporter.cjs} +0 -0
- /package/src/main/lib/{telemetryConsent.cjs → telemetry/telemetryConsent.cjs} +0 -0
- /package/src/main/lib/{telemetryCounters.cjs → telemetry/telemetryCounters.cjs} +0 -0
- /package/src/main/lib/{atomicFs.cjs → util/atomicFs.cjs} +0 -0
- /package/src/main/lib/{encodeCwd.cjs → util/encodeCwd.cjs} +0 -0
- /package/src/main/lib/{expandHome.cjs → util/expandHome.cjs} +0 -0
- /package/src/main/lib/{extractJson.cjs → util/extractJson.cjs} +0 -0
- /package/src/main/lib/{fileTail.cjs → util/fileTail.cjs} +0 -0
- /package/src/main/lib/{headTailBuffer.cjs → util/headTailBuffer.cjs} +0 -0
- /package/src/main/lib/{kebabCase.cjs → util/kebabCase.cjs} +0 -0
- /package/src/main/lib/{loopDelay.cjs → util/loopDelay.cjs} +0 -0
- /package/src/main/lib/{lruCache.cjs → util/lruCache.cjs} +0 -0
- /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
|
+
};
|