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
|
@@ -31,14 +31,13 @@
|
|
|
31
31
|
const fs = require('node:fs');
|
|
32
32
|
const fsp = require('node:fs/promises');
|
|
33
33
|
const path = require('node:path');
|
|
34
|
-
const os = require('node:os');
|
|
35
34
|
const crypto = require('node:crypto');
|
|
36
|
-
const { allProjectCwds, activeProjectCwds } = require('
|
|
35
|
+
const { allProjectCwds, activeProjectCwds, bustProjectCwdCache } = require('./activeSessions.cjs');
|
|
36
|
+
const { classifyCwd } = require('./cwdClassify.cjs');
|
|
37
37
|
const { assertOpsWrite, resolveOpsRoot, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
38
38
|
const { ScheduleJobSchema } = require('./scheduleJobSchema.cjs');
|
|
39
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
39
40
|
|
|
40
|
-
const MACHINE_STATE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduler-machine.json');
|
|
41
|
-
const LEGACY_QUEUE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'queue.json');
|
|
42
41
|
const OPS_DIRNAME = OPS_ROOT_DIR;
|
|
43
42
|
const STATE_SUBPATH = ['scheduler', 'state'];
|
|
44
43
|
|
|
@@ -118,7 +117,12 @@ function projectHistoryPath(cwd) {
|
|
|
118
117
|
*/
|
|
119
118
|
function queuePathOrSkip(cwd, context) {
|
|
120
119
|
try {
|
|
121
|
-
|
|
120
|
+
const file = projectQueuePath(cwd);
|
|
121
|
+
// Unprovable project identity: the row stays dispatchable and is never
|
|
122
|
+
// dropped or emptied — the write itself is refused by assertOpsWrite.
|
|
123
|
+
const { kind, reason } = classifyCwd(cwd);
|
|
124
|
+
if (kind === 'unknown') console.warn(`[queueStore] ${context}: project identity unprovable for "${cwd}" (${reason}) — reads allowed, writes refused`);
|
|
125
|
+
return file;
|
|
122
126
|
} catch (e) {
|
|
123
127
|
if (e?.ephemeral) {
|
|
124
128
|
console.warn(`[queueStore] ${context}: refusing ephemeral cwd "${cwd}" — ${e.message}`);
|
|
@@ -190,8 +194,10 @@ async function writeJsonAtomic(file, value) {
|
|
|
190
194
|
// list briefly. Correctness fallback: a brand-new project appears at worst
|
|
191
195
|
// CACHE_MS late, and its first write goes through writeSplit which busts the
|
|
192
196
|
// cache.
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
// Above the 60 s dispatch-loop interval so one pass never re-scans cold; the
|
|
198
|
+
// cached array is frozen (shared, never mutated).
|
|
199
|
+
const CACHE_MS = 120_000;
|
|
200
|
+
let cwdCache = { at: 0, cwds: Object.freeze([]) };
|
|
195
201
|
|
|
196
202
|
function stateCwds(opts) {
|
|
197
203
|
const now = Date.now();
|
|
@@ -205,12 +211,18 @@ function stateCwds(opts) {
|
|
|
205
211
|
}
|
|
206
212
|
// ...and active projects are included even before their first write.
|
|
207
213
|
for (const cwd of activeProjectCwds(undefined, opts)) add(cwd);
|
|
214
|
+
Object.freeze(cwds);
|
|
208
215
|
if (!opts) cwdCache = { at: now, cwds };
|
|
209
216
|
return cwds;
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
function bustCwdCache() {
|
|
213
|
-
cwdCache = { at: 0, cwds: [] };
|
|
220
|
+
cwdCache = { at: 0, cwds: Object.freeze([]) };
|
|
221
|
+
// Without this chain, activeSessions' own scan cache hands stateCwds a
|
|
222
|
+
// stale list even after this bust — turning a caller's "I just wrote a
|
|
223
|
+
// brand-new project's first state file, make it visible now" into a
|
|
224
|
+
// no-op until activeSessions' independent TTL expires.
|
|
225
|
+
bustProjectCwdCache();
|
|
214
226
|
}
|
|
215
227
|
|
|
216
228
|
// ---------- merged read ----------
|
|
@@ -226,6 +238,10 @@ function shapeMachine(data) {
|
|
|
226
238
|
// classifyQueueStarvation's header for why the two must never merge.
|
|
227
239
|
lastDispatchAttemptAt: data.lastDispatchAttemptAt ?? null,
|
|
228
240
|
paused: data.paused ?? null,
|
|
241
|
+
// Upgrade-drain state (lib/upgradeDrain.cjs): deliberately NOT part of
|
|
242
|
+
// `paused`, so a rate-limit pause can neither overwrite it nor have the
|
|
243
|
+
// manual-clear cooldown swallow it.
|
|
244
|
+
drain: data.drain && typeof data.drain === 'object' ? data.drain : null,
|
|
229
245
|
// Launch circuit breaker (lib/launchFailure.cjs): per-persona blocks and
|
|
230
246
|
// the degraded-mode env a persona is currently launching with. Machine
|
|
231
247
|
// state, not per-project: the broken thing is the installed CLI.
|
|
@@ -286,7 +302,6 @@ function findLongestValidJsonPrefix(raw) {
|
|
|
286
302
|
// project's own ops root is the natural home for a machine-level log entry,
|
|
287
303
|
// mirroring scheduler.cjs's own `job.cwd || DEFAULT_PROJECT_CWD` fallback for
|
|
288
304
|
// cwd-less machine errors (schedulerBatch.cjs's DEFAULT_PROJECT_CWD).
|
|
289
|
-
const MACHINE_STATE_LOG_CWD = path.join(os.homedir(), 'Projects', 'session-manager');
|
|
290
305
|
|
|
291
306
|
// The truncated tail in the real 2026-09-11 tear contained an orphaned
|
|
292
307
|
// `resumeAt` fragment with no way to reconstruct the `paused` object it
|
|
@@ -301,7 +316,7 @@ const PAUSED_HINT_RE = /"paused"|resumeAt/;
|
|
|
301
316
|
function logMachineStateRecovery({ level, message, meta }) {
|
|
302
317
|
try {
|
|
303
318
|
const { appendError } = require('./opsErrorLog.cjs');
|
|
304
|
-
appendError({ cwd:
|
|
319
|
+
appendError({ cwd: schedulerPaths.machineStateLogCwd(), scope: 'scheduler', level, message, meta });
|
|
305
320
|
} catch { /* durable logging must never block recovery */ }
|
|
306
321
|
console.error(`[queueStore] ${message}`);
|
|
307
322
|
}
|
|
@@ -326,7 +341,7 @@ function buildMachineStateRecovery(raw, parseError) {
|
|
|
326
341
|
? '; the discarded tail looks like it contained paused/resumeAt data that could not '
|
|
327
342
|
+ 'be reconstructed — verify pause status manually'
|
|
328
343
|
: ''),
|
|
329
|
-
meta: { path:
|
|
344
|
+
meta: { path: schedulerPaths.machineStatePath(), prefixLength: prefix.prefixLength, totalLength: raw.length, pausedHint },
|
|
330
345
|
};
|
|
331
346
|
}
|
|
332
347
|
// No valid JSON prefix at all: fall back to defaults (shapeMachine({}) —
|
|
@@ -339,7 +354,7 @@ function buildMachineStateRecovery(raw, parseError) {
|
|
|
339
354
|
message:
|
|
340
355
|
`scheduler-machine.json unrecoverable (${parseError?.message}) — falling back to defaults `
|
|
341
356
|
+ 'so dispatch does not silently halt',
|
|
342
|
-
meta: { path:
|
|
357
|
+
meta: { path: schedulerPaths.machineStatePath(), totalLength: raw.length },
|
|
343
358
|
};
|
|
344
359
|
}
|
|
345
360
|
|
|
@@ -347,7 +362,7 @@ function recoverTornMachineStateSync(raw, parseError) {
|
|
|
347
362
|
const recovery = buildMachineStateRecovery(raw, parseError);
|
|
348
363
|
logMachineStateRecovery(recovery);
|
|
349
364
|
try {
|
|
350
|
-
writeJsonAtomicSync(
|
|
365
|
+
writeJsonAtomicSync(schedulerPaths.machineStatePath(), recovery.value);
|
|
351
366
|
} catch (e) {
|
|
352
367
|
console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
|
|
353
368
|
}
|
|
@@ -358,7 +373,7 @@ async function recoverTornMachineState(raw, parseError) {
|
|
|
358
373
|
const recovery = buildMachineStateRecovery(raw, parseError);
|
|
359
374
|
logMachineStateRecovery(recovery);
|
|
360
375
|
try {
|
|
361
|
-
await writeJsonAtomic(
|
|
376
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), recovery.value);
|
|
362
377
|
} catch (e) {
|
|
363
378
|
console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
|
|
364
379
|
}
|
|
@@ -373,10 +388,10 @@ async function recoverTornMachineState(raw, parseError) {
|
|
|
373
388
|
function loadMachineStateSync() {
|
|
374
389
|
let raw;
|
|
375
390
|
try {
|
|
376
|
-
raw = fs.readFileSync(
|
|
391
|
+
raw = fs.readFileSync(schedulerPaths.machineStatePath(), 'utf8');
|
|
377
392
|
} catch (e) {
|
|
378
393
|
if (e?.code === 'ENOENT') return {};
|
|
379
|
-
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath:
|
|
394
|
+
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
|
|
380
395
|
}
|
|
381
396
|
try {
|
|
382
397
|
return { shaped: shapeMachine(JSON.parse(raw)) };
|
|
@@ -389,10 +404,10 @@ function loadMachineStateSync() {
|
|
|
389
404
|
async function loadMachineState() {
|
|
390
405
|
let raw;
|
|
391
406
|
try {
|
|
392
|
-
raw = await fsp.readFile(
|
|
407
|
+
raw = await fsp.readFile(schedulerPaths.machineStatePath(), 'utf8');
|
|
393
408
|
} catch (e) {
|
|
394
409
|
if (e?.code === 'ENOENT') return {};
|
|
395
|
-
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath:
|
|
410
|
+
return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
|
|
396
411
|
}
|
|
397
412
|
try {
|
|
398
413
|
return { shaped: shapeMachine(JSON.parse(raw)) };
|
|
@@ -437,15 +452,17 @@ function shapeJobs(raw, file) {
|
|
|
437
452
|
|
|
438
453
|
/**
|
|
439
454
|
* readMergedSync(opts?) → { config, jobs, scheduledFor, lastRunAt, paused,
|
|
440
|
-
* unreadable?, unreadablePath?, sourceCwds }.
|
|
455
|
+
* unreadable?, unreadablePath?, unreadableCwds, sourceCwds }.
|
|
441
456
|
*
|
|
442
|
-
* `unreadable`
|
|
443
|
-
* exists but fails to parse
|
|
444
|
-
*
|
|
445
|
-
*
|
|
457
|
+
* `unreadable` is reserved for the machine-runtime file alone (a machine-wide
|
|
458
|
+
* halt). A project shard that exists but fails to read/parse is quarantined
|
|
459
|
+
* per cwd in `unreadableCwds: [{cwd, file, error}]` — other projects keep
|
|
460
|
+
* dispatching, and the torn shard is never treated as empty, recovered, or
|
|
461
|
+
* written. `sourceCwds` records every healthy project file consulted so
|
|
462
|
+
* writeSplit can persist "this project now has zero jobs".
|
|
446
463
|
*/
|
|
447
464
|
function readMergedSync(opts) {
|
|
448
|
-
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
|
|
465
|
+
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
|
|
449
466
|
const sourceCwds = [];
|
|
450
467
|
const machine = loadMachineStateSync();
|
|
451
468
|
if (machine.shaped) {
|
|
@@ -468,8 +485,9 @@ function readMergedSync(opts) {
|
|
|
468
485
|
sourceCwds.push(cwd);
|
|
469
486
|
} catch (e) {
|
|
470
487
|
if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
|
|
471
|
-
|
|
472
|
-
|
|
488
|
+
// Quarantine this ONE shard; never recover or rewrite it (a job shard is a
|
|
489
|
+
// ledger — a prefix could time-travel a running row back to pending).
|
|
490
|
+
out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
|
|
473
491
|
}
|
|
474
492
|
}
|
|
475
493
|
defineSources(out, sourceCwds);
|
|
@@ -478,7 +496,7 @@ function readMergedSync(opts) {
|
|
|
478
496
|
|
|
479
497
|
/** Async twin of readMergedSync for IPC hot paths. */
|
|
480
498
|
async function readMerged(opts) {
|
|
481
|
-
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
|
|
499
|
+
const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
|
|
482
500
|
const sourceCwds = [];
|
|
483
501
|
const machine = await loadMachineState();
|
|
484
502
|
if (machine.shaped) {
|
|
@@ -501,8 +519,9 @@ async function readMerged(opts) {
|
|
|
501
519
|
sourceCwds.push(cwd);
|
|
502
520
|
} catch (e) {
|
|
503
521
|
if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
|
|
504
|
-
|
|
505
|
-
|
|
522
|
+
// Quarantine this ONE shard; never recover or rewrite it (a job shard is a
|
|
523
|
+
// ledger — a prefix could time-travel a running row back to pending).
|
|
524
|
+
out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
|
|
506
525
|
}
|
|
507
526
|
}
|
|
508
527
|
defineSources(out, sourceCwds);
|
|
@@ -526,21 +545,25 @@ function defineSources(state, sourceCwds) {
|
|
|
526
545
|
* deletions stick.
|
|
527
546
|
*/
|
|
528
547
|
async function writeSplit(state, defaultCwd) {
|
|
529
|
-
await writeJsonAtomic(
|
|
548
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), {
|
|
530
549
|
config: state.config,
|
|
531
550
|
scheduledFor: state.scheduledFor ?? null,
|
|
532
551
|
lastRunAt: state.lastRunAt ?? null,
|
|
533
552
|
lastDispatchAttemptAt: state.lastDispatchAttemptAt ?? null,
|
|
534
553
|
paused: state.paused ?? null,
|
|
554
|
+
drain: state.drain ?? null,
|
|
535
555
|
launchBlocks: state.launchBlocks ?? {},
|
|
536
556
|
launchMitigations: state.launchMitigations ?? {},
|
|
537
557
|
});
|
|
538
558
|
|
|
559
|
+
// A quarantined shard is left byte-identical: never re-created or clobbered,
|
|
560
|
+
// even if merged rows carry that cwd.
|
|
561
|
+
const quarantined = new Set((state.unreadableCwds ?? []).map((u) => u.cwd));
|
|
539
562
|
const byCwd = new Map();
|
|
540
|
-
for (const cwd of state.sourceCwds ?? []) byCwd.set(cwd, []);
|
|
563
|
+
for (const cwd of state.sourceCwds ?? []) if (!quarantined.has(cwd)) byCwd.set(cwd, []);
|
|
541
564
|
for (const job of state.jobs ?? []) {
|
|
542
565
|
const cwd = job.cwd || defaultCwd;
|
|
543
|
-
if (!cwd) continue; // nowhere to put it; job is dropped from persistence rather than crashing
|
|
566
|
+
if (!cwd || quarantined.has(cwd)) continue; // nowhere to put it; job is dropped from persistence rather than crashing
|
|
544
567
|
if (!byCwd.has(cwd)) byCwd.set(cwd, []);
|
|
545
568
|
byCwd.get(cwd).push(job);
|
|
546
569
|
}
|
|
@@ -571,7 +594,7 @@ async function writeSplit(state, defaultCwd) {
|
|
|
571
594
|
async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
572
595
|
let raw;
|
|
573
596
|
try {
|
|
574
|
-
raw = await fsp.readFile(
|
|
597
|
+
raw = await fsp.readFile(schedulerPaths.legacyQueuePath(), 'utf8');
|
|
575
598
|
} catch {
|
|
576
599
|
return { migrated: false };
|
|
577
600
|
}
|
|
@@ -583,8 +606,8 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
|
583
606
|
return { migrated: false, error: e?.message };
|
|
584
607
|
}
|
|
585
608
|
|
|
586
|
-
if (!fs.existsSync(
|
|
587
|
-
await writeJsonAtomic(
|
|
609
|
+
if (!fs.existsSync(schedulerPaths.machineStatePath())) {
|
|
610
|
+
await writeJsonAtomic(schedulerPaths.machineStatePath(), {
|
|
588
611
|
config: legacy.config || {},
|
|
589
612
|
scheduledFor: legacy.scheduledFor ?? null,
|
|
590
613
|
lastRunAt: legacy.lastRunAt ?? null,
|
|
@@ -617,14 +640,12 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
|
617
640
|
}
|
|
618
641
|
}
|
|
619
642
|
|
|
620
|
-
await fsp.rename(
|
|
643
|
+
await fsp.rename(schedulerPaths.legacyQueuePath(), `${schedulerPaths.legacyQueuePath()}.retired-${Date.now()}`);
|
|
621
644
|
bustCwdCache();
|
|
622
645
|
return { migrated: true, moved, projects: byCwd.size };
|
|
623
646
|
}
|
|
624
647
|
|
|
625
648
|
module.exports = {
|
|
626
|
-
MACHINE_STATE_PATH,
|
|
627
|
-
LEGACY_QUEUE_PATH,
|
|
628
649
|
STATE_SUBPATH,
|
|
629
650
|
projectStateDir,
|
|
630
651
|
projectQueuePath,
|
|
@@ -640,3 +661,7 @@ module.exports = {
|
|
|
640
661
|
writeJsonAtomicSync,
|
|
641
662
|
findLongestValidJsonPrefix,
|
|
642
663
|
};
|
|
664
|
+
|
|
665
|
+
// Lazy path getters (SM_SCHEDULER_HOME resolved at read, never at require).
|
|
666
|
+
Object.defineProperty(module.exports, 'MACHINE_STATE_PATH', { get: schedulerPaths.machineStatePath, enumerable: true });
|
|
667
|
+
Object.defineProperty(module.exports, 'LEGACY_QUEUE_PATH', { get: schedulerPaths.legacyQueuePath, enumerable: true });
|
|
@@ -12,17 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
+
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
16
|
+
const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
|
|
17
|
+
|
|
15
18
|
// slug of the job currently holding the lease, or null.
|
|
16
19
|
let heldBySlug = null;
|
|
20
|
+
let heldClaimedAt = 0;
|
|
17
21
|
|
|
18
22
|
function isHeld() {
|
|
19
23
|
return heldBySlug !== null;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** acquire(slug) → true if the lease was free and is now held by `slug`. */
|
|
23
|
-
function acquire(slug) {
|
|
27
|
+
function acquire(slug, { claimedAt = Date.now() } = {}) {
|
|
24
28
|
if (heldBySlug !== null) return false;
|
|
25
29
|
heldBySlug = String(slug);
|
|
30
|
+
heldClaimedAt = claimedAt;
|
|
26
31
|
return true;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -36,6 +41,17 @@ function release(slug) {
|
|
|
36
41
|
return true;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
/** expireDead — free a lease whose holder has no running row and is past the grace window
|
|
45
|
+
* (the lease never carries a pid; the row-liveness + grace proof is the whole test). */
|
|
46
|
+
function expireDead({ liveSlugs, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
|
|
47
|
+
if (heldBySlug === null) return false;
|
|
48
|
+
const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
|
|
49
|
+
if (!isProvablyDead({ claimedAt: heldClaimedAt, pid: null }, { live: live.has(heldBySlug), now, graceMs })) return false;
|
|
50
|
+
appendAuditEvent('quiet_lease_expired', { slug: heldBySlug });
|
|
51
|
+
heldBySlug = null;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
function holder() {
|
|
40
56
|
return heldBySlug;
|
|
41
57
|
}
|
|
@@ -43,6 +59,7 @@ function holder() {
|
|
|
43
59
|
/** Test hook: force the lease back to free. */
|
|
44
60
|
function __resetForTests() {
|
|
45
61
|
heldBySlug = null;
|
|
62
|
+
heldClaimedAt = 0;
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
module.exports = { isHeld, acquire, release, holder, __resetForTests };
|
|
65
|
+
module.exports = { isHeld, acquire, release, expireDead, holder, __resetForTests };
|
|
@@ -36,26 +36,37 @@ function claudePidAlive(pid) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* findLiveProcessForJob(job, { worktreeDir }) → pid | null
|
|
39
|
+
* findLiveProcessForJob(job, { worktreeDir, runCwd }) → pid | null
|
|
40
40
|
*
|
|
41
41
|
* Positive liveness scan for a 'running' row whose `runtime.pid` is missing —
|
|
42
42
|
* the case a missing pid must NOT be read as "the process is gone" (2026-09-06
|
|
43
43
|
* incident: 234-uranus-eight-tails-ox marked failed/never_ran while PID
|
|
44
44
|
* 2174739 was a live `claude -p` still writing to that job's own worktree).
|
|
45
45
|
*
|
|
46
|
-
* Linux-`/proc` only. Scans every numeric `/proc/<pid>` entry and matches
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
* Linux-`/proc` only. Scans every numeric `/proc/<pid>` entry and matches any
|
|
47
|
+
* of, in order:
|
|
48
|
+
* - `/proc/<pid>/cwd` resolves to `worktreeDir` (or a path nested under it)
|
|
49
|
+
* — sufficient alone, no cmdline check needed.
|
|
50
|
+
* - `/proc/<pid>/cmdline` contains both `claude` and the job's slug —
|
|
51
|
+
* fallback for a worktree-disabled/in-place run with no dedicated
|
|
52
|
+
* worktreeDir to match against.
|
|
53
|
+
* - `/proc/<pid>/cwd` resolves to `runCwd` (or nested under it) AND
|
|
54
|
+
* `/proc/<pid>/cmdline` contains `claude` (2026-09-13 incident:
|
|
55
|
+
* runId 2026-09-13T16-15-56-818Z ran in-place in the shared tree — no
|
|
56
|
+
* worktree, and dispatch never puts the slug into argv — so neither of
|
|
57
|
+
* the above could ever match a live pid 3138458). `runCwd` is deliberately
|
|
58
|
+
* NEVER matched alone: several concurrent jobs commonly share one tree's
|
|
59
|
+
* cwd, so a bare cwd match without the process-identity check would
|
|
60
|
+
* cross-match the wrong job's process.
|
|
61
|
+
*
|
|
62
|
+
* Returns the first matching pid, or null if none is found.
|
|
52
63
|
*
|
|
53
64
|
* Safe fallback by construction: on any platform without `/proc` (macOS,
|
|
54
65
|
* Windows) `fs.readdirSync('/proc')` throws and this returns null immediately
|
|
55
66
|
* — i.e. exactly today's behaviour (fail toward terminalizing), never a hang
|
|
56
67
|
* or a thrown error propagating to the caller.
|
|
57
68
|
*/
|
|
58
|
-
function findLiveProcessForJob(job, { worktreeDir } = {}) {
|
|
69
|
+
function findLiveProcessForJob(job, { worktreeDir, runCwd } = {}) {
|
|
59
70
|
const slug = job?.slug;
|
|
60
71
|
let entries;
|
|
61
72
|
try {
|
|
@@ -67,27 +78,107 @@ function findLiveProcessForJob(job, { worktreeDir } = {}) {
|
|
|
67
78
|
if (!/^\d+$/.test(name)) continue;
|
|
68
79
|
const pid = Number(name);
|
|
69
80
|
if (!pid || pid <= 1) continue;
|
|
70
|
-
|
|
81
|
+
|
|
82
|
+
let cwdLink = null;
|
|
83
|
+
if (worktreeDir || runCwd) {
|
|
71
84
|
try {
|
|
72
|
-
|
|
73
|
-
if (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep)) return pid;
|
|
85
|
+
cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
74
86
|
} catch {
|
|
75
87
|
// Process exited mid-scan, or permission denied — try the argv
|
|
76
88
|
// fallback below before giving up on this pid.
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
|
-
|
|
91
|
+
|
|
92
|
+
if (worktreeDir && cwdLink && (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep))) {
|
|
93
|
+
return pid;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let cmd = null;
|
|
97
|
+
if (slug || (runCwd && cwdLink)) {
|
|
80
98
|
try {
|
|
81
|
-
|
|
82
|
-
if (/\bclaude\b/.test(cmd) && cmd.includes(slug)) return pid;
|
|
99
|
+
cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
|
|
83
100
|
} catch {
|
|
84
101
|
// Same as above — process gone or unreadable, keep scanning.
|
|
85
102
|
}
|
|
86
103
|
}
|
|
104
|
+
|
|
105
|
+
if (slug && cmd && /\bclaude\b/.test(cmd) && cmd.includes(slug)) return pid;
|
|
106
|
+
|
|
107
|
+
if (runCwd && cwdLink && cmd && /\bclaude\b/.test(cmd) &&
|
|
108
|
+
(cwdLink === runCwd || cwdLink.startsWith(runCwd + path.sep))) {
|
|
109
|
+
return pid;
|
|
110
|
+
}
|
|
87
111
|
}
|
|
88
112
|
return null;
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
/**
|
|
116
|
+
* readSpawnedPidFromLog(logPath) → pid | null
|
|
117
|
+
*
|
|
118
|
+
* Recovers the pid a pidless row's spawn actually produced by reading the
|
|
119
|
+
* run's own log — proof of life that pre-dates and is independent of the
|
|
120
|
+
* `runtime.pid` persistence this reap path exists to route around (2026-09-13
|
|
121
|
+
* incident: the mutate that stamps runtime.pid failed silently while the
|
|
122
|
+
* spawn itself, and this exact log line, succeeded). Reads only the first
|
|
123
|
+
* ~8KB — scheduler.cjs's spawnJob writes `[scheduler] spawned pid=<n> ...`
|
|
124
|
+
* synchronously right after fork, always near the top of the log — so this
|
|
125
|
+
* stays cheap against a multi-MB log. Pure IO, never throws: a missing file,
|
|
126
|
+
* an empty file, or a log with no such line all return null.
|
|
127
|
+
*/
|
|
128
|
+
function readSpawnedPidFromLog(logPath) {
|
|
129
|
+
if (!logPath) return null;
|
|
130
|
+
let fd;
|
|
131
|
+
try {
|
|
132
|
+
fd = fs.openSync(logPath, 'r');
|
|
133
|
+
const buf = Buffer.alloc(8192);
|
|
134
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
135
|
+
const text = buf.toString('utf8', 0, bytesRead);
|
|
136
|
+
const match = text.match(/\[scheduler\] spawned pid=(\d+)/);
|
|
137
|
+
if (!match) return null;
|
|
138
|
+
const pid = Number(match[1]);
|
|
139
|
+
return Number.isFinite(pid) && pid > 1 ? pid : null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
} finally {
|
|
143
|
+
if (fd !== undefined) {
|
|
144
|
+
try { fs.closeSync(fd); } catch { /* already closed / never opened */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* readLogMtimeMs(logPath) → number | null
|
|
151
|
+
*
|
|
152
|
+
* Same never-throws IO contract as logHasOutput — returns the run log's
|
|
153
|
+
* last-modified time, or null when it can't be statted (missing file,
|
|
154
|
+
* permission denied).
|
|
155
|
+
*/
|
|
156
|
+
function readLogMtimeMs(logPath) {
|
|
157
|
+
if (!logPath) return null;
|
|
158
|
+
try {
|
|
159
|
+
return fs.statSync(logPath).mtimeMs;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* isLogFresh(logMtimeMs, now, windowMs) → boolean
|
|
167
|
+
*
|
|
168
|
+
* Pure predicate: true when a run log was modified within `windowMs` of
|
|
169
|
+
* `now`. A log that grew recently is positive, independent proof its process
|
|
170
|
+
* is still alive — stronger evidence than "no runtime.pid recorded", which is
|
|
171
|
+
* only bookkeeping about a side-channel write that can fail on its own (see
|
|
172
|
+
* readSpawnedPidFromLog's header). Returns false for a missing/invalid mtime
|
|
173
|
+
* or window so a caller that couldn't stat the log falls through to the
|
|
174
|
+
* existing reap logic rather than treating "unknown" as "fresh".
|
|
175
|
+
*/
|
|
176
|
+
function isLogFresh(logMtimeMs, now, windowMs) {
|
|
177
|
+
if (typeof logMtimeMs !== 'number' || Number.isNaN(logMtimeMs)) return false;
|
|
178
|
+
if (typeof windowMs !== 'number' || windowMs <= 0) return false;
|
|
179
|
+
return (now - logMtimeMs) < windowMs;
|
|
180
|
+
}
|
|
181
|
+
|
|
91
182
|
/**
|
|
92
183
|
* True when `logPath` exists and has non-zero content — the literal "did the
|
|
93
184
|
* run dir produce any log output" check that gates whether a pidless reap may
|
|
@@ -224,6 +315,20 @@ const ORPHAN_REQUEUE_CAP = 5;
|
|
|
224
315
|
* work (2026-09-06 incident — see findLiveProcessForJob's header). Omitting
|
|
225
316
|
* `findLiveProcess` (existing callers/tests) preserves prior behaviour
|
|
226
317
|
* exactly: every pidless row past grace reaps, none are ever recovered.
|
|
318
|
+
*
|
|
319
|
+
* Two more optional injections, checked BEFORE `findLiveProcess`, for a
|
|
320
|
+
* pidless row past grace (2026-09-13 incident — see readSpawnedPidFromLog's
|
|
321
|
+
* and isLogFresh's own headers):
|
|
322
|
+
* - `getLogMtimeMs` (`(job) → number | null`): if the row's run log was
|
|
323
|
+
* modified within `logFreshWindowMs`, the row is skipped entirely — not
|
|
324
|
+
* pushed to `reapable` OR `recovered` — because a growing log is positive
|
|
325
|
+
* proof of life even with no pid evidence at all. One `warnings` entry is
|
|
326
|
+
* still emitted so a permanently-pidless-but-chatty row stays visible.
|
|
327
|
+
* - `getLogPid` (`(job) → pid | null`): reads the run's own log for the pid
|
|
328
|
+
* its spawn actually produced. If `pidAlive(pid)` is true, the row is
|
|
329
|
+
* diverted into `recovered` exactly like a `findLiveProcess` hit.
|
|
330
|
+
* Both default to a no-op when omitted, so existing callers/tests are
|
|
331
|
+
* unaffected.
|
|
227
332
|
*/
|
|
228
333
|
/**
|
|
229
334
|
* Pure, never-throws formatter for the dispatch-phase breadcrumb appended to
|
|
@@ -246,7 +351,47 @@ function formatDispatchPhaseSuffix(j) {
|
|
|
246
351
|
: ` (last dispatch phase: ${j.dispatchPhase})`;
|
|
247
352
|
}
|
|
248
353
|
|
|
249
|
-
|
|
354
|
+
/**
|
|
355
|
+
* resolvePidlessFailureOverride(job) → { verdict, landedCommit, reason } | null
|
|
356
|
+
*
|
|
357
|
+
* The evidence-before-failure guard this PRD adds (1173): a pidless reap
|
|
358
|
+
* about to stamp 'failed' purely because `runtime.pid` was never recorded
|
|
359
|
+
* must first check whether THIS ROW already carries a `landedCommit` — proof
|
|
360
|
+
* (from an earlier dispatch of the same slug, since `landedCommit` is
|
|
361
|
+
* deliberately never cleared by a reset — see scheduler.cjs's comment near
|
|
362
|
+
* resetJobFields) that a real commit landed on the branch. Returns null when
|
|
363
|
+
* there is no such evidence, leaving the existing 'failed' transition for a
|
|
364
|
+
* genuinely-never-spawned row completely untouched.
|
|
365
|
+
*
|
|
366
|
+
* DESTINATION CHOICE — needs_review, never completed. `landedCommit` is
|
|
367
|
+
* real git evidence (it is only ever stamped from an actual HEAD advance or
|
|
368
|
+
* a proven branch-integration — see jobLandedCommitThisRun / the dead-pid
|
|
369
|
+
* reap's own isBranchAlreadyIntegrated+computeCommittedDuringRun proof in
|
|
370
|
+
* scheduler.cjs), so it is never speculative in the sense of "guessed before
|
|
371
|
+
* the work ran". But the pidless-reap path performs NO fresh integration
|
|
372
|
+
* check of its own for the CURRENT run (that proof only runs for the dead-pid
|
|
373
|
+
* branch's `dead` entries with outcome==='success' — see
|
|
374
|
+
* scheduler.cjs's integrationResults loop) — so at this call site there is no
|
|
375
|
+
* re-verified guarantee that the recorded commit corresponds to *this*
|
|
376
|
+
* dispatch rather than surviving, unreaped, from an earlier one. Real
|
|
377
|
+
* evidence that under-specifies correspondence to the current run is exactly
|
|
378
|
+
* the needs_review shape, not completed: a human (or the finish-protocol
|
|
379
|
+
* commit-guard on the next dispatch) can resolve it conclusively with
|
|
380
|
+
* `git show <sha>`, but this pure predicate must not guess.
|
|
381
|
+
*/
|
|
382
|
+
function resolvePidlessFailureOverride(job) {
|
|
383
|
+
const landedCommit = job?.landedCommit || null;
|
|
384
|
+
if (!landedCommit) return null;
|
|
385
|
+
return {
|
|
386
|
+
verdict: 'pidless_reap_with_landed_commit',
|
|
387
|
+
landedCommit,
|
|
388
|
+
reason: `pidless reap found no runtime.pid, but this row already carries landedCommit ${landedCommit} — routing to needs_review instead of failed; verify with \`git show ${landedCommit}\``,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function selectReapableJobs(jobs, now, {
|
|
393
|
+
pidAlive, grace, findLiveProcess, getLogMtimeMs, getLogPid, logFreshWindowMs,
|
|
394
|
+
} = {}) {
|
|
250
395
|
const reapable = [];
|
|
251
396
|
const warnings = [];
|
|
252
397
|
const recovered = [];
|
|
@@ -265,6 +410,22 @@ function selectReapableJobs(jobs, now, { pidAlive, grace, findLiveProcess } = {}
|
|
|
265
410
|
}
|
|
266
411
|
const ageMs = now - startedAt;
|
|
267
412
|
if (ageMs < grace) continue; // spawn may still be mid-flight
|
|
413
|
+
|
|
414
|
+
const logMtimeMs = typeof getLogMtimeMs === 'function' ? getLogMtimeMs(j) : null;
|
|
415
|
+
if (isLogFresh(logMtimeMs, now, logFreshWindowMs)) {
|
|
416
|
+
warnings.push({
|
|
417
|
+
slug: j.slug,
|
|
418
|
+
reason: `pidless row skipped — run log modified ${Math.round((now - logMtimeMs) / 1000)}s ago, still alive by log evidence`,
|
|
419
|
+
});
|
|
420
|
+
continue; // neither reapable nor recovered — the log proves it's alive
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const logPid = typeof getLogPid === 'function' ? getLogPid(j) : null;
|
|
424
|
+
if (logPid && pidAlive(logPid)) {
|
|
425
|
+
recovered.push({ slug: j.slug, pid: logPid });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
268
429
|
const livePid = typeof findLiveProcess === 'function' ? findLiveProcess(j) : null;
|
|
269
430
|
if (livePid) {
|
|
270
431
|
recovered.push({ slug: j.slug, pid: livePid });
|
|
@@ -275,6 +436,7 @@ function selectReapableJobs(jobs, now, { pidAlive, grace, findLiveProcess } = {}
|
|
|
275
436
|
pid: null,
|
|
276
437
|
pidless: true,
|
|
277
438
|
reason: `reaped: no runtime.pid recorded after ${Math.round(grace / 60_000)}m — spawn never completed${formatDispatchPhaseSuffix(j)}`,
|
|
439
|
+
failureOverride: resolvePidlessFailureOverride(j),
|
|
278
440
|
});
|
|
279
441
|
}
|
|
280
442
|
return { reapable, warnings, recovered };
|
|
@@ -343,7 +505,11 @@ module.exports = {
|
|
|
343
505
|
findLiveProcessForJob,
|
|
344
506
|
logHasOutput,
|
|
345
507
|
resolvePidlessGateOutcome,
|
|
508
|
+
resolvePidlessFailureOverride,
|
|
346
509
|
isAlreadySatisfiedOnMain,
|
|
347
510
|
resolveCommitGuardOutcome,
|
|
348
511
|
formatDispatchPhaseSuffix,
|
|
512
|
+
readSpawnedPidFromLog,
|
|
513
|
+
readLogMtimeMs,
|
|
514
|
+
isLogFresh,
|
|
349
515
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reservationExpiry.cjs — the single fail-CLOSED "is this reservation's owner
|
|
3
|
+
* provably dead?" predicate shared by sessionSlots, quietMachineLease, the
|
|
4
|
+
* investigation set and gitWorktree's accounting. A reservation is released
|
|
5
|
+
* only on proof: no live row for its slug AND (a stamped pid that is dead, or
|
|
6
|
+
* no pid ever stamped and claimedAt older than graceMs). Anything ambiguous
|
|
7
|
+
* (live row, live pid, inside the grace window, claimed after the pass began)
|
|
8
|
+
* is kept. Never signals a process.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_GRACE_MS = 10 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function defaultPidAlive(pid) {
|
|
15
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e?.code === 'EPERM'; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ claimedAt: number, pid: number|null }} res
|
|
20
|
+
* @param {{ live: boolean, pidAlive?: (pid:number)=>boolean, now: number, graceMs?: number }} ctx
|
|
21
|
+
*/
|
|
22
|
+
function isProvablyDead(res, { live, pidAlive = defaultPidAlive, now, graceMs = DEFAULT_GRACE_MS }) {
|
|
23
|
+
if (live) return false;
|
|
24
|
+
// Made after the pass began (i.e. this tick) — never expire it.
|
|
25
|
+
if (!Number.isFinite(res.claimedAt) || res.claimedAt > now) return false;
|
|
26
|
+
if (Number.isInteger(res.pid) && res.pid > 0) return !pidAlive(res.pid);
|
|
27
|
+
return now - res.claimedAt > graceMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { isProvablyDead, defaultPidAlive, DEFAULT_GRACE_MS };
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { spawn } = require('node:child_process');
|
|
18
|
-
const { resolveClaudeBin } = require('./claudeBin.cjs');
|
|
18
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
|
|
19
|
+
const { withProcRole } = require('./cleanEnv.cjs');
|
|
19
20
|
|
|
20
21
|
const MAX_OUT_BYTES = 8 * 1024 * 1024;
|
|
21
22
|
|
|
@@ -31,7 +32,8 @@ function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, systemPromp
|
|
|
31
32
|
'--output-format', 'text',
|
|
32
33
|
];
|
|
33
34
|
if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
|
|
34
|
-
const
|
|
35
|
+
const target = claudeSpawnTarget('aux', 'runclaudep', bin);
|
|
36
|
+
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
35
37
|
let out = '';
|
|
36
38
|
let err = '';
|
|
37
39
|
let killedForSize = false;
|