claude-code-session-manager 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -53
- package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/Permissions-m0KAnccF.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/Settings-BHSGGFEc.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
- package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +14 -17
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +35 -71
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +4 -4
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +65 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +12 -7
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +268 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +6 -1
- package/src/main/index.cjs +152 -50
- package/src/main/ipcSchemas.cjs +53 -17
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +94 -24
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +332 -0
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +136 -19
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +33 -78
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +214 -45
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +27 -8
- package/src/main/lib/queueStore.cjs +63 -38
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +10 -2
- package/src/main/lib/scheduleJobTransitions.cjs +60 -0
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +206 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +2841 -482
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/usage.cjs +140 -58
- package/src/preload/api.d.ts +110 -65
- package/src/preload/index.cjs +12 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
- package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
- package/scripts/render-project-pages.cjs +0 -70
- package/scripts/validate-project-pages-summary.cjs +0 -62
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* upgradeDrain.cjs — restart as a REQUEST that drains first and never kills.
|
|
5
|
+
*
|
|
6
|
+
* requestRestart() → writes <schedulerHome>/restart-request.json (atomic).
|
|
7
|
+
* evaluateDrain() → PURE state machine: none | pause | wait | restart | abort.
|
|
8
|
+
* scheduler.cjs → runs it from the existing 60 s heartbeat interval (no
|
|
9
|
+
* new driver) and owns the `drain` field on the machine
|
|
10
|
+
* runtime file. `drain` is deliberately NOT `state.paused`:
|
|
11
|
+
* a rate-limit pause can't overwrite it and the manual-clear
|
|
12
|
+
* cooldown (isCooldownSuppressed) can't swallow it.
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle: request → pause (drain.active) → wait (running/investigating rows
|
|
15
|
+
* finish; nothing new dispatches) → restart (zero busy, checked inside a mutate
|
|
16
|
+
* right before exit) | abort (deadline; retires the request, clears the drain).
|
|
17
|
+
* The request is stamped drainCompletedAt before exit; the next boot clears a
|
|
18
|
+
* leftover drain whose request is complete.
|
|
19
|
+
*
|
|
20
|
+
* A `restarting.json` marker with a bounded lifetime covers the deliberate exit
|
|
21
|
+
* window so scripts/scheduler-watchdog.cjs does not relaunch a second instance.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
27
|
+
|
|
28
|
+
// Observed max job run is 240 min; 4 h converts a stuck drain to an abort.
|
|
29
|
+
const DEFAULT_DRAIN_DEADLINE_MS = 4 * 60 * 60_000;
|
|
30
|
+
// Long enough for a cold `npx` resolve + boot, short enough that a failed
|
|
31
|
+
// relaunch is picked up by the watchdog again.
|
|
32
|
+
const RESTARTING_MARKER_TTL_MS = 5 * 60_000;
|
|
33
|
+
|
|
34
|
+
function writeJsonAtomicSync(file, value) {
|
|
35
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
37
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
38
|
+
fs.renameSync(tmp, file);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readJsonSafe(file) {
|
|
42
|
+
try {
|
|
43
|
+
const v = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
44
|
+
return v && typeof v === 'object' ? v : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------- request file ----------
|
|
51
|
+
|
|
52
|
+
function readRestartRequest(file = schedulerPaths.restartRequestPath()) {
|
|
53
|
+
return readJsonSafe(file);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* requestRestart({reason, requestedBy}) → the request written. Idempotent: an
|
|
58
|
+
* already-pending (not completed) request is returned unchanged so a repeated
|
|
59
|
+
* click or the auto trigger can never reset the drain deadline.
|
|
60
|
+
*/
|
|
61
|
+
function requestRestart({ reason, requestedBy } = {}, { file = schedulerPaths.restartRequestPath(), now = Date.now() } = {}) {
|
|
62
|
+
const existing = readRestartRequest(file);
|
|
63
|
+
if (existing && !existing.drainCompletedAt) return existing;
|
|
64
|
+
const request = {
|
|
65
|
+
reason: typeof reason === 'string' && reason ? reason : 'unspecified',
|
|
66
|
+
requestedBy: typeof requestedBy === 'string' && requestedBy ? requestedBy : 'unknown',
|
|
67
|
+
requestedAt: new Date(now).toISOString(),
|
|
68
|
+
};
|
|
69
|
+
writeJsonAtomicSync(file, request);
|
|
70
|
+
return request;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Stamp drainCompletedAt (called immediately before exit). */
|
|
74
|
+
function stampDrainCompleted(now = Date.now(), file = schedulerPaths.restartRequestPath()) {
|
|
75
|
+
const existing = readRestartRequest(file);
|
|
76
|
+
if (!existing) return null;
|
|
77
|
+
const stamped = { ...existing, drainCompletedAt: new Date(now).toISOString() };
|
|
78
|
+
writeJsonAtomicSync(file, stamped);
|
|
79
|
+
return stamped;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function retireRestartRequest(file = schedulerPaths.restartRequestPath()) {
|
|
83
|
+
try { fs.unlinkSync(file); } catch (e) { if (e?.code !== 'ENOENT') throw e; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------- pure state machine ----------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* queueBusyCount(jobs, extraInFlight?) → running + investigating rows, plus any
|
|
90
|
+
* in-memory investigations the queue file cannot see.
|
|
91
|
+
*/
|
|
92
|
+
function queueBusyCount(jobs, extraInFlight = 0) {
|
|
93
|
+
let n = 0;
|
|
94
|
+
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
95
|
+
if (j && (j.status === 'running' || j.status === 'investigating')) n++;
|
|
96
|
+
}
|
|
97
|
+
return n + (Number.isFinite(extraInFlight) ? extraInFlight : 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* evaluateDrain({request, queueSnapshot, drainState, now, deadlineMs}) → {action, reason}
|
|
102
|
+
*
|
|
103
|
+
* queueSnapshot: { running, investigating } counts (in-memory investigations
|
|
104
|
+
* already folded in by the caller). drainState: the machine `drain` field
|
|
105
|
+
* (null / {active}). Pure — no IO, no clock.
|
|
106
|
+
*
|
|
107
|
+
* no request, or request already completed → none (abort if a stale drain is still active)
|
|
108
|
+
* deadline passed → abort
|
|
109
|
+
* request pending, drain not yet active → pause (engage drain)
|
|
110
|
+
* drain active, anything running/investigating → wait
|
|
111
|
+
* drain active, zero busy → restart
|
|
112
|
+
*/
|
|
113
|
+
function evaluateDrain({ request, queueSnapshot, drainState, now, deadlineMs = DEFAULT_DRAIN_DEADLINE_MS } = {}) {
|
|
114
|
+
const active = Boolean(drainState && drainState.active);
|
|
115
|
+
if (!request || request.drainCompletedAt) {
|
|
116
|
+
return active && !request
|
|
117
|
+
? { action: 'abort', reason: 'drain-without-request' }
|
|
118
|
+
: { action: 'none', reason: request ? 'request-complete' : 'no-request' };
|
|
119
|
+
}
|
|
120
|
+
const requestedMs = Date.parse(request.requestedAt ?? '');
|
|
121
|
+
if (Number.isFinite(requestedMs) && now - requestedMs >= deadlineMs) {
|
|
122
|
+
return { action: 'abort', reason: 'deadline' };
|
|
123
|
+
}
|
|
124
|
+
if (!active) return { action: 'pause', reason: 'engage' };
|
|
125
|
+
const busy = (queueSnapshot?.running ?? 0) + (queueSnapshot?.investigating ?? 0);
|
|
126
|
+
return busy > 0 ? { action: 'wait', reason: `busy:${busy}` } : { action: 'restart', reason: 'idle' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* bootDrainAction({drainState, request}) → 'clear' | 'keep' | 'none'.
|
|
131
|
+
* A drain whose request is complete (the restart happened) or gone is stale.
|
|
132
|
+
*/
|
|
133
|
+
function bootDrainAction({ drainState, request } = {}) {
|
|
134
|
+
if (!drainState || !drainState.active) return 'none';
|
|
135
|
+
if (!request || request.drainCompletedAt) return 'clear';
|
|
136
|
+
return 'keep';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The pause-like view every dispatch classifier must use: paused OR draining. */
|
|
140
|
+
function effectivePaused(state) {
|
|
141
|
+
if (state?.paused) return state.paused;
|
|
142
|
+
return state?.drain?.active ? { reason: 'drain-for-upgrade' } : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------- automatic trigger ----------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* installedBuildDiffers({running, installed}) — true only when BOTH shas are
|
|
149
|
+
* known and differ (an install/update already happened). Never consults npm.
|
|
150
|
+
*/
|
|
151
|
+
function installedBuildDiffers({ running, installed } = {}) {
|
|
152
|
+
return typeof running === 'string' && running !== ''
|
|
153
|
+
&& typeof installed === 'string' && installed !== ''
|
|
154
|
+
&& running !== installed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------- restarting marker (watchdog) ----------
|
|
158
|
+
|
|
159
|
+
function markRestarting({ file = schedulerPaths.restartingMarkerPath(), now = Date.now(), ttlMs = RESTARTING_MARKER_TTL_MS } = {}) {
|
|
160
|
+
writeJsonAtomicSync(file, { pid: process.pid, at: now, expiresAt: now + ttlMs });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clearRestartingMarker(file = schedulerPaths.restartingMarkerPath()) {
|
|
164
|
+
try { fs.unlinkSync(file); } catch { /* absent */ }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** True while the marker exists and has not expired (bounded lifetime). */
|
|
168
|
+
function isRestartingMarkerActive({ file = schedulerPaths.restartingMarkerPath(), now = Date.now() } = {}) {
|
|
169
|
+
const m = readJsonSafe(file);
|
|
170
|
+
return Boolean(m) && typeof m.expiresAt === 'number' && now < m.expiresAt;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
DEFAULT_DRAIN_DEADLINE_MS,
|
|
175
|
+
RESTARTING_MARKER_TTL_MS,
|
|
176
|
+
readRestartRequest,
|
|
177
|
+
requestRestart,
|
|
178
|
+
stampDrainCompleted,
|
|
179
|
+
retireRestartRequest,
|
|
180
|
+
queueBusyCount,
|
|
181
|
+
evaluateDrain,
|
|
182
|
+
bootDrainAction,
|
|
183
|
+
effectivePaused,
|
|
184
|
+
installedBuildDiffers,
|
|
185
|
+
markRestarting,
|
|
186
|
+
clearRestartingMarker,
|
|
187
|
+
isRestartingMarkerActive,
|
|
188
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usageCircuit.cjs — a breaker over the /api/oauth/usage meter (PRD ss-02).
|
|
5
|
+
* Degraded mode is explicit, never a silent 0: repeated failures open the
|
|
6
|
+
* circuit with jittered backoff; degradedBudget() carries forward the last
|
|
7
|
+
* known-good BINDING window's utilization (never 0), pinning to 100 on a
|
|
8
|
+
* fresh executor-observed 429 and capping concurrency at 2. Pure Node.
|
|
9
|
+
*
|
|
10
|
+
* Kill switches (read live from process.env, so a test/operator can flip
|
|
11
|
+
* them without restarting anything that already holds a circuit instance):
|
|
12
|
+
* SM_USAGE_CIRCUIT=0 — bypasses the breaker entirely: state() always
|
|
13
|
+
* reports 'closed' and recordFailure() never
|
|
14
|
+
* opens it. Escape hatch if the breaker itself
|
|
15
|
+
* is ever suspected of wedging dispatch.
|
|
16
|
+
* SM_USAGE_DEGRADED_CAP=n — overrides degradedBudget()'s concurrencyCap
|
|
17
|
+
* outright (bypasses the normal min(cap, 2)),
|
|
18
|
+
* for an operator who needs a different ceiling
|
|
19
|
+
* while the meter is degraded.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const FLOOR_MS = 30_000;
|
|
23
|
+
const CAP_MS = 8 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
/** Doubles from FLOOR_MS, capped at CAP_MS. 0/falsy prevMs starts at the floor. */
|
|
26
|
+
function doubledBase(prevMs) {
|
|
27
|
+
return prevMs > 0 ? Math.min(prevMs * 2, CAP_MS) : FLOOR_MS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Next backoff interval: doubles `prevMs` (floor FLOOR_MS, cap CAP_MS), then
|
|
32
|
+
* jitters by 0.5 + rand()*0.5 so concurrent instances don't retry in lockstep.
|
|
33
|
+
*/
|
|
34
|
+
function backoffWithJitter(prevMs) {
|
|
35
|
+
const base = doubledBase(prevMs);
|
|
36
|
+
return base * (0.5 + Math.random() * 0.5);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A cached reset timestamp is fresh only if it's strictly in the future
|
|
41
|
+
* relative to `now` — null/unparseable/at-or-before-now all read as stale,
|
|
42
|
+
* since a passed reset no longer describes a live window.
|
|
43
|
+
*/
|
|
44
|
+
function isResetFresh(iso, now) {
|
|
45
|
+
if (!iso) return false;
|
|
46
|
+
const resetMs = Date.parse(iso);
|
|
47
|
+
if (!Number.isFinite(resetMs)) return false;
|
|
48
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now);
|
|
49
|
+
if (!Number.isFinite(nowMs)) return false;
|
|
50
|
+
return resetMs > nowMs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which window is actually binding dispatch. Prefers `payload.limits[]`'s
|
|
55
|
+
* `is_active` entry (the richer per-window shape); falls back to the flat
|
|
56
|
+
* `five_hour` field when `limits[]` is absent, matching usage.cjs's shape.
|
|
57
|
+
*/
|
|
58
|
+
function bindingWindow(payload) {
|
|
59
|
+
if (payload && Array.isArray(payload.limits) && payload.limits.length) {
|
|
60
|
+
const active = payload.limits.find((l) => l && l.is_active);
|
|
61
|
+
if (active) {
|
|
62
|
+
return {
|
|
63
|
+
name: active.type || active.name || 'unknown',
|
|
64
|
+
utilization: active.utilization,
|
|
65
|
+
resets_at: active.resets_at ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const fh = payload && payload.five_hour;
|
|
70
|
+
return {
|
|
71
|
+
name: 'five_hour',
|
|
72
|
+
utilization: fh ? fh.utilization : undefined,
|
|
73
|
+
resets_at: fh ? fh.resets_at : null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* SM_USAGE_DEGRADED_CAP=<n> overrides the normal min(configuredCap, 2)
|
|
79
|
+
* ceiling outright — an operator escape hatch, not a default path. Falls
|
|
80
|
+
* through to the normal min(cap, 2) rule when unset/non-positive.
|
|
81
|
+
*/
|
|
82
|
+
function degradedConcurrencyCap(configuredCap) {
|
|
83
|
+
const override = Number(process.env.SM_USAGE_DEGRADED_CAP);
|
|
84
|
+
if (Number.isFinite(override) && override > 0) return override;
|
|
85
|
+
const cap = Number.isFinite(configuredCap) ? configuredCap : 2;
|
|
86
|
+
return Math.min(cap, 2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Conservative budget to run on while the meter is down. utilization is
|
|
91
|
+
* carried forward from the last known-good BINDING window — never 0, which
|
|
92
|
+
* would read as "plenty of headroom" instead of "we don't know." A fresh
|
|
93
|
+
* executor-observed 429 (its own window not yet passed) pins utilization to
|
|
94
|
+
* 100 regardless of the stale cached value. concurrencyCap never exceeds 2
|
|
95
|
+
* (or SM_USAGE_DEGRADED_CAP, if set).
|
|
96
|
+
*/
|
|
97
|
+
function degradedBudget(lastGoodPayload, executorEvidence = {}) {
|
|
98
|
+
const window = bindingWindow(lastGoodPayload);
|
|
99
|
+
let utilization = Number.isFinite(window.utilization) && window.utilization > 0
|
|
100
|
+
? window.utilization
|
|
101
|
+
: 100;
|
|
102
|
+
|
|
103
|
+
const concurrencyCap = degradedConcurrencyCap(executorEvidence.configuredCap);
|
|
104
|
+
|
|
105
|
+
if (executorEvidence.observed429 && isResetFresh(executorEvidence.resetsAt, executorEvidence.now)) {
|
|
106
|
+
utilization = 100;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { utilization, concurrencyCap };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
|
|
113
|
+
function singleFlight(fn) {
|
|
114
|
+
let inFlight = null;
|
|
115
|
+
return function singleFlightWrapped(...args) {
|
|
116
|
+
if (inFlight) return inFlight;
|
|
117
|
+
inFlight = Promise.resolve(fn(...args)).finally(() => { inFlight = null; });
|
|
118
|
+
return inFlight;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** SM_USAGE_CIRCUIT=0 (or 'false') bypasses the breaker entirely. */
|
|
123
|
+
function breakerEnabled() {
|
|
124
|
+
const v = process.env.SM_USAGE_CIRCUIT;
|
|
125
|
+
return v == null || v === '' || (v !== '0' && String(v).toLowerCase() !== 'false');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Breaker state machine over the meter: closed -> open after 3 consecutive
|
|
130
|
+
* failures; open allows exactly one probe per backoff interval (half_open);
|
|
131
|
+
* a successful probe closes the circuit and resets backoff, a failed probe
|
|
132
|
+
* reopens it with backoff doubled.
|
|
133
|
+
*/
|
|
134
|
+
function createUsageCircuit({ now = () => Date.now() } = {}) {
|
|
135
|
+
let consecutiveFailures = 0;
|
|
136
|
+
let currentState = 'closed';
|
|
137
|
+
let backoffBaseMs = 0;
|
|
138
|
+
let nextProbeAtMs = 0;
|
|
139
|
+
let probeInFlight = false;
|
|
140
|
+
// ms timestamp the circuit most recently transitioned closed -> open; kept
|
|
141
|
+
// through half_open <-> open cycles of the SAME streak (a failed probe
|
|
142
|
+
// re-arms the timer but the meter has been down continuously since this
|
|
143
|
+
// timestamp), cleared only by recordSuccess. Feeds health.cjs's
|
|
144
|
+
// GREEN/YELLOW/RED ladder (open duration), not just a failure count.
|
|
145
|
+
let openedAtMs = null;
|
|
146
|
+
|
|
147
|
+
function scheduleNextProbe() {
|
|
148
|
+
const intervalMs = backoffWithJitter(backoffBaseMs);
|
|
149
|
+
backoffBaseMs = doubledBase(backoffBaseMs);
|
|
150
|
+
nextProbeAtMs = now() + intervalMs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function state() {
|
|
154
|
+
if (!breakerEnabled()) return 'closed';
|
|
155
|
+
if (currentState === 'open' && !probeInFlight && now() >= nextProbeAtMs) {
|
|
156
|
+
currentState = 'half_open';
|
|
157
|
+
probeInFlight = true;
|
|
158
|
+
}
|
|
159
|
+
return currentState;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function recordFailure(kind) { // eslint-disable-line no-unused-vars
|
|
163
|
+
consecutiveFailures += 1;
|
|
164
|
+
if (!breakerEnabled()) return;
|
|
165
|
+
if (currentState === 'half_open') {
|
|
166
|
+
probeInFlight = false;
|
|
167
|
+
currentState = 'open';
|
|
168
|
+
scheduleNextProbe();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (currentState === 'closed' && consecutiveFailures >= 3) {
|
|
172
|
+
currentState = 'open';
|
|
173
|
+
openedAtMs = now();
|
|
174
|
+
scheduleNextProbe();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function recordSuccess(payload) { // eslint-disable-line no-unused-vars
|
|
179
|
+
consecutiveFailures = 0;
|
|
180
|
+
probeInFlight = false;
|
|
181
|
+
backoffBaseMs = 0;
|
|
182
|
+
currentState = 'closed';
|
|
183
|
+
openedAtMs = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** ms timestamp this streak opened, or null while closed. */
|
|
187
|
+
function openedAt() {
|
|
188
|
+
return currentState === 'open' || currentState === 'half_open' ? openedAtMs : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getConsecutiveFailures() {
|
|
192
|
+
return consecutiveFailures;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { recordSuccess, recordFailure, state, openedAt, getConsecutiveFailures };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
createUsageCircuit,
|
|
200
|
+
backoffWithJitter,
|
|
201
|
+
isResetFresh,
|
|
202
|
+
bindingWindow,
|
|
203
|
+
degradedBudget,
|
|
204
|
+
degradedConcurrencyCap,
|
|
205
|
+
singleFlight,
|
|
206
|
+
};
|
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
// Testable without spawning the watchdog entry script.
|
|
5
5
|
|
|
6
6
|
const fs = require('node:fs');
|
|
7
|
-
const os = require('node:os');
|
|
8
7
|
const path = require('node:path');
|
|
8
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
9
|
+
const { isRestartingMarkerActive } = require('./upgradeDrain.cjs');
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
// Mirrors scheduler.cjs:215 (single source of truth there; kept in sync here).
|
|
12
|
-
const DEFAULT_HEARTBEAT_PATH = path.join(
|
|
13
|
-
os.homedir(), '.claude', 'session-manager', 'scheduler-heartbeat.log',
|
|
14
|
-
);
|
|
15
12
|
|
|
16
13
|
// The in-app heartbeat ticks every 60 s; 3 missed ticks = stale.
|
|
17
14
|
const DEFAULT_MAX_AGE_MS = 180_000;
|
|
@@ -40,7 +37,7 @@ const TAIL_BYTES = 4096;
|
|
|
40
37
|
* Single source of truth for the file-read + reverse-scan logic; used by
|
|
41
38
|
* readLastHeartbeatTs() and heartbeatFresh().
|
|
42
39
|
*/
|
|
43
|
-
function readLastHeartbeat(heartbeatPath =
|
|
40
|
+
function readLastHeartbeat(heartbeatPath = schedulerPaths.heartbeatPath()) {
|
|
44
41
|
let buf;
|
|
45
42
|
try {
|
|
46
43
|
const stat = fs.statSync(heartbeatPath);
|
|
@@ -79,7 +76,7 @@ function readLastHeartbeat(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
|
|
|
79
76
|
* Returns the `ts` field (epoch ms) from the last heartbeat entry.
|
|
80
77
|
* Returns null on missing / empty / unparseable file or missing ts field.
|
|
81
78
|
*/
|
|
82
|
-
function readLastHeartbeatTs(heartbeatPath =
|
|
79
|
+
function readLastHeartbeatTs(heartbeatPath = schedulerPaths.heartbeatPath()) {
|
|
83
80
|
const entry = readLastHeartbeat(heartbeatPath);
|
|
84
81
|
return (entry !== null && typeof entry.ts === 'number') ? entry.ts : null;
|
|
85
82
|
}
|
|
@@ -88,12 +85,43 @@ function readLastHeartbeatTs(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
|
|
|
88
85
|
* heartbeatFresh(heartbeatPath?, maxAgeMs?) → boolean
|
|
89
86
|
*
|
|
90
87
|
* Returns true iff the last heartbeat ts is within maxAgeMs of `now`.
|
|
91
|
-
* Missing / empty / unparseable file → false.
|
|
88
|
+
* Missing / empty / unparseable file → false. A `degraded: true` line (a
|
|
89
|
+
* heartbeat subsystem threw — see scheduler.cjs heartbeatTick) is NOT fresh:
|
|
90
|
+
* the loop ran but could not read state, so it must not disarm the watchdog.
|
|
92
91
|
*/
|
|
93
|
-
function heartbeatFresh(heartbeatPath =
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return (Date.now() - ts) < maxAgeMs;
|
|
92
|
+
function heartbeatFresh(heartbeatPath = schedulerPaths.heartbeatPath(), maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
93
|
+
const entry = readLastHeartbeat(heartbeatPath);
|
|
94
|
+
if (entry === null || entry.degraded === true || typeof entry.ts !== 'number') return false;
|
|
95
|
+
return (Date.now() - entry.ts) < maxAgeMs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DEFAULT_DISPATCH_DEAD_MS = 30 * 60_000;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* evaluateDispatchLiveness(entry, now, { deadMs }?) → { dead, reason }
|
|
102
|
+
*
|
|
103
|
+
* Pure. Observability only — a logically-dead scheduler (wedged tick chain,
|
|
104
|
+
* leaked slots) still writes fresh heartbeats, so freshness alone can't see
|
|
105
|
+
* it. `dead` only when ALL hold: heartbeat fresh (not degraded, within
|
|
106
|
+
* DEFAULT_MAX_AGE_MS), dispatch.lastRunAt older than deadMs (absent/unparseable
|
|
107
|
+
* counts as older), pendingDispatchable > 0, runningCount === 0, not paused.
|
|
108
|
+
* Callers must NOT relaunch on this: the process is alive, and a relaunch
|
|
109
|
+
* would spawn a second app on top of it.
|
|
110
|
+
*/
|
|
111
|
+
function evaluateDispatchLiveness(entry, now = Date.now(), { deadMs = DEFAULT_DISPATCH_DEAD_MS } = {}) {
|
|
112
|
+
if (!entry || entry.degraded === true || typeof entry.ts !== 'number') {
|
|
113
|
+
return { dead: false, reason: 'no-fresh-heartbeat' };
|
|
114
|
+
}
|
|
115
|
+
if (now - entry.ts >= DEFAULT_MAX_AGE_MS) return { dead: false, reason: 'heartbeat-stale' };
|
|
116
|
+
const d = entry.dispatch;
|
|
117
|
+
if (!d || typeof d !== 'object') return { dead: false, reason: 'no-dispatch-field' };
|
|
118
|
+
if (d.paused) return { dead: false, reason: 'paused' };
|
|
119
|
+
if (d.drain) return { dead: false, reason: 'draining' };
|
|
120
|
+
if (!(d.pendingDispatchable > 0)) return { dead: false, reason: 'nothing-dispatchable' };
|
|
121
|
+
if (d.runningCount !== 0) return { dead: false, reason: 'jobs-running' };
|
|
122
|
+
const lastRunMs = Date.parse(d.lastRunAt ?? '');
|
|
123
|
+
if (Number.isFinite(lastRunMs) && now - lastRunMs <= deadMs) return { dead: false, reason: 'recent-dispatch' };
|
|
124
|
+
return { dead: true, reason: 'dispatch-dead' };
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
/**
|
|
@@ -132,12 +160,6 @@ function isPidAlive(pid) {
|
|
|
132
160
|
// 60 s by scheduler.cjs's heartbeatInterval regardless of pause state) as the
|
|
133
161
|
// liveness signal — no second heartbeat file.
|
|
134
162
|
|
|
135
|
-
const DEFAULT_RELAUNCH_STATE_PATH = path.join(
|
|
136
|
-
os.homedir(), '.claude', 'session-manager', 'watchdog-relaunch-state.json',
|
|
137
|
-
);
|
|
138
|
-
const DEFAULT_RELAUNCH_LOG_PATH = path.join(
|
|
139
|
-
os.homedir(), '.claude', 'logs', 'scheduler-watchdog-relaunch.log',
|
|
140
|
-
);
|
|
141
163
|
const DEFAULT_RELAUNCH_DEBOUNCE_MS = 90_000;
|
|
142
164
|
const DEFAULT_MAX_RELAUNCH_ATTEMPTS = 3;
|
|
143
165
|
|
|
@@ -150,7 +172,7 @@ const DEFAULT_MAX_RELAUNCH_ATTEMPTS = 3;
|
|
|
150
172
|
* rather than relaunching on top of a live process.
|
|
151
173
|
*/
|
|
152
174
|
function checkAppLiveness({
|
|
153
|
-
heartbeatPath =
|
|
175
|
+
heartbeatPath = schedulerPaths.heartbeatPath(),
|
|
154
176
|
maxAgeMs = DEFAULT_MAX_AGE_MS,
|
|
155
177
|
} = {}) {
|
|
156
178
|
if (heartbeatFresh(heartbeatPath, maxAgeMs)) {
|
|
@@ -164,7 +186,7 @@ function checkAppLiveness({
|
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
/** Default { lastAttemptTs: null, attemptCount: 0 } on missing/unparseable state file. */
|
|
167
|
-
function readRelaunchState(statePath =
|
|
189
|
+
function readRelaunchState(statePath = schedulerPaths.watchdogRelaunchStatePath()) {
|
|
168
190
|
try {
|
|
169
191
|
const raw = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
170
192
|
return {
|
|
@@ -203,7 +225,7 @@ function logRelaunchLine(logPath, message) {
|
|
|
203
225
|
* (short-lived) process doesn't block on the launched app and the app
|
|
204
226
|
* survives the watchdog exiting.
|
|
205
227
|
*/
|
|
206
|
-
function defaultSpawnRelaunch({ logPath =
|
|
228
|
+
function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath() } = {}) {
|
|
207
229
|
const { spawn } = require('node:child_process');
|
|
208
230
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
209
231
|
const fd = fs.openSync(logPath, 'a');
|
|
@@ -236,14 +258,15 @@ function defaultSpawnRelaunch({ logPath = DEFAULT_RELAUNCH_LOG_PATH } = {}) {
|
|
|
236
258
|
* intervenes or the app comes up some other way.
|
|
237
259
|
*/
|
|
238
260
|
function maybeRelaunchApp({
|
|
239
|
-
heartbeatPath =
|
|
261
|
+
heartbeatPath = schedulerPaths.heartbeatPath(),
|
|
240
262
|
maxAgeMs = DEFAULT_MAX_AGE_MS,
|
|
241
|
-
statePath =
|
|
242
|
-
logPath =
|
|
263
|
+
statePath = schedulerPaths.watchdogRelaunchStatePath(),
|
|
264
|
+
logPath = schedulerPaths.watchdogRelaunchLogPath(),
|
|
243
265
|
debounceMs = DEFAULT_RELAUNCH_DEBOUNCE_MS,
|
|
244
266
|
maxAttempts = DEFAULT_MAX_RELAUNCH_ATTEMPTS,
|
|
245
267
|
now = Date.now(),
|
|
246
268
|
spawnFn = defaultSpawnRelaunch,
|
|
269
|
+
restartingMarkerPath = schedulerPaths.restartingMarkerPath(),
|
|
247
270
|
} = {}) {
|
|
248
271
|
const liveness = checkAppLiveness({ heartbeatPath, maxAgeMs });
|
|
249
272
|
const state = readRelaunchState(statePath);
|
|
@@ -255,6 +278,14 @@ function maybeRelaunchApp({
|
|
|
255
278
|
return { relaunched: false, reason: 'alive', attemptCount: 0 };
|
|
256
279
|
}
|
|
257
280
|
|
|
281
|
+
// A deliberate drain-restart is mid-exit: the old process is going away ON
|
|
282
|
+
// PURPOSE and its successor is booting. Relaunching now would race the
|
|
283
|
+
// single-instance lock. The marker's lifetime is bounded, so a restart that
|
|
284
|
+
// never comes up is picked up again once it expires.
|
|
285
|
+
if (isRestartingMarkerActive({ file: restartingMarkerPath, now })) {
|
|
286
|
+
return { relaunched: false, reason: 'restarting', attemptCount: state.attemptCount };
|
|
287
|
+
}
|
|
288
|
+
|
|
258
289
|
if (state.attemptCount >= maxAttempts) {
|
|
259
290
|
logRelaunchLine(
|
|
260
291
|
logPath,
|
|
@@ -286,12 +317,6 @@ function maybeRelaunchApp({
|
|
|
286
317
|
// walk/persist logic — this is just the once-per-day scheduling + lock
|
|
287
318
|
// wrapper around it, tailored to being invoked repeatedly by a systemd timer.
|
|
288
319
|
|
|
289
|
-
const DEFAULT_STAMP_PATH = path.join(
|
|
290
|
-
os.homedir(), '.claude', 'session-manager', 'history-rollup.stamp',
|
|
291
|
-
);
|
|
292
|
-
const DEFAULT_LOCK_PATH = path.join(
|
|
293
|
-
os.homedir(), '.claude', 'session-manager', 'history-rollup.lock',
|
|
294
|
-
);
|
|
295
320
|
const DEFAULT_LOCK_STALE_MS = 10 * 60 * 1000; // 10 min
|
|
296
321
|
const DEFAULT_FINALIZE_BUDGET_MS = 60_000;
|
|
297
322
|
|
|
@@ -362,7 +387,7 @@ function releaseLock(lockPath) {
|
|
|
362
387
|
* dependency fails inside maybeFinalizeHistory's try/catch, not at require
|
|
363
388
|
* time for the whole watchdog script. */
|
|
364
389
|
function defaultFinalizeClosedDays(opts) {
|
|
365
|
-
const { finalizeClosedDays } = require('
|
|
390
|
+
const { finalizeClosedDays } = require('../historyAggregator.cjs');
|
|
366
391
|
return finalizeClosedDays(opts);
|
|
367
392
|
}
|
|
368
393
|
|
|
@@ -384,8 +409,8 @@ function defaultFinalizeClosedDays(opts) {
|
|
|
384
409
|
* finalizeFn — injectable finalizeClosedDays for testing
|
|
385
410
|
*/
|
|
386
411
|
async function maybeFinalizeHistory({
|
|
387
|
-
stampPath =
|
|
388
|
-
lockPath =
|
|
412
|
+
stampPath = schedulerPaths.historyRollupStampPath(),
|
|
413
|
+
lockPath = schedulerPaths.historyRollupLockPath(),
|
|
389
414
|
staleLockMs = DEFAULT_LOCK_STALE_MS,
|
|
390
415
|
budgetMs = DEFAULT_FINALIZE_BUDGET_MS,
|
|
391
416
|
dryRun = process.env.SM_WATCHDOG_DRYRUN === '1',
|
|
@@ -423,6 +448,7 @@ module.exports = {
|
|
|
423
448
|
readLastHeartbeat,
|
|
424
449
|
readLastHeartbeatTs,
|
|
425
450
|
heartbeatFresh,
|
|
451
|
+
evaluateDispatchLiveness,
|
|
426
452
|
isPidAlive,
|
|
427
453
|
localDateStr,
|
|
428
454
|
maybeFinalizeHistory,
|
|
@@ -433,14 +459,21 @@ module.exports = {
|
|
|
433
459
|
writeRelaunchState,
|
|
434
460
|
defaultSpawnRelaunch,
|
|
435
461
|
maybeRelaunchApp,
|
|
436
|
-
DEFAULT_HEARTBEAT_PATH,
|
|
437
462
|
DEFAULT_MAX_AGE_MS,
|
|
438
|
-
|
|
439
|
-
DEFAULT_LOCK_PATH,
|
|
463
|
+
DEFAULT_DISPATCH_DEAD_MS,
|
|
440
464
|
DEFAULT_LOCK_STALE_MS,
|
|
441
465
|
DEFAULT_FINALIZE_BUDGET_MS,
|
|
442
|
-
DEFAULT_RELAUNCH_STATE_PATH,
|
|
443
|
-
DEFAULT_RELAUNCH_LOG_PATH,
|
|
444
466
|
DEFAULT_RELAUNCH_DEBOUNCE_MS,
|
|
445
467
|
DEFAULT_MAX_RELAUNCH_ATTEMPTS,
|
|
446
468
|
};
|
|
469
|
+
|
|
470
|
+
// Lazy default-path getters (SM_SCHEDULER_HOME resolved at read, never at require).
|
|
471
|
+
for (const [name, resolve] of [
|
|
472
|
+
['DEFAULT_HEARTBEAT_PATH', schedulerPaths.heartbeatPath],
|
|
473
|
+
['DEFAULT_STAMP_PATH', schedulerPaths.historyRollupStampPath],
|
|
474
|
+
['DEFAULT_LOCK_PATH', schedulerPaths.historyRollupLockPath],
|
|
475
|
+
['DEFAULT_RELAUNCH_STATE_PATH', schedulerPaths.watchdogRelaunchStatePath],
|
|
476
|
+
['DEFAULT_RELAUNCH_LOG_PATH', schedulerPaths.watchdogRelaunchLogPath],
|
|
477
|
+
]) {
|
|
478
|
+
Object.defineProperty(module.exports, name, { get: resolve, enumerable: true });
|
|
479
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* withTimeout.cjs — bound an async body with a wall-clock budget.
|
|
3
|
+
*
|
|
4
|
+
* `withTimeout(promiseFactory, ms, onTimeout)` starts `promiseFactory()` and
|
|
5
|
+
* resolves with its result. If it has not settled after `ms`, `onTimeout()`
|
|
6
|
+
* is called and ITS return value becomes the resolved value; the original
|
|
7
|
+
* promise is abandoned (never cancelled — callers that must neutralise a
|
|
8
|
+
* late-resuming body do so with their own generation fence). The timer is
|
|
9
|
+
* always cleared on settle, and unref'd so it can never hold the process open.
|
|
10
|
+
* `ms <= 0` (or non-finite) disables the bound entirely.
|
|
11
|
+
*
|
|
12
|
+
* Plain Node module (no Electron deps).
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
function withTimeout(promiseFactory, ms, onTimeout) {
|
|
17
|
+
if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve().then(promiseFactory);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let done = false;
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
if (done) return;
|
|
22
|
+
done = true;
|
|
23
|
+
try { resolve(onTimeout()); } catch (e) { reject(e); }
|
|
24
|
+
}, ms);
|
|
25
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
26
|
+
Promise.resolve().then(promiseFactory).then(
|
|
27
|
+
(v) => { if (done) return; done = true; clearTimeout(timer); resolve(v); },
|
|
28
|
+
(e) => { if (done) return; done = true; clearTimeout(timer); reject(e); },
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { withTimeout };
|
package/src/main/mcpStatus.cjs
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { spawn } = require('node:child_process');
|
|
13
|
-
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
13
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
|
|
14
|
+
const { withProcRole } = require('./lib/cleanEnv.cjs');
|
|
14
15
|
|
|
15
16
|
const PROBE_TIMEOUT_MS = 30_000;
|
|
16
17
|
|
|
@@ -56,7 +57,8 @@ function runProbe() {
|
|
|
56
57
|
}
|
|
57
58
|
let child;
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
+
const target = claudeSpawnTarget('aux', 'mcp-list', bin);
|
|
61
|
+
child = spawn(target.command, ['mcp', 'list'], { env: withProcRole(process.env, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
60
62
|
} catch (e) {
|
|
61
63
|
resolve({ ok: false, servers: [], error: e?.message || 'spawn error', checkedAt: Date.now() });
|
|
62
64
|
return;
|