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
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Directory shape: one subdirectory per tick (`RUNS_DIR/<iso-ts>/`, minted by
|
|
14
14
|
* scheduler.cjs's pickRunDir), which commonly holds MANY different PRD
|
|
15
|
-
* slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`,
|
|
15
|
+
* slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`, the
|
|
16
|
+
* `<slug>.supervisor.json` dispatch record (jobSupervisorRecord.cjs), and an
|
|
16
17
|
* optional `root-cause-<slug>.md` (rcaReport.cjs) per slug. Retention is
|
|
17
18
|
* therefore computed per (runId, slug) ENTRY, not per directory: a directory
|
|
18
19
|
* is only "fully removable" once every entry it holds is independently
|
|
@@ -131,7 +132,7 @@ function scanRunEntries(runsDir) {
|
|
|
131
132
|
for (const metaFile of metaFiles) {
|
|
132
133
|
const slug = metaFile.slice(0, -META_SUFFIX.length);
|
|
133
134
|
const relatedNames = files.filter(
|
|
134
|
-
(f) => f === metaFile || f === `${slug}.log` || f === `root-cause-${slug}.md`
|
|
135
|
+
(f) => f === metaFile || f === `${slug}.log` || f === `${slug}.supervisor.json` || f === `root-cause-${slug}.md`
|
|
135
136
|
);
|
|
136
137
|
|
|
137
138
|
let sizeBytes = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canonical runtime schema for a ScheduleJob record, mirroring the TS
|
|
3
|
-
* `ScheduleJob` interface
|
|
3
|
+
* `ScheduleJob` interface (`ScheduleJob` in src/preload/api.d.ts) field-for-field.
|
|
4
4
|
*
|
|
5
5
|
* Modelled directly on promptSessionSchema.cjs's "zod schema asserted at the
|
|
6
6
|
* main-process boundary" pattern: the Epic entity has had this guarantee
|
|
@@ -52,7 +52,7 @@ const ScheduleJobStatusHistoryEntrySchema = z
|
|
|
52
52
|
.passthrough();
|
|
53
53
|
|
|
54
54
|
// Only `slug` and `status` are required. Every other ScheduleJob field
|
|
55
|
-
// (src/preload/api.d.ts
|
|
55
|
+
// (`ScheduleJob` in src/preload/api.d.ts) is declared here for documentation and type
|
|
56
56
|
// checking when present, but kept optional: a job row moves through several
|
|
57
57
|
// legitimate partial shapes across its life (freshly minted with nulls,
|
|
58
58
|
// mid-run with runtime fields, terminal with exitCode/error) and plenty of
|
|
@@ -124,6 +124,14 @@ const ScheduleJobSchema = z
|
|
|
124
124
|
// finalize (both the normal completion path and reapDeadRunningJobs).
|
|
125
125
|
dispatchPhase: z.string().optional(),
|
|
126
126
|
dispatchPhaseAt: z.string().optional(),
|
|
127
|
+
// needs_review episode bookkeeping (needsReviewLedger.cjs / PRD: needs_review
|
|
128
|
+
// durability): stamped by scheduleJobTransitions.cjs the instant a job
|
|
129
|
+
// transitions INTO 'needs_review', so the RESOLUTION line written when it
|
|
130
|
+
// later transitions back OUT can reference the same runId and compute a
|
|
131
|
+
// truthful dwell time — same lifecycle as heldReason/dispatchPhase (stamped
|
|
132
|
+
// at entry, deleted at resolution).
|
|
133
|
+
needsReviewEntryRunId: z.string().nullable().optional(),
|
|
134
|
+
needsReviewEnteredAt: z.string().nullable().optional(),
|
|
127
135
|
})
|
|
128
136
|
.passthrough();
|
|
129
137
|
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
34
34
|
const telemetryCounters = require('./telemetryCounters.cjs');
|
|
35
|
+
const queueHistory = require('./queueHistory.cjs');
|
|
36
|
+
const { buildNeedsReviewEntryLine, buildNeedsReviewResolutionLine } = require('./needsReviewLedger.cjs');
|
|
35
37
|
|
|
36
38
|
// A run genuinely finished when it leaves 'running' for one of these —
|
|
37
39
|
// distinct from every other legal edge in LEGAL_TRANSITIONS (retries,
|
|
@@ -195,6 +197,64 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
|
|
|
195
197
|
telemetryCounters.trackSchedulerJobFinish({ status: toStatus });
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// needs_review ledger (PRD: needs_review durability). This IS the single
|
|
201
|
+
// funnel every needs_review transition — entry or resolution — passes
|
|
202
|
+
// through (see this file's own header comment: transitionJob is the ONE
|
|
203
|
+
// place `status` is assigned), so hooking here, rather than any one of
|
|
204
|
+
// needs_review's several distinct entry call sites in scheduler.cjs,
|
|
205
|
+
// guarantees no path can bypass it.
|
|
206
|
+
//
|
|
207
|
+
// Fire-and-forget, exactly like every other best-effort side-channel write
|
|
208
|
+
// in this codebase (rcaReport.cjs, auditLog.cjs's own posture) — a ledger
|
|
209
|
+
// write failure must never fail or delay the transition it's recording.
|
|
210
|
+
// 'investigating' is a PAUSE, not a resolution, when it's reached FROM
|
|
211
|
+
// needs_review: spawnInvestigation always restores the job to exactly the
|
|
212
|
+
// status it left (a snapshot taken before the pause), so
|
|
213
|
+
// needs_review -> investigating -> needs_review is one continuous episode,
|
|
214
|
+
// not two. Without this check, the pause mints a spurious 'auto-fix'
|
|
215
|
+
// resolution mid-episode, then the resume mints a spurious fresh entry
|
|
216
|
+
// that overwrites the original episode's runId/enteredAt/reason in the
|
|
217
|
+
// ledger's by-key maps (keyed on slug+runId) — corrupting byLadderRung and
|
|
218
|
+
// understating dwell time. Detected via the stamped episode fields
|
|
219
|
+
// themselves rather than the transition's source string, since the same
|
|
220
|
+
// 'investigating' status is also reached fresh from 'failed' (a job that
|
|
221
|
+
// was never parked needs_review at all, and has no episode to pause).
|
|
222
|
+
const isNeedsReviewPause = from === 'needs_review' && toStatus === 'investigating';
|
|
223
|
+
const pausedEpisodeOpen = job.needsReviewEntryRunId !== undefined;
|
|
224
|
+
const isNeedsReviewResume = from === 'investigating' && pausedEpisodeOpen;
|
|
225
|
+
|
|
226
|
+
if (toStatus === 'needs_review' && from !== 'needs_review' && !isNeedsReviewResume) {
|
|
227
|
+
// ENTRY: stamp the episode's runId/enteredAt on the job itself so the
|
|
228
|
+
// RESOLUTION line (below) can reference the SAME runId even if the job's
|
|
229
|
+
// own `runId` is reassigned in between (e.g. a resume-recovery
|
|
230
|
+
// re-dispatch mints a fresh one before this episode resolves).
|
|
231
|
+
job.needsReviewEntryRunId = job.runId ?? null;
|
|
232
|
+
job.needsReviewEnteredAt = entry.at;
|
|
233
|
+
queueHistory.appendHistory([buildNeedsReviewEntryLine(job, entry)]).catch((e) => {
|
|
234
|
+
console.error('[scheduleJobTransitions] failed to append needs_review entry', e?.message ?? String(e));
|
|
235
|
+
});
|
|
236
|
+
} else if (
|
|
237
|
+
(from === 'needs_review' && toStatus !== 'needs_review' && !isNeedsReviewPause)
|
|
238
|
+
|| (isNeedsReviewResume && toStatus !== 'needs_review')
|
|
239
|
+
) {
|
|
240
|
+
// RESOLUTION: read the episode stamped at entry, then clear it — the
|
|
241
|
+
// episode is over regardless of what this resolution line looks like.
|
|
242
|
+
// The `isNeedsReviewResume` arm covers an episode that paused for
|
|
243
|
+
// investigation and is now resolving to something other than
|
|
244
|
+
// needs_review (e.g. the investigation actually fixed it) — the episode
|
|
245
|
+
// stamps were deliberately left untouched across the pause, so this
|
|
246
|
+
// still references the ORIGINAL entry time, not the pause's.
|
|
247
|
+
const episode = { runId: job.needsReviewEntryRunId ?? null, enteredAt: job.needsReviewEnteredAt ?? null };
|
|
248
|
+
delete job.needsReviewEntryRunId;
|
|
249
|
+
delete job.needsReviewEnteredAt;
|
|
250
|
+
queueHistory.appendHistory([buildNeedsReviewResolutionLine(job, entry, episode)]).catch((e) => {
|
|
251
|
+
console.error('[scheduleJobTransitions] failed to append needs_review resolution', e?.message ?? String(e));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// else: isNeedsReviewPause (needs_review -> investigating), or
|
|
255
|
+
// isNeedsReviewResume && toStatus === 'needs_review' (investigating ->
|
|
256
|
+
// needs_review, still the same open episode) — intentionally a no-op.
|
|
257
|
+
|
|
198
258
|
return true;
|
|
199
259
|
}
|
|
200
260
|
|
|
@@ -75,7 +75,7 @@ function dedupeCycles(cycles) {
|
|
|
75
75
|
return out;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function
|
|
78
|
+
function makeDepResolver(projectJobs, satisfiedSlugs = new Set()) {
|
|
79
79
|
const rowBySlug = new Map(projectJobs.map((j) => [j.slug, j]));
|
|
80
80
|
const rowsByBareSlug = new Map();
|
|
81
81
|
for (const j of projectJobs) {
|
|
@@ -102,6 +102,31 @@ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
|
102
102
|
}
|
|
103
103
|
return satisfiedBareSlugs.has(bareSlug(slug));
|
|
104
104
|
};
|
|
105
|
+
return { rowsForDep, isKnownSatisfied };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* findUnresolvableDepRoots(projectJobs, satisfiedSlugs) → string[]
|
|
110
|
+
*
|
|
111
|
+
* Pure. The dep slugs that name NO live row and NO completion/archive record
|
|
112
|
+
* — the roots findBlockingDep holds as 'unresolved'. Unlike a failed/pending
|
|
113
|
+
* dep row, nothing in the queue will ever change one of these; only a human
|
|
114
|
+
* (or a restored record) can. Sorted, de-duplicated. O(jobs + deps).
|
|
115
|
+
*/
|
|
116
|
+
function findUnresolvableDepRoots(projectJobs, satisfiedSlugs = new Set()) {
|
|
117
|
+
const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
|
|
118
|
+
const roots = new Set();
|
|
119
|
+
for (const j of projectJobs) {
|
|
120
|
+
if (j.status !== 'pending') continue;
|
|
121
|
+
for (const dep of j.dependsOn ?? []) {
|
|
122
|
+
if (rowsForDep(dep).length === 0 && !isKnownSatisfied(dep)) roots.add(dep);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return [...roots].sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
129
|
+
const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
|
|
105
130
|
// A dep row is blocking unless it's 'completed', OR it's a 'skipped' row
|
|
106
131
|
// stamped `needsReviewAutoResolvedSkip` — the bounded needs_review
|
|
107
132
|
// auto-resolve terminal decision (scheduler.cjs's
|
|
@@ -658,6 +683,6 @@ function findStarvedProjects(jobs, now, thresholdMs) {
|
|
|
658
683
|
}
|
|
659
684
|
|
|
660
685
|
module.exports = {
|
|
661
|
-
pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, DEFAULT_PROJECT_CWD,
|
|
686
|
+
pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD,
|
|
662
687
|
DEP_HISTORY_FAIL_OPEN,
|
|
663
688
|
};
|
|
@@ -56,6 +56,16 @@ module.exports = {
|
|
|
56
56
|
// (pause/resume, job start/finish/reap/reset) bypass this via
|
|
57
57
|
// broadcast({ flush: true }).
|
|
58
58
|
BROADCAST_COALESCE_MS: 200,
|
|
59
|
+
// reconcile() pass duration above which a single warn-level breakdown is
|
|
60
|
+
// logged (per-phase ms, PRD file count, resolved dir count) — the only
|
|
61
|
+
// evidence trail for a slow pass otherwise requires hand-timing the scan
|
|
62
|
+
// from a throwaway script. Baseline measured 2026-09-13 on a machine with
|
|
63
|
+
// ~160 PRD dirs: allProjectCwds() ~270ms x5/pass, resolvePrdsDirs()
|
|
64
|
+
// ~430ms, historyTerminalBySlug() ~418ms over 784 entries — a healthy pass
|
|
65
|
+
// today is comfortably under 2s; this should trip well before a
|
|
66
|
+
// regression back toward those numbers compounds into the reported
|
|
67
|
+
// "schedule.state timed out after 5000ms" toast.
|
|
68
|
+
RECONCILE_SLOW_PASS_MS: 2_000,
|
|
59
69
|
// Terminal (completed/failed) jobs older than this move from queue.json's
|
|
60
70
|
// hot jobs[] into the append-only history.jsonl sidecar. See
|
|
61
71
|
// src/main/lib/queueHistory.cjs.
|
|
@@ -109,6 +119,20 @@ module.exports = {
|
|
|
109
119
|
// SM_JOB_OVERRUN_FLOOR_MINUTES.
|
|
110
120
|
JOB_OVERRUN_FLOOR_MS: 45 * 60_000,
|
|
111
121
|
|
|
122
|
+
// Wall-clock KILL budget (distinct from JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS
|
|
123
|
+
// above, which only ever ESCALATE — log + audit, deliberately never kill).
|
|
124
|
+
// This one ACTS: a RUNNING job past `budgetMs = clamp(estimateMinutes *
|
|
125
|
+
// JOB_BUDGET_FACTOR, JOB_BUDGET_FLOOR_MS, JOB_BUDGET_CEILING_MS)` is
|
|
126
|
+
// SIGTERMed and parked needs_review. Calibrated against 606 run-meta
|
|
127
|
+
// records since 2026-08-25: p50=13m, p90=60m, max=240m, against a
|
|
128
|
+
// concurrencyCap of 4 — two 4h outliers hold half the machine's dispatch
|
|
129
|
+
// capacity for an afternoon with nothing noticing or acting on it. Override
|
|
130
|
+
// with SM_JOB_BUDGET_FACTOR / SM_JOB_BUDGET_FLOOR_MINUTES /
|
|
131
|
+
// SM_JOB_BUDGET_CEILING_MINUTES.
|
|
132
|
+
JOB_BUDGET_FACTOR: 3,
|
|
133
|
+
JOB_BUDGET_FLOOR_MS: 45 * 60_000,
|
|
134
|
+
JOB_BUDGET_CEILING_MS: 180 * 60_000,
|
|
135
|
+
|
|
112
136
|
// A 'running' row with no runtime.pid recorded (spawnJob's status:running
|
|
113
137
|
// mutate at scheduler.cjs:~3524 landed, but the pid-bearing runtime={}
|
|
114
138
|
// mutate that follows executeJob's spawn callback never did — the spawn
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* schedulerPaths.cjs — single resolver for every machine-wide, home-rooted
|
|
5
|
+
* scheduler path. Plain Node (no electron) so queueStore.cjs and the watchdog
|
|
6
|
+
* can load it.
|
|
7
|
+
*
|
|
8
|
+
* Every export is a LAZY function: the root is read from the environment at
|
|
9
|
+
* point of use, never at require time, so a test (or a second instance) that
|
|
10
|
+
* sets SM_SCHEDULER_HOME redirects ALL scheduler state — queue shards, machine
|
|
11
|
+
* state, runs, PRDs, heartbeat, slot config, admin token — with no
|
|
12
|
+
* half-redirect. SM_SCHEDULER_HOME set on the app propagates to spawned
|
|
13
|
+
* executor jobs BY DESIGN (children inherit env), so a redirected app runs
|
|
14
|
+
* redirected jobs.
|
|
15
|
+
*
|
|
16
|
+
* Worktree roots are NOT under schedulerHome (they live on the tmp filesystem)
|
|
17
|
+
* but are resolved here all the same, via SM_WORKTREE_ROOT (else os.tmpdir()),
|
|
18
|
+
* so the job and epic roots always move together under one override.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
function isUnder(child, parent) {
|
|
25
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
26
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Test-time fail-closed guard (same shape as opsOwnership.assertOpsWrite): under
|
|
31
|
+
* vitest (process.env.VITEST) a resolved path must sit under SM_SCHEDULER_HOME,
|
|
32
|
+
* SM_WORKTREE_ROOT, or os.tmpdir() — anything else is a LIVE root (real ~/.claude,
|
|
33
|
+
* real home) that a unit test must never touch. Returns `resolved` so resolvers
|
|
34
|
+
* can `return assertNotLiveRoot(...)`. O(#roots) per call.
|
|
35
|
+
*/
|
|
36
|
+
function assertNotLiveRoot(resolved, resolverName) {
|
|
37
|
+
if (!process.env.VITEST) return resolved;
|
|
38
|
+
// Narrow, per-test opt-in for a READ-ONLY probe of this machine's real user config
|
|
39
|
+
// (health-delegation-chain.test.cjs). Never set it around anything that writes or sweeps.
|
|
40
|
+
if (process.env.SM_ALLOW_LIVE_ROOT_READS === '1') return resolved;
|
|
41
|
+
// HOLE CLOSED (2026-09-18 defaultCwd leak): the allow-list below is env-derived, and so was
|
|
42
|
+
// the notion of "live" — both follow process.env.HOME / SM_SCHEDULER_HOME, which a test (or an
|
|
43
|
+
// env inherited from the live app) freely reassigns. A live SM_SCHEDULER_HOME therefore
|
|
44
|
+
// ALLOW-LISTED the very root the guard exists to refuse, and a HOME swapped mid-test moved
|
|
45
|
+
// "live" out from under it. The passwd-database home (os.userInfo(), immune to $HOME) is the
|
|
46
|
+
// one anchor a test cannot rewrite: nothing under its ~/.claude may be touched, whatever the
|
|
47
|
+
// allow-list says.
|
|
48
|
+
if (isLivePasswdRoot(resolved)) throw liveRootError(resolved, resolverName);
|
|
49
|
+
const roots = [process.env.SM_SCHEDULER_HOME, process.env.SM_WORKTREE_ROOT, os.tmpdir()].filter(Boolean);
|
|
50
|
+
if (roots.some((r) => isUnder(resolved, r))) return resolved;
|
|
51
|
+
throw liveRootError(resolved, resolverName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function liveRootError(resolved, resolverName) {
|
|
55
|
+
return new Error(
|
|
56
|
+
`schedulerPaths.${resolverName}() resolved to live root ${resolved} under vitest — ` +
|
|
57
|
+
'set SM_SCHEDULER_HOME / SM_WORKTREE_ROOT / HOME to a temp dir (tests/setup/schedulerSandbox.cjs does this per run)',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when `resolved` sits under the passwd-home's ~/.claude (HOME-env independent). */
|
|
62
|
+
function isLivePasswdRoot(resolved) {
|
|
63
|
+
let home;
|
|
64
|
+
try { home = os.userInfo().homedir; } catch { return false; }
|
|
65
|
+
if (!home) return false;
|
|
66
|
+
// A sandbox that genuinely lives under the passwd home's tmp is not live; only ~/.claude is.
|
|
67
|
+
return isUnder(resolved, path.join(home, '.claude'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** SM_SCHEDULER_HOME, else ~/.claude/session-manager. */
|
|
71
|
+
function schedulerHome() {
|
|
72
|
+
return assertNotLiveRoot(
|
|
73
|
+
process.env.SM_SCHEDULER_HOME || path.join(os.homedir(), '.claude', 'session-manager'),
|
|
74
|
+
'schedulerHome',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** ~/.claude — Claude Code's own config dir (NOT redirected by SM_SCHEDULER_HOME). */
|
|
79
|
+
function claudeHome() { return assertNotLiveRoot(path.join(os.homedir(), '.claude'), 'claudeHome'); }
|
|
80
|
+
function claudeProjectsDir() { return assertNotLiveRoot(path.join(claudeHome(), 'projects'), 'claudeProjectsDir'); }
|
|
81
|
+
|
|
82
|
+
function scheduledPlansRoot() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduled-plans'), 'scheduledPlansRoot'); }
|
|
83
|
+
function runsDir() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'runs'), 'runsDir'); }
|
|
84
|
+
function prdsRoot() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'prds'), 'prdsRoot'); }
|
|
85
|
+
function machineStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-machine.json'), 'machineStatePath'); }
|
|
86
|
+
function legacyQueuePath() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'queue.json'), 'legacyQueuePath'); }
|
|
87
|
+
function schedulerStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-state.json'), 'schedulerStatePath'); }
|
|
88
|
+
function heartbeatPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-heartbeat.log'), 'heartbeatPath'); }
|
|
89
|
+
function sessionSlotsConfigPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'session-slots-config.json'), 'sessionSlotsConfigPath'); }
|
|
90
|
+
function watchdogLogsDir() { return assertNotLiveRoot(path.join(schedulerHome(), 'logs'), 'watchdogLogsDir'); }
|
|
91
|
+
function restartRequestPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'restart-request.json'), 'restartRequestPath'); }
|
|
92
|
+
function restartingMarkerPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'restarting.json'), 'restartingMarkerPath'); }
|
|
93
|
+
function watchdogRelaunchStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'watchdog-relaunch-state.json'), 'watchdogRelaunchStatePath'); }
|
|
94
|
+
// Sibling of schedulerHome() (i.e. ~/.claude/logs by default), not inside it.
|
|
95
|
+
function watchdogRelaunchLogPath() {
|
|
96
|
+
return assertNotLiveRoot(
|
|
97
|
+
path.join(path.dirname(schedulerHome()), 'logs', 'scheduler-watchdog-relaunch.log'),
|
|
98
|
+
'watchdogRelaunchLogPath',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function historyRollupStampPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.stamp'), 'historyRollupStampPath'); }
|
|
102
|
+
function historyRollupLockPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.lock'), 'historyRollupLockPath'); }
|
|
103
|
+
function historyRollupPath() { return path.join(schedulerHome(), 'history-rollup.jsonl'); }
|
|
104
|
+
function auditLogPath() { return path.join(schedulerHome(), 'audit-log.jsonl'); }
|
|
105
|
+
function procnamesRoot() { return path.join(schedulerHome(), 'procnames'); }
|
|
106
|
+
/** Legacy global history sidecar (read-only; new appends go to per-project shards). */
|
|
107
|
+
function queueHistoryPath() { return path.join(scheduledPlansRoot(), 'history.jsonl'); }
|
|
108
|
+
function instanceLockPath() { return path.join(schedulerHome(), 'scheduler-owner.lock'); }
|
|
109
|
+
/** Where heap snapshots are written — the scheduler home itself. */
|
|
110
|
+
function heapSnapshotDir() { return schedulerHome(); }
|
|
111
|
+
|
|
112
|
+
/** SM_WORKTREE_ROOT, else os.tmpdir() — the parent of every managed worktree root. */
|
|
113
|
+
function worktreeBase() { return assertNotLiveRoot(process.env.SM_WORKTREE_ROOT || os.tmpdir(), 'worktreeBase'); }
|
|
114
|
+
|
|
115
|
+
/** Managed worktree root for `kind` ('job' | 'epic'), resolved fresh on every call. */
|
|
116
|
+
function worktreeRoot(kind) {
|
|
117
|
+
return assertNotLiveRoot(path.join(worktreeBase(), `session-manager-${kind}-worktrees`), 'worktreeRoot');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Mode-aware admin token path, resolved fresh on every call. Precedence:
|
|
122
|
+
* SM_ADMIN_TOKEN_PATH (explicit override) > SM_DEV > SM_E2E > production file.
|
|
123
|
+
* index.cjs treats SM_DEV and SM_E2E as one OR'd boolean; if both are set the
|
|
124
|
+
* SM_DEV path wins (arbitrary but harmless — real launches set one).
|
|
125
|
+
*/
|
|
126
|
+
function adminTokenPath() {
|
|
127
|
+
if (process.env.SM_ADMIN_TOKEN_PATH) return assertNotLiveRoot(process.env.SM_ADMIN_TOKEN_PATH, 'adminTokenPath');
|
|
128
|
+
if (process.env.SM_DEV === '1') return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.dev.json'), 'adminTokenPath');
|
|
129
|
+
if (process.env.SM_E2E === '1') return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.e2e.json'), 'adminTokenPath');
|
|
130
|
+
return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.json'), 'adminTokenPath');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* cwd that machine-level scheduler errors are attributed to (they have no
|
|
135
|
+
* owning project). SM_SCHEDULER_LOG_CWD, else the literal
|
|
136
|
+
* ~/Projects/session-manager — deliberately NOT inferred from process.cwd().
|
|
137
|
+
*/
|
|
138
|
+
function machineStateLogCwd() {
|
|
139
|
+
return assertNotLiveRoot(
|
|
140
|
+
process.env.SM_SCHEDULER_LOG_CWD || path.join(os.homedir(), 'Projects', 'session-manager'),
|
|
141
|
+
'machineStateLogCwd',
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
assertNotLiveRoot,
|
|
147
|
+
claudeHome,
|
|
148
|
+
claudeProjectsDir,
|
|
149
|
+
schedulerHome,
|
|
150
|
+
scheduledPlansRoot,
|
|
151
|
+
runsDir,
|
|
152
|
+
prdsRoot,
|
|
153
|
+
machineStatePath,
|
|
154
|
+
legacyQueuePath,
|
|
155
|
+
schedulerStatePath,
|
|
156
|
+
heartbeatPath,
|
|
157
|
+
sessionSlotsConfigPath,
|
|
158
|
+
adminTokenPath,
|
|
159
|
+
machineStateLogCwd,
|
|
160
|
+
worktreeBase,
|
|
161
|
+
worktreeRoot,
|
|
162
|
+
watchdogLogsDir,
|
|
163
|
+
restartRequestPath,
|
|
164
|
+
restartingMarkerPath,
|
|
165
|
+
watchdogRelaunchStatePath,
|
|
166
|
+
watchdogRelaunchLogPath,
|
|
167
|
+
historyRollupStampPath,
|
|
168
|
+
historyRollupLockPath,
|
|
169
|
+
historyRollupPath,
|
|
170
|
+
auditLogPath,
|
|
171
|
+
procnamesRoot,
|
|
172
|
+
queueHistoryPath,
|
|
173
|
+
instanceLockPath,
|
|
174
|
+
heapSnapshotDir,
|
|
175
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedulerRuntimeState.cjs — process-local runtime reservations that used to
|
|
3
|
+
* be bare ++/-- counters in scheduler.cjs. The auto-fix investigation cap is
|
|
4
|
+
* now a Map of slug → { claimedAt, pid } so a reservation stranded by a
|
|
5
|
+
* promise that never settles can be expired when its owner is provably dead
|
|
6
|
+
* (reservationExpiry.isProvablyDead), instead of shrinking the cap forever.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
11
|
+
const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
|
|
12
|
+
|
|
13
|
+
const investigations = new Map(); // slug -> { claimedAt, pid }
|
|
14
|
+
|
|
15
|
+
function investigationCount() { return investigations.size; }
|
|
16
|
+
|
|
17
|
+
/** reserveInvestigation(slug) — false if already reserved. Cap checks stay with the caller. */
|
|
18
|
+
function reserveInvestigation(slug, { claimedAt = Date.now() } = {}) {
|
|
19
|
+
const key = String(slug);
|
|
20
|
+
if (investigations.has(key)) return false;
|
|
21
|
+
investigations.set(key, { claimedAt, pid: null });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stampInvestigationPid(slug, pid) {
|
|
26
|
+
const r = investigations.get(String(slug));
|
|
27
|
+
if (!r || !Number.isInteger(pid) || pid <= 0) return false;
|
|
28
|
+
r.pid = pid;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** releaseInvestigation — idempotent; returns whether a reservation was removed. */
|
|
33
|
+
function releaseInvestigation(slug) {
|
|
34
|
+
return investigations.delete(String(slug));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Expire investigations whose slug has no `investigating` row and whose owner is provably dead. */
|
|
38
|
+
function expireDeadInvestigations({ liveSlugs, pidAlive, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
|
|
39
|
+
const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
|
|
40
|
+
const expired = [];
|
|
41
|
+
for (const [slug, r] of [...investigations]) {
|
|
42
|
+
if (!isProvablyDead(r, { live: live.has(slug), pidAlive, now, graceMs })) continue;
|
|
43
|
+
investigations.delete(slug);
|
|
44
|
+
expired.push(slug);
|
|
45
|
+
appendAuditEvent('investigation_reservation_expired', { slug, pid: r.pid });
|
|
46
|
+
}
|
|
47
|
+
return expired;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function __resetForTests() { investigations.clear(); }
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
investigationCount,
|
|
54
|
+
reserveInvestigation,
|
|
55
|
+
stampInvestigationPid,
|
|
56
|
+
releaseInvestigation,
|
|
57
|
+
expireDeadInvestigations,
|
|
58
|
+
__resetForTests,
|
|
59
|
+
};
|
|
@@ -25,14 +25,15 @@
|
|
|
25
25
|
|
|
26
26
|
const crypto = require('node:crypto');
|
|
27
27
|
const fs = require('node:fs');
|
|
28
|
-
const os = require('node:os');
|
|
29
28
|
const path = require('node:path');
|
|
29
|
+
const { sessionSlotsConfigPath } = require('./schedulerPaths.cjs');
|
|
30
|
+
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
31
|
+
const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
|
|
30
32
|
|
|
31
33
|
const MIN_SLOTS = 0;
|
|
32
34
|
const MAX_SLOTS = 10;
|
|
33
35
|
const DEFAULT_SLOTS = 5;
|
|
34
36
|
|
|
35
|
-
const CONFIG_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'session-slots-config.json');
|
|
36
37
|
|
|
37
38
|
function clamp(n) {
|
|
38
39
|
return Math.min(MAX_SLOTS, Math.max(MIN_SLOTS, n));
|
|
@@ -40,7 +41,7 @@ function clamp(n) {
|
|
|
40
41
|
|
|
41
42
|
function readPersistedCap() {
|
|
42
43
|
try {
|
|
43
|
-
const raw = fs.readFileSync(
|
|
44
|
+
const raw = fs.readFileSync(sessionSlotsConfigPath(), 'utf8');
|
|
44
45
|
const parsed = JSON.parse(raw);
|
|
45
46
|
const cap = Number(parsed.cap);
|
|
46
47
|
return Number.isFinite(cap) ? clamp(Math.trunc(cap)) : DEFAULT_SLOTS;
|
|
@@ -55,10 +56,11 @@ function setCap(cap) {
|
|
|
55
56
|
if (!Number.isFinite(n) || Math.trunc(n) !== n || n < MIN_SLOTS || n > MAX_SLOTS) {
|
|
56
57
|
throw new Error(`sessionSlots.setCap: cap must be an integer in [${MIN_SLOTS}, ${MAX_SLOTS}]`);
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const configPath = sessionSlotsConfigPath();
|
|
60
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
61
|
+
const tmp = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
60
62
|
fs.writeFileSync(tmp, JSON.stringify({ cap: n }, null, 2));
|
|
61
|
-
fs.renameSync(tmp,
|
|
63
|
+
fs.renameSync(tmp, configPath);
|
|
62
64
|
for (const fn of listeners) {
|
|
63
65
|
try { fn(); } catch { /* a consumer's pump error is its own problem */ }
|
|
64
66
|
}
|
|
@@ -73,7 +75,7 @@ function totalSlots() {
|
|
|
73
75
|
return readPersistedCap();
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
// token → { owner, at }
|
|
78
|
+
// token → { owner, at, claimedAt (ms), pid (null until stampPid) }
|
|
77
79
|
const holders = new Map();
|
|
78
80
|
|
|
79
81
|
function inUse() {
|
|
@@ -89,13 +91,47 @@ function available() {
|
|
|
89
91
|
* `owner` is a diagnostic label ("scheduler:<slug>", "chat:<tabId>") shown in
|
|
90
92
|
* snapshot() so a stuck holder is attributable.
|
|
91
93
|
*/
|
|
92
|
-
function acquire(owner) {
|
|
94
|
+
function acquire(owner, { claimedAt = Date.now() } = {}) {
|
|
93
95
|
if (holders.size >= totalSlots()) return null;
|
|
94
96
|
const token = crypto.randomUUID();
|
|
95
|
-
holders.set(token, { owner: String(owner || 'unknown'), at: new Date().toISOString() });
|
|
97
|
+
holders.set(token, { owner: String(owner || 'unknown'), at: new Date(claimedAt).toISOString(), claimedAt, pid: null });
|
|
96
98
|
return token;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/** stampPid(token, pid) — record the child pid once known, so expireDead can prove death by pid. */
|
|
102
|
+
function stampPid(token, pid) {
|
|
103
|
+
const h = holders.get(token);
|
|
104
|
+
if (!h || !Number.isInteger(pid) || pid <= 0) return false;
|
|
105
|
+
h.pid = pid;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* expireDead — release `scheduler:` reservations whose owner is provably dead
|
|
111
|
+
* (see reservationExpiry.isProvablyDead). `chat:` / `project-brief:` tokens are
|
|
112
|
+
* never touched. Accounting only: no process is signalled. Returns the released
|
|
113
|
+
* owners. Each release is audited once (the token is gone after release).
|
|
114
|
+
* @param {{ liveSlugs: Set<string>, pidAlive?: Function, now?: number, graceMs?: number }} opts
|
|
115
|
+
*/
|
|
116
|
+
function expireDead({ liveSlugs, pidAlive, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
|
|
117
|
+
const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
|
|
118
|
+
const expired = [];
|
|
119
|
+
for (const [token, h] of [...holders]) {
|
|
120
|
+
if (!h.owner.startsWith('scheduler:')) continue;
|
|
121
|
+
const slug = h.owner.slice('scheduler:'.length);
|
|
122
|
+
if (!isProvablyDead(h, { live: live.has(slug), pidAlive, now, graceMs })) continue;
|
|
123
|
+
holders.delete(token);
|
|
124
|
+
expired.push(h.owner);
|
|
125
|
+
appendAuditEvent('slot_reservation_expired', { owner: h.owner, pid: h.pid, claimedAt: h.at });
|
|
126
|
+
}
|
|
127
|
+
if (expired.length) {
|
|
128
|
+
for (const fn of listeners) {
|
|
129
|
+
try { fn(); } catch { /* a consumer's pump error is its own problem */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return expired;
|
|
133
|
+
}
|
|
134
|
+
|
|
99
135
|
// Release listeners: each consumer registers its own "a slot freed — try to
|
|
100
136
|
// start work" pump so a scheduler release wakes the chat lane and vice versa.
|
|
101
137
|
// Also fired when the cap itself increases (setCap), for the same reason.
|
|
@@ -143,6 +179,8 @@ module.exports = {
|
|
|
143
179
|
inUse,
|
|
144
180
|
available,
|
|
145
181
|
acquire,
|
|
182
|
+
stampPid,
|
|
183
|
+
expireDead,
|
|
146
184
|
release,
|
|
147
185
|
subscribe,
|
|
148
186
|
snapshot,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smProcNames.cjs — kernel `comm` names for the processes Session Manager
|
|
3
|
+
* itself leaves in the user's process list (pn-03). `claude` children are
|
|
4
|
+
* named separately (procName.cjs smArgv0 / pn-02).
|
|
5
|
+
*
|
|
6
|
+
* Every name is <= 15 chars (Linux TASK_COMM_LEN). `session-manager` is
|
|
7
|
+
* EXACTLY 15, so NEVER derive another name from it by suffixing — it would
|
|
8
|
+
* truncate back to `session-manager` and be indistinguishable from main.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { aliasBinFor } = require('./procName.cjs');
|
|
14
|
+
|
|
15
|
+
const PROC_NAMES = Object.freeze({
|
|
16
|
+
main: 'session-manager',
|
|
17
|
+
mcpServer: 'sm-mcp-server',
|
|
18
|
+
watchdog: 'sm-watchdog',
|
|
19
|
+
inhibitHold: 'sm-inhibit-hold',
|
|
20
|
+
shell: 'sm-shell',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Shells whose argv0-basename behaviour (sh/rsh/ksh emulation, $0-branching rc
|
|
24
|
+
// files) is known to be safe under the `sm-shell` alias. Anything else spawns
|
|
25
|
+
// the real path — never guess at another shell's argv0 semantics.
|
|
26
|
+
const ALIASABLE_SHELLS = new Set(['bash', 'zsh']);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* On Linux Node's process.title setter calls prctl(PR_SET_NAME), changing
|
|
30
|
+
* `comm`. It overwrites the argv memory region, but process.argv is a JS
|
|
31
|
+
* array copied at startup and is unaffected. Never throws.
|
|
32
|
+
*/
|
|
33
|
+
function setProcessTitle(name) {
|
|
34
|
+
try { process.title = name; } catch { /* naming must never break boot */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Alias `shell` as `sm-shell` iff it is bash/zsh; otherwise return it unchanged. Fail-open. */
|
|
38
|
+
function aliasedShellBin(shell) {
|
|
39
|
+
try {
|
|
40
|
+
if (typeof shell !== 'string' || !shell) return shell;
|
|
41
|
+
if (!ALIASABLE_SHELLS.has(path.basename(shell))) return shell;
|
|
42
|
+
return aliasBinFor(shell, PROC_NAMES.shell);
|
|
43
|
+
} catch { return shell; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `sh` aliased as `sm-inhibit-hold` for the systemd-inhibit poll holder. Fail-open. */
|
|
47
|
+
function inhibitHolderShell() {
|
|
48
|
+
try { return aliasBinFor('/bin/sh', PROC_NAMES.inhibitHold); } catch { return '/bin/sh'; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { PROC_NAMES, ALIASABLE_SHELLS, setProcessTitle, aliasedShellBin, inhibitHolderShell };
|