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,12 +12,22 @@
|
|
|
12
12
|
* live. Active-project discovery reuses activeSessions.cjs's
|
|
13
13
|
* activeProjectCwds (the same discovery watchdogHelpers.cjs's sweep() uses)
|
|
14
14
|
* rather than re-implementing transcript scanning here.
|
|
15
|
+
*
|
|
16
|
+
* CACHING NOTE (PRD adding mtime-keyed memoization to this module): what's
|
|
17
|
+
* cached here is only WHICH DIRECTORIES EXIST, never their CONTENTS. A new
|
|
18
|
+
* PRD .md file dropped into an already-known `prds/` dir does not change
|
|
19
|
+
* that dir's own existence, so it needs no cache invalidation here at all —
|
|
20
|
+
* `schedule:rescan` (scheduler.cjs's `ipcMain.handle('schedule:rescan', …)`)
|
|
21
|
+
* still reports it because the actual file-listing happens one layer down,
|
|
22
|
+
* in scheduler/prdParser.cjs's `listPrdFiles(dir)`, which has its OWN
|
|
23
|
+
* separate mtime cache keyed on that specific dir's mtime (bumped by the new
|
|
24
|
+
* file) and is unaffected by anything cached in this module.
|
|
15
25
|
*/
|
|
16
26
|
'use strict';
|
|
17
27
|
|
|
18
28
|
const fs = require('node:fs');
|
|
19
29
|
const path = require('node:path');
|
|
20
|
-
const { activeProjectCwds, allProjectCwds } = require('
|
|
30
|
+
const { activeProjectCwds, allProjectCwds } = require('./activeSessions.cjs');
|
|
21
31
|
|
|
22
32
|
const { opsPath, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
23
33
|
|
|
@@ -53,18 +63,49 @@ function resolveEpicPrdWriteDir(cwd, epicId) {
|
|
|
53
63
|
return path.join(resolveEpicsRoot(cwd), epicId, 'prds');
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
// listEpicPrdDirs' readdir-the-Epics-root-plus-existsSync-per-subdir walk is
|
|
67
|
+
// the expensive half of prdLocations' per-reconcile()-pass cost (159/166
|
|
68
|
+
// dirs, ~430ms/~400ms measured). A new Epic's prds/ dir is always created in
|
|
69
|
+
// the SAME recursive mkdirSync call that creates `epics/<newId>/` itself
|
|
70
|
+
// (epicMint.cjs's ensureEpic, both the mint branch and the join branch's
|
|
71
|
+
// belt-and-suspenders mkdirSync) — so a brand-new `epics/<id>/prds` dir
|
|
72
|
+
// first coming into existence always bumps the Epics root's OWN mtime (a
|
|
73
|
+
// new directory entry under it), never just an existing subdir's mtime.
|
|
74
|
+
// Cache keyed on that mtime, same dir-mtime idiom as
|
|
75
|
+
// scheduler/prdParser.cjs's dirCache — no TTL, so the very next call after a
|
|
76
|
+
// mint sees the new dir with no wait.
|
|
77
|
+
const epicPrdDirsCache = new Map(); // epicsRootPath -> { mtimeMs, result }
|
|
78
|
+
|
|
56
79
|
/** Enumerate every existing `epics/<id>/prds` dir under one project cwd. */
|
|
57
80
|
function listEpicPrdDirs(cwd) {
|
|
58
81
|
let root;
|
|
59
82
|
try { root = resolveEpicsRoot(cwd); } catch { return []; }
|
|
83
|
+
let mtimeMs;
|
|
84
|
+
try {
|
|
85
|
+
mtimeMs = fs.statSync(root).mtimeMs;
|
|
86
|
+
} catch {
|
|
87
|
+
// Missing/unreadable root: nothing to scan, and any prior cache entry
|
|
88
|
+
// must not survive to be handed back once the dir later appears.
|
|
89
|
+
epicPrdDirsCache.delete(root);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const cached = epicPrdDirsCache.get(root);
|
|
93
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.result;
|
|
94
|
+
|
|
60
95
|
let entries;
|
|
61
|
-
try {
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
98
|
+
} catch {
|
|
99
|
+
epicPrdDirsCache.delete(root);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
62
102
|
const dirs = [];
|
|
63
103
|
for (const ent of entries) {
|
|
64
104
|
if (!ent.isDirectory()) continue;
|
|
65
105
|
const prdDir = path.join(root, ent.name, 'prds');
|
|
66
106
|
if (fs.existsSync(prdDir)) dirs.push(prdDir);
|
|
67
107
|
}
|
|
108
|
+
epicPrdDirsCache.set(root, { mtimeMs, result: dirs });
|
|
68
109
|
return dirs;
|
|
69
110
|
}
|
|
70
111
|
|
|
@@ -101,6 +142,85 @@ function resolvePrdWriteDir(cwd) {
|
|
|
101
142
|
return opsPath(cwd, ...PRD_SUBPATH);
|
|
102
143
|
}
|
|
103
144
|
|
|
145
|
+
// resolvePrdsDirs/resolveArchivedPrdsDirs each redo a fs.existsSync(dir) +
|
|
146
|
+
// listEpicPrdDirs(cwd) walk across EVERY project on every call, and
|
|
147
|
+
// reconcile() calls each of them once per pass. Cache the assembled result
|
|
148
|
+
// keyed on the caller's own (maxAgeMin, opts) tuple — a call with an
|
|
149
|
+
// explicit `{ projectsDir }` opts object is a DIFFERENT key from the
|
|
150
|
+
// no-opts default, so the two never read or write each other's entry — PLUS
|
|
151
|
+
// a freshness key built from every visited project's Epics-root mtimeMs,
|
|
152
|
+
// same composite-mtime-key idiom as queueHistory.cjs's historyCacheKey. The
|
|
153
|
+
// freshness key is cheap to rebuild every call (one statSync per project,
|
|
154
|
+
// far cheaper than the readdir+existsSync-per-Epic walk it guards), so a
|
|
155
|
+
// cache hit still costs O(projects) stats but skips the expensive walk
|
|
156
|
+
// entirely — "one filesystem walk" per reconcile() pass, not the full
|
|
157
|
+
// listEpicPrdDirs cost N times over.
|
|
158
|
+
//
|
|
159
|
+
// The Epics-root mtime alone has a real blind spot: `prds-archived/` is
|
|
160
|
+
// created (archiveCompletedPrd, scheduler.cjs) as a NEW entry INSIDE an
|
|
161
|
+
// ALREADY-EXISTING `epics/<id>/` dir — unlike a brand-new Epic's `prds/`,
|
|
162
|
+
// which is always created in the SAME mkdirSync call as `epics/<id>/`
|
|
163
|
+
// itself (see listEpicPrdDirs's header), that does not bump the Epics
|
|
164
|
+
// root's own mtime. Re-stating every Epic subdirectory's own mtime on every
|
|
165
|
+
// call to close that gap would cost about what the walk itself costs,
|
|
166
|
+
// defeating the cache — so this cache pairs the mtime key (instant
|
|
167
|
+
// invalidation for the case the non-negotiable freshness test covers: a
|
|
168
|
+
// brand-new Epic's `prds/` dir) with the SAME short TTL backstop
|
|
169
|
+
// src/main/lib/activeSessions.cjs's cwdScanCache already established for
|
|
170
|
+
// this exact module chain, bounding the archived-dir blind spot to one
|
|
171
|
+
// TTL window instead of leaving it stale indefinitely. Nothing reads
|
|
172
|
+
// resolveArchivedPrdsDirs off the dispatch-correctness path — the
|
|
173
|
+
// archived-twin stale-queue-row guard (scheduler.cjs's archivedTwinExists)
|
|
174
|
+
// calls listArchivedPrdDirs directly, uncached — so this is purely a
|
|
175
|
+
// reporting-freshness bound (schedule:list-prds's per-Epic PRD/run counts).
|
|
176
|
+
// Above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS).
|
|
177
|
+
const ASSEMBLED_DIRS_CACHE_TTL_MS = 120_000;
|
|
178
|
+
const assembledPrdsDirsCache = new Map(); // callerKey -> { freshnessKey, cachedAt, result }
|
|
179
|
+
const assembledArchivedPrdsDirsCache = new Map();
|
|
180
|
+
|
|
181
|
+
// Sorted-keys JSON.stringify: a plain JSON.stringify(opts) would key
|
|
182
|
+
// { a: 1, b: 2 } and { b: 2, a: 1 } as two different cache entries — a real
|
|
183
|
+
// (if currently latent, since every caller today passes at most one opts
|
|
184
|
+
// key) correctness trap for a cache key.
|
|
185
|
+
function stableStringify(obj) {
|
|
186
|
+
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function callerOptsKey(maxAgeMin, opts) {
|
|
190
|
+
return `${maxAgeMin}::${opts ? stableStringify(opts) : ''}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// mtimeMs of each visited project's Epics root, joined into one string. Any
|
|
194
|
+
// project whose root doesn't exist/isn't readable contributes a stable
|
|
195
|
+
// sentinel so the key still changes the moment that root is created.
|
|
196
|
+
function epicsRootsFreshnessKey(cwds) {
|
|
197
|
+
return cwds
|
|
198
|
+
.map((cwd) => {
|
|
199
|
+
let root;
|
|
200
|
+
try { root = resolveEpicsRoot(cwd); } catch { return `${cwd}:noroot`; }
|
|
201
|
+
try { return `${root}:${fs.statSync(root).mtimeMs}`; } catch { return `${root}:-1`; }
|
|
202
|
+
})
|
|
203
|
+
.join('|');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function memoizedAssembledDirs(cache, maxAgeMin, opts, compute) {
|
|
207
|
+
const callerKey = callerOptsKey(maxAgeMin, opts);
|
|
208
|
+
const allCwds = allProjectCwds(opts);
|
|
209
|
+
const activeCwds = activeProjectCwds(maxAgeMin, opts);
|
|
210
|
+
const cwdUnion = [...new Set([...allCwds, ...activeCwds])];
|
|
211
|
+
const freshnessKey = `${allCwds.join(',')}#${activeCwds.join(',')}#${epicsRootsFreshnessKey(cwdUnion)}`;
|
|
212
|
+
|
|
213
|
+
const cached = cache.get(callerKey);
|
|
214
|
+
if (cached && cached.freshnessKey === freshnessKey && Date.now() - cached.cachedAt < ASSEMBLED_DIRS_CACHE_TTL_MS) {
|
|
215
|
+
return cached.result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = compute(allCwds, activeCwds);
|
|
219
|
+
if (Array.isArray(result)) Object.freeze(result);
|
|
220
|
+
cache.set(callerKey, { freshnessKey, cachedAt: Date.now(), result });
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
104
224
|
/**
|
|
105
225
|
* resolvePrdsDirs(maxAgeMin?, opts?) → string[]
|
|
106
226
|
*
|
|
@@ -122,34 +242,36 @@ function resolvePrdWriteDir(cwd) {
|
|
|
122
242
|
* underlying scan (e.g. `projectsDir` override for tests).
|
|
123
243
|
*/
|
|
124
244
|
function resolvePrdsDirs(maxAgeMin, opts) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Every historical project that has a PRD dir on disk — the set that
|
|
134
|
-
// matters for discovery, regardless of when it was last touched.
|
|
135
|
-
for (const cwd of allProjectCwds(opts)) {
|
|
136
|
-
let dir;
|
|
137
|
-
try { dir = resolvePrdWriteDir(cwd); } catch { continue; }
|
|
138
|
-
if (fs.existsSync(dir)) add(dir);
|
|
139
|
-
// Epic-scoped dirs (the write layout for all new PRDs) are first-class
|
|
140
|
-
// scan sources alongside the legacy flat dir.
|
|
141
|
-
for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
|
|
142
|
-
}
|
|
245
|
+
return memoizedAssembledDirs(assembledPrdsDirsCache, maxAgeMin, opts, (allCwds, activeCwds) => {
|
|
246
|
+
const dirs = [];
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
const add = (dir) => {
|
|
249
|
+
if (seen.has(dir)) return;
|
|
250
|
+
seen.add(dir);
|
|
251
|
+
dirs.push(dir);
|
|
252
|
+
};
|
|
143
253
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
254
|
+
// Every historical project that has a PRD dir on disk — the set that
|
|
255
|
+
// matters for discovery, regardless of when it was last touched.
|
|
256
|
+
for (const cwd of allCwds) {
|
|
257
|
+
let dir;
|
|
258
|
+
try { dir = resolvePrdWriteDir(cwd); } catch { continue; }
|
|
259
|
+
if (fs.existsSync(dir)) add(dir);
|
|
260
|
+
// Epic-scoped dirs (the write layout for all new PRDs) are first-class
|
|
261
|
+
// scan sources alongside the legacy flat dir.
|
|
262
|
+
for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
|
|
263
|
+
}
|
|
151
264
|
|
|
152
|
-
|
|
265
|
+
// Active projects are added unconditionally: a brand-new project has no
|
|
266
|
+
// prds/ dir yet, and callers that resolve a write destination must still
|
|
267
|
+
// find it. Scans over a non-existent dir are a harmless ENOENT no-op.
|
|
268
|
+
for (const cwd of activeCwds) {
|
|
269
|
+
try { add(resolvePrdWriteDir(cwd)); } catch { /* unusable cwd */ }
|
|
270
|
+
for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return dirs;
|
|
274
|
+
});
|
|
153
275
|
}
|
|
154
276
|
|
|
155
277
|
/**
|
|
@@ -163,26 +285,28 @@ function resolvePrdsDirs(maxAgeMin, opts) {
|
|
|
163
285
|
* history, not something list-prds should stop counting.
|
|
164
286
|
*/
|
|
165
287
|
function resolveArchivedPrdsDirs(maxAgeMin, opts) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const
|
|
176
|
-
|
|
288
|
+
return memoizedAssembledDirs(assembledArchivedPrdsDirsCache, maxAgeMin, opts, (allCwds, activeCwds) => {
|
|
289
|
+
const dirs = [];
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
const add = (dir) => {
|
|
292
|
+
if (seen.has(dir)) return;
|
|
293
|
+
seen.add(dir);
|
|
294
|
+
dirs.push(dir);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
for (const cwd of allCwds) {
|
|
298
|
+
for (const dir of listArchivedPrdDirs(cwd)) {
|
|
299
|
+
if (fs.existsSync(dir)) add(dir);
|
|
300
|
+
}
|
|
177
301
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
302
|
+
for (const cwd of activeCwds) {
|
|
303
|
+
for (const dir of listArchivedPrdDirs(cwd)) {
|
|
304
|
+
if (fs.existsSync(dir)) add(dir);
|
|
305
|
+
}
|
|
182
306
|
}
|
|
183
|
-
}
|
|
184
307
|
|
|
185
|
-
|
|
308
|
+
return dirs;
|
|
309
|
+
});
|
|
186
310
|
}
|
|
187
311
|
|
|
188
312
|
/**
|
|
@@ -216,6 +340,50 @@ function deriveEpicIdFromPrdPath(filePath) {
|
|
|
216
340
|
return epicId;
|
|
217
341
|
}
|
|
218
342
|
|
|
343
|
+
/**
|
|
344
|
+
* deriveProjectCwdFromPrdPath(filePath) → projectCwd | null
|
|
345
|
+
*
|
|
346
|
+
* The inverse of resolvePrdWriteDir/resolveEpicPrdWriteDir: given an absolute
|
|
347
|
+
* PRD file path (flat `prds/`, Epic-scoped `epics/<id>/prds/`, or either's
|
|
348
|
+
* `prds-archived/` sibling), returns the project root it was discovered
|
|
349
|
+
* under — by finding the `session-manager-operations` segment and validating
|
|
350
|
+
* the path round-trips back through the same resolver it walked up from.
|
|
351
|
+
* Returns null for anything that doesn't match one of those shapes (never
|
|
352
|
+
* guesses). Used by reconcile()'s fresh-discovery path so a PRD file with no
|
|
353
|
+
* `cwd:` frontmatter still gets a real project cwd instead of null (which
|
|
354
|
+
* would otherwise fall through to schedulerBatch.js's DEFAULT_PROJECT_CWD
|
|
355
|
+
* and relocate the row into the wrong project's queue.json shard).
|
|
356
|
+
*/
|
|
357
|
+
function deriveProjectCwdFromPrdPath(filePath) {
|
|
358
|
+
if (!filePath || typeof filePath !== 'string' || !path.isAbsolute(filePath)) return null;
|
|
359
|
+
const segments = filePath.split(path.sep);
|
|
360
|
+
const opsIdx = segments.lastIndexOf(OPS_ROOT_DIR);
|
|
361
|
+
if (opsIdx <= 0) return null;
|
|
362
|
+
const projectCwd = segments.slice(0, opsIdx).join(path.sep);
|
|
363
|
+
const prdsDir = path.dirname(filePath);
|
|
364
|
+
const dirName = path.basename(prdsDir);
|
|
365
|
+
if (dirName !== 'prds' && dirName !== 'prds-archived') return null;
|
|
366
|
+
if (dirName === 'prds') {
|
|
367
|
+
try {
|
|
368
|
+
if (resolvePrdWriteDir(projectCwd) === prdsDir) return projectCwd;
|
|
369
|
+
} catch { /* fall through to the Epic-scoped check below */ }
|
|
370
|
+
}
|
|
371
|
+
if (dirName === 'prds-archived') {
|
|
372
|
+
// Flat legacy archive sibling of resolvePrdWriteDir's flat `prds/` (see
|
|
373
|
+
// listArchivedPrdDirs' first entry: opsPath(cwd, ...PRD_SUBPATH, '..',
|
|
374
|
+
// 'prds-archived')) — round-trip it the same way the flat `prds` branch
|
|
375
|
+
// above does, before falling through to the Epic-scoped check (which
|
|
376
|
+
// can never match a flat path: deriveEpicIdFromPrdPath requires an
|
|
377
|
+
// `epics/<id>/` segment a flat archive dir doesn't have).
|
|
378
|
+
try {
|
|
379
|
+
if (opsPath(projectCwd, ...PRD_SUBPATH, '..', 'prds-archived') === prdsDir) return projectCwd;
|
|
380
|
+
} catch { /* fall through to the Epic-scoped check below */ }
|
|
381
|
+
}
|
|
382
|
+
const epicId = deriveEpicIdFromPrdPath(filePath);
|
|
383
|
+
if (epicId) return projectCwd;
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
219
387
|
module.exports = {
|
|
220
388
|
resolvePrdWriteDir,
|
|
221
389
|
resolvePrdsDirs,
|
|
@@ -225,6 +393,7 @@ module.exports = {
|
|
|
225
393
|
listEpicPrdDirs,
|
|
226
394
|
listArchivedPrdDirs,
|
|
227
395
|
deriveEpicIdFromPrdPath,
|
|
396
|
+
deriveProjectCwdFromPrdPath,
|
|
228
397
|
PRD_SUBPATH,
|
|
229
398
|
EPICS_SUBPATH,
|
|
230
399
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* procIdentity.cjs — fail-closed process identity, so a recycled pid can
|
|
3
|
+
* never be mistaken for the process that originally held it.
|
|
4
|
+
*
|
|
5
|
+
* identity(pid) reads /proc/<pid>/stat field 22 (starttime) — located AFTER
|
|
6
|
+
* the last ')', because `comm` (field 2) can itself contain spaces or
|
|
7
|
+
* parens — plus /proc/<pid>/cmdline. On a platform with no /proc (macOS),
|
|
8
|
+
* falls back to `ps -o lstart=,command=` (lstart has only second
|
|
9
|
+
* granularity, which is the known limit of that path).
|
|
10
|
+
*
|
|
11
|
+
* isDifferentProcess(recorded, live) is the ONLY decision function callers
|
|
12
|
+
* should use, and it is deliberately fail-closed in the SAFE direction: it
|
|
13
|
+
* returns true — "safe to treat the recorded holder as gone" — ONLY when
|
|
14
|
+
* BOTH sides are a complete identity AND they differ. An unreadable /proc
|
|
15
|
+
* entry, a missing live probe, or a legacy record with no identity fields at
|
|
16
|
+
* all all read as "not different" — i.e. today's pid-only behaviour, never a
|
|
17
|
+
* false positive that could wrongly break a live lock or veto a real kill.
|
|
18
|
+
*
|
|
19
|
+
* SM_PROC_ROOT overrides the /proc root — tests only, so stat/cmdline
|
|
20
|
+
* fixtures can be written under a temp dir instead of touching real /proc.
|
|
21
|
+
*/
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { execFileSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
29
|
+
|
|
30
|
+
function procRoot() {
|
|
31
|
+
return process.env.SM_PROC_ROOT || '/proc';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasProc() {
|
|
35
|
+
try {
|
|
36
|
+
return fs.existsSync(procRoot());
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Field 22 (starttime) sits AFTER the last ')' — comm can hold spaces/parens. */
|
|
43
|
+
function parseStatStartTicks(raw) {
|
|
44
|
+
const lastParen = raw.lastIndexOf(')');
|
|
45
|
+
if (lastParen === -1) return null;
|
|
46
|
+
const rest = raw.slice(lastParen + 1).trim();
|
|
47
|
+
if (!rest) return null;
|
|
48
|
+
const fields = rest.split(/\s+/);
|
|
49
|
+
// `rest[0]` is field 3 (state); field N sits at index (N - 3), so field 22
|
|
50
|
+
// (starttime) is index 19.
|
|
51
|
+
const startTicksStr = fields[19];
|
|
52
|
+
if (startTicksStr === undefined) return null;
|
|
53
|
+
const startTicks = Number(startTicksStr);
|
|
54
|
+
return Number.isFinite(startTicks) ? startTicks : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readCmdlineFile(pid) {
|
|
58
|
+
const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'cmdline'), 'utf8');
|
|
59
|
+
const cmd = raw.replace(/\0+$/, '').split('\0').join(' ').trim();
|
|
60
|
+
return cmd.length > 0 ? cmd : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function identityFromProc(pid) {
|
|
64
|
+
let startTicks = null;
|
|
65
|
+
let cmdline = null;
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'stat'), 'utf8');
|
|
68
|
+
startTicks = parseStatStartTicks(raw);
|
|
69
|
+
} catch { /* unreadable stat (dead pid, hidepid, race) — leave null */ }
|
|
70
|
+
try {
|
|
71
|
+
cmdline = readCmdlineFile(pid);
|
|
72
|
+
} catch { /* unreadable cmdline — leave null */ }
|
|
73
|
+
return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseLstart(lstart) {
|
|
77
|
+
const m = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(lstart.trim());
|
|
78
|
+
if (!m) return null;
|
|
79
|
+
const monthIdx = MONTHS.indexOf(m[1]);
|
|
80
|
+
if (monthIdx === -1) return null;
|
|
81
|
+
const ms = Date.UTC(Number(m[6]), monthIdx, Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]));
|
|
82
|
+
return Number.isFinite(ms) ? ms : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function identityFromPs(pid) {
|
|
86
|
+
let out;
|
|
87
|
+
try {
|
|
88
|
+
out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart=,command='], { encoding: 'utf8' });
|
|
89
|
+
} catch {
|
|
90
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
91
|
+
}
|
|
92
|
+
const trimmed = out.replace(/\n$/, '');
|
|
93
|
+
const m = /^(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s(.*)$/.exec(trimmed);
|
|
94
|
+
if (!m) return { pid, startTicks: null, cmdline: null, complete: false };
|
|
95
|
+
const startTicks = parseLstart(m[1]);
|
|
96
|
+
const cmdline = m[2].trim() || null;
|
|
97
|
+
return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** identity(pid) → {pid, startTicks, cmdline, complete}. Never throws. */
|
|
101
|
+
function identity(pid) {
|
|
102
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
103
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return hasProc() ? identityFromProc(pid) : identityFromPs(pid);
|
|
107
|
+
} catch {
|
|
108
|
+
return { pid, startTicks: null, cmdline: null, complete: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* isDifferentProcess(recorded, live) — true ONLY when both sides are a
|
|
114
|
+
* COMPLETE identity and differ. Every other case (missing side, incomplete
|
|
115
|
+
* side, a legacy record with no identity fields) is "not different", i.e.
|
|
116
|
+
* today's pid-only behaviour — the fail-closed default.
|
|
117
|
+
*/
|
|
118
|
+
function isDifferentProcess(recorded, live) {
|
|
119
|
+
if (!recorded || !live) return false;
|
|
120
|
+
if (!recorded.complete || !live.complete) return false;
|
|
121
|
+
return recorded.pid !== live.pid
|
|
122
|
+
|| recorded.startTicks !== live.startTicks
|
|
123
|
+
|| recorded.cmdline !== live.cmdline;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { identity, isDifferentProcess };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* procName.cjs — make Session Manager's spawned processes distinguishable in
|
|
3
|
+
* GNOME System Monitor / ps / htop.
|
|
4
|
+
*
|
|
5
|
+
* - aliasBinFor(realBin, alias): the kernel sets `comm` (the "Process Name"
|
|
6
|
+
* column) to the basename of the path handed to execve, capped at 15 chars
|
|
7
|
+
* (TASK_COMM_LEN 16 incl. NUL). argv0 does NOT affect it, so we exec a
|
|
8
|
+
* symlink named `alias` that points at the real binary.
|
|
9
|
+
* - smArgv0(role, detail): a bounded argv0 label for /proc/pid/cmdline. It
|
|
10
|
+
* ALWAYS contains `claude` as a word — /\bclaude\b/ gates killOrphanClaudePid
|
|
11
|
+
* (scheduler.cjs), claudePidAlive and findLiveProcessForJob (reaperHelpers.cjs).
|
|
12
|
+
*
|
|
13
|
+
* Fail-open is absolute: any error returns the ORIGINAL bin. Naming must never
|
|
14
|
+
* stop a job from launching.
|
|
15
|
+
*
|
|
16
|
+
* DELIBERATE exceptions to CLAUDE.md laws (do not "fix"):
|
|
17
|
+
* - The tmp+rename symlink primitive below is NOT config.cjs writeJson /
|
|
18
|
+
* writeTextAtomic: those write file contents and cannot create symlinks.
|
|
19
|
+
* - procnamesRoot() is intentionally NOT registered with config.cjs validateWrite /
|
|
20
|
+
* addAllowedRoot: that containment governs IPC-reachable writes; this is a
|
|
21
|
+
* main-process-internal write.
|
|
22
|
+
*
|
|
23
|
+
* Not to be confused with procIdentity.cjs (pid-recycling fingerprint).
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const { procnamesRoot } = require('./schedulerPaths.cjs');
|
|
31
|
+
const MAX_ALIAS_LEN = 15; // Linux TASK_COMM_LEN (16) minus NUL
|
|
32
|
+
const MAX_LABEL_LEN = 120;
|
|
33
|
+
|
|
34
|
+
let tmpSeq = 0;
|
|
35
|
+
|
|
36
|
+
function resolveBare(bin) {
|
|
37
|
+
if (path.isAbsolute(bin)) return bin;
|
|
38
|
+
try {
|
|
39
|
+
// Lazy: openExternalApp requires 'electron', unavailable outside the app.
|
|
40
|
+
const { findCommand } = require('./openExternalApp.cjs');
|
|
41
|
+
const found = findCommand(bin);
|
|
42
|
+
return found && path.isAbsolute(found) ? found : null;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** O(1) fs ops. Returns alias path on success, else `realBin` unchanged. */
|
|
47
|
+
function aliasBinFor(realBin, alias) {
|
|
48
|
+
if (typeof alias !== 'string' || !alias || alias.length > MAX_ALIAS_LEN || /[\\/\0]/.test(alias) || alias === '.' || alias === '..') {
|
|
49
|
+
throw new Error(`procName: alias "${alias}" must be 1-${MAX_ALIAS_LEN} chars (Linux TASK_COMM_LEN) with no path separators`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (typeof realBin !== 'string' || !realBin) return realBin;
|
|
53
|
+
const target = resolveBare(realBin);
|
|
54
|
+
if (!target) return realBin;
|
|
55
|
+
const aliasPath = path.join(procnamesRoot(), alias);
|
|
56
|
+
let current = null;
|
|
57
|
+
try { current = fs.readlinkSync(aliasPath); } catch { /* absent or not a link */ }
|
|
58
|
+
if (current !== target) {
|
|
59
|
+
fs.mkdirSync(procnamesRoot(), { recursive: true });
|
|
60
|
+
const tmp = path.join(procnamesRoot(), `.${alias}.${process.pid}.${Date.now()}.${tmpSeq++}.tmp`);
|
|
61
|
+
try {
|
|
62
|
+
fs.symlinkSync(target, tmp);
|
|
63
|
+
fs.renameSync(tmp, aliasPath); // atomic replace; never unlink-then-symlink
|
|
64
|
+
} catch (err) {
|
|
65
|
+
try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ }
|
|
66
|
+
if (err && err.code !== 'EEXIST') throw err; // losing racer: swallow
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fs.accessSync(aliasPath, fs.constants.X_OK); // noexec / dangling probe
|
|
70
|
+
return aliasPath;
|
|
71
|
+
} catch {
|
|
72
|
+
return realBin;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `sm-claude-<role>:<detail>`, sanitised to [A-Za-z0-9._-], <= 120 chars. Always contains word `claude`. */
|
|
77
|
+
function smArgv0(role, detail) {
|
|
78
|
+
const clean = (v) => String(v == null ? '' : v).replace(/[^A-Za-z0-9._-]/g, '');
|
|
79
|
+
const r = clean(role) || 'proc';
|
|
80
|
+
const d = clean(detail);
|
|
81
|
+
return `sm-claude-${r}${d ? `:${d}` : ''}`.slice(0, MAX_LABEL_LEN);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Remove alias entries whose target no longer resolves to an executable. Never throws. O(entries). */
|
|
85
|
+
function pruneStaleAliases() {
|
|
86
|
+
try {
|
|
87
|
+
for (const name of fs.readdirSync(procnamesRoot())) {
|
|
88
|
+
const p = path.join(procnamesRoot(), name);
|
|
89
|
+
try {
|
|
90
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
91
|
+
} catch {
|
|
92
|
+
try { fs.unlinkSync(p); } catch { /* best effort */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch { /* root absent/unreadable */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { aliasBinFor, smArgv0, procnamesRoot, pruneStaleAliases };
|