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
|
@@ -54,6 +54,8 @@ const path = require('node:path');
|
|
|
54
54
|
const os = require('node:os');
|
|
55
55
|
const crypto = require('node:crypto');
|
|
56
56
|
const { execFile } = require('node:child_process');
|
|
57
|
+
const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
58
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
57
59
|
|
|
58
60
|
// Per-kind configuration. Roots are kept OUTSIDE any project's own tree
|
|
59
61
|
// (os.tmpdir(), not `<cwd>/.git/...`) so a managed worktree never shows up in
|
|
@@ -85,7 +87,7 @@ const { execFile } = require('node:child_process');
|
|
|
85
87
|
// days, same rationale as defaultMax above.
|
|
86
88
|
const KIND_CONFIG = {
|
|
87
89
|
job: {
|
|
88
|
-
root
|
|
90
|
+
get root() { return schedulerPaths.worktreeRoot('job'); },
|
|
89
91
|
branchPrefix: 'sm-job/',
|
|
90
92
|
disableEnv: 'SM_JOB_WORKTREE_DISABLE',
|
|
91
93
|
maxEnv: 'SM_JOB_WORKTREE_MAX',
|
|
@@ -94,7 +96,7 @@ const KIND_CONFIG = {
|
|
|
94
96
|
defaultStaleSweepAgeMs: 24 * 60 * 60 * 1000,
|
|
95
97
|
},
|
|
96
98
|
epic: {
|
|
97
|
-
root
|
|
99
|
+
get root() { return schedulerPaths.worktreeRoot('epic'); },
|
|
98
100
|
branchPrefix: 'sm-epic/',
|
|
99
101
|
disableEnv: 'SM_EPIC_WORKTREE_DISABLE',
|
|
100
102
|
maxEnv: 'SM_EPIC_WORKTREE_MAX',
|
|
@@ -183,6 +185,13 @@ function isUnderRoot(p, root) {
|
|
|
183
185
|
// corrected down to match reality when it's proven to have leaked.
|
|
184
186
|
const activeWorktreeCount = { job: 0, epic: 0 };
|
|
185
187
|
|
|
188
|
+
// Registered checkouts this process created and has not yet cleaned up:
|
|
189
|
+
// dir -> { kind, branch }. Lets `expireDeadWorktreeRegistrations` prove a
|
|
190
|
+
// counted checkout is dead (no cwd holder + terminal row) and release its
|
|
191
|
+
// count — accounting only, never a kill or an rm.
|
|
192
|
+
const registeredCheckouts = new Map();
|
|
193
|
+
const expiredCheckoutDirs = new Set(); // released by expiry, awaiting their (late) cleanupWorktree
|
|
194
|
+
|
|
186
195
|
// Per-kind promise chain used as a simple async mutex (see `withKindLock`)
|
|
187
196
|
// so the cap-check-and-reserve decision in `reserveWorktreeSlot` — which now
|
|
188
197
|
// sometimes needs to `await` a fresh on-disk observation — stays a single
|
|
@@ -215,6 +224,46 @@ function execGit(args, { cwd, timeout = 20_000 } = {}) {
|
|
|
215
224
|
});
|
|
216
225
|
}
|
|
217
226
|
|
|
227
|
+
// Unquote a single git porcelain v1 path token. Git wraps a path in double
|
|
228
|
+
// quotes and C-style-escapes it (\", \\, \t, \n, and \NNN octal per raw UTF-8
|
|
229
|
+
// byte) whenever it contains a double quote, backslash, control character, or
|
|
230
|
+
// any byte >= 0x80 (core.quotepath's default "ASCII-safe" behavior) — a plain
|
|
231
|
+
// path with none of those passes through untouched. Pure. Lives here (not
|
|
232
|
+
// scheduler.cjs, which also parses porcelain output) because scheduler.cjs
|
|
233
|
+
// requires this module — the reverse would be circular — and every porcelain
|
|
234
|
+
// consumer in either file must unquote a path before treating it as a real
|
|
235
|
+
// on-disk path (fs.existsSync, a pathspec, a Set membership check).
|
|
236
|
+
function unquotePorcelainPath(raw) {
|
|
237
|
+
if (raw.length < 2 || raw[0] !== '"' || raw[raw.length - 1] !== '"') return raw;
|
|
238
|
+
const inner = raw.slice(1, -1);
|
|
239
|
+
const bytes = [];
|
|
240
|
+
const simpleEscapes = { '"': 0x22, '\\': 0x5c, n: 0x0a, t: 0x09, r: 0x0d, a: 0x07, b: 0x08, f: 0x0c, v: 0x0b };
|
|
241
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
242
|
+
const c = inner[i];
|
|
243
|
+
if (c === '\\' && i + 1 < inner.length) {
|
|
244
|
+
const next = inner[i + 1];
|
|
245
|
+
if (Object.prototype.hasOwnProperty.call(simpleEscapes, next)) {
|
|
246
|
+
bytes.push(simpleEscapes[next]);
|
|
247
|
+
i += 1;
|
|
248
|
+
} else if (next >= '0' && next <= '7') {
|
|
249
|
+
let octal = '';
|
|
250
|
+
let j = i + 1;
|
|
251
|
+
while (j < inner.length && octal.length < 3 && inner[j] >= '0' && inner[j] <= '7') {
|
|
252
|
+
octal += inner[j];
|
|
253
|
+
j += 1;
|
|
254
|
+
}
|
|
255
|
+
bytes.push(parseInt(octal, 8) & 0xff);
|
|
256
|
+
i = j - 1;
|
|
257
|
+
} else {
|
|
258
|
+
bytes.push(c.charCodeAt(0));
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
for (const b of Buffer.from(c, 'utf8')) bytes.push(b);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return Buffer.from(bytes).toString('utf8');
|
|
265
|
+
}
|
|
266
|
+
|
|
218
267
|
async function isGitRepo(cwd) {
|
|
219
268
|
if (!cwd) return false;
|
|
220
269
|
try {
|
|
@@ -281,11 +330,11 @@ async function removeWorktreeDir(cwd, dir) {
|
|
|
281
330
|
/* best-effort */
|
|
282
331
|
}
|
|
283
332
|
const parentDir = path.dirname(dir);
|
|
284
|
-
// Guard against ever rmdir-ing os.tmpdir() itself — every real caller's
|
|
333
|
+
// Guard against ever rmdir-ing the worktree base (os.tmpdir() or SM_WORKTREE_ROOT) itself — every real caller's
|
|
285
334
|
// `dir` is `<kind root>/<hash>/<key>`, so `parentDir` is always the hash
|
|
286
335
|
// dir, never the tmpdir root, but this keeps the guarantee explicit rather
|
|
287
336
|
// than relying solely on call-site discipline.
|
|
288
|
-
if (parentDir ===
|
|
337
|
+
if (parentDir === schedulerPaths.worktreeBase()) return;
|
|
289
338
|
try {
|
|
290
339
|
const remaining = await fsp.readdir(parentDir);
|
|
291
340
|
if (remaining.length === 0) await fsp.rmdir(parentDir);
|
|
@@ -304,7 +353,7 @@ async function removeWorktreeDir(cwd, dir) {
|
|
|
304
353
|
* <name>` segment stripped off.
|
|
305
354
|
*
|
|
306
355
|
* Round-trip verified before being trusted, mirroring the same defense
|
|
307
|
-
* `
|
|
356
|
+
* `src/main/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
|
|
308
357
|
* `.git` pointer file is a plain text file, so its content must not be
|
|
309
358
|
* trusted to redirect `git worktree remove --force` at an arbitrary path
|
|
310
359
|
* without the candidate main tree's OWN admin dir
|
|
@@ -481,29 +530,62 @@ async function sweepStaleWorktreeCheckouts(kind, opts = {}) {
|
|
|
481
530
|
* copy of the human's WIP stays in `cwd` the whole time. Nothing here ever
|
|
482
531
|
* `git add`s or commits the carried paths on the job's behalf, and nothing
|
|
483
532
|
* ever restores them back into `cwd` — they were never removed from it.
|
|
533
|
+
*
|
|
534
|
+
* The diff itself is captured via `git diff --output=<file>` so git writes
|
|
535
|
+
* the patch straight to disk — never through Node's stdout buffer. A prior
|
|
536
|
+
* version piped the patch through `execFile`'s default-`maxBuffer` stdout
|
|
537
|
+
* capture, which overflowed on a real dirty tree of ~240 changed paths
|
|
538
|
+
* (`stdout maxBuffer length exceeded`, job 1192) and silently fell back to
|
|
539
|
+
* running the job in the SHARED working tree — the exact moment isolation
|
|
540
|
+
* matters most. `--output` removes the buffer ceiling entirely regardless of
|
|
541
|
+
* how large the tree's outstanding diff is. The temp patch file lives
|
|
542
|
+
* inside `dir` (the job's own fresh worktree checkout, not a bare
|
|
543
|
+
* `os.tmpdir()` path) so it is guaranteed to be cleaned up either by the
|
|
544
|
+
* explicit `fsp.rm` below or, on any earlier failure, by the caller tearing
|
|
545
|
+
* the whole `dir` down.
|
|
484
546
|
*/
|
|
485
547
|
async function captureAndCarryBaseDiff({ cwd, dir }) {
|
|
486
548
|
let paths = [];
|
|
487
|
-
let patch = '';
|
|
488
549
|
try {
|
|
489
550
|
const nameOut = await execGit(['diff', 'HEAD', '--name-only'], { cwd, timeout: 15_000 });
|
|
490
551
|
paths = nameOut.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
491
|
-
patch = await execGit(['diff', 'HEAD', '--binary'], { cwd, timeout: 30_000 });
|
|
492
552
|
} catch (e) {
|
|
493
|
-
return { ok: false, reason: `
|
|
553
|
+
return { ok: false, reason: `listing changed paths failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
494
554
|
}
|
|
495
|
-
if (!patch || !patch.trim()) return { ok: true, paths: [] };
|
|
496
555
|
|
|
497
|
-
|
|
556
|
+
// Everything from here on operates on a real (or attempted) file at
|
|
557
|
+
// `patchFile` — wrapped in one try/finally so the temp file is removed on
|
|
558
|
+
// EVERY exit path, including a `git diff` that errors after partially
|
|
559
|
+
// writing it, not just the clean-success path.
|
|
560
|
+
const patchFile = path.join(dir, `.sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
|
|
498
561
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
562
|
+
try {
|
|
563
|
+
await execGit(['diff', 'HEAD', '--binary', `--output=${patchFile}`], { cwd, timeout: 120_000 });
|
|
564
|
+
} catch (e) {
|
|
565
|
+
return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let stat;
|
|
569
|
+
try {
|
|
570
|
+
stat = await fsp.stat(patchFile);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
return { ok: false, reason: `reading captured diff file failed: ${(e && e.message) || e}` };
|
|
573
|
+
}
|
|
574
|
+
if (stat.size === 0) return { ok: true, paths: [] };
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
// Same 120s ceiling as the capture step above: applying a patch large
|
|
578
|
+
// enough to need that long to generate can just as easily need that
|
|
579
|
+
// long to apply — leaving this at the old 30s reintroduces the same
|
|
580
|
+
// large-dirty-tree degrade-to-shared-tree failure one call later.
|
|
581
|
+
await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 120_000 });
|
|
582
|
+
} catch (e) {
|
|
583
|
+
return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
|
|
584
|
+
}
|
|
585
|
+
return { ok: true, paths };
|
|
503
586
|
} finally {
|
|
504
587
|
try { await fsp.rm(patchFile, { force: true }); } catch { /* best-effort */ }
|
|
505
588
|
}
|
|
506
|
-
return { ok: true, paths };
|
|
507
589
|
}
|
|
508
590
|
|
|
509
591
|
/**
|
|
@@ -770,9 +852,31 @@ async function createWorktree({ kind, cwd, key }) {
|
|
|
770
852
|
carriedPaths = carry.paths;
|
|
771
853
|
}
|
|
772
854
|
|
|
855
|
+
registeredCheckouts.set(dir, { kind, branch });
|
|
773
856
|
return { ok: true, dir, branch, baseCwd: cwd, carriedPaths };
|
|
774
857
|
}
|
|
775
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Release the count of any registered checkout whose branch has no live
|
|
861
|
+
* holder (no process cwd under its dir) AND whose row is terminal
|
|
862
|
+
* (`isTerminalBranch(branch)`). Fail-closed: an unknown/live row or a live
|
|
863
|
+
* holder keeps the count. Returns the expired dirs. O(registered) plus one
|
|
864
|
+
* /proc scan.
|
|
865
|
+
*/
|
|
866
|
+
function expireDeadWorktreeRegistrations({ isTerminalBranch, holders = listCwdHolders() } = {}) {
|
|
867
|
+
const expired = [];
|
|
868
|
+
if (typeof isTerminalBranch !== 'function') return expired;
|
|
869
|
+
for (const [dir, reg] of [...registeredCheckouts]) {
|
|
870
|
+
if (hasLiveHolder(dir, holders)) continue;
|
|
871
|
+
if (!isTerminalBranch(reg.branch)) continue;
|
|
872
|
+
registeredCheckouts.delete(dir);
|
|
873
|
+
expiredCheckoutDirs.add(dir);
|
|
874
|
+
activeWorktreeCount[reg.kind] = Math.max(0, activeWorktreeCount[reg.kind] - 1);
|
|
875
|
+
expired.push(dir);
|
|
876
|
+
}
|
|
877
|
+
return expired;
|
|
878
|
+
}
|
|
879
|
+
|
|
776
880
|
/**
|
|
777
881
|
* True iff every path in `paths` has working-tree content in `cwd`
|
|
778
882
|
* byte-identical to its committed blob on `branch`. Used both by the
|
|
@@ -1034,7 +1138,10 @@ async function cleanupWorktree({ kind, cwd, dir, branch, keepBranch }) {
|
|
|
1034
1138
|
try { await execGit(['branch', '-D', branch], { cwd, timeout: 10_000 }); } catch { /* already gone */ }
|
|
1035
1139
|
}
|
|
1036
1140
|
try { await execGit(['worktree', 'prune'], { cwd, timeout: 10_000 }); } catch { /* best effort */ }
|
|
1037
|
-
|
|
1141
|
+
// A checkout the expiry pass already released must not be counted down twice.
|
|
1142
|
+
const alreadyExpired = dir ? expiredCheckoutDirs.delete(dir) : false;
|
|
1143
|
+
if (dir) registeredCheckouts.delete(dir);
|
|
1144
|
+
if (!alreadyExpired) activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
|
|
1038
1145
|
}
|
|
1039
1146
|
|
|
1040
1147
|
/**
|
|
@@ -1113,7 +1220,12 @@ async function salvageDirtyDelta({ cwd, paths, outFile }) {
|
|
|
1113
1220
|
for (const line of String(statusOut || '').split('\n')) {
|
|
1114
1221
|
if (!line) continue;
|
|
1115
1222
|
const code = line.slice(0, 2);
|
|
1116
|
-
|
|
1223
|
+
// Unquote before use: a raw quoted+C-escaped token (any path with a
|
|
1224
|
+
// quote, backslash, control char, or non-ASCII byte) is not a real
|
|
1225
|
+
// on-disk path — passing it straight through as a pathspec to the
|
|
1226
|
+
// `git diff` calls below silently fails to match the real file,
|
|
1227
|
+
// dropping that path's content out of the salvaged patch entirely.
|
|
1228
|
+
const p = unquotePorcelainPath(line.slice(3));
|
|
1117
1229
|
if (!p) continue;
|
|
1118
1230
|
if (code === '??') untrackedPaths.push(p);
|
|
1119
1231
|
else trackedPaths.push(p);
|
|
@@ -1271,9 +1383,12 @@ async function reclaimTerminalJobOrphans({ cwd, terminalSlugs, isLive }) {
|
|
|
1271
1383
|
} catch {
|
|
1272
1384
|
continue; // can't prove the checkout is clean-of-real-work — never assume
|
|
1273
1385
|
}
|
|
1274
|
-
const dirtyPaths = statusOut.split('\n')
|
|
1386
|
+
const dirtyPaths = statusOut.split('\n')
|
|
1387
|
+
.filter((l) => l.length > 0)
|
|
1388
|
+
.map((l) => unquotePorcelainPath(l.slice(3).trim()))
|
|
1389
|
+
.filter(Boolean);
|
|
1275
1390
|
const opsOnly = dirtyPaths.every(
|
|
1276
|
-
(p) => p ===
|
|
1391
|
+
(p) => p === OPS_ROOT_DIR || p.startsWith(`${OPS_ROOT_DIR}/`)
|
|
1277
1392
|
);
|
|
1278
1393
|
if (!opsOnly) continue; // real non-ops dirty content present — leave for a human
|
|
1279
1394
|
|
|
@@ -1402,6 +1517,7 @@ module.exports = {
|
|
|
1402
1517
|
getMaxConcurrentWorktrees,
|
|
1403
1518
|
getStaleSweepAgeMs,
|
|
1404
1519
|
isGitRepo,
|
|
1520
|
+
unquotePorcelainPath,
|
|
1405
1521
|
isBaseTreeClean,
|
|
1406
1522
|
worktreeDirFor,
|
|
1407
1523
|
branchNameFor,
|
|
@@ -1424,6 +1540,7 @@ module.exports = {
|
|
|
1424
1540
|
mainTreeFromWorktreeGitFile,
|
|
1425
1541
|
getObservedWorktreeCount,
|
|
1426
1542
|
reserveWorktreeSlot,
|
|
1543
|
+
expireDeadWorktreeRegistrations,
|
|
1427
1544
|
reclaimTerminalJobOrphans,
|
|
1428
1545
|
// Job-kind convenience wrappers — same call shape jobWorktree.cjs has
|
|
1429
1546
|
// always exposed.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guardShims.cjs — stable, app-upgrade-proof indirection for every
|
|
3
|
+
* PreToolUse guard hook (each name in GUARD_NAMES) that delegationReadiness.cjs installs into an
|
|
4
|
+
* adopting project's `.claude/settings.json`.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: those installers used to bake
|
|
7
|
+
* `path.resolve(__dirname, '../../../scripts/hooks/guard-*.cjs')` — THIS
|
|
8
|
+
* app's own copy of the script — directly into the hook command. Run via
|
|
9
|
+
* `npx claude-code-session-manager@latest` (the normal install path), that
|
|
10
|
+
* resolves under `~/.npm/_npx/<hash>/node_modules/claude-code-session-manager/`
|
|
11
|
+
* — a directory scoped to ONE npx resolution. The next `npx ...@latest` (a
|
|
12
|
+
* new release) or an npm cache prune creates/removes a different hash dir, so
|
|
13
|
+
* every previously-installed hook decays into a dead path. A PreToolUse
|
|
14
|
+
* command pointing at a missing script exits non-zero WITHOUT exit code 2 (a
|
|
15
|
+
* non-blocking error to the harness), so it silently guards nothing — while
|
|
16
|
+
* checkPrdWriteGuard correctly flips the readiness banner back to red. That
|
|
17
|
+
* decay is why "Delegation not ready" reappears across projects even after a
|
|
18
|
+
* successful install.
|
|
19
|
+
*
|
|
20
|
+
* Fix: install a STABLE shim under the user's home
|
|
21
|
+
* (`~/.claude/session-manager/hooks/guard-<name>.cjs`) that never moves
|
|
22
|
+
* across app upgrades. The shim contains no guard logic of its own — CLAUDE.md's
|
|
23
|
+
* "adopt by REFERENCE, never vendor" law — it reads a pointer file
|
|
24
|
+
* (`app-root.json`, rewritten on every app boot with the CURRENTLY running
|
|
25
|
+
* app's root) and `require()`s the real `scripts/hooks/guard-<name>.cjs` from
|
|
26
|
+
* there. An app upgrade (a new npx hash dir) just rewrites the pointer on the
|
|
27
|
+
* next boot; every already-installed project hook keeps resolving through
|
|
28
|
+
* the same shim path with no re-install.
|
|
29
|
+
*
|
|
30
|
+
* The guard scripts run their logic as a require-time side effect (stdin ->
|
|
31
|
+
* decision -> process.exit, not gated on `require.main === module`), so a
|
|
32
|
+
* plain `require()` from the shim is enough to invoke them.
|
|
33
|
+
*/
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
const os = require('node:os');
|
|
39
|
+
const { writeJson, writeTextAtomic, addAllowedRoot } = require('../config.cjs');
|
|
40
|
+
|
|
41
|
+
// src/main/lib -> app root (three levels up) — the same resolution
|
|
42
|
+
// delegationReadiness.cjs's *_GUARD_SCRIPT constants use.
|
|
43
|
+
const APP_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
44
|
+
|
|
45
|
+
const GUARD_NAMES = ['guard-prd-writes.cjs', 'guard-destructive-git.cjs', 'guard-inline-implementation.cjs', 'guard-self-schedule.cjs'];
|
|
46
|
+
|
|
47
|
+
function hooksDir(homeDir = os.homedir()) {
|
|
48
|
+
return path.join(homeDir, '.claude', 'session-manager', 'hooks');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pointerPath(homeDir = os.homedir()) {
|
|
52
|
+
return path.join(hooksDir(homeDir), 'app-root.json');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shimPath(guardName, homeDir = os.homedir()) {
|
|
56
|
+
return path.join(hooksDir(homeDir), guardName);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function shimBody(guardName) {
|
|
60
|
+
return `'use strict';
|
|
61
|
+
const fs = require('node:fs');
|
|
62
|
+
const path = require('node:path');
|
|
63
|
+
const ptr = JSON.parse(fs.readFileSync(path.join(__dirname, 'app-root.json'), 'utf8'));
|
|
64
|
+
require(path.join(ptr.appRoot, 'scripts', 'hooks', '${guardName}'));
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Write the pointer file + every shim in GUARD_NAMES under `homeDir`. Idempotent: a
|
|
70
|
+
* re-run with the same `appRoot` produces byte-identical files; a re-run with
|
|
71
|
+
* a DIFFERENT `appRoot` (an app upgrade) rewrites only the pointer, which is
|
|
72
|
+
* exactly what lets already-installed project hooks follow the upgrade with
|
|
73
|
+
* no re-install. Never throws — returns `{ ok, error }` so a boot-time caller
|
|
74
|
+
* can fire-and-forget and an installer can refuse cleanly rather than writing
|
|
75
|
+
* a hook command that points at a shim that doesn't exist.
|
|
76
|
+
*/
|
|
77
|
+
async function writeGuardShims({ homeDir = os.homedir(), appRoot = APP_ROOT } = {}) {
|
|
78
|
+
try {
|
|
79
|
+
addAllowedRoot(homeDir);
|
|
80
|
+
await writeJson(pointerPath(homeDir), { appRoot });
|
|
81
|
+
for (const name of GUARD_NAMES) {
|
|
82
|
+
await writeTextAtomic(shimPath(name, homeDir), shimBody(name));
|
|
83
|
+
}
|
|
84
|
+
return { ok: true };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when every shim + the pointer file already exist on disk under `homeDir`. */
|
|
91
|
+
function guardShimsExist(homeDir = os.homedir()) {
|
|
92
|
+
return fs.existsSync(pointerPath(homeDir)) && GUARD_NAMES.every((name) => fs.existsSync(shimPath(name, homeDir)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Given an absolute script path a hook command resolved to, determine
|
|
97
|
+
* whether it's actually a guard SHIM (its directory carries a sibling
|
|
98
|
+
* `app-root.json`) rather than a direct app-root script. A direct script has
|
|
99
|
+
* no pointer and is trivially "real" once `fs.existsSync` is true — the
|
|
100
|
+
* caller doesn't need this function for that case.
|
|
101
|
+
*
|
|
102
|
+
* For a shim, existence of the shim FILE is not enough: the shim's own
|
|
103
|
+
* require() target is `ptr.appRoot/scripts/hooks/<name>.cjs`, resolved only
|
|
104
|
+
* at hook-invocation time. If the pointer is missing, unparseable, or now
|
|
105
|
+
* names a script that's gone, the shim silently guards nothing the next time
|
|
106
|
+
* a hook actually fires — decay one hop downstream of the hook command
|
|
107
|
+
* itself. Returns `null` when `scriptAbsPath` is not a shim (no sibling
|
|
108
|
+
* pointer) — the caller's plain existence check already covers that case.
|
|
109
|
+
* Throws when it IS a shim and the pointer has decayed.
|
|
110
|
+
*/
|
|
111
|
+
function resolveShimTarget(scriptAbsPath) {
|
|
112
|
+
const dir = path.dirname(scriptAbsPath);
|
|
113
|
+
const ptrFile = path.join(dir, 'app-root.json');
|
|
114
|
+
if (!fs.existsSync(ptrFile)) return null;
|
|
115
|
+
|
|
116
|
+
const guardName = path.basename(scriptAbsPath);
|
|
117
|
+
let appRoot;
|
|
118
|
+
try {
|
|
119
|
+
appRoot = JSON.parse(fs.readFileSync(ptrFile, 'utf8'))?.appRoot;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new Error(`guard shim pointer ${ptrFile} is unparseable: ${err?.message ?? err}`);
|
|
122
|
+
}
|
|
123
|
+
if (typeof appRoot !== 'string' || !appRoot) {
|
|
124
|
+
throw new Error(`guard shim pointer ${ptrFile} has no appRoot`);
|
|
125
|
+
}
|
|
126
|
+
const real = path.join(appRoot, 'scripts', 'hooks', guardName);
|
|
127
|
+
if (!fs.existsSync(real)) {
|
|
128
|
+
throw new Error(`guard shim pointer ${ptrFile} resolves to a script that no longer exists: ${real}`);
|
|
129
|
+
}
|
|
130
|
+
return real;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `writeGuardShims({ homeDir })`, but returns `null` on success and an
|
|
135
|
+
* install-function-shaped error object (`{ ok:false, action:'error', error }`)
|
|
136
|
+
* on failure — the one call every `install*Guard` in delegationReadiness.cjs
|
|
137
|
+
* needs before it can safely write a hook command pointing at the shim.
|
|
138
|
+
*/
|
|
139
|
+
async function ensureGuardShimsOrError(homeDir) {
|
|
140
|
+
const result = await writeGuardShims({ homeDir });
|
|
141
|
+
if (result.ok) return null;
|
|
142
|
+
return { ok: false, action: 'error', error: `could not write the stable guard shim under ${hooksDir(homeDir)}: ${result.error}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
APP_ROOT,
|
|
147
|
+
GUARD_NAMES,
|
|
148
|
+
hooksDir,
|
|
149
|
+
pointerPath,
|
|
150
|
+
shimPath,
|
|
151
|
+
shimBody,
|
|
152
|
+
writeGuardShims,
|
|
153
|
+
guardShimsExist,
|
|
154
|
+
resolveShimTarget,
|
|
155
|
+
ensureGuardShimsOrError,
|
|
156
|
+
};
|
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
'use strict';
|
|
27
27
|
|
|
28
28
|
const path = require('node:path');
|
|
29
|
-
const os = require('node:os');
|
|
30
29
|
const config = require('../config.cjs');
|
|
31
30
|
|
|
32
|
-
const
|
|
31
|
+
const { historyRollupPath } = require('./schedulerPaths.cjs');
|
|
33
32
|
const COMPACT_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
34
33
|
|
|
35
34
|
const TOTALS_MODEL_ID = '';
|
|
@@ -75,7 +74,7 @@ function mergeRollupLines(lines) {
|
|
|
75
74
|
* only want data lines should filter on modelId/projectDir.
|
|
76
75
|
*/
|
|
77
76
|
async function readRollup(fromDate, toDate) {
|
|
78
|
-
const { exists, text } = await config.readText(
|
|
77
|
+
const { exists, text } = await config.readText(historyRollupPath());
|
|
79
78
|
if (!exists || !text) return new Map();
|
|
80
79
|
const merged = mergeRollupLines(text.split('\n'));
|
|
81
80
|
const filtered = new Map();
|
|
@@ -108,7 +107,7 @@ function isDateFinalized(mergedMap, date) {
|
|
|
108
107
|
async function appendRollupDays(entries) {
|
|
109
108
|
if (!entries || entries.length === 0) return;
|
|
110
109
|
|
|
111
|
-
const real = config.validatePath(
|
|
110
|
+
const real = config.validatePath(historyRollupPath());
|
|
112
111
|
config.validateWrite(real);
|
|
113
112
|
const fsp = require('node:fs/promises');
|
|
114
113
|
await fsp.mkdir(path.dirname(real), { recursive: true });
|
|
@@ -125,15 +124,15 @@ async function appendRollupDays(entries) {
|
|
|
125
124
|
|
|
126
125
|
/** Dedupe the rollup file down to its last-write-wins merged lines. */
|
|
127
126
|
async function compact() {
|
|
128
|
-
const { exists, text } = await config.readText(
|
|
127
|
+
const { exists, text } = await config.readText(historyRollupPath());
|
|
129
128
|
if (!exists || !text) return;
|
|
130
129
|
const merged = mergeRollupLines(text.split('\n'));
|
|
131
130
|
const rewritten = Array.from(merged.values()).map((b) => JSON.stringify(b)).join('\n') + '\n';
|
|
132
|
-
await config.writeTextAtomic(
|
|
131
|
+
await config.writeTextAtomic(historyRollupPath(), rewritten);
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
module.exports = {
|
|
136
|
-
|
|
135
|
+
historyRollupPath,
|
|
137
136
|
COMPACT_THRESHOLD_BYTES,
|
|
138
137
|
TOTALS_MODEL_ID,
|
|
139
138
|
FINALIZED_MODEL_ID,
|
|
@@ -26,14 +26,16 @@
|
|
|
26
26
|
'use strict';
|
|
27
27
|
|
|
28
28
|
const fs = require('node:fs');
|
|
29
|
-
const os = require('node:os');
|
|
30
29
|
const path = require('node:path');
|
|
30
|
+
const { identity, isDifferentProcess } = require('./procIdentity.cjs');
|
|
31
|
+
const { instanceLockPath } = require('./schedulerPaths.cjs');
|
|
32
|
+
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
31
33
|
|
|
32
34
|
// Env override is for unit tests only (isolates the lock from the real
|
|
33
35
|
// ~/.claude of the machine running the suite).
|
|
34
36
|
function lockPath() {
|
|
35
37
|
return process.env.SM_SCHEDULER_LOCK_PATH
|
|
36
|
-
||
|
|
38
|
+
|| instanceLockPath();
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function pidAlive(pid) {
|
|
@@ -58,7 +60,11 @@ function readLock() {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
function writeLockExclusive() {
|
|
61
|
-
const body = JSON.stringify({
|
|
63
|
+
const body = JSON.stringify({
|
|
64
|
+
pid: process.pid,
|
|
65
|
+
startedAt: new Date().toISOString(),
|
|
66
|
+
identity: identity(process.pid),
|
|
67
|
+
});
|
|
62
68
|
fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
|
|
63
69
|
// 'wx' = atomic create-or-fail — two racing instances cannot both win.
|
|
64
70
|
fs.writeFileSync(lockPath(), body, { flag: 'wx', mode: 0o600 });
|
|
@@ -67,14 +73,35 @@ function writeLockExclusive() {
|
|
|
67
73
|
/**
|
|
68
74
|
* Try to become the machine's scheduler owner.
|
|
69
75
|
* Returns { owner: true } or { owner: false, holderPid } — never throws.
|
|
76
|
+
*
|
|
77
|
+
* A holder pid that is alive is normally respected — but if the lock body
|
|
78
|
+
* carries a COMPLETE recorded identity that provably differs from the live
|
|
79
|
+
* process now holding that pid (procIdentity.isDifferentProcess), the pid
|
|
80
|
+
* was recycled after a hard crash: today's bare pidAlive() check would pin
|
|
81
|
+
* ownership on that unrelated live process forever. That case is treated as
|
|
82
|
+
* stale and the lock is broken (audited as `stale_lock_broken`). A missing
|
|
83
|
+
* identity (legacy lock body, or an unreadable /proc on either side) is
|
|
84
|
+
* fail-closed to "not different" — i.e. today's pid-only behaviour.
|
|
70
85
|
*/
|
|
71
86
|
function acquireSchedulerOwnership() {
|
|
72
87
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
73
88
|
const existing = readLock();
|
|
74
89
|
if (existing && existing.pid !== process.pid && pidAlive(existing.pid)) {
|
|
75
|
-
|
|
90
|
+
const liveIdentity = identity(existing.pid);
|
|
91
|
+
if (!isDifferentProcess(existing.identity, liveIdentity)) {
|
|
92
|
+
return { owner: false, holderPid: existing.pid };
|
|
93
|
+
}
|
|
94
|
+
appendAuditEvent('stale_lock_broken', {
|
|
95
|
+
path: lockPath(),
|
|
96
|
+
recordedPid: existing.pid,
|
|
97
|
+
recordedStartedAt: existing.startedAt ?? null,
|
|
98
|
+
recordedIdentity: existing.identity ?? null,
|
|
99
|
+
liveIdentity,
|
|
100
|
+
});
|
|
76
101
|
}
|
|
77
|
-
// Missing, unreadable, our own,
|
|
102
|
+
// Missing, unreadable, our own, stale (holder pid dead), or a recorded
|
|
103
|
+
// identity proven different from the live process at that recycled pid
|
|
104
|
+
// — take it.
|
|
78
105
|
try {
|
|
79
106
|
if (existing || fs.existsSync(lockPath())) fs.unlinkSync(lockPath());
|
|
80
107
|
} catch { /* raced with another breaker — retry loop below settles it */ }
|
|
@@ -27,8 +27,13 @@
|
|
|
27
27
|
* must still park.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The one app-owned root. Matches at any depth, and only as a path segment.
|
|
32
|
+
* Sourced from opsOwnership.cjs (the single-writer law's own root name)
|
|
33
|
+
* rather than a second hardcoded literal — this file matches git-porcelain
|
|
34
|
+
* RELATIVE paths, never builds an fs path, so opsPath() itself doesn't apply.
|
|
35
|
+
*/
|
|
36
|
+
const { OPS_ROOT_DIR: OPS_ROOT_SEGMENT } = require('./opsOwnership.cjs');
|
|
32
37
|
|
|
33
38
|
/** True when `p` lives under an app-owned operations root. Pure. */
|
|
34
39
|
function isAppOwnedChurn(p) {
|