claude-code-session-manager 0.111.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-rBO1daqM.js → AgentLibrary-RwPvEgqm.js} +2 -2
- package/dist/assets/{Button-DsEYRZg0.js → Button-36j3PzYv.js} +1 -1
- package/dist/assets/{DataModel-D8sbvvBC.js → DataModel-DUhANo4l.js} +1 -1
- package/dist/assets/{History-BEHLtxia.js → History-BeZzNjmv.js} +2 -2
- package/dist/assets/{Hooks-CKpvlXrg.js → Hooks-iQ8845Hp.js} +3 -3
- package/dist/assets/{Library-CeF4qh9d.js → Library-DPE1h-1L.js} +6 -6
- package/dist/assets/{ListDetail-DcfmxPFk.js → ListDetail-PStOuQAo.js} +1 -1
- package/dist/assets/{MarkdownEditor-CvK1pyto.js → MarkdownEditor-DPw-N2-m.js} +1 -1
- package/dist/assets/{McpServers-D-h2ihzJ.js → McpServers-CUyRTNep.js} +2 -2
- package/dist/assets/{Memory-BnF89x8v.js → Memory-bZ2xjBGy.js} +3 -3
- package/dist/assets/{Panel-CQY5zano.js → Panel-_VZ_uXNg.js} +1 -1
- package/dist/assets/{Permissions-DLukmmsz.js → Permissions-PPRmfNbK.js} +3 -3
- package/dist/assets/{Plugins-0Pf0OZ4l.js → Plugins-BFcvfvlx.js} +2 -2
- package/dist/assets/{ProvenanceBadge-vC7Tj-JP.js → ProvenanceBadge-B1WUknak.js} +1 -1
- package/dist/assets/RemoteMachines-D5V0lzgW.js +3 -0
- package/dist/assets/{SaveBar-DjijiUb0.js → SaveBar-Dw64FVdM.js} +1 -1
- package/dist/assets/{Scheduler-Cm3EXZvE.js → Scheduler-C8s_pJ3I.js} +9 -9
- package/dist/assets/{ScopeSwitcher-B-P-kSfq.js → ScopeSwitcher-D-nKDMAE.js} +1 -1
- package/dist/assets/{Settings-DYDlp2m_.js → Settings-CQSrR4mO.js} +4 -4
- package/dist/assets/{SkillReferenceGraph-Epv2GJmj.js → SkillReferenceGraph-BbZFMHim.js} +1 -1
- package/dist/assets/{Skills-DHO0N0C2.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-DAMO3DhO.js → TiptapBody-Cr-hxX8L.js} +1 -1
- package/dist/assets/Toggle-7WpPzcZH.js +1 -0
- package/dist/assets/{ViewTabs-CGX6MAhc.js → ViewTabs-9lQCRwWJ.js} +1 -1
- package/dist/assets/{historyTotals-vgfk8EOL.js → historyTotals-BrFxw5J1.js} +1 -1
- package/dist/assets/{index-CPieAFsl.js → index-CTBpf-26.js} +250 -250
- package/dist/assets/{settingsSchema-D0FxNrTy.js → settingsSchema-Dh5Kl7VO.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-DCu6BNYN.js → useAgentsDirWatch-CMpx0d04.js} +1 -1
- package/dist/assets/{useRemotePeers-D4cdJDyh.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/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
- package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
- 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/dist/assets/RemoteMachines-CWnKbFHc.js +0 -3
- package/dist/assets/SystemPrompt-DxeNofL9.js +0 -1
- package/dist/assets/TagLibrary-QOAs7ISO.js +0 -1
- package/dist/assets/Toggle-Ba_f7q_m.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/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +0 -0
- /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
- /package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler/budget.cjs — pure stall / stranded-row / job-overrun / wall-clock-budget
|
|
3
|
+
* selectors, failure-streak/backoff predicates and the dependsOn history-satisfaction
|
|
4
|
+
* lookup. Extracted verbatim from scheduler.cjs; the watchdogs and queue
|
|
5
|
+
* mutations stay there and call in.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fsp = require('node:fs/promises');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { claudePidAlive } = require('../lib/scheduler/reaperHelpers.cjs');
|
|
12
|
+
const queueHistory = require('../lib/scheduler/queueHistory.cjs');
|
|
13
|
+
const { listArchivedPrdDirs } = require('../lib/prd/prdLocations.cjs');
|
|
14
|
+
const { LEGAL_TRANSITIONS } = require('../lib/scheduler/scheduleJobTransitions.cjs');
|
|
15
|
+
const { DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('../lib/scheduler/schedulerBatch.cjs');
|
|
16
|
+
const {
|
|
17
|
+
JOB_OVERRUN_FACTOR: JOB_OVERRUN_FACTOR_DEFAULT,
|
|
18
|
+
JOB_OVERRUN_FLOOR_MS: JOB_OVERRUN_FLOOR_MS_DEFAULT,
|
|
19
|
+
JOB_BUDGET_FACTOR: JOB_BUDGET_FACTOR_DEFAULT,
|
|
20
|
+
JOB_BUDGET_FLOOR_MS: JOB_BUDGET_FLOOR_MS_DEFAULT,
|
|
21
|
+
JOB_BUDGET_CEILING_MS: JOB_BUDGET_CEILING_MS_DEFAULT,
|
|
22
|
+
} = require('../lib/scheduler/schedulerConfig.cjs');
|
|
23
|
+
|
|
24
|
+
// Shared by every SM_*-env-overridable numeric constant below (bare factors
|
|
25
|
+
// use unitMs=1; minute-denominated knobs use unitMs=60_000) — one parse rule
|
|
26
|
+
// instead of one hand-copied ternary per constant.
|
|
27
|
+
function numEnvOverride(envVar, unitMs, fallback) {
|
|
28
|
+
const raw = process.env[envVar];
|
|
29
|
+
return raw ? Number(raw) * unitMs : fallback;
|
|
30
|
+
}
|
|
31
|
+
const JOB_OVERRUN_FACTOR = numEnvOverride('SM_JOB_OVERRUN_FACTOR', 1, JOB_OVERRUN_FACTOR_DEFAULT);
|
|
32
|
+
const JOB_OVERRUN_FLOOR_MS = numEnvOverride('SM_JOB_OVERRUN_FLOOR_MINUTES', 60_000, JOB_OVERRUN_FLOOR_MS_DEFAULT);
|
|
33
|
+
// Same three numbers as JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS today (3x,
|
|
34
|
+
// 45min) is coincidental, not structural — this triad ACTS (kills) where
|
|
35
|
+
// JOB_OVERRUN_* only ever escalates (see JOB_OVERRUN_FACTOR's own header);
|
|
36
|
+
// tune them independently, don't re-couple on a future pass just because the
|
|
37
|
+
// defaults happen to match right now.
|
|
38
|
+
const JOB_BUDGET_FACTOR = numEnvOverride('SM_JOB_BUDGET_FACTOR', 1, JOB_BUDGET_FACTOR_DEFAULT);
|
|
39
|
+
const JOB_BUDGET_FLOOR_MS = numEnvOverride('SM_JOB_BUDGET_FLOOR_MINUTES', 60_000, JOB_BUDGET_FLOOR_MS_DEFAULT);
|
|
40
|
+
const JOB_BUDGET_CEILING_MS = numEnvOverride('SM_JOB_BUDGET_CEILING_MINUTES', 60_000, JOB_BUDGET_CEILING_MS_DEFAULT);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* computeStallSummary(state) → { stalled, total, running, pending, byProject }
|
|
44
|
+
*
|
|
45
|
+
* Pure, no IO. `state` is a merged queue-store read ({ jobs, invalidJobs,
|
|
46
|
+
* paused }). The engine (reconcile/reaper/auto-fix/reverify) already
|
|
47
|
+
* operates machine-wide via queueStore's stateCwds() — this function is
|
|
48
|
+
* MONITORING, and monitoring must not collapse per-project reality into one
|
|
49
|
+
* boolean. `stalled` (top-level) is the pre-existing machine-wide roll-up:
|
|
50
|
+
* the queue holds work — valid rows OR rows quarantined for an invalid
|
|
51
|
+
* status — but nothing anywhere is running or pending and the scheduler
|
|
52
|
+
* isn't paused. The 2026-08-07 incident sat exactly in this state for 4+
|
|
53
|
+
* hours: 2 jobs, 0 running, 0 pending, and the only visible symptom was a
|
|
54
|
+
* heartbeat `counts` object that had silently minted a `queued` bucket
|
|
55
|
+
* instead of reporting anything actionable.
|
|
56
|
+
*
|
|
57
|
+
* `byProject[cwd].stalled` is the PER-PROJECT verdict added for the
|
|
58
|
+
* "burrow went dark while other projects were busy" gap: a project can hold
|
|
59
|
+
* jobs (including ones parked `quarantined`) with 0 running and 0 pending
|
|
60
|
+
* while the machine-wide `stalled` above reads false because a different
|
|
61
|
+
* project has running/pending work. Each project's own status counts
|
|
62
|
+
* (`byProject[cwd][status]`) already summed to a total before this — the
|
|
63
|
+
* fix is only the boolean, not the counting.
|
|
64
|
+
*/
|
|
65
|
+
function computeStallSummary(state) {
|
|
66
|
+
const jobs = Array.isArray(state?.jobs) ? state.jobs : [];
|
|
67
|
+
const invalidJobs = Array.isArray(state?.invalidJobs) ? state.invalidJobs : [];
|
|
68
|
+
let running = 0;
|
|
69
|
+
let pending = 0;
|
|
70
|
+
const byProject = {};
|
|
71
|
+
for (const j of jobs) {
|
|
72
|
+
if (j.status === 'running') running += 1;
|
|
73
|
+
if (j.status === 'pending') pending += 1;
|
|
74
|
+
const key = j.cwd || '(unknown)';
|
|
75
|
+
byProject[key] = byProject[key] || {};
|
|
76
|
+
byProject[key][j.status] = (byProject[key][j.status] || 0) + 1;
|
|
77
|
+
}
|
|
78
|
+
for (const inv of invalidJobs) {
|
|
79
|
+
const key = inv.row?.cwd || '(unknown)';
|
|
80
|
+
byProject[key] = byProject[key] || {};
|
|
81
|
+
byProject[key].invalid = (byProject[key].invalid || 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
const total = jobs.length + invalidJobs.length;
|
|
84
|
+
// A drained queue (every row completed/skipped) is idle, not stalled — a
|
|
85
|
+
// stall needs at least one parked problem row (failed/needs_review/
|
|
86
|
+
// quarantined/invalid) that is waiting on someone.
|
|
87
|
+
const isProblemStatus = (st) => st !== 'completed' && st !== 'skipped';
|
|
88
|
+
const stalled = total > 0 && running === 0 && pending === 0 && !state?.paused
|
|
89
|
+
&& (invalidJobs.length > 0 || jobs.some((j) => isProblemStatus(j.status)));
|
|
90
|
+
for (const key of Object.keys(byProject)) {
|
|
91
|
+
const counts = byProject[key];
|
|
92
|
+
const projRunning = counts.running || 0;
|
|
93
|
+
const projPending = counts.pending || 0;
|
|
94
|
+
const projTotal = Object.keys(counts)
|
|
95
|
+
.filter((k) => k !== 'stalled')
|
|
96
|
+
.reduce((sum, k) => sum + counts[k], 0);
|
|
97
|
+
const projProblem = Object.keys(counts)
|
|
98
|
+
.some((k) => k !== 'stalled' && k !== 'completed' && k !== 'skipped' && counts[k] > 0);
|
|
99
|
+
counts.stalled = projTotal > 0 && projRunning === 0 && projPending === 0 && !state?.paused && projProblem;
|
|
100
|
+
}
|
|
101
|
+
return { stalled, total, running, pending, byProject };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* findStaleQuarantinedJobs(jobs, now, thresholdMs) → [{ slug, cwd, ageMs }]
|
|
106
|
+
*
|
|
107
|
+
* Pure, no IO. A 'quarantined' row (no createdVia provenance) can otherwise
|
|
108
|
+
* sit forever with nothing looking at it — quarantine used to clear only via
|
|
109
|
+
* a human adopting or archiving it; autoResolveQuarantine below now gives it
|
|
110
|
+
* a bounded automatic exit too. This function stays the escalation/warn half
|
|
111
|
+
* of that gate: any quarantined row whose recorded quarantine timestamp
|
|
112
|
+
* (statusHistory's `to === 'quarantined'` entry — stamped at creation, or
|
|
113
|
+
* backfilled from the PRD file's mtime by reconcile() for rows quarantined
|
|
114
|
+
* before that stamp existed) is older than `thresholdMs` is reported so the
|
|
115
|
+
* caller can warn-log and surface it distinctly. A row with no recoverable
|
|
116
|
+
* timestamp is skipped rather than guessed at.
|
|
117
|
+
*/
|
|
118
|
+
function findStaleQuarantinedJobs(jobs, now, thresholdMs) {
|
|
119
|
+
const stale = [];
|
|
120
|
+
for (const j of jobs ?? []) {
|
|
121
|
+
if (j.status !== 'quarantined') continue;
|
|
122
|
+
const entry = (j.statusHistory || []).find((h) => h.to === 'quarantined');
|
|
123
|
+
if (!entry) continue;
|
|
124
|
+
const since = Date.parse(entry.at);
|
|
125
|
+
if (Number.isNaN(since)) continue;
|
|
126
|
+
const ageMs = now - since;
|
|
127
|
+
if (ageMs >= thresholdMs) stale.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs });
|
|
128
|
+
}
|
|
129
|
+
return stale;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* findOverrunningJobs(jobs, now, { factor, floorMs }) → [{ slug, cwd, estimateMinutes, ranMs, ratio }]
|
|
134
|
+
*
|
|
135
|
+
* Pure, no IO. A RUNNING job whose elapsed time exceeds
|
|
136
|
+
* `max(estimateMinutes * factor, floorMs)` is overrunning its own PRD's
|
|
137
|
+
* declared estimate.
|
|
138
|
+
*
|
|
139
|
+
* This closes the gap between the two existing kill paths, which a
|
|
140
|
+
* long-running-but-chatty job slips straight through:
|
|
141
|
+
* - MAX_JOB_DURATION_MS (4h) is a deadman — a 20-minute PRD at 3h is 9x
|
|
142
|
+
* over estimate and still an hour away from it.
|
|
143
|
+
* - IDLE_OUTPUT_KILL_MS (20m) only fires when the log mtime STALLS; an
|
|
144
|
+
* agent stuck in a productive-looking loop keeps writing and never trips it.
|
|
145
|
+
* `estimateMinutes` was parsed, stored and displayed but never once compared
|
|
146
|
+
* against actual runtime, so the only thing standing between a runaway job
|
|
147
|
+
* and the 4h ceiling was a human noticing. (2026-08-08: a PRD ran 3h+ and was
|
|
148
|
+
* caught only because the operator opened a second session to look.)
|
|
149
|
+
*
|
|
150
|
+
* Escalation only — see JOB_OVERRUN_FACTOR's note on why this must not kill.
|
|
151
|
+
* A job with no usable estimate is skipped rather than guessed at.
|
|
152
|
+
*/
|
|
153
|
+
function findOverrunningJobs(jobs, now, { factor, floorMs } = {}) {
|
|
154
|
+
const f = typeof factor === 'number' && factor > 0 ? factor : JOB_OVERRUN_FACTOR;
|
|
155
|
+
const floor = typeof floorMs === 'number' && floorMs >= 0 ? floorMs : JOB_OVERRUN_FLOOR_MS;
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const j of jobs ?? []) {
|
|
158
|
+
if (j.status !== 'running') continue;
|
|
159
|
+
const est = Number(j.estimateMinutes);
|
|
160
|
+
if (!Number.isFinite(est) || est <= 0) continue; // no estimate to overrun
|
|
161
|
+
const startedAt = Date.parse(j.startedAt ?? '');
|
|
162
|
+
if (Number.isNaN(startedAt)) continue;
|
|
163
|
+
const ranMs = now - startedAt;
|
|
164
|
+
if (ranMs <= 0) continue;
|
|
165
|
+
const thresholdMs = Math.max(est * 60_000 * f, floor);
|
|
166
|
+
if (ranMs < thresholdMs) continue;
|
|
167
|
+
out.push({
|
|
168
|
+
slug: j.slug,
|
|
169
|
+
cwd: j.cwd ?? null,
|
|
170
|
+
estimateMinutes: est,
|
|
171
|
+
ranMs,
|
|
172
|
+
ratio: ranMs / (est * 60_000),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* computeJobBudgetMs(estimateMinutes, { factor, floorMs, ceilingMs }) → number
|
|
180
|
+
*
|
|
181
|
+
* Pure. `budgetMs = clamp(estimateMinutes * factor, floorMs, ceilingMs)` — see
|
|
182
|
+
* JOB_BUDGET_FACTOR's header comment (schedulerConfig.cjs) for the measured
|
|
183
|
+
* p50/p90/max this is calibrated against. A missing/zero/non-finite estimate
|
|
184
|
+
* is treated as 0, which the floor clamp then dominates — "jobs with a
|
|
185
|
+
* missing estimate get the floor" falls straight out of the clamp, no
|
|
186
|
+
* special-casing needed.
|
|
187
|
+
*/
|
|
188
|
+
function computeJobBudgetMs(estimateMinutes, { factor, floorMs, ceilingMs } = {}) {
|
|
189
|
+
const f = typeof factor === 'number' && factor > 0 ? factor : JOB_BUDGET_FACTOR;
|
|
190
|
+
const floor = typeof floorMs === 'number' && floorMs >= 0 ? floorMs : JOB_BUDGET_FLOOR_MS;
|
|
191
|
+
const ceiling = typeof ceilingMs === 'number' && ceilingMs > 0 ? ceilingMs : JOB_BUDGET_CEILING_MS;
|
|
192
|
+
const est = Number(estimateMinutes);
|
|
193
|
+
const minutes = Number.isFinite(est) && est > 0 ? est : 0;
|
|
194
|
+
return Math.min(Math.max(minutes * f * 60_000, floor), ceiling);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* classifyBudgetKill(res, landedCommitEvidence) → { status, reason, landedCommit } | null
|
|
199
|
+
*
|
|
200
|
+
* Pure. `res` is executeJob's resolved outcome — only fires when
|
|
201
|
+
* `res.killedByWatchdog === 'budget'` (stamped by the budget watchdog inside
|
|
202
|
+
* executeJob, never inferred from exit code/duration alone, so it can never
|
|
203
|
+
* collide with an ordinary idle-tail/deadman/external kill). ALWAYS routes to
|
|
204
|
+
* needs_review — never 'failed' (spawnJob's ordinary non-zero-exit default)
|
|
205
|
+
* and never silently 'completed' (executeJob's onExit excludes
|
|
206
|
+
* killedByWatchdog === 'budget' from the result=success → exit 0 mapping
|
|
207
|
+
* idle-tail/deadman get) — so a budget kill is a visible, actionable park,
|
|
208
|
+
* never a retry (classifyFailureOutcome/selectAutoFixTargets only ever see
|
|
209
|
+
* 'failed'/ordinary needs_review rows, not this one — see
|
|
210
|
+
* selectAutoFixTargets' own budget_exceeded exclusion) and never a discard:
|
|
211
|
+
* `landedCommitEvidence`, when the caller resolved one via the SAME
|
|
212
|
+
* commit-guard evidence check the plain sigterm/exit paths already use, is
|
|
213
|
+
* threaded onto the row as `landedCommit` so a job that HAD already
|
|
214
|
+
* committed before overrunning is still adjudicated on its git evidence.
|
|
215
|
+
*/
|
|
216
|
+
function classifyBudgetKill(res, landedCommitEvidence) {
|
|
217
|
+
if (!res || res.killedByWatchdog !== 'budget') return null;
|
|
218
|
+
return {
|
|
219
|
+
status: 'needs_review',
|
|
220
|
+
reason: res.budgetKillReason || `wall-clock budget exceeded (exit ${res.exitCode})`,
|
|
221
|
+
landedCommit: landedCommitEvidence || null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* isJobBudgetExempt(job) → boolean
|
|
227
|
+
*
|
|
228
|
+
* Pure. `quietMachine: true` PRDs (their whole point is running alone,
|
|
229
|
+
* un-contended, for a timing-sensitive measurement) and any PRD with an
|
|
230
|
+
* explicit `budgetExempt: true` opt-out have no wall-clock kill ceiling.
|
|
231
|
+
*/
|
|
232
|
+
function isJobBudgetExempt(job) {
|
|
233
|
+
return job?.quietMachine === true || job?.budgetExempt === true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Pure predicate the budget watchdog's shouldFire calls — single source of
|
|
237
|
+
* truth for "has this job run past its own budget" so it's unit-testable
|
|
238
|
+
* without spinning up real timers. */
|
|
239
|
+
function shouldKillForBudget(elapsedMs, budgetMs) {
|
|
240
|
+
return elapsedMs >= budgetMs;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* resolveBudgetKillOutcome({ killedByWatchdog, killedBySignal, durationMs, jobBudgetMs, estimateMinutes })
|
|
245
|
+
* → { killedByWatchdog: 'budget'|null, budgetKillReason: string|null }
|
|
246
|
+
*
|
|
247
|
+
* Pure. `ctx.killedByWatchdog` is stamped by the budget watchdog's action()
|
|
248
|
+
* the instant its periodic shouldFire() observes elapsedMs >= jobBudgetMs —
|
|
249
|
+
* but that setInterval tick and the child's real 'exit' event both run on
|
|
250
|
+
* the SAME single-threaded event loop, so it's possible to observe the
|
|
251
|
+
* budget threshold crossed and call ctx.killTree() in the same window the
|
|
252
|
+
* agent happens to exit cleanly (exit 0) or fails on its own for an
|
|
253
|
+
* unrelated reason — ctx.killTree() against an already-exited pid is a
|
|
254
|
+
* silent no-op (ESRCH, caught), but the flag would still read 'budget'
|
|
255
|
+
* unless gated here. Only trusted when the exit SHAPE actually looks like a
|
|
256
|
+
* signal kill (killedBySignal — mirrors the exact same check onExit already
|
|
257
|
+
* uses for its own mappedToSuccess exclusion), so a clean exit=0 or an
|
|
258
|
+
* ordinary unrelated non-zero failure racing the watchdog's tick is never
|
|
259
|
+
* misclassified as a budget kill downstream.
|
|
260
|
+
*/
|
|
261
|
+
function resolveBudgetKillOutcome({ killedByWatchdog, killedBySignal, durationMs, jobBudgetMs, estimateMinutes }) {
|
|
262
|
+
if (killedByWatchdog !== 'budget' || !killedBySignal) {
|
|
263
|
+
return { killedByWatchdog: killedByWatchdog === 'budget' ? null : (killedByWatchdog ?? null), budgetKillReason: null };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
killedByWatchdog: 'budget',
|
|
267
|
+
budgetKillReason: `wall-clock budget exceeded: ran ${Math.round(durationMs / 60_000)}m against a ${Math.round(jobBudgetMs / 60_000)}m budget (estimateMinutes=${estimateMinutes ?? 0})`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* findStrandedInvestigations(jobs, now, maxMs, isAlive = claudePidAlive)
|
|
273
|
+
* → [{ slug, cwd, ageMs, restoreStatus }]
|
|
274
|
+
*
|
|
275
|
+
* Pure (besides the warn-log side effect on the two unprovable-age cases
|
|
276
|
+
* below), no other IO. spawnInvestigation's own restore of a job's
|
|
277
|
+
* pre-investigation status runs entirely inside the process that spawned the
|
|
278
|
+
* probe (its withChildAndLog onExit handler, or the synchronous-throw catch
|
|
279
|
+
* path) — so a job left 'investigating' when the app itself dies or restarts
|
|
280
|
+
* has NOTHING left to restore it. The comment at spawnInvestigation's onExit
|
|
281
|
+
* asserts "'investigating' must never be the job's resting state"; this is
|
|
282
|
+
* the sweep that makes that true across a restart, not just within one.
|
|
283
|
+
*
|
|
284
|
+
* A row qualifies only when ALL of:
|
|
285
|
+
* - status is 'investigating'
|
|
286
|
+
* - its most recent transition INTO 'investigating' (statusHistory's last
|
|
287
|
+
* `to === 'investigating'` entry — a job can be investigated more than
|
|
288
|
+
* once across its life, e.g. a retried auto-fix) is older than `maxMs`
|
|
289
|
+
* - it has no live probe process behind it (checked via runtime.pid, set by
|
|
290
|
+
* spawnInvestigation once its child spawns and cleared on every restore
|
|
291
|
+
* path, the same shape reapDeadRunningJobs already uses for 'running' rows)
|
|
292
|
+
*
|
|
293
|
+
* `restoreStatus` is that transition entry's `from` — the exact value
|
|
294
|
+
* spawnInvestigation itself would have restored to (`failedJob.status ||
|
|
295
|
+
* 'failed'`), which for a row that already finished and recorded
|
|
296
|
+
* finishedAt+exitCode (the burrow-834 shape) is whatever terminal status was
|
|
297
|
+
* computed for that outcome BEFORE the probe was spawned — this sweep never
|
|
298
|
+
* re-derives it from exitCode, only replays the already-recorded decision.
|
|
299
|
+
*
|
|
300
|
+
* A row with no recoverable transition timestamp cannot have its age proven,
|
|
301
|
+
* so it is warn-logged and left alone rather than guessed at — same posture
|
|
302
|
+
* as findStaleQuarantinedJobs/findOverrunningJobs.
|
|
303
|
+
*
|
|
304
|
+
* `restoreStatus` is validated against LEGAL_TRANSITIONS['investigating']
|
|
305
|
+
* before being returned — `statusHistory`'s `from` should only ever be
|
|
306
|
+
* 'failed' or 'needs_review' (the only two states LEGAL_TRANSITIONS allows
|
|
307
|
+
* into 'investigating'), but a corrupted/unexpected value must not be handed
|
|
308
|
+
* straight to transitionJob: an illegal target is refused outright (row stays
|
|
309
|
+
* stuck at 'investigating', re-detected as stranded every sweep with no path
|
|
310
|
+
* out), so an out-of-set `from` falls back to 'failed' here instead.
|
|
311
|
+
*/
|
|
312
|
+
const INVESTIGATING_RESTORE_TARGETS = new Set(LEGAL_TRANSITIONS.investigating);
|
|
313
|
+
function findStrandedInvestigations(jobs, now, maxMs, isAlive = claudePidAlive) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const j of jobs ?? []) {
|
|
316
|
+
if (j.status !== 'investigating') continue;
|
|
317
|
+
const entries = (j.statusHistory || []).filter((h) => h.to === 'investigating');
|
|
318
|
+
const entry = entries[entries.length - 1];
|
|
319
|
+
if (!entry) {
|
|
320
|
+
console.warn(`[scheduler] findStrandedInvestigations: ${j.slug} is 'investigating' with no statusHistory entry recording the transition — cannot prove age, leaving alone`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const since = Date.parse(entry.at ?? '');
|
|
324
|
+
if (Number.isNaN(since)) {
|
|
325
|
+
console.warn(`[scheduler] findStrandedInvestigations: ${j.slug} has an unparseable investigating-transition timestamp (${entry.at}) — cannot prove age, leaving alone`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const ageMs = now - since;
|
|
329
|
+
if (ageMs < maxMs) continue; // a live probe must not be yanked out from under itself
|
|
330
|
+
const pid = j.runtime?.pid;
|
|
331
|
+
if (pid && isAlive(pid)) continue; // probe genuinely still running — not stranded
|
|
332
|
+
const restoreStatus = INVESTIGATING_RESTORE_TARGETS.has(entry.from) ? entry.from : 'failed';
|
|
333
|
+
out.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs, restoreStatus });
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* computeDepHistorySatisfaction(state) → Map<cwd, Set<string>|symbol>
|
|
340
|
+
*
|
|
341
|
+
* PRD 1122's once-per-tick dependsOn history/archive lookup: for every
|
|
342
|
+
* distinct project cwd with jobs this tick, builds the set of dep slugs that
|
|
343
|
+
* have no live queue row but are nonetheless known-satisfied — a completed
|
|
344
|
+
* record in that project's own `state/history.jsonl` shard
|
|
345
|
+
* (queueHistory.completedSlugsForCwd, scoped per-project so a same-named PRD
|
|
346
|
+
* in an unrelated project can never satisfy a dep here), or a `.md` file
|
|
347
|
+
* under any of that project's `prds-archived/` dirs (listArchivedPrdDirs —
|
|
348
|
+
* covers both the retired flat layout and every Epic's own sibling archive).
|
|
349
|
+
* findBlockingDep (schedulerBatch.cjs) treats a dep slug as blocking
|
|
350
|
+
* whenever it has no live row AND is absent from this set, so a typo or a
|
|
351
|
+
* double-prefixed slug (the exact 2026-09-06 starry-night-ships incident)
|
|
352
|
+
* HOLDS its dependent instead of silently dispatching it.
|
|
353
|
+
*
|
|
354
|
+
* Fails OPEN per project, never queue-wide: a history-shard or archive-scan
|
|
355
|
+
* read error for one cwd degrades that cwd's value to
|
|
356
|
+
* `DEP_HISTORY_FAIL_OPEN` (findBlockingDep then treats every rowless dep in
|
|
357
|
+
* that project as satisfied, exactly today's pre-1122 behaviour) with a
|
|
358
|
+
* logged warning — it never throws out of this function and never blocks
|
|
359
|
+
* every OTHER project's dispatch for one project's bad fs state.
|
|
360
|
+
*
|
|
361
|
+
* Computed ONCE here, before pickNextBatch runs, and threaded down as pure
|
|
362
|
+
* data (quietOpts.satisfiedSlugsByCwd) — schedulerBatch.cjs itself does no
|
|
363
|
+
* I/O, so this is the only fs read this gate costs per tick, not one per job
|
|
364
|
+
* per dep.
|
|
365
|
+
*/
|
|
366
|
+
async function computeDepHistorySatisfaction(state) {
|
|
367
|
+
const byCwd = new Map();
|
|
368
|
+
const cwds = new Set((state?.jobs || []).map((j) => j.cwd || DEFAULT_PROJECT_CWD));
|
|
369
|
+
for (const cwd of cwds) {
|
|
370
|
+
const satisfied = new Set();
|
|
371
|
+
try {
|
|
372
|
+
for (const slug of await queueHistory.completedSlugsForCwd(cwd)) satisfied.add(slug);
|
|
373
|
+
for (const dir of listArchivedPrdDirs(cwd)) {
|
|
374
|
+
let entries;
|
|
375
|
+
try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { continue; }
|
|
376
|
+
for (const ent of entries) {
|
|
377
|
+
if (ent.isFile() && ent.name.endsWith('.md')) { satisfied.add(ent.name.slice(0, -3)); continue; }
|
|
378
|
+
// Option (b) of PRD 1286: a manual archive (queueOps.archiveOne, the
|
|
379
|
+
// schedule:archive-prd route and scheduler_archive_prd MCP tool) files the PRD
|
|
380
|
+
// under prds-archived/<ISO-ts>/<slug>.md — one level DEEPER than the auto-archive
|
|
381
|
+
// layout. Reading only the top level made every manually-archived slug invisible
|
|
382
|
+
// here, so once its row aged out its dependents held forever as 'unresolved'.
|
|
383
|
+
// A dep whose PRD file was archived is satisfied; a dep with NO file, row or
|
|
384
|
+
// history record (never ran, or a typo) still holds — nothing is dropped.
|
|
385
|
+
if (!ent.isDirectory()) continue;
|
|
386
|
+
let inner;
|
|
387
|
+
try { inner = await fsp.readdir(path.join(dir, ent.name)); } catch { continue; }
|
|
388
|
+
for (const name of inner) {
|
|
389
|
+
if (name.endsWith('.md')) satisfied.add(name.slice(0, -3));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (e) {
|
|
394
|
+
console.warn(`[scheduler] depHistorySatisfaction: history/archive lookup failed for ${cwd} (${e?.message}) — falling back to fail-open dep resolution for this project this tick`);
|
|
395
|
+
byCwd.set(cwd, DEP_HISTORY_FAIL_OPEN);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
byCwd.set(cwd, satisfied);
|
|
399
|
+
}
|
|
400
|
+
return byCwd;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Bounded automatic exit for a quarantined row (this PRD): up to
|
|
404
|
+
// QUARANTINE_RESOLVE_CAP auto-resolve attempts, each gated on having sat
|
|
405
|
+
// `quarantined` for QUARANTINE_ESCALATE_MS, before autoResolveQuarantine
|
|
406
|
+
// below settles the row to 'skipped' rather than leaving it as a dead end
|
|
407
|
+
// only a human `scheduler_reset_job`/adopt action could ever clear. A single
|
|
408
|
+
// attempt is enough in practice — the outcome is terminal — but the counter
|
|
409
|
+
// still guards against two overlapping ticks both trying to resolve the
|
|
410
|
+
// same row.
|
|
411
|
+
const QUARANTINE_RESOLVE_CAP = 1;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Kill-switch gate for the quarantine auto-resolve pass below
|
|
415
|
+
* (SM_QUARANTINE_AUTORESOLVE_DISABLE=1), same shape as
|
|
416
|
+
* failedAutoResetDisabled/needsReviewAutoResolveDisabled.
|
|
417
|
+
*/
|
|
418
|
+
function quarantineAutoResolveDisabled() {
|
|
419
|
+
return process.env.SM_QUARANTINE_AUTORESOLVE_DISABLE === '1';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* selectQuarantineAutoResolveTargets(jobs, now, thresholdMs) →
|
|
424
|
+
* [{ slug, cwd, ageMs }]
|
|
425
|
+
*
|
|
426
|
+
* Pure selector — no IO. Same age computation as findStaleQuarantinedJobs
|
|
427
|
+
* above, bounded additionally by quarantineResolveAttempts so a row already
|
|
428
|
+
* auto-resolved (or mid-resolve on a race) is never re-selected. Deliberately
|
|
429
|
+
* does NOT check createdVia here — that requires a disk read of the PRD
|
|
430
|
+
* file, and doing it at selection time would let this pass act on a
|
|
431
|
+
* snapshot that's gone stale by the time the mutate() pass actually runs.
|
|
432
|
+
* autoResolveQuarantine below re-reads createdVia fresh, immediately before
|
|
433
|
+
* transitioning, inside the same mutate() callback that applies this
|
|
434
|
+
* selector's targets — see that function's own header for why.
|
|
435
|
+
*/
|
|
436
|
+
function selectQuarantineAutoResolveTargets(jobs, now, thresholdMs) {
|
|
437
|
+
const targets = [];
|
|
438
|
+
for (const j of jobs ?? []) {
|
|
439
|
+
if (j.status !== 'quarantined') continue;
|
|
440
|
+
if ((j.quarantineResolveAttempts ?? 0) >= QUARANTINE_RESOLVE_CAP) continue;
|
|
441
|
+
const entry = (j.statusHistory || []).find((h) => h.to === 'quarantined');
|
|
442
|
+
if (!entry) continue;
|
|
443
|
+
const since = Date.parse(entry.at);
|
|
444
|
+
if (Number.isNaN(since)) continue;
|
|
445
|
+
const ageMs = now - since;
|
|
446
|
+
if (ageMs < thresholdMs) continue;
|
|
447
|
+
targets.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs });
|
|
448
|
+
}
|
|
449
|
+
return targets;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Ceiling on pollLoop's exponential poll backoff (both the 'transient'/'config'
|
|
453
|
+
// branch and the 'meter_rate_limited' branch below share this cap — a single
|
|
454
|
+
// constant so the two never drift to different ceilings).
|
|
455
|
+
const BACKOFF_MAX_MS = 480_000; // 8 minutes
|
|
456
|
+
// Threshold past which a growing consecutiveFailures streak stops being normal
|
|
457
|
+
// jitter and becomes worth a human's attention. health.cjs imports this so the
|
|
458
|
+
// WARN and the `npm run health` non-GREEN trip at the exact same count.
|
|
459
|
+
const FAILURE_STREAK_WARN_THRESHOLD = 5;
|
|
460
|
+
// How often a PERSISTING failure streak re-escalates (audit event +
|
|
461
|
+
// opsErrorLog line) after the initial WARN, and the health.cjs YELLOW->RED
|
|
462
|
+
// threshold for how long the usageCircuit has been open — one constant so
|
|
463
|
+
// the log cadence and the health-color flip never drift apart.
|
|
464
|
+
const FAILURE_STREAK_ESCALATION_MS = 30 * 60_000; // 30 minutes
|
|
465
|
+
|
|
466
|
+
/** Pure: exponential backoff with a cap, shared by every pollLoop failure branch. Exported for unit testing. */
|
|
467
|
+
function nextBackoffMs(prevBackoffMs) {
|
|
468
|
+
return prevBackoffMs ? Math.min(prevBackoffMs * 2, BACKOFF_MAX_MS) : 30_000;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Pure: should this failure count trigger the one-time streak WARN? Exported
|
|
473
|
+
* for unit testing. `alreadyWarned` is the current streak's warned flag —
|
|
474
|
+
* true for every failure after the threshold-crossing one, so this returns
|
|
475
|
+
* true exactly once per streak.
|
|
476
|
+
*/
|
|
477
|
+
function shouldWarnFailureStreak(consecutiveFailures, alreadyWarned, threshold = FAILURE_STREAK_WARN_THRESHOLD) {
|
|
478
|
+
return consecutiveFailures >= threshold && !alreadyWarned;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Pure: `failureStreakWarned === true` must always carry a numeric
|
|
483
|
+
* `failureStreakWarnedAt` (a state file may hold one without the other), so
|
|
484
|
+
* the escalation message never renders "after nullm". Backfills `nowMs`.
|
|
485
|
+
*/
|
|
486
|
+
function restoreFailureStreakWarnedAt(warned, warnedAt, nowMs) {
|
|
487
|
+
if (!warned) return typeof warnedAt === 'number' ? warnedAt : null;
|
|
488
|
+
return typeof warnedAt === 'number' ? warnedAt : nowMs;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Pure: whole minutes a warned streak has persisted; never null/NaN. */
|
|
492
|
+
function persistedStreakMinutes(warnedAt, nowMs) {
|
|
493
|
+
return typeof warnedAt === 'number' ? Math.round((nowMs - warnedAt) / 60_000) : 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Pure: does a PERSISTING failure streak warrant another escalation (audit
|
|
498
|
+
* event + opsErrorLog line) at `nowMs`? Exported for unit testing. Only
|
|
499
|
+
* relevant once the streak has already crossed `warnThreshold` (the initial
|
|
500
|
+
* WARN); `lastEscalatedAtMs` null means no escalation has fired yet this
|
|
501
|
+
* streak, so the first one is due immediately. Re-arms automatically once a
|
|
502
|
+
* streak clears (the caller resets `lastEscalatedAtMs` to null alongside
|
|
503
|
+
* `failureStreakWarned`), so a later independent streak escalates again.
|
|
504
|
+
*/
|
|
505
|
+
function shouldEscalateFailureStreak(consecutiveFailures, lastEscalatedAtMs, nowMs, thresholdMs = FAILURE_STREAK_ESCALATION_MS, warnThreshold = FAILURE_STREAK_WARN_THRESHOLD) {
|
|
506
|
+
if (consecutiveFailures < warnThreshold) return false;
|
|
507
|
+
if (!lastEscalatedAtMs) return true;
|
|
508
|
+
return nowMs - lastEscalatedAtMs >= thresholdMs;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Leftover quarantine (PRD 1128). Resume-first recovery gets exactly one
|
|
513
|
+
* `--resume` attempt (selectResumeRecoveryTarget above); when that attempt
|
|
514
|
+
* ALSO parks needs_review with 'uncommitted_changes', the leftovers are
|
|
515
|
+
* about to sit dirty in the SHARED tree forever — git then refuses any later
|
|
516
|
+
* worktree merge for this cwd that would overwrite them, turning one parked
|
|
517
|
+
* job into a project-wide stall (216-jupiter-sand-kazekage, 2026-09-06).
|
|
518
|
+
* Pure/no I/O, mirroring selectResumeRecoveryTarget so the eligibility rule
|
|
519
|
+
* is unit-testable directly.
|
|
520
|
+
*
|
|
521
|
+
* Bounded to exactly one attempt via job.leftoverQuarantineAttempted, stamped
|
|
522
|
+
* synchronously by the caller in the SAME mutate as this decision (never
|
|
523
|
+
* here) — see spawnJob's finalize and reverifyNeedsReview's periodic pass.
|
|
524
|
+
*
|
|
525
|
+
* Kill-switch: SM_LEFTOVER_QUARANTINE_DISABLE=1 restores today's behaviour
|
|
526
|
+
* exactly (always returns null), mirroring SM_RESUME_RECOVERY_DISABLE.
|
|
527
|
+
*/
|
|
528
|
+
function selectLeftoverQuarantineTarget(job) {
|
|
529
|
+
if (process.env.SM_LEFTOVER_QUARANTINE_DISABLE === '1') return null;
|
|
530
|
+
if (!job || job.status !== 'needs_review') return null;
|
|
531
|
+
if (job.verifierVerdict !== 'uncommitted_changes') return null;
|
|
532
|
+
if (job.resumeRecoveryAttempted !== true) return null;
|
|
533
|
+
if (job.leftoverQuarantineAttempted === true) return null;
|
|
534
|
+
const uncommittedPaths = Array.isArray(job.uncommittedPaths)
|
|
535
|
+
? job.uncommittedPaths.filter((p) => typeof p === 'string' && p.length > 0)
|
|
536
|
+
: [];
|
|
537
|
+
if (!uncommittedPaths.length) return null;
|
|
538
|
+
// The single most important constraint: never touch a path that was
|
|
539
|
+
// ALREADY dirty at this run's own dispatch time (preRunDirtyPaths) — that
|
|
540
|
+
// is foreign WIP (a human's or a sibling's), not this job's own leftover.
|
|
541
|
+
const preRunDirty = new Set(Array.isArray(job.preRunDirtyPaths) ? job.preRunDirtyPaths : []);
|
|
542
|
+
const paths = uncommittedPaths.filter((p) => !preRunDirty.has(p));
|
|
543
|
+
if (!paths.length) return null;
|
|
544
|
+
return { slug: job.slug, cwd: job.cwd, paths };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
module.exports = {
|
|
548
|
+
BACKOFF_MAX_MS,
|
|
549
|
+
FAILURE_STREAK_WARN_THRESHOLD,
|
|
550
|
+
FAILURE_STREAK_ESCALATION_MS,
|
|
551
|
+
nextBackoffMs,
|
|
552
|
+
shouldWarnFailureStreak,
|
|
553
|
+
restoreFailureStreakWarnedAt,
|
|
554
|
+
persistedStreakMinutes,
|
|
555
|
+
shouldEscalateFailureStreak,
|
|
556
|
+
selectLeftoverQuarantineTarget,
|
|
557
|
+
|
|
558
|
+
QUARANTINE_RESOLVE_CAP,
|
|
559
|
+
quarantineAutoResolveDisabled,
|
|
560
|
+
selectQuarantineAutoResolveTargets,
|
|
561
|
+
JOB_OVERRUN_FACTOR,
|
|
562
|
+
JOB_OVERRUN_FLOOR_MS,
|
|
563
|
+
JOB_BUDGET_FACTOR,
|
|
564
|
+
JOB_BUDGET_FLOOR_MS,
|
|
565
|
+
JOB_BUDGET_CEILING_MS,
|
|
566
|
+
computeStallSummary,
|
|
567
|
+
findStaleQuarantinedJobs,
|
|
568
|
+
findOverrunningJobs,
|
|
569
|
+
computeJobBudgetMs,
|
|
570
|
+
classifyBudgetKill,
|
|
571
|
+
isJobBudgetExempt,
|
|
572
|
+
shouldKillForBudget,
|
|
573
|
+
resolveBudgetKillOutcome,
|
|
574
|
+
findStrandedInvestigations,
|
|
575
|
+
computeDepHistorySatisfaction,
|
|
576
|
+
};
|