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,1011 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler/reviewLadder.cjs — needs_review / auto-fix / auto-reset eligibility
|
|
3
|
+
* predicates, selectors and their verdict sets + thresholds. Extracted verbatim
|
|
4
|
+
* from scheduler.cjs; the queue-mutating ladder rungs stay there and call in.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const schedulerPaths = require('../lib/scheduler/schedulerPaths.cjs');
|
|
11
|
+
const jobWorktree = require('../lib/git/jobWorktree.cjs');
|
|
12
|
+
const { classifyRunOutcome } = require('../lib/scheduler/reaperHelpers.cjs');
|
|
13
|
+
const { COMPLETED_EQUIVALENT_VERDICTS } = require('../lib/scheduler/terminalRunOutcome.cjs');
|
|
14
|
+
const { resolveIsFixPlan } = require('../lib/scheduler/fixPlanSlug.cjs');
|
|
15
|
+
const { GATE_AUTHORITY_VERDICTS } = require('../lib/scheduler/gateAuthority.cjs');
|
|
16
|
+
const { DEFAULT_PROJECT_CWD } = require('../lib/scheduler/schedulerBatch.cjs');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resume-first recovery (PRD 1111). A job parked in needs_review with verdict
|
|
20
|
+
* 'uncommitted_changes' has a live claude session (job.sessionId, minted by
|
|
21
|
+
* spawnJob's `--session-id`) that already has full context of the work it
|
|
22
|
+
* left uncommitted — resuming it via `claude -p --resume <sessionId>` lets it
|
|
23
|
+
* finish its own finish-protocol COMMIT step, instead of spawnInvestigation
|
|
24
|
+
* cold-reading the log to author a fix-plan PRD that a FRESH session then has
|
|
25
|
+
* to re-derive that same context for. Pure/no I/O so the eligibility rule can
|
|
26
|
+
* be unit-tested directly, matching classifyFailureOutcome/commitGuardVerdict.
|
|
27
|
+
*
|
|
28
|
+
* Bounded to exactly one attempt via job.resumeRecoveryAttempted, stamped
|
|
29
|
+
* atomically with the 'running' transition inside spawnJob's own dispatch
|
|
30
|
+
* mutate (see spawnJob) — never here — so a crash between this function
|
|
31
|
+
* returning a target and the resume child actually spawning cannot leave the
|
|
32
|
+
* job re-eligible.
|
|
33
|
+
*
|
|
34
|
+
* Kill-switch: SM_RESUME_RECOVERY_DISABLE=1 restores today's behaviour
|
|
35
|
+
* exactly (always returns null), mirroring SM_RCA_DISABLE/SM_DOD_DISABLE.
|
|
36
|
+
*/
|
|
37
|
+
function selectResumeRecoveryTarget(job) {
|
|
38
|
+
if (process.env.SM_RESUME_RECOVERY_DISABLE === '1') return null;
|
|
39
|
+
if (!job || job.status !== 'needs_review') return null;
|
|
40
|
+
if (job.verifierVerdict !== 'uncommitted_changes') return null;
|
|
41
|
+
if (typeof job.sessionId !== 'string' || job.sessionId.length === 0) return null;
|
|
42
|
+
if (job.resumeRecoveryAttempted === true) return null;
|
|
43
|
+
const dirtyPaths = Array.isArray(job.uncommittedPaths)
|
|
44
|
+
? job.uncommittedPaths.filter((p) => typeof p === 'string' && p.length > 0)
|
|
45
|
+
: [];
|
|
46
|
+
if (!dirtyPaths.length) return null;
|
|
47
|
+
return { slug: job.slug, sessionId: job.sessionId, dirtyPaths, salvagePatch: job.salvagePatch || null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mechanical recovery (PRD 1130). isFixPlanBeyondDepthCap (below) is the
|
|
52
|
+
* ONLY gate on re-investigating a fix-plan job at investigationDepth >= 2 —
|
|
53
|
+
* correct for open-ended "author another plan" recursion, but it also
|
|
54
|
+
* strands a depth-capped job whose failure was fully mechanical (no
|
|
55
|
+
* judgement required) with no other ladder rung, since resume-first recovery
|
|
56
|
+
* (selectResumeRecoveryTarget above) is hard-gated on verdict
|
|
57
|
+
* 'uncommitted_changes'. This rung is evaluated INDEPENDENTLY of
|
|
58
|
+
* isFixPlanBeyondDepthCap — depth never disqualifies it, because unlike
|
|
59
|
+
* auto-fix it authors no plan and spawns no model; it is pure git.
|
|
60
|
+
*
|
|
61
|
+
* The closed set of mechanically-resolvable verdicts starts at exactly
|
|
62
|
+
* 'worktree_integration_failed', which covers two subtypes (job.
|
|
63
|
+
* integrationFailureKind): `blocking_paths` — PRD 1125 taught integrateBranch
|
|
64
|
+
* to parse git's "would be overwritten by merge" stderr, verify the blocking
|
|
65
|
+
* paths are byte-identical to the branch, discard the proven duplicates and
|
|
66
|
+
* retry once, so a re-call can genuinely succeed once the dirty files are
|
|
67
|
+
* gone — and `content_conflict`, which PRD 1125 explicitly does NOT handle
|
|
68
|
+
* (parseBlockingMergePaths returns null for it). Only pure-addition text
|
|
69
|
+
* conflicts are auto-resolved (mc-02, gitWorktree.cjs, kill switch
|
|
70
|
+
* SM_PURE_ADDITION_MERGE_DISABLE=1) — and that already ran inside the
|
|
71
|
+
* original failed integration. Re-calling integrateBranch for a content
|
|
72
|
+
* conflict that survived it, against an unmoved base HEAD, fails
|
|
73
|
+
* deterministically, so the selector skips it (WITHOUT spending the one
|
|
74
|
+
* attempt) until HEAD moves — a sibling merge may make the retry succeed.
|
|
75
|
+
* A job parked with this verdict has its `sm-job/<slug>` branch preserved
|
|
76
|
+
* (cleanupJobWorktree's `keepBranch: !integration.ok`).
|
|
77
|
+
*
|
|
78
|
+
* Bounded to exactly one attempt via job.mechanicalRecoveryAttempted,
|
|
79
|
+
* stamped in the SAME mutate as the outcome (performMechanicalRecovery,
|
|
80
|
+
* below) — never here — so this selector alone can be unit-tested exactly
|
|
81
|
+
* like selectResumeRecoveryTarget/selectLeftoverQuarantineTarget.
|
|
82
|
+
*
|
|
83
|
+
* Kill-switch: SM_MECHANICAL_RECOVERY_DISABLE=1 restores today's behaviour
|
|
84
|
+
* exactly (always returns null), mirroring SM_RESUME_RECOVERY_DISABLE.
|
|
85
|
+
*/
|
|
86
|
+
const MECHANICALLY_RESOLVABLE_VERDICTS = new Set(['worktree_integration_failed']);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True when a retry is provably futile: a content_conflict whose base HEAD is
|
|
90
|
+
* the same as at failure time. Unknown state (no stamp, null current HEAD)
|
|
91
|
+
* is NOT futile — falls back to attempting the retry.
|
|
92
|
+
*/
|
|
93
|
+
function isMechanicalRecoveryFutile(job, currentHeadSha) {
|
|
94
|
+
return job.integrationFailureKind === 'content_conflict'
|
|
95
|
+
&& !!job.integrationBaseHeadSha
|
|
96
|
+
&& !!currentHeadSha
|
|
97
|
+
&& job.integrationBaseHeadSha === currentHeadSha;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
|
|
101
|
+
if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
|
|
102
|
+
if (!job || job.status !== 'needs_review') return null;
|
|
103
|
+
if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
|
|
104
|
+
if (job.mechanicalRecoveryAttempted === true) return null;
|
|
105
|
+
if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
|
|
106
|
+
const cwd = job.cwd || DEFAULT_PROJECT_CWD;
|
|
107
|
+
return {
|
|
108
|
+
slug: job.slug,
|
|
109
|
+
cwd,
|
|
110
|
+
branch: jobWorktree.branchNameFor(job.slug),
|
|
111
|
+
carriedPaths: job.carriedPaths || [],
|
|
112
|
+
baseBranch: job.worktreeBaseBranch || null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pure predicate: a needs_review row parked as shared_tree_reverted that
|
|
118
|
+
* carries a landedCommit — the only shape reverifyNeedsReview can re-check
|
|
119
|
+
* against ground truth (landedCommitIsAncestorOfHead). Deliberately
|
|
120
|
+
* independent of autoFixAttempted: 1229-fo-03 was parked with
|
|
121
|
+
* autoFixAttempted:true and no autoFixOutcome (isStrandedAutoFixPark shape),
|
|
122
|
+
* yet isStrandedAutoFixPark could not release it — that ladder only resolves
|
|
123
|
+
* once job.looksDone is set, and reverifyNeedsReview computes looksDone only
|
|
124
|
+
* for isRescanCandidate / isGuardParkedWithoutAutoFix rows, neither of which
|
|
125
|
+
* a shared_tree_reverted + autoFixAttempted row is.
|
|
126
|
+
*/
|
|
127
|
+
function isStaleSharedTreeRevertedPark(job) {
|
|
128
|
+
return !!job && job.status === 'needs_review'
|
|
129
|
+
&& job.verifierVerdict === 'shared_tree_reverted'
|
|
130
|
+
&& typeof job.landedCommit === 'string' && job.landedCommit.length > 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The fix-plan slug spawnInvestigation authors for a given failed job —
|
|
135
|
+
* shared by selectAutoFixTargets (queue-membership check) and
|
|
136
|
+
* reverifyNeedsReview's autofix_plan_unqueued annotation (same check, run
|
|
137
|
+
* one or more passes later) so both use one slug formula.
|
|
138
|
+
*/
|
|
139
|
+
function fixSlugFor(job) {
|
|
140
|
+
return `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Transcript-scan verdicts that re-running verifyRun can re-evaluate. NOT
|
|
144
|
+
// 'uncommitted_changes' — that comes from the git commit-guard, which verifyRun
|
|
145
|
+
// does not inspect, so re-scanning it would always return 'clean' and wrongly
|
|
146
|
+
// heal a genuinely-unfinished job. 'no_verdict_sentinel' is included because
|
|
147
|
+
// its raising condition (sentinel === null && !committedDuringRun) depends on
|
|
148
|
+
// commit-detection, which committedInWindow() can fix retroactively (e.g. the
|
|
149
|
+
// git-log --all scan added for missed non-HEAD commits) — rescanning lets a
|
|
150
|
+
// job whose commit is now correctly detected clear on its own instead of
|
|
151
|
+
// staying stuck in needs_review forever. 'pass_no_commit' is included because
|
|
152
|
+
// verifyRun now exempts fix-plan jobs (slug ^\d+-fix-) from that check — a
|
|
153
|
+
// fix-plan job flagged before that exemption shipped gets a genuinely
|
|
154
|
+
// different verdict on rescan (2026-07-12: false-positive cascade where
|
|
155
|
+
// investigation jobs correctly found "nothing to fix" but were flagged
|
|
156
|
+
// anyway). For non-fix-plan jobs the exemption never applies, so rescanning
|
|
157
|
+
// their pass_no_commit verdict is a harmless no-op (same facts, same verdict).
|
|
158
|
+
const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable', 'no_verdict_sentinel', 'abandoned_background_task', 'pass_no_commit', 'pass_no_commit_already_shipped']);
|
|
159
|
+
// RESCANNABLE_VERDICTS is a HINT, not a gate: it names the verdicts whose
|
|
160
|
+
// recovery rung is a transcript re-verification (verifyRun). Every other
|
|
161
|
+
// needs_review verdict is still a heal candidate (isRescanCandidate) — it just
|
|
162
|
+
// gets the evidence-only rung (computeLooksDone) instead of a transcript
|
|
163
|
+
// rescan, because verifyRun cannot see a commit-guard / shared-tree verdict and
|
|
164
|
+
// would return 'clean' and falsely heal it.
|
|
165
|
+
|
|
166
|
+
// The ONLY needs_review verdicts NOT eligible for the periodic heal ladder.
|
|
167
|
+
// An allow-list here was reopened three times (2026-09-12 x2, 2026-09-18
|
|
168
|
+
// shared_tree_reverted) because a new park reason was born invisible to
|
|
169
|
+
// self-healing. Add a verdict here only with a one-line proof that no
|
|
170
|
+
// re-verification or evidence scan can ever change it.
|
|
171
|
+
const RESCAN_EXCLUDED_VERDICTS = new Set([
|
|
172
|
+
// Commit-guard verdict verifyRun never inspects: a rescan returns 'clean' and would heal genuinely unfinished work.
|
|
173
|
+
'uncommitted_changes',
|
|
174
|
+
// Its damage IS a commit stranded on an unmerged sm-job branch — a landedCommit restates it; selectMechanicalRecoveryTarget owns the real re-merge.
|
|
175
|
+
'worktree_integration_failed',
|
|
176
|
+
// The run overran its own time/cost estimate; no transcript or git evidence can un-overrun it (selectAutoFixTargets excludes it too).
|
|
177
|
+
'budget_exceeded',
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// Per-pass / per-row bounds on the evidence-only rung (the widened candidate
|
|
181
|
+
// set). Each scan costs one computeLooksDone: a per-cwd-deduped `git fetch`
|
|
182
|
+
// (<=~20s) + a git log. Unbounded, a backlog of N parked rows would pay N of
|
|
183
|
+
// those every 10 minutes forever.
|
|
184
|
+
const REVERIFY_INTERVAL_MS = 10 * 60_000;
|
|
185
|
+
const EVIDENCE_SCAN_MAX_PER_PASS = 20;
|
|
186
|
+
const EVIDENCE_SCAN_MIN_INTERVAL_MS = 6 * REVERIFY_INTERVAL_MS;
|
|
187
|
+
|
|
188
|
+
// Bounds fix-plan recursion: cap N permits at most N+1 fix jobs per original
|
|
189
|
+
// slug (depth 1 = the original job, depth 2 = its `-fix`, depth 3+ is
|
|
190
|
+
// excluded). With N=1 that's `<slug>-fix` and `<slug>-fix-fix`, never a third
|
|
191
|
+
// `-fix-fix-fix`. Lowered from 2 to 1 on 2026-08-31 (starry-night-ships):
|
|
192
|
+
// three concurrent chains (115-fix-fix, 113-fix-fix, 111-fix-fix-fix) were
|
|
193
|
+
// riding the old cap, and 115-fix-fix's own root-cause section read "The
|
|
194
|
+
// code was already CORRECT. Only verification and commit failed." — a third
|
|
195
|
+
// auto-retry re-runs an entire PRD and test battery to redo a `git commit`,
|
|
196
|
+
// at near-zero marginal success probability. Shared by selectAutoFixTargets
|
|
197
|
+
// and spawnInvestigation so both call sites agree on one threshold.
|
|
198
|
+
const MAX_INVESTIGATION_DEPTH = 1;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* True when a fix-plan job's investigationDepth is at or past the recursion
|
|
202
|
+
* cap and it must be excluded from auto-fix eligibility. A fix-plan job with
|
|
203
|
+
* no recorded investigationDepth (a job already in the queue before this
|
|
204
|
+
* depth tracking shipped) is treated as excluded too, preserving the
|
|
205
|
+
* pre-existing blanket-exclusion behavior for legacy jobs — no retroactive
|
|
206
|
+
* migration. Non-fix-plan jobs are never capped here.
|
|
207
|
+
*
|
|
208
|
+
* `isFixPlan` (PRD 1131) is the job's own persisted classification stamp
|
|
209
|
+
* (see lib/scheduler/fixPlanSlug.cjs's resolveIsFixPlan) — an explicit true/false wins
|
|
210
|
+
* over the slug; only a row with the field entirely absent (persisted
|
|
211
|
+
* before this change shipped) falls back to the legacy slug-only heuristic.
|
|
212
|
+
* Exported for tests.
|
|
213
|
+
*/
|
|
214
|
+
function isFixPlanBeyondDepthCap(slug, investigationDepth, isFixPlan) {
|
|
215
|
+
if (!resolveIsFixPlan(slug, isFixPlan)) return false;
|
|
216
|
+
if (investigationDepth == null) return true;
|
|
217
|
+
return investigationDepth >= MAX_INVESTIGATION_DEPTH + 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Backfill a job's missing runId by scanning RUNS_DIR for a run directory
|
|
222
|
+
* whose contents reference this job's slug (a '<slug>.log' file inside it).
|
|
223
|
+
* A needs_review job can lose its runId (e.g. an old queue-schema gap) and
|
|
224
|
+
* become invisible to every auto-resolution path that requires job.runId
|
|
225
|
+
* truthy. O(number of run dirs) — a single readdir + per-dir existsSync
|
|
226
|
+
* check, no nested loop over user-scaled data. Dir names are ISO timestamps,
|
|
227
|
+
* so lexical-descending sort picks the newest match. Exported for tests.
|
|
228
|
+
*/
|
|
229
|
+
function resolveRunId(job, { runsDir = schedulerPaths.runsDir() } = {}) {
|
|
230
|
+
if (!job || job.runId) return job?.runId || null;
|
|
231
|
+
if (!job.slug) return null;
|
|
232
|
+
let dirs;
|
|
233
|
+
try {
|
|
234
|
+
dirs = fs.readdirSync(runsDir);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const matches = dirs.filter((d) => {
|
|
239
|
+
try {
|
|
240
|
+
return fs.existsSync(path.join(runsDir, d, `${job.slug}.log`));
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
if (!matches.length) return null;
|
|
246
|
+
matches.sort().reverse();
|
|
247
|
+
return matches[0];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Pure predicate: a needs_review job with no runId and no backfillable run
|
|
252
|
+
* directory can never self-heal or get an auto-fix investigation — it must
|
|
253
|
+
* be surfaced for a human rather than silently dead-ended. Exported for
|
|
254
|
+
* tests.
|
|
255
|
+
*/
|
|
256
|
+
function isUnresolvableNeedsReview(job, { hasRunDir }) {
|
|
257
|
+
return !!job && job.status === 'needs_review' && !job.runId && !hasRunDir;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Pure predicate: an auto-fix attempt whose one bounded retry is already
|
|
262
|
+
* spent, and whose outcome was NOT 'plan' (a produced plan is judged by
|
|
263
|
+
* isPlanUnqueued instead — a plan is never retried, only annotated if it
|
|
264
|
+
* never reaches the queue). Covers all three exhaustible outcomes
|
|
265
|
+
* ('no-plan', 'error', and unstamped/undefined) — mirrors the retry
|
|
266
|
+
* eligibility rule in selectAutoFixTargets so a job can never be retry-
|
|
267
|
+
* eligible there and simultaneously un-annotatable here.
|
|
268
|
+
*
|
|
269
|
+
* A parent stamped `autoFixReopened: true` (its dead fix-plan child earned
|
|
270
|
+
* it exactly one further attempt — see isFixPlanDead) is a separate
|
|
271
|
+
* exhaustion path: it is spent as soon as that second investigation
|
|
272
|
+
* concludes with ANY outcome, including another 'plan' — a reopened parent
|
|
273
|
+
* never gets a third attempt, so unlike the fresh case a 'plan' outcome does
|
|
274
|
+
* not exempt it here.
|
|
275
|
+
*/
|
|
276
|
+
function isExhaustedAutoFix(job) {
|
|
277
|
+
if (!job || job.status !== 'needs_review' || job.autoFixAttempted !== true) return false;
|
|
278
|
+
if (job.autoFixReopened === true) return job.autoFixOutcome != null;
|
|
279
|
+
return job.autoFixOutcome !== 'plan' && (job.autoFixRetries ?? 0) >= 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Verdicts from the POST-RUN GUARDS (commit-guard / shared-tree guard) that a
|
|
284
|
+
* later, independently-checkable commit/looksDone signal can meaningfully
|
|
285
|
+
* confirm or refute. Auto-fix investigations only ever launch for FAILING
|
|
286
|
+
* runs (see isExhaustedAutoFix above and selectAutoFixTargets) — a job parked
|
|
287
|
+
* by one of these GUARD verdicts exits 0 and never has autoFixAttempted set,
|
|
288
|
+
* so it is invisible to isExhaustedAutoFix and can sit in needs_review
|
|
289
|
+
* forever with nothing to spend and nothing to exhaust (PRD 1181, 2026-09-12:
|
|
290
|
+
* exit 0, commit aff5607 landed, parked on a shared-tree verdict, cleared
|
|
291
|
+
* only by a human).
|
|
292
|
+
*
|
|
293
|
+
* 'worktree_integration_failed' is deliberately EXCLUDED — its damage IS a
|
|
294
|
+
* commit: one stranded on an unmerged `sm-job/<slug>` branch. A `landedCommit`
|
|
295
|
+
* existing is not evidence against that verdict, it is a restatement of it,
|
|
296
|
+
* so admitting it here would auto-complete a row whose work never actually
|
|
297
|
+
* reached the target branch. It already has its own dedicated, git-native
|
|
298
|
+
* resolution path (selectMechanicalRecoveryTarget / performMechanicalRecovery
|
|
299
|
+
* — a real re-attempted merge) and must never be pulled into this ladder.
|
|
300
|
+
*
|
|
301
|
+
* 'pidless_reap_with_landed_commit' (PRD 1173, resolvePidlessFailureOverride
|
|
302
|
+
* in reaperHelpers.cjs) is included: it parks on the exact same shape (exit
|
|
303
|
+
* never observed / no autoFixAttempted, real landedCommit evidence) as
|
|
304
|
+
* 'silent_no_op' and 'shared_tree_reverted', and this ladder never trusts
|
|
305
|
+
* landedCommit alone anyway — applyNeedsReviewAutoResolve only resolves once
|
|
306
|
+
* job.looksDone independently reconfirms via a fresh commits-since-this-run
|
|
307
|
+
* scan, which is exactly the "does the commit correspond to THIS dispatch"
|
|
308
|
+
* re-verification resolvePidlessFailureOverride's own header says the
|
|
309
|
+
* pidless-reap path itself cannot do. Omitting it here reproduces the same
|
|
310
|
+
* "nothing to spend, nothing to exhaust" needs_review stall this PRD exists
|
|
311
|
+
* to fix, just for a third verdict.
|
|
312
|
+
*/
|
|
313
|
+
const GUARD_VERDICT_EVIDENCE_ELIGIBLE = new Set(['silent_no_op', 'shared_tree_reverted', 'pidless_reap_with_landed_commit']);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Pure predicate, no I/O: a needs_review row parked directly by one of the
|
|
317
|
+
* GUARD_VERDICT_EVIDENCE_ELIGIBLE verdicts, that never went through an
|
|
318
|
+
* auto-fix investigation at all (autoFixAttempted is not true) — the
|
|
319
|
+
* structural gap this PRD closes, distinct from isExhaustedAutoFix's "went
|
|
320
|
+
* through auto-fix and spent it" case. A job that DID get an auto-fix
|
|
321
|
+
* investigation is left to isExhaustedAutoFix's own ladder rather than this
|
|
322
|
+
* one, even if its verifierVerdict happens to also be in the eligible set.
|
|
323
|
+
* Exported for tests.
|
|
324
|
+
*/
|
|
325
|
+
function isGuardParkedWithoutAutoFix(job) {
|
|
326
|
+
if (!job || job.status !== 'needs_review') return false;
|
|
327
|
+
if (job.autoFixAttempted === true) return false;
|
|
328
|
+
return GUARD_VERDICT_EVIDENCE_ELIGIBLE.has(job.verifierVerdict);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Pure predicate, no I/O: a needs_review row whose auto-fix investigation
|
|
333
|
+
* genuinely ran (autoFixAttempted === true) but whose outcome was NEVER
|
|
334
|
+
* durably stamped at all — and that has nothing left in flight to wait on:
|
|
335
|
+
* no live/queued fix-plan row at fixSlugFor(job).
|
|
336
|
+
*
|
|
337
|
+
* Distinct from isExhaustedAutoFix, which requires autoFixRetries >= 1 to
|
|
338
|
+
* have already accumulated. spawnInvestigation's onExit handler restores the
|
|
339
|
+
* job's status from 'investigating' back to needs_review in ONE mutate()
|
|
340
|
+
* call (scheduler.cjs's spawnInvestigation, source
|
|
341
|
+
* 'spawnInvestigation:onExit') and stamps autoFixOutcome ('plan' / 'no-plan'
|
|
342
|
+
* / 'error') in a SEPARATE, later mutate() call — an app restart or process
|
|
343
|
+
* death between the two leaves autoFixOutcome permanently unset, with
|
|
344
|
+
* autoFixRetries never incremented either, so isExhaustedAutoFix never fires
|
|
345
|
+
* and the row falls through every existing resolving door forever, re-scanned
|
|
346
|
+
* by the periodic reverify pass against the same frozen transcript with no
|
|
347
|
+
* new outcome to observe.
|
|
348
|
+
*
|
|
349
|
+
* Job 1218-fo-01 (2026-09-13, findings filed at
|
|
350
|
+
* session-manager-operations/reviews/2026-09-13-scheduler-stability-investigation.md,
|
|
351
|
+
* "post-run adjudication" section) sat exactly in this state: needs_review,
|
|
352
|
+
* verifierVerdict transcript_errors, autoFixAttempted: true, autoFixOutcome:
|
|
353
|
+
* undefined, autoFixRetries: undefined, statusHistory ending in
|
|
354
|
+
* "investigation probe exited — restoring prior status" — with a landed
|
|
355
|
+
* commit no existing ladder rung would credit.
|
|
356
|
+
*
|
|
357
|
+
* Deliberately narrower than "unset, 'error', or 'no-plan'": a row that DID
|
|
358
|
+
* get a durably-stamped 'error'/'no-plan' outcome with its one bounded retry
|
|
359
|
+
* still unspent (autoFixRetries < 1) is exactly the row
|
|
360
|
+
* selectAutoFixTargets's own retryEligible check still owns and will retry
|
|
361
|
+
* on its own — pulling it into THIS ladder instead would race it away from
|
|
362
|
+
* that retry (scheduler-needs-review-autoresolve.test.cjs's "a non-exhausted
|
|
363
|
+
* needs_review row … is left alone" guards exactly this). Only the
|
|
364
|
+
* outcome-truly-never-stamped case is structurally unrecoverable by any
|
|
365
|
+
* OTHER existing door, because nothing ever wrote a value selectAutoFixTargets
|
|
366
|
+
* or isExhaustedAutoFix could act on.
|
|
367
|
+
* Exported for tests.
|
|
368
|
+
*/
|
|
369
|
+
function isStrandedAutoFixPark(job, jobsInProject) {
|
|
370
|
+
if (!job || job.status !== 'needs_review') return false;
|
|
371
|
+
if (job.autoFixAttempted !== true) return false;
|
|
372
|
+
if (job.autoFixOutcome != null) return false;
|
|
373
|
+
const fixSlug = fixSlugFor(job);
|
|
374
|
+
const liveOrQueuedChild = (jobsInProject || []).some(
|
|
375
|
+
(j) => j.slug === fixSlug && j.status !== 'completed' && !DEAD_FIX_CHILD_STATUSES.has(j.status),
|
|
376
|
+
);
|
|
377
|
+
return !liveOrQueuedChild;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Pure predicate, no I/O: is this needs_review row eligible for the bounded
|
|
382
|
+
* auto-resolve ladder at all — either because its auto-fix path is genuinely
|
|
383
|
+
* spent (isExhaustedAutoFix), because it was parked by a GUARD verdict that
|
|
384
|
+
* never entered auto-fix in the first place (isGuardParkedWithoutAutoFix),
|
|
385
|
+
* or because its auto-fix investigation ran but was stranded before
|
|
386
|
+
* recording any outcome (isStrandedAutoFixPark). All three classes share ONE
|
|
387
|
+
* ladder (applyNeedsReviewAutoResolve) rather than a duplicated one — the
|
|
388
|
+
* ladder itself doesn't care which door a row came through, only whether it
|
|
389
|
+
* now carries completion evidence (job.looksDone). `jobsInProject` is only
|
|
390
|
+
* consulted by isStrandedAutoFixPark (to check for a live/queued fix-plan
|
|
391
|
+
* child) and defaults to empty so existing single-arg callers are unaffected.
|
|
392
|
+
* Exported for tests.
|
|
393
|
+
*/
|
|
394
|
+
function isEligibleForNeedsReviewAutoResolve(job, jobsInProject = []) {
|
|
395
|
+
return isExhaustedAutoFix(job) || isGuardParkedWithoutAutoFix(job) || isStrandedAutoFixPark(job, jobsInProject);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Pure predicate: an investigation produced a fix plan (autoFixOutcome ===
|
|
400
|
+
* 'plan') but its fix-plan slug is not present among `queuedSlugs` — the
|
|
401
|
+
* plan file failed to become a queue row (write failure, or some other
|
|
402
|
+
* reason reconcile() never ingested it). `queuedSlugs` is the caller's
|
|
403
|
+
* current job-slug set, passed in rather than read here so this stays pure.
|
|
404
|
+
*/
|
|
405
|
+
function isPlanUnqueued(job, queuedSlugs) {
|
|
406
|
+
if (!job || job.status !== 'needs_review') return false;
|
|
407
|
+
if (job.autoFixOutcome !== 'plan') return false;
|
|
408
|
+
return !queuedSlugs.has(fixSlugFor(job));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Terminal-and-not-completed statuses a fix-plan child can die in — see
|
|
412
|
+
// isFixPlanDead.
|
|
413
|
+
const DEAD_FIX_CHILD_STATUSES = new Set(['needs_review', 'failed', 'quarantined', 'skipped']);
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Pure predicate: a parent stuck at outcome 'plan' whose own fix-plan child
|
|
417
|
+
* (fixSlugFor(job)) has ITSELF died — reached a terminal non-completed
|
|
418
|
+
* status — with nothing left in the ladder that will ever revisit either
|
|
419
|
+
* row again (selectAutoFixTargets skips a 'plan' outcome outright, and
|
|
420
|
+
* isPlanUnqueued only fires when the child never reached the queue at all,
|
|
421
|
+
* which isn't true once a dead child row exists). `job.autoFixReopened`
|
|
422
|
+
* gates this to exactly once per parent — once stamped, this always returns
|
|
423
|
+
* false so the parent can never be reopened a second time. Exported for
|
|
424
|
+
* tests.
|
|
425
|
+
*/
|
|
426
|
+
function isFixPlanDead(job, jobsInProject) {
|
|
427
|
+
if (!job || job.status !== 'needs_review') return false;
|
|
428
|
+
if (job.autoFixOutcome !== 'plan') return false;
|
|
429
|
+
if (job.autoFixReopened === true) return false;
|
|
430
|
+
const fixSlug = fixSlugFor(job);
|
|
431
|
+
const child = (jobsInProject || []).find((j) => j.slug === fixSlug);
|
|
432
|
+
if (!child) return false;
|
|
433
|
+
return DEAD_FIX_CHILD_STATUSES.has(child.status);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Pure predicate: is this job eligible for the boot re-verify self-heal? Only
|
|
438
|
+
* needs_review jobs with a run log (own or backfilled via resolveRunId) AND a
|
|
439
|
+
* transcript-scan verdict. Crucially EXCLUDES 'uncommitted_changes' (git
|
|
440
|
+
* commit-guard) — verifyRun can't see git, so re-scanning it would falsely
|
|
441
|
+
* heal an unfinished job. Exported for tests.
|
|
442
|
+
*/
|
|
443
|
+
/**
|
|
444
|
+
* healRefusalReason(job, verdict, committedDuringRun) → string | null
|
|
445
|
+
*
|
|
446
|
+
* Guards reverifyNeedsReview's needs_review → completed self-heal. Returns a
|
|
447
|
+
* refusal reason when the heal must NOT happen, or null to allow it.
|
|
448
|
+
*
|
|
449
|
+
* THE INCIDENT (PRD 983, 2026-08-03). Job 972 ran 34 s, edited zero files and
|
|
450
|
+
* exited 0. verifyRun correctly raised `no_verdict_sentinel` (no commit AND no
|
|
451
|
+
* SCHEDULER_VERDICT sentinel) with downgradeTo 'needs_review', and the job was
|
|
452
|
+
* parked. One minute later this self-heal pass promoted it to `completed`, and
|
|
453
|
+
* that false green shipped to the user.
|
|
454
|
+
*
|
|
455
|
+
* The mechanism is subtle and worth spelling out, because the individual parts
|
|
456
|
+
* all look correct: `no_verdict_sentinel` is in RESCANNABLE_VERDICTS, so the
|
|
457
|
+
* job was re-verified. The rescan recomputes `committedDuringRun` via
|
|
458
|
+
* `committedInWindow`, which runs `git log --all --since=<startedAt>
|
|
459
|
+
* --until=<finishedAt+60s>` with NO author, message, or slug filter — it
|
|
460
|
+
* answers "did ANY commit land in this repo during that window", not "did THIS
|
|
461
|
+
* job commit". With up to 3 concurrent scheduler jobs plus interactive
|
|
462
|
+
* sessions sharing one repo, a sibling job's commit lands inside the window
|
|
463
|
+
* routinely. `committedDuringRun` therefore flipped to true on the rescan,
|
|
464
|
+
* runVerify's `if (sentinel === null && !committedDuringRun)` no longer fired,
|
|
465
|
+
* `no_verdict_sentinel` was never re-raised, the issue list came back empty,
|
|
466
|
+
* and the verdict concluded `clean` — a COMPLETED_EQUIVALENT verdict, so the
|
|
467
|
+
* job healed. Another job's commit was silently credited to this one.
|
|
468
|
+
*
|
|
469
|
+
* The rule: a run that emitted no sentinel and landed no commit ATTRIBUTABLE
|
|
470
|
+
* TO ITSELF has produced no evidence it did anything, and repo-wide
|
|
471
|
+
* time-window evidence is not attribution. `landedCommit` is per-job and
|
|
472
|
+
* survives a resetJob (resetJobFields), so it is the honest signal here.
|
|
473
|
+
* Bias to needs_review: a false yellow costs a human glance, a false green
|
|
474
|
+
* costs a silently-unfixed bug — which is exactly what happened.
|
|
475
|
+
*/
|
|
476
|
+
// abandoned_background_task shares no_verdict_sentinel's exact rescan path
|
|
477
|
+
// (same "sentinel === null && !commitEvidence" gate in runVerify, same
|
|
478
|
+
// committedDuringRun repo-wide-not-per-job attribution problem) — the PRD 983
|
|
479
|
+
// incident mechanism above applies identically, so it gets the same guard
|
|
480
|
+
// rather than a carve-out that would silently reopen the same false-heal hole.
|
|
481
|
+
const NO_ATTRIBUTABLE_COMMIT_VERDICTS = new Set(['no_verdict_sentinel', 'abandoned_background_task']);
|
|
482
|
+
|
|
483
|
+
function healRefusalReason(job, verdict, committedDuringRun) {
|
|
484
|
+
if (!job || !verdict) return null;
|
|
485
|
+
if (!COMPLETED_EQUIVALENT_VERDICTS.has(verdict.verdict)) return null;
|
|
486
|
+
if (!NO_ATTRIBUTABLE_COMMIT_VERDICTS.has(job.verifierVerdict)) return null;
|
|
487
|
+
// A commit this job actually recorded as its own is real evidence; the
|
|
488
|
+
// repo-wide window scan is not.
|
|
489
|
+
if (job.landedCommit) return null;
|
|
490
|
+
return `${job.verifierVerdict} with no job-attributable commit — refusing to heal`
|
|
491
|
+
+ ` (committedInWindow=${committedDuringRun === true} is repo-wide, not proof this job delivered)`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* True when a `failed` job's failure is unverified-shaped — no result event
|
|
496
|
+
* was ever recorded for its run (classifyRunOutcome === 'no_result'), so no
|
|
497
|
+
* SCHEDULER_VERDICT sentinel could have been parsed either, OR it already
|
|
498
|
+
* carries a RESCANNABLE_VERDICTS verifierVerdict. A row that failed with a
|
|
499
|
+
* real result event (classifyRunOutcome === 'failed', i.e. a genuine red
|
|
500
|
+
* gate or a real non-zero-exit error) is excluded — that failure is
|
|
501
|
+
* evidence, not silence, and must never become a heal candidate (PRD 1102).
|
|
502
|
+
*/
|
|
503
|
+
function isFailedUnverifiedShaped(job) {
|
|
504
|
+
if (!job || job.status !== 'failed') return false;
|
|
505
|
+
if (job.verifierVerdict && RESCANNABLE_VERDICTS.has(job.verifierVerdict)) return true;
|
|
506
|
+
const runId = job.runId || resolveRunId(job);
|
|
507
|
+
if (!runId) return false;
|
|
508
|
+
const logPath = path.join(schedulerPaths.runsDir(), runId, `${job.slug}.log`);
|
|
509
|
+
return classifyRunOutcome(logPath) === 'no_result';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isRescanCandidate(job) {
|
|
513
|
+
if (!job) return false;
|
|
514
|
+
// Default-ELIGIBLE: every needs_review row is a heal candidate unless its
|
|
515
|
+
// verdict is in RESCAN_EXCLUDED_VERDICTS. No runId requirement here — a row
|
|
516
|
+
// without one still gets the evidence rung and the unresolvable annotation.
|
|
517
|
+
if (job.status === 'needs_review') return !RESCAN_EXCLUDED_VERDICTS.has(job.verifierVerdict);
|
|
518
|
+
if (!(job.runId || resolveRunId(job))) return false;
|
|
519
|
+
if (job.status === 'failed') return isFailedUnverifiedShaped(job);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Which rung a needs_review candidate gets (RESCANNABLE_VERDICTS as a hint):
|
|
525
|
+
* true = transcript re-verification (needs a run dir to read); false = the
|
|
526
|
+
* evidence-only rung. I/O only when a rescannable-verdict row lacks a runId.
|
|
527
|
+
*/
|
|
528
|
+
function isTranscriptRescannable(job) {
|
|
529
|
+
return !!job && RESCANNABLE_VERDICTS.has(job.verifierVerdict) && !!(job.runId || resolveRunId(job));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Pure, no I/O: the bounded subset of evidence-only needs_review candidates
|
|
534
|
+
* reverifyNeedsReview scans this pass. Skips rows already carrying looksDone,
|
|
535
|
+
* rows scanned within EVIDENCE_SCAN_MIN_INTERVAL_MS (evidenceScannedAt), and —
|
|
536
|
+
* PRD 1136 — rows with a live auto-fix history unless they are an
|
|
537
|
+
* auto-resolve door (isEligibleForNeedsReviewAutoResolve, which is what
|
|
538
|
+
* consumes looksDone). Never-scanned rows go first, then least-recently
|
|
539
|
+
* scanned; capped at EVIDENCE_SCAN_MAX_PER_PASS. O(n log n) in needs_review rows.
|
|
540
|
+
* Per-pass cost ceiling: EVIDENCE_SCAN_MAX_PER_PASS computeLooksDone calls.
|
|
541
|
+
*/
|
|
542
|
+
function selectEvidenceScanTargets(jobs, now = Date.now()) {
|
|
543
|
+
const due = [];
|
|
544
|
+
for (const j of jobs ?? []) {
|
|
545
|
+
if (j.status !== 'needs_review' || !isRescanCandidate(j)) continue;
|
|
546
|
+
if (isTranscriptRescannable(j)) continue;
|
|
547
|
+
if (j.looksDone) continue;
|
|
548
|
+
if (j.autoFixAttempted === true && !isEligibleForNeedsReviewAutoResolve(j, jobs)) continue;
|
|
549
|
+
const last = Date.parse(j.evidenceScannedAt ?? '');
|
|
550
|
+
if (!Number.isNaN(last) && now - last < EVIDENCE_SCAN_MIN_INTERVAL_MS) continue;
|
|
551
|
+
due.push({ j, last: Number.isNaN(last) ? 0 : last });
|
|
552
|
+
}
|
|
553
|
+
due.sort((a, b) => a.last - b.last);
|
|
554
|
+
return due.slice(0, EVIDENCE_SCAN_MAX_PER_PASS).map((d) => d.j);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Cheap-guard for the 10-minute periodic reverify tick. MUST be expressed in
|
|
559
|
+
* terms of isRescanCandidate — not a hand-written status test — because the
|
|
560
|
+
* two drifted once already: the guard only looked for a `needs_review` row
|
|
561
|
+
* while isRescanCandidate had grown a `failed`/isFailedUnverifiedShaped arm,
|
|
562
|
+
* so reverifyNeedsReview's failed → needs_review looksDone branch was
|
|
563
|
+
* unreachable from the periodic path on any queue holding zero needs_review
|
|
564
|
+
* rows. A `failed` row with no run log sat wedged for five days with the
|
|
565
|
+
* self-heal that exists to inspect it never firing (only an app restart, via
|
|
566
|
+
* the unguarded boot call, could reach it). Reported 2026-09-10 from
|
|
567
|
+
* social-signals-trader.
|
|
568
|
+
*
|
|
569
|
+
* Reopened through a different door 2026-09-12 (starry-night-ships
|
|
570
|
+
* 231-saturn-record-and-docs / 243-neptune-kurama-mode): the guard above only
|
|
571
|
+
* covered isRescanCandidate (re-verification), but reverifyNeedsReview's auto-
|
|
572
|
+
* fix loop ALSO lives behind this same guard, and a `needs_review` row whose
|
|
573
|
+
* mechanical recovery already ran and failed (mechanicalRecoveryAttempted:
|
|
574
|
+
* true, verdict still 'worktree_integration_failed', not itself a
|
|
575
|
+
* RESCANNABLE_VERDICTS member) is invisible to isRescanCandidate — so the
|
|
576
|
+
* next rung (a fix-plan investigation via selectAutoFixTargets) never got a
|
|
577
|
+
* chance to fire either. Widened to OR in every live target of the recovery
|
|
578
|
+
* ladder the periodic pass actually drives (selectMechanicalRecoveryTarget /
|
|
579
|
+
* selectResumeRecoveryTarget / selectAutoFixTargets) so the guard can never
|
|
580
|
+
* again be narrower than the work reverifyNeedsReview performs.
|
|
581
|
+
*
|
|
582
|
+
* Widened again (this PRD): a `needs_review` row parked directly by a GUARD
|
|
583
|
+
* verdict with no auto-fix history (isGuardParkedWithoutAutoFix) is not an
|
|
584
|
+
* isRescanCandidate either — RESCANNABLE_VERDICTS covers transcript-verifier
|
|
585
|
+
* verdicts, not commit-guard/shared-tree-guard verdicts — but
|
|
586
|
+
* reverifyNeedsReview's looksDone-annotation pass now runs for it too (see
|
|
587
|
+
* that function). Same rule as always: never let this guard be narrower than
|
|
588
|
+
* the work reverifyNeedsReview actually performs.
|
|
589
|
+
*
|
|
590
|
+
* Reopened a THIRD time 2026-09-18 (shared_tree_reverted parked 1229-fo-03
|
|
591
|
+
* falsely, 19 of 20 pending rows held): the fix was not another OR-clause but
|
|
592
|
+
* inverting the default — isRescanCandidate is now default-ELIGIBLE for every
|
|
593
|
+
* needs_review row (RESCAN_EXCLUDED_VERDICTS names the few exceptions), so a
|
|
594
|
+
* new park reason can never again be born unhealable. The OR-clauses below
|
|
595
|
+
* are now redundant for needs_review rows and kept only for their
|
|
596
|
+
* non-needs_review inputs.
|
|
597
|
+
*
|
|
598
|
+
* Cost: selectMechanicalRecoveryTarget/selectResumeRecoveryTarget and
|
|
599
|
+
* isGuardParkedWithoutAutoFix are pure (no I/O). selectAutoFixTargets is
|
|
600
|
+
* called with an injected fixSlugExists that always returns false — cheap
|
|
601
|
+
* and deliberately over-inclusive (a false positive here just means one
|
|
602
|
+
* extra periodic pass, never a missed one) so this guard never pays
|
|
603
|
+
* selectAutoFixTargets's production fs.existsSync scan per tick.
|
|
604
|
+
* resolveRunId's IO only fires for rows missing job.runId, same as
|
|
605
|
+
* isRescanCandidate already incurs above.
|
|
606
|
+
*/
|
|
607
|
+
function shouldRunPeriodicReverify(jobs) {
|
|
608
|
+
if (!Array.isArray(jobs)) return false;
|
|
609
|
+
if (jobs.some((j) => isRescanCandidate(j) || isGuardParkedWithoutAutoFix(j) || isStaleSharedTreeRevertedPark(j))) return true;
|
|
610
|
+
if (jobs.some((j) => selectMechanicalRecoveryTarget(j) || selectResumeRecoveryTarget(j))) return true;
|
|
611
|
+
return selectAutoFixTargets(jobs, { fixSlugExists: () => false }).length > 0;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Default 24h, overridable via SM_STUCK_FAILED_ESCALATE_HOURS — same
|
|
615
|
+
// env-override shape as QUARANTINE_ESCALATE_MS above.
|
|
616
|
+
const STUCK_FAILED_ESCALATE_MS = process.env.SM_STUCK_FAILED_ESCALATE_HOURS
|
|
617
|
+
? Number(process.env.SM_STUCK_FAILED_ESCALATE_HOURS) * 60 * 60_000
|
|
618
|
+
: 24 * 60 * 60_000;
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Kill-switch gate for the stuck-failed escalation below
|
|
622
|
+
* (SM_STUCK_FAILED_ESCALATE_DISABLE=1), mirroring the
|
|
623
|
+
* SM_REVERIFY_PERIODIC_DISABLE / SM_RCA_DISABLE convention. A tiny wrapper
|
|
624
|
+
* so the disable path is unit-testable without invoking the 10-minute
|
|
625
|
+
* setInterval body directly.
|
|
626
|
+
*/
|
|
627
|
+
function stuckFailedEscalationDisabled() {
|
|
628
|
+
return process.env.SM_STUCK_FAILED_ESCALATE_DISABLE === '1';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Bounded automatic failed -> pending recovery (PRD 1151): a failed row gets
|
|
632
|
+
// up to FAILED_AUTORESET_CAP auto-reset attempts, each gated on having sat
|
|
633
|
+
// `failed` for FAILED_AUTORESET_MS, before the stuck-failed escalation below
|
|
634
|
+
// is allowed to page a human. Same env-override shape as
|
|
635
|
+
// STUCK_FAILED_ESCALATE_MS/QUARANTINE_ESCALATE_MS above.
|
|
636
|
+
const FAILED_AUTORESET_CAP = 3;
|
|
637
|
+
const FAILED_AUTORESET_MS = process.env.SM_FAILED_AUTORESET_MINUTES
|
|
638
|
+
? Number(process.env.SM_FAILED_AUTORESET_MINUTES) * 60_000
|
|
639
|
+
: 10 * 60_000;
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Kill-switch gate for the failed-autoreset pass below
|
|
643
|
+
* (SM_FAILED_AUTORESET_DISABLE=1), same shape as stuckFailedEscalationDisabled
|
|
644
|
+
* above.
|
|
645
|
+
*/
|
|
646
|
+
function failedAutoResetDisabled() {
|
|
647
|
+
return process.env.SM_FAILED_AUTORESET_DISABLE === '1';
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* selectFailedAutoResetTargets(jobs, now, thresholdMs) →
|
|
652
|
+
* [{ slug, cwd, ageMs, attempts }]
|
|
653
|
+
*
|
|
654
|
+
* Pure selector — no IO, no `require` inside the function. Selects `failed`
|
|
655
|
+
* rows whose newest statusHistory entry with `to === 'failed'` is older than
|
|
656
|
+
* `thresholdMs` and whose failedAutoResetAttempts counter hasn't yet spent
|
|
657
|
+
* FAILED_AUTORESET_CAP attempts. "Newest" (not first) matters because a row
|
|
658
|
+
* can have failed more than once across its lifetime (an earlier auto-reset
|
|
659
|
+
* attempt that itself failed again) — only the most recent failed-since
|
|
660
|
+
* timestamp should gate the next attempt.
|
|
661
|
+
*
|
|
662
|
+
* Excludes a row whose newest failed-entry came from spawnJob:fail-dirty —
|
|
663
|
+
* that source means a transient failure left genuinely uncommitted work in
|
|
664
|
+
* the job's worktree and the system already decided once, deliberately, not
|
|
665
|
+
* to auto-requeue it (see that call site's own comment: "could discard
|
|
666
|
+
* uncommitted work left by the failed run"). This bounded auto-reset is a
|
|
667
|
+
* different, slower mechanism and must not quietly override that decision
|
|
668
|
+
* 10 minutes later — a human should look at a dirty worktree before it gets
|
|
669
|
+
* re-driven.
|
|
670
|
+
*/
|
|
671
|
+
function selectFailedAutoResetTargets(jobs, now, thresholdMs) {
|
|
672
|
+
const targets = [];
|
|
673
|
+
for (const j of jobs ?? []) {
|
|
674
|
+
if (j.status !== 'failed') continue;
|
|
675
|
+
const attempts = j.failedAutoResetAttempts ?? 0;
|
|
676
|
+
if (attempts >= FAILED_AUTORESET_CAP) continue;
|
|
677
|
+
const history = j.statusHistory || [];
|
|
678
|
+
let entry = null;
|
|
679
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
680
|
+
if (history[i].to === 'failed') { entry = history[i]; break; }
|
|
681
|
+
}
|
|
682
|
+
if (!entry) continue;
|
|
683
|
+
if (entry.source === 'spawnJob:fail-dirty') continue;
|
|
684
|
+
const since = Date.parse(entry.at);
|
|
685
|
+
if (Number.isNaN(since)) continue;
|
|
686
|
+
const ageMs = now - since;
|
|
687
|
+
if (ageMs < thresholdMs) continue;
|
|
688
|
+
targets.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs, attempts });
|
|
689
|
+
}
|
|
690
|
+
return targets;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* findStuckFailedJobs(jobs, now, thresholdMs) → [{ slug, cwd, ageMs }]
|
|
695
|
+
*
|
|
696
|
+
* Pure predicate (isRescanCandidate's own resolveRunId/classifyRunOutcome log
|
|
697
|
+
* read is the only IO, gated per-job exactly like shouldRunPeriodicReverify
|
|
698
|
+
* above). `failed` used to be a fully terminal state for every automated
|
|
699
|
+
* recovery path — selectResumeRecoveryTarget/selectAutoFixTargets both
|
|
700
|
+
* require needs_review, reapDeadRunningJobs only ever writes running →
|
|
701
|
+
* failed, and reconcile-repair's to-pending is for structurally invalid rows.
|
|
702
|
+
* That is no longer true: selectFailedAutoResetTargets above now drives a
|
|
703
|
+
* bounded failed → pending auto-reset (LEGAL_TRANSITIONS already allowed the
|
|
704
|
+
* edge). This escalation now only fires once that auto-reset budget is
|
|
705
|
+
* genuinely spent (see the interval body's filter on failedAutoResetAttempts)
|
|
706
|
+
* — a rescan candidate (isRescanCandidate) that has sat failed longer than
|
|
707
|
+
* `thresholdMs` AND exhausted its auto-reset attempts can therefore go
|
|
708
|
+
* silently stuck forever — job 4056-outcome-stats sat `failed` for five days
|
|
709
|
+
* with no operator signal (reported 2026-09-10, social-signals-trader) even
|
|
710
|
+
* though the periodic reverify pass (once shouldRunPeriodicReverify's guard
|
|
711
|
+
* was fixed) WAS firing on it — reverifyNeedsReview's failed branch can
|
|
712
|
+
* annotate looksDone but can never resolve a failed row itself (see its own
|
|
713
|
+
* header). This is the visibility half that guard fix was missing: escalate
|
|
714
|
+
* once per exhausted row, never requeue from here.
|
|
715
|
+
*
|
|
716
|
+
* `stuckFailedNotified` gates this to exactly once per row — once the caller
|
|
717
|
+
* stamps it, this always excludes that row so a human is never re-paged on
|
|
718
|
+
* every 10-minute tick for the same stuck job. A row with no recoverable
|
|
719
|
+
* 'failed' timestamp is skipped rather than guessed at (mirrors
|
|
720
|
+
* findStaleQuarantinedJobs above).
|
|
721
|
+
*/
|
|
722
|
+
function findStuckFailedJobs(jobs, now, thresholdMs) {
|
|
723
|
+
const stuck = [];
|
|
724
|
+
for (const j of jobs ?? []) {
|
|
725
|
+
if (j.status !== 'failed') continue;
|
|
726
|
+
if (j.stuckFailedNotified === true) continue;
|
|
727
|
+
if (!isRescanCandidate(j)) continue;
|
|
728
|
+
// Newest (not first) to === 'failed' entry — same rationale as
|
|
729
|
+
// selectFailedAutoResetTargets above: a row can have failed more than
|
|
730
|
+
// once across its lifetime (an earlier auto-reset attempt that itself
|
|
731
|
+
// failed again), and only the CURRENT failure episode's age should gate
|
|
732
|
+
// escalation. Using the first/oldest entry would report a stale age
|
|
733
|
+
// (and become instantly escalation-eligible) for a row that failed
|
|
734
|
+
// months ago, recovered, and has only just failed again.
|
|
735
|
+
const history = j.statusHistory || [];
|
|
736
|
+
let entry = null;
|
|
737
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
738
|
+
if (history[i].to === 'failed') { entry = history[i]; break; }
|
|
739
|
+
}
|
|
740
|
+
if (!entry) continue;
|
|
741
|
+
const since = Date.parse(entry.at);
|
|
742
|
+
if (Number.isNaN(since)) continue;
|
|
743
|
+
const ageMs = now - since;
|
|
744
|
+
if (ageMs >= thresholdMs) stuck.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs });
|
|
745
|
+
}
|
|
746
|
+
return stuck;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Bounded automatic terminal decision for an EXHAUSTED needs_review row
|
|
750
|
+
// (isExhaustedAutoFix === true — auto-fix attempted, no plan produced,
|
|
751
|
+
// retries spent): up to NEEDS_REVIEW_RESOLVE_CAP requeue attempts (a
|
|
752
|
+
// needs_review -> pending -> ... -> needs_review round trip counts as one
|
|
753
|
+
// spent attempt), each gated on having sat exhausted-needs_review for
|
|
754
|
+
// NEEDS_REVIEW_RESOLVE_MS, before the row is auto-skipped so a `dependsOn`
|
|
755
|
+
// chain behind it always drains without an operator. Same env-override
|
|
756
|
+
// shape as FAILED_AUTORESET_MS above.
|
|
757
|
+
const NEEDS_REVIEW_RESOLVE_CAP = 2;
|
|
758
|
+
const NEEDS_REVIEW_RESOLVE_MS = process.env.SM_NEEDS_REVIEW_RESOLVE_MINUTES
|
|
759
|
+
? Number(process.env.SM_NEEDS_REVIEW_RESOLVE_MINUTES) * 60_000
|
|
760
|
+
: 30 * 60_000;
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Kill-switch gate for the needs_review auto-resolve pass below
|
|
764
|
+
* (SM_NEEDS_REVIEW_AUTORESOLVE_DISABLE=1), same shape as
|
|
765
|
+
* failedAutoResetDisabled/stuckFailedEscalationDisabled above.
|
|
766
|
+
*/
|
|
767
|
+
function needsReviewAutoResolveDisabled() {
|
|
768
|
+
return process.env.SM_NEEDS_REVIEW_AUTORESOLVE_DISABLE === '1';
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* selectExhaustedNeedsReviewTargets(jobs, now, thresholdMs) →
|
|
773
|
+
* [{ slug, cwd, ageMs, attempts }]
|
|
774
|
+
*
|
|
775
|
+
* Pure selector — no IO. Selects `needs_review` rows eligible for the
|
|
776
|
+
* bounded auto-resolve ladder (isEligibleForNeedsReviewAutoResolve — auto-fix
|
|
777
|
+
* genuinely spent, parked by a GUARD verdict that never entered auto-fix at
|
|
778
|
+
* all, or a stranded auto-fix park with no outcome ever recorded), whose
|
|
779
|
+
* newest statusHistory entry with `to === 'needs_review'` is older than
|
|
780
|
+
* `thresholdMs`, and whose exhaustedResolveAttempts counter has not yet
|
|
781
|
+
* spent its cap.
|
|
782
|
+
*
|
|
783
|
+
* The inclusion bound is inclusive of the cap itself (`<= CAP`, not `<
|
|
784
|
+
* CAP`): NEEDS_REVIEW_RESOLVE_CAP counts REQUEUE attempts already spent, and
|
|
785
|
+
* the pass that observes attempts === CAP is exactly the one that must fire
|
|
786
|
+
* the terminal skip (see the interval body's branch below) — excluding that
|
|
787
|
+
* row here would mean the cap-exhausted row is never selected again and the
|
|
788
|
+
* dependsOn chain behind it never drains, defeating this PRD's own purpose.
|
|
789
|
+
*/
|
|
790
|
+
function selectExhaustedNeedsReviewTargets(jobs, now, thresholdMs) {
|
|
791
|
+
const targets = [];
|
|
792
|
+
for (const j of jobs ?? []) {
|
|
793
|
+
if (j.status !== 'needs_review') continue;
|
|
794
|
+
if (!isEligibleForNeedsReviewAutoResolve(j, jobs)) continue;
|
|
795
|
+
if ((j.exhaustedResolveAttempts ?? 0) > NEEDS_REVIEW_RESOLVE_CAP) continue;
|
|
796
|
+
const history = j.statusHistory || [];
|
|
797
|
+
let entry = null;
|
|
798
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
799
|
+
if (history[i].to === 'needs_review') { entry = history[i]; break; }
|
|
800
|
+
}
|
|
801
|
+
if (!entry) continue;
|
|
802
|
+
const since = Date.parse(entry.at);
|
|
803
|
+
if (Number.isNaN(since)) continue;
|
|
804
|
+
const ageMs = now - since;
|
|
805
|
+
if (ageMs < thresholdMs) continue;
|
|
806
|
+
targets.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs, attempts: j.exhaustedResolveAttempts ?? 0 });
|
|
807
|
+
}
|
|
808
|
+
return targets;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Pure helper — no I/O by default (resolveJobRunId is injectable; production
|
|
813
|
+
* caller passes resolveRunId, which does touch disk). Returns the subset of
|
|
814
|
+
* jobs eligible for automatic fix-plan authoring after a reverify pass
|
|
815
|
+
* leaves them still in needs_review.
|
|
816
|
+
*
|
|
817
|
+
* Exclusion rules (all must pass):
|
|
818
|
+
* - status === 'needs_review'
|
|
819
|
+
* - a runId, own or backfilled via resolveJobRunId (need a run log to investigate)
|
|
820
|
+
* - not itself a fix-plan slug (avoids infinite recursion)
|
|
821
|
+
* - autoFixAttempted cap: a job with no prior attempt is eligible; a job
|
|
822
|
+
* whose prior attempt outcome was 'no-plan', 'error', or UNSTAMPED
|
|
823
|
+
* (investigation exited without recording any outcome — unknown is not
|
|
824
|
+
* "succeeded") gets ONE bounded retry (autoFixRetries < 1); a job whose
|
|
825
|
+
* outcome was 'plan' (a fix plan was produced) is never retried here —
|
|
826
|
+
* it either lands as a queue row on its own or gets flagged
|
|
827
|
+
* autofix_plan_unqueued by reverifyNeedsReview's annotation pass; an
|
|
828
|
+
* exhausted retry is excluded
|
|
829
|
+
* - no fix sibling on disk (fixSlugExists) or already in the queue
|
|
830
|
+
*/
|
|
831
|
+
|
|
832
|
+
function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRunId }) {
|
|
833
|
+
const slugsInQueue = new Set(jobs.map((j) => j.slug));
|
|
834
|
+
return jobs.filter((job) => {
|
|
835
|
+
if (job.status !== 'needs_review') return false;
|
|
836
|
+
// A job parked here because it was blocked by a sibling's foreign WIP
|
|
837
|
+
// three times in a row (never its own regression) has nothing for a
|
|
838
|
+
// fix-plan investigation to diagnose — there is no code defect to
|
|
839
|
+
// author a PRD against, only another job's still-uncommitted tree.
|
|
840
|
+
if (job.blockedByForeignWip === true) return false;
|
|
841
|
+
// A budget-killed job parks for a human/ladder decision, never an
|
|
842
|
+
// auto-fix investigation or auto-retry — the run didn't fail, it simply
|
|
843
|
+
// overran its own estimate; there's no code defect to diagnose (Out of
|
|
844
|
+
// scope: "retrying or auto-resuming a budget-killed job" for this PRD).
|
|
845
|
+
if (job.verifierVerdict === 'budget_exceeded') return false;
|
|
846
|
+
// A stale re-run whose work already shipped (rcaReport's 'already-shipped'
|
|
847
|
+
// class) must never buy a fix-plan PRD — there is nothing to fix, and the
|
|
848
|
+
// correct recovery (archiving the PRD) is a human/reconcile action, not
|
|
849
|
+
// an investigation.
|
|
850
|
+
if (job.rcaRecoveryAction === 'archive') return false;
|
|
851
|
+
// Resume-first recovery (PRD 1111): a job still eligible for its one
|
|
852
|
+
// bounded `--resume` attempt must never also become a fix-plan target
|
|
853
|
+
// in the same pass — see spawnInvestigation's own identical guard.
|
|
854
|
+
if (selectResumeRecoveryTarget(job)) return false;
|
|
855
|
+
// Mechanical recovery (PRD 1130): a job eligible for a pure-git retry
|
|
856
|
+
// must never also become a fix-plan target — it needs no plan and no
|
|
857
|
+
// model. Defensive: today's single mechanically-resolvable verdict
|
|
858
|
+
// (worktree_integration_failed) is already excluded below via the depth
|
|
859
|
+
// cap, but this must hold even if that stops being true.
|
|
860
|
+
if (selectMechanicalRecoveryTarget(job)) return false;
|
|
861
|
+
const runId = job.runId || resolveJobRunId(job);
|
|
862
|
+
if (!runId) return false;
|
|
863
|
+
if (isFixPlanBeyondDepthCap(job.slug, job.investigationDepth, job.isFixPlan)) return false;
|
|
864
|
+
// A dead fix-plan child (PRD 1129) earns its parent exactly one further
|
|
865
|
+
// attempt, bypassing the normal 'plan' exclusion and the fix-slug/queue
|
|
866
|
+
// membership checks below — those checks exist to stop a FRESH
|
|
867
|
+
// investigation from clobbering a live sibling, but here the sibling is
|
|
868
|
+
// dead and reusing its slug is the whole point of the reopen.
|
|
869
|
+
const dead = isFixPlanDead(job, jobs);
|
|
870
|
+
if (job.autoFixAttempted) {
|
|
871
|
+
const retryEligible = dead
|
|
872
|
+
|| job.autoFixOutcome === 'no-plan'
|
|
873
|
+
|| job.autoFixOutcome === 'error'
|
|
874
|
+
|| job.autoFixOutcome == null;
|
|
875
|
+
if (!retryEligible) return false;
|
|
876
|
+
if (!dead && (job.autoFixRetries ?? 0) >= 1) return false;
|
|
877
|
+
}
|
|
878
|
+
if (dead) return true;
|
|
879
|
+
const fixSlug = fixSlugFor(job);
|
|
880
|
+
if (fixSlugExists(fixSlug)) return false;
|
|
881
|
+
if (slugsInQueue.has(fixSlug)) return false;
|
|
882
|
+
return true;
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Pure helper — no I/O by default (fixSlugExists is injectable). Answers
|
|
888
|
+
* "does this single needs_review job qualify for an auto-fix investigation
|
|
889
|
+
* right now?" by delegating to selectAutoFixTargets's eligibility rules
|
|
890
|
+
* (same-1-attempt cap, depth cap, no fix sibling) against the full job list,
|
|
891
|
+
* then checking whether this job is among the returned targets. Shared by
|
|
892
|
+
* spawnJob's immediate same-tick path and (indirectly, via
|
|
893
|
+
* selectAutoFixTargets itself) reverifyNeedsReview's periodic path, so both
|
|
894
|
+
* use one eligibility definition rather than two divergent copies.
|
|
895
|
+
*/
|
|
896
|
+
function isEligibleForImmediateAutoFix(job, allJobs, fixSlugExists) {
|
|
897
|
+
const targets = selectAutoFixTargets(allJobs, {
|
|
898
|
+
fixSlugExists,
|
|
899
|
+
resolveJobRunId: () => job.runId,
|
|
900
|
+
});
|
|
901
|
+
return targets.some((t) => t.slug === job.slug);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Pure: picks the one needs_review row reverifyNeedsReview's background
|
|
906
|
+
* shadow gate should run this pass, or null when none qualifies. No I/O:
|
|
907
|
+
* `headByCwd` is a Map of cwd -> HEAD sha (or null), pre-fetched by the
|
|
908
|
+
* caller with `gitHead` (which never rejects, so building it needs no
|
|
909
|
+
* try/catch — a cwd whose read failed just maps to null here).
|
|
910
|
+
*
|
|
911
|
+
* A row is never picked when its cwd's HEAD is unknown (null) — there is
|
|
912
|
+
* nothing to compare a recorded gateShadow against.
|
|
913
|
+
*
|
|
914
|
+
* Tier 1, skipped when `authorityDisabled`: a stale, gate-authority-eligible
|
|
915
|
+
* row — verdict in GATE_AUTHORITY_VERDICTS, a non-empty landedCommit, and
|
|
916
|
+
* stale (no gateShadow, or one at a different HEAD, or one not marked
|
|
917
|
+
* `definitive`). Ordered so a row that never got a shadow run goes first,
|
|
918
|
+
* then the oldest `gateShadow.ranAt` (an unparseable or missing `ranAt`
|
|
919
|
+
* sorts as oldest), then queue order. Rule first, why second: the old pick
|
|
920
|
+
* took the first stale row in queue order with no rotation, so in a busy
|
|
921
|
+
* plan one row whose gate stays red forever could crowd out every other
|
|
922
|
+
* row's turn (review finding #8).
|
|
923
|
+
*
|
|
924
|
+
* Tier 2: the first needs_review row with no gateShadow at all, in queue
|
|
925
|
+
* order — today's plain observe-only rule, unchanged.
|
|
926
|
+
*
|
|
927
|
+
* Returns the first tier-1 row, else the tier-2 row, else null.
|
|
928
|
+
*/
|
|
929
|
+
function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false } = {}) {
|
|
930
|
+
const eligible = jobs.filter((j) => {
|
|
931
|
+
if (j.status !== 'needs_review') return false;
|
|
932
|
+
const head = headByCwd.get(j.cwd);
|
|
933
|
+
return typeof head === 'string' && head.length > 0;
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const isStale = (j) => {
|
|
937
|
+
const head = headByCwd.get(j.cwd);
|
|
938
|
+
return !j.gateShadow || j.gateShadow.head !== head || j.gateShadow.definitive !== true;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
if (!authorityDisabled) {
|
|
942
|
+
const ranAtMs = (j) => {
|
|
943
|
+
const t = j.gateShadow ? Date.parse(j.gateShadow.ranAt) : NaN;
|
|
944
|
+
return Number.isFinite(t) ? t : -Infinity; // never-run or unparseable sorts oldest
|
|
945
|
+
};
|
|
946
|
+
const tier1 = eligible.filter((j) => isStale(j)
|
|
947
|
+
&& j.verifierVerdict
|
|
948
|
+
&& GATE_AUTHORITY_VERDICTS.includes(j.verifierVerdict)
|
|
949
|
+
&& typeof j.landedCommit === 'string'
|
|
950
|
+
&& j.landedCommit.length > 0);
|
|
951
|
+
if (tier1.length) {
|
|
952
|
+
const ranked = tier1
|
|
953
|
+
.map((j, i) => ({ j, i, neverRun: !j.gateShadow, ranAt: ranAtMs(j) }))
|
|
954
|
+
.sort((a, b) => {
|
|
955
|
+
if (a.neverRun !== b.neverRun) return a.neverRun ? -1 : 1;
|
|
956
|
+
if (a.ranAt !== b.ranAt) return a.ranAt - b.ranAt;
|
|
957
|
+
return a.i - b.i; // tie: queue order
|
|
958
|
+
});
|
|
959
|
+
return ranked[0].j;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return eligible.find((j) => !j.gateShadow) ?? null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
module.exports = {
|
|
967
|
+
selectResumeRecoveryTarget,
|
|
968
|
+
MECHANICALLY_RESOLVABLE_VERDICTS,
|
|
969
|
+
isMechanicalRecoveryFutile,
|
|
970
|
+
selectMechanicalRecoveryTarget,
|
|
971
|
+
isStaleSharedTreeRevertedPark,
|
|
972
|
+
fixSlugFor,
|
|
973
|
+
RESCANNABLE_VERDICTS,
|
|
974
|
+
RESCAN_EXCLUDED_VERDICTS,
|
|
975
|
+
REVERIFY_INTERVAL_MS,
|
|
976
|
+
EVIDENCE_SCAN_MAX_PER_PASS,
|
|
977
|
+
EVIDENCE_SCAN_MIN_INTERVAL_MS,
|
|
978
|
+
MAX_INVESTIGATION_DEPTH,
|
|
979
|
+
isFixPlanBeyondDepthCap,
|
|
980
|
+
resolveRunId,
|
|
981
|
+
isUnresolvableNeedsReview,
|
|
982
|
+
isExhaustedAutoFix,
|
|
983
|
+
GUARD_VERDICT_EVIDENCE_ELIGIBLE,
|
|
984
|
+
isGuardParkedWithoutAutoFix,
|
|
985
|
+
isStrandedAutoFixPark,
|
|
986
|
+
isEligibleForNeedsReviewAutoResolve,
|
|
987
|
+
isPlanUnqueued,
|
|
988
|
+
DEAD_FIX_CHILD_STATUSES,
|
|
989
|
+
isFixPlanDead,
|
|
990
|
+
NO_ATTRIBUTABLE_COMMIT_VERDICTS,
|
|
991
|
+
healRefusalReason,
|
|
992
|
+
isFailedUnverifiedShaped,
|
|
993
|
+
isRescanCandidate,
|
|
994
|
+
isTranscriptRescannable,
|
|
995
|
+
selectEvidenceScanTargets,
|
|
996
|
+
shouldRunPeriodicReverify,
|
|
997
|
+
STUCK_FAILED_ESCALATE_MS,
|
|
998
|
+
stuckFailedEscalationDisabled,
|
|
999
|
+
FAILED_AUTORESET_CAP,
|
|
1000
|
+
FAILED_AUTORESET_MS,
|
|
1001
|
+
failedAutoResetDisabled,
|
|
1002
|
+
selectFailedAutoResetTargets,
|
|
1003
|
+
findStuckFailedJobs,
|
|
1004
|
+
NEEDS_REVIEW_RESOLVE_CAP,
|
|
1005
|
+
NEEDS_REVIEW_RESOLVE_MS,
|
|
1006
|
+
needsReviewAutoResolveDisabled,
|
|
1007
|
+
selectExhaustedNeedsReviewTargets,
|
|
1008
|
+
selectAutoFixTargets,
|
|
1009
|
+
isEligibleForImmediateAutoFix,
|
|
1010
|
+
selectGateShadowTarget,
|
|
1011
|
+
};
|