claude-code-session-manager 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -53
- package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/Permissions-m0KAnccF.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/Settings-BHSGGFEc.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
- package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +14 -17
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +35 -71
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +4 -4
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +65 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +12 -7
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +268 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +6 -1
- package/src/main/index.cjs +152 -50
- package/src/main/ipcSchemas.cjs +53 -17
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +94 -24
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +332 -0
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +136 -19
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +33 -78
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +214 -45
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +27 -8
- package/src/main/lib/queueStore.cjs +63 -38
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +10 -2
- package/src/main/lib/scheduleJobTransitions.cjs +60 -0
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +206 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +2841 -482
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/usage.cjs +140 -58
- package/src/preload/api.d.ts +110 -65
- package/src/preload/index.cjs +12 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
- package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
- package/scripts/render-project-pages.cjs +0 -70
- package/scripts/validate-project-pages-summary.cjs +0 -62
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bootSelfHeal.cjs — the epic-index self-heal scan, split out of
|
|
5
|
+
* app.whenReady() so it can run off the boot critical path.
|
|
6
|
+
*
|
|
7
|
+
* A project whose active-index.json is MISSING or fails to parse has lost
|
|
8
|
+
* every open Epic's status unless the per-Epic status mirror
|
|
9
|
+
* (epicStatusMirror.cjs) can rebuild it. `allProjectCwds()` is a synchronous
|
|
10
|
+
* ~270ms directory scan; looping it straight through every project inside
|
|
11
|
+
* app.whenReady()'s synchronous body blocked the event loop long enough that
|
|
12
|
+
* the renderer's first IPC round trips (schedule.state, billing.fetch,
|
|
13
|
+
* teams.list) hit their 5s deadlines before any handler could dispatch.
|
|
14
|
+
*
|
|
15
|
+
* `scheduleBootSelfHeal` defers `runEpicIndexSelfHeal` until the main window
|
|
16
|
+
* has painted (`did-finish-load`), with an unconditional `setImmediate`
|
|
17
|
+
* fallback for the window-destroyed-before-load and no-window (headless)
|
|
18
|
+
* cases, so the scan still runs exactly once per boot either way. It yields
|
|
19
|
+
* to the event loop between projects so a machine with thousands of
|
|
20
|
+
* projects can't stall it for a second straight.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
async function runEpicIndexSelfHeal(deps) {
|
|
24
|
+
const { fs, allProjectCwds, promptSessionsActiveIndexPath, rebuildActiveIndex, logs } = deps;
|
|
25
|
+
try {
|
|
26
|
+
for (const projectCwd of allProjectCwds()) {
|
|
27
|
+
const indexPath = promptSessionsActiveIndexPath(projectCwd);
|
|
28
|
+
let unclean = false;
|
|
29
|
+
let parseError = null;
|
|
30
|
+
if (!fs.existsSync(indexPath)) {
|
|
31
|
+
unclean = true;
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
unclean = true;
|
|
37
|
+
parseError = e.message;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (unclean) {
|
|
41
|
+
try {
|
|
42
|
+
const { rows, skipped } = rebuildActiveIndex(projectCwd);
|
|
43
|
+
logs.writeLine({
|
|
44
|
+
scope: 'active-index-rebuild',
|
|
45
|
+
level: 'info',
|
|
46
|
+
message: 'rebuilt active-index.json from status mirrors',
|
|
47
|
+
meta: { cwd: projectCwd, parseError, restored: rows.length, skipped: skipped.length },
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'rebuild failed', meta: { cwd: projectCwd, error: e?.message } });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Yield between projects so a multi-thousand-project machine can't
|
|
54
|
+
// stall the event loop for a second at a time.
|
|
55
|
+
await new Promise((resolve) => { setImmediate(resolve); });
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'boot sweep failed', meta: { error: e?.message } });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scheduleBootSelfHeal(mainWindow, deps) {
|
|
63
|
+
let ran = false;
|
|
64
|
+
const runOnce = () => {
|
|
65
|
+
if (ran) return;
|
|
66
|
+
ran = true;
|
|
67
|
+
runEpicIndexSelfHeal(deps);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const webContents = mainWindow && typeof mainWindow.isDestroyed === 'function' && !mainWindow.isDestroyed()
|
|
71
|
+
? mainWindow.webContents
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
if (webContents && !webContents.isDestroyed()) {
|
|
75
|
+
webContents.once('did-finish-load', runOnce);
|
|
76
|
+
// Edge case: the window can be destroyed before it ever finishes
|
|
77
|
+
// loading (closed during boot, crashed renderer). did-finish-load would
|
|
78
|
+
// then never fire and the self-heal would silently never run — fall
|
|
79
|
+
// back to an unconditional setImmediate so it still runs exactly once.
|
|
80
|
+
webContents.once('destroyed', () => setImmediate(runOnce));
|
|
81
|
+
} else {
|
|
82
|
+
// Headless / no-visible-window boot — nothing will ever emit
|
|
83
|
+
// did-finish-load, so run on the very next event-loop turn instead.
|
|
84
|
+
setImmediate(runOnce);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { scheduleBootSelfHeal, runEpicIndexSelfHeal };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* buildIdentity.cjs — answers "which code produced this run" for both a
|
|
5
|
+
* published npx install (no .git shipped) and a dev checkout / job worktree
|
|
6
|
+
* (a real .git, but never the source of truth once a build has been baked).
|
|
7
|
+
*
|
|
8
|
+
* Resolution order, memoized after first call:
|
|
9
|
+
* 1. src/main/build-info.json (written by scripts/write-build-info.cjs at
|
|
10
|
+
* `prepack`/`prepublishOnly` time, from the PUBLISHING checkout) — the
|
|
11
|
+
* only artifact that is honest in production, since the npx cache the
|
|
12
|
+
* app actually runs from ships no .git directory at all.
|
|
13
|
+
* 2. `git rev-parse` against the package root, but ONLY when a .git
|
|
14
|
+
* directory exists AT that exact root — never git's own upward walk.
|
|
15
|
+
* Without this guard, a relocated/nested install whose npm cache
|
|
16
|
+
* happens to sit under an ancestor .git would silently stamp a FOREIGN
|
|
17
|
+
* sha (git -C only sets the starting point; it still searches upward
|
|
18
|
+
* for the nearest .git by default). Checking existence at the package
|
|
19
|
+
* root ourselves, and only then invoking git, means the first (and
|
|
20
|
+
* only) .git git can find IS the one we already verified.
|
|
21
|
+
* 3. null.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { execFileSync } = require('node:child_process');
|
|
27
|
+
const { resolveInstallChannel } = require('./machineProfile.cjs');
|
|
28
|
+
|
|
29
|
+
// src/main/lib -> src/main -> src -> package root (repo root in dev, or the
|
|
30
|
+
// npx-cached package dir in production).
|
|
31
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
32
|
+
|
|
33
|
+
function loadBuildInfoJson(packageRoot) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(packageRoot, 'src', 'main', 'build-info.json'), 'utf8'));
|
|
36
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolvePackageVersion(packageRoot) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
45
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveGitCodeSha(packageRoot) {
|
|
52
|
+
if (!fs.existsSync(path.join(packageRoot, '.git'))) return null;
|
|
53
|
+
try {
|
|
54
|
+
return execFileSync('git', ['-C', packageRoot, 'rev-parse', '--short', 'HEAD'], {
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
58
|
+
}).trim();
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function computeBuildIdentity(packageRoot) {
|
|
65
|
+
const buildInfo = loadBuildInfoJson(packageRoot);
|
|
66
|
+
const version = (buildInfo && typeof buildInfo.version === 'string') ? buildInfo.version : resolvePackageVersion(packageRoot);
|
|
67
|
+
const codeSha = buildInfo
|
|
68
|
+
? ((typeof buildInfo.gitShortSha === 'string') ? buildInfo.gitShortSha : null)
|
|
69
|
+
: resolveGitCodeSha(packageRoot);
|
|
70
|
+
const builtAt = (buildInfo && typeof buildInfo.builtAt === 'string') ? buildInfo.builtAt : null;
|
|
71
|
+
return {
|
|
72
|
+
version,
|
|
73
|
+
codeSha,
|
|
74
|
+
builtAt,
|
|
75
|
+
appPath: packageRoot,
|
|
76
|
+
installChannel: resolveInstallChannel({ appPath: packageRoot, devFlag: !!process.env.SM_DEV }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Keyed by resolved root: a real process only ever resolves ONE root (this
|
|
81
|
+
// module's own package root), so in production/dev this behaves as a plain
|
|
82
|
+
// singleton memo — the Map only grows past size 1 under test, where each
|
|
83
|
+
// case injects a distinct fixture packageRoot.
|
|
84
|
+
const identityCache = new Map();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {object} [opts]
|
|
88
|
+
* @param {string|null} [opts.bootedAt] - the caller's OWN already-captured
|
|
89
|
+
* process-boot timestamp (e.g. scheduler.cjs's SCHEDULER_BOOTED_AT). Never
|
|
90
|
+
* re-captured here — a memoized resolver that stamped its own "now" would
|
|
91
|
+
* drift from what SCHEDULER_BOOTED_AT was introduced to expose (PRD 812:
|
|
92
|
+
* process staleness relative to on-disk source).
|
|
93
|
+
* @param {string} [opts.packageRoot] - test-only override; production/dev
|
|
94
|
+
* callers never pass this, so it always resolves to this module's own
|
|
95
|
+
* package root.
|
|
96
|
+
*/
|
|
97
|
+
function resolveBuildIdentity(opts = {}) {
|
|
98
|
+
const packageRoot = opts.packageRoot || PACKAGE_ROOT;
|
|
99
|
+
if (!identityCache.has(packageRoot)) identityCache.set(packageRoot, computeBuildIdentity(packageRoot));
|
|
100
|
+
return { ...identityCache.get(packageRoot), bootedAt: opts.bootedAt ?? null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* readInstalledBuildInfo() → the build-info.json currently ON DISK at this
|
|
105
|
+
* package root (fresh read, never the boot-time memo), or null. Compared with
|
|
106
|
+
* the running identity's codeSha to detect that an install/update already
|
|
107
|
+
* replaced the files under a live process.
|
|
108
|
+
*/
|
|
109
|
+
function readInstalledBuildInfo(packageRoot = PACKAGE_ROOT) {
|
|
110
|
+
return loadBuildInfoJson(packageRoot);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { resolveBuildIdentity, readInstalledBuildInfo };
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const { spawn } = require('node:child_process');
|
|
19
|
-
const { resolveClaudeBin } = require('./claudeBin.cjs');
|
|
19
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
|
|
20
20
|
const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
|
|
21
21
|
|
|
22
22
|
const CLASSIFY_TIMEOUT_MS = 30_000;
|
|
@@ -78,7 +78,8 @@ function classifyPromptTicket(text) {
|
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
// stdin closed — `claude -p` otherwise blocks waiting for piped stdin.
|
|
81
|
-
|
|
81
|
+
const target = claudeSpawnTarget('aux', 'classify', claudeBin);
|
|
82
|
+
child = spawnImpl(target.command, args, { env: childEnv, stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
82
83
|
} catch {
|
|
83
84
|
finish('inline');
|
|
84
85
|
return;
|
|
@@ -39,6 +39,40 @@ function resolveClaudeBin() {
|
|
|
39
39
|
return cached;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// ---------- process naming ----------
|
|
43
|
+
//
|
|
44
|
+
// Every `claude` spawn goes through claudeSpawnTarget so GNOME System Monitor
|
|
45
|
+
// shows a distinct comm per role (sm-claude-job / -chat / -aux) and a labelled
|
|
46
|
+
// argv0. See lib/procName.cjs for the mechanics. Invariants:
|
|
47
|
+
// - SM_CLAUDE_BIN set → aliasing SKIPPED, raw value used verbatim (tests point
|
|
48
|
+
// it at self-locating stubs; it must stay ONE binary, no path splitting).
|
|
49
|
+
// - aliasBinFor fails open (returns realBin) → no argv0 is set either, so the
|
|
50
|
+
// fallback is byte-identical to the pre-naming spawn.
|
|
51
|
+
// - Every alias and argv0 label contains the word `claude`, which the four
|
|
52
|
+
// /\bclaude\b/ cmdline gates (killOrphanClaudePid, claudePidAlive,
|
|
53
|
+
// findLiveProcessForJob x2) depend on.
|
|
54
|
+
const ROLE_ALIASES = { job: 'sm-claude-job', chat: 'sm-claude-chat', aux: 'sm-claude-aux' };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {'job'|'chat'|'aux'} role
|
|
58
|
+
* @param {string} [detail] argv0 label detail (job slug / epic session id)
|
|
59
|
+
* @param {string} [realBin] defaults to resolveClaudeBin()
|
|
60
|
+
* @returns {{ command: string, argv0?: string }} spread `argv0` into spawn options
|
|
61
|
+
*/
|
|
62
|
+
function claudeSpawnTarget(role, detail, realBin = resolveClaudeBin()) {
|
|
63
|
+
if (process.env.SM_CLAUDE_BIN) return { command: realBin };
|
|
64
|
+
const alias = ROLE_ALIASES[role];
|
|
65
|
+
if (!alias) return { command: realBin };
|
|
66
|
+
try {
|
|
67
|
+
const { aliasBinFor, smArgv0 } = require('./procName.cjs');
|
|
68
|
+
const command = aliasBinFor(realBin, alias);
|
|
69
|
+
if (command === realBin) return { command: realBin };
|
|
70
|
+
return { command, argv0: smArgv0(role, detail) };
|
|
71
|
+
} catch {
|
|
72
|
+
return { command: realBin };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
// ---------- version probe ----------
|
|
43
77
|
//
|
|
44
78
|
// `claude --version`, cached. The scheduler's launch circuit breaker
|
|
@@ -75,7 +109,8 @@ function probeClaudeVersion({ now = Date.now(), execFileImpl } = {}) {
|
|
|
75
109
|
// it, and the binary must never be handed the caller's working tree —
|
|
76
110
|
// a stub `claude` (tests point SM_CLAUDE_BIN at one) that writes
|
|
77
111
|
// markers or commits into process.cwd() once did so in the real repo.
|
|
78
|
-
|
|
112
|
+
const target = claudeSpawnTarget('aux', 'version', bin);
|
|
113
|
+
execFile(target.command, ['--version'], { timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true, cwd: os.tmpdir(), ...(target.argv0 ? { argv0: target.argv0 } : {}) }, (err, stdout) => {
|
|
79
114
|
if (err) return done(null);
|
|
80
115
|
const m = /(\d+\.\d+\.\d+[^\s]*)/.exec(String(stdout || ''));
|
|
81
116
|
done(m ? m[1] : (String(stdout || '').trim() || null));
|
|
@@ -92,4 +127,4 @@ function resetClaudeVersionCache() {
|
|
|
92
127
|
versionCache = { at: 0, value: null, inflight: null };
|
|
93
128
|
}
|
|
94
129
|
|
|
95
|
-
module.exports = { resolveClaudeBin, probeClaudeVersion, resetClaudeVersionCache };
|
|
130
|
+
module.exports = { resolveClaudeBin, claudeSpawnTarget, probeClaudeVersion, resetClaudeVersionCache };
|
|
@@ -35,8 +35,34 @@ function pathWithUserBins() {
|
|
|
35
35
|
return base ? `${base}:${userBinDirs().join(':')}` : userBinDirs().join(':');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* SM_PROC_ROLE attributes a child (and, by inheritance, every MCP server the
|
|
40
|
+
* `claude` child spawns — we cannot rename those) via /proc/<pid>/environ.
|
|
41
|
+
* Read by scripts/sm-ps.cjs. NOT SM_PROC_ROOT (procIdentity.cjs's /proc-root
|
|
42
|
+
* test override) — unrelated variable.
|
|
43
|
+
*/
|
|
44
|
+
const PROC_ROLE_ENV = 'SM_PROC_ROLE';
|
|
45
|
+
|
|
46
|
+
/** Explicit `SM_PROC_ROLE` in `extra` wins; else inferred from the attribution
|
|
47
|
+
* vars each spawn site already sets (job slug → job, tab id → shell, chat
|
|
48
|
+
* session → chat); else `aux`. Inference keeps role stamping in ONE place. */
|
|
49
|
+
function inferProcRole(extra) {
|
|
50
|
+
if (extra[PROC_ROLE_ENV]) return String(extra[PROC_ROLE_ENV]);
|
|
51
|
+
if (extra.SM_SCHEDULER_JOB_SLUG) return 'job';
|
|
52
|
+
if (extra.SESSION_MANAGER_TAB_ID) return 'shell';
|
|
53
|
+
if (extra.SM_CHAT_SESSION_ID) return 'chat';
|
|
54
|
+
return 'aux';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Stamp SM_PROC_ROLE onto a raw env object (for sites that deliberately do not
|
|
58
|
+
* use cleanChildEnv's secret stripping, e.g. runClaudeP/docEdit). Returns a copy. */
|
|
59
|
+
function withProcRole(env, role) {
|
|
60
|
+
return { ...env, [PROC_ROLE_ENV]: role };
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
function cleanChildEnv(extra = {}) {
|
|
39
64
|
const env = { ...process.env, ...extra };
|
|
65
|
+
env[PROC_ROLE_ENV] = inferProcRole(extra);
|
|
40
66
|
for (const k of Object.keys(env)) {
|
|
41
67
|
if (
|
|
42
68
|
k === 'CLAUDE_EFFORT' ||
|
|
@@ -52,4 +78,4 @@ function cleanChildEnv(extra = {}) {
|
|
|
52
78
|
return env;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
module.exports = { cleanChildEnv, userBinDirs, pathWithUserBins };
|
|
81
|
+
module.exports = { cleanChildEnv, withProcRole, inferProcRole, PROC_ROLE_ENV, userBinDirs, pathWithUserBins };
|
|
@@ -6,7 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const os = require('node:os');
|
|
7
7
|
const { spawn, execFileSync } = require('node:child_process');
|
|
8
8
|
const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
|
|
9
|
-
const { resolveClaudeBin } = require('./claudeBin.cjs');
|
|
9
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
|
|
10
10
|
|
|
11
11
|
const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
12
12
|
|
|
@@ -188,7 +188,9 @@ function tryCliFallback() {
|
|
|
188
188
|
try {
|
|
189
189
|
// Resolve the absolute binary + inject Homebrew/user bins so this works
|
|
190
190
|
// even when Electron launched from Finder/Dock with a stripped PATH (mac).
|
|
191
|
-
|
|
191
|
+
const target = claudeSpawnTarget('aux', 'credcheck');
|
|
192
|
+
child = spawn(target.command, ['--version'], {
|
|
193
|
+
...(target.argv0 ? { argv0: target.argv0 } : {}),
|
|
192
194
|
stdio: 'ignore',
|
|
193
195
|
env: cleanChildEnv({ PATH: pathWithUserBins() }),
|
|
194
196
|
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cwdClassify.cjs — THE one answer to "what project is this path?".
|
|
5
|
+
*
|
|
6
|
+
* activeSessions.projectRootOf, ephemeralCwd.isEphemeralCwd and
|
|
7
|
+
* opsOwnership.resolveProjectRoot each used to re-derive that answer and each
|
|
8
|
+
* failed OPEN: when a worktree's `.git` FILE survives but its admin-dir
|
|
9
|
+
* back-reference is gone, worktreeMainRootOf returned null, projectRootOf
|
|
10
|
+
* handed the worktree path back unchanged, isLinkedWorktreeRoot said false, and
|
|
11
|
+
* resolveOpsRoot built `<worktree>/session-manager-operations` — the shape
|
|
12
|
+
* behind the lost-Epics incidents. classifyCwd reports BOTH facts (which
|
|
13
|
+
* project, and how sure we are) so a caller can refuse WRITES for the
|
|
14
|
+
* unprovable case (`unknown`) while dispatch and reads carry on.
|
|
15
|
+
*
|
|
16
|
+
* Kinds:
|
|
17
|
+
* worktree — `.git` is a FILE whose gitdir sits under `<main>/.git/worktrees/`
|
|
18
|
+
* AND the main tree's admin dir back-references it (round trip).
|
|
19
|
+
* projectRoot = <main>.
|
|
20
|
+
* project — `.git` is a directory, OR a FILE whose gitdir is NOT under a
|
|
21
|
+
* worktrees/ dir (separate-git-dir / submodule), OR no `.git` at
|
|
22
|
+
* all. projectRoot = the dir holding `.git` (else the cwd).
|
|
23
|
+
* ephemeral — os.tmpdir() itself, or inside a managed job/epic worktree root,
|
|
24
|
+
* and not provably a live worktree.
|
|
25
|
+
* unknown — a `.git` FILE we cannot resolve (garbled, unreadable, worktrees
|
|
26
|
+
* gitdir with a missing/mismatched back-reference). projectRoot is
|
|
27
|
+
* the (ops-truncated) cwd unchanged; ops WRITES must be refused.
|
|
28
|
+
*
|
|
29
|
+
* Two ops-root facts are returned and deliberately NOT unified:
|
|
30
|
+
* outermostOpsRoot — first `session-manager-operations` segment (indexOf).
|
|
31
|
+
* activeSessions truncates here: a stray nested ops root is itself the bug.
|
|
32
|
+
* innermostOpsRoot — last segment (lastIndexOf). opsOwnership.assertOpsWrite
|
|
33
|
+
* derives the project a write targets from here.
|
|
34
|
+
* Both are null when the path is not inside an ops root.
|
|
35
|
+
*
|
|
36
|
+
* Pure fs, synchronous, never throws. Must stay light — required by
|
|
37
|
+
* scripts/hooks/guard-inline-implementation.cjs (via nearestGitEntry) and
|
|
38
|
+
* activeSessions.cjs; no gitWorktree/config requires.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const os = require('node:os');
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
const schedulerPaths = require('./schedulerPaths.cjs');
|
|
45
|
+
|
|
46
|
+
const OPS_DIRNAME = 'session-manager-operations';
|
|
47
|
+
const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
|
|
48
|
+
// Bound on the ancestor walk — past any real fs depth, guarantees termination.
|
|
49
|
+
const MAX_WALK = 40;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* nearestGitEntry(cwd) → { dir, isFile } for the nearest ancestor-or-self that
|
|
53
|
+
* has a `.git` entry (`isFile` true = linked worktree / submodule pointer file),
|
|
54
|
+
* or null. The tiny pure helper for callers that must not load git tooling.
|
|
55
|
+
*/
|
|
56
|
+
function nearestGitEntry(cwd) {
|
|
57
|
+
let dir = cwd;
|
|
58
|
+
for (let i = 0; i < MAX_WALK; i++) {
|
|
59
|
+
let stat = null;
|
|
60
|
+
try { stat = fs.statSync(path.join(dir, '.git')); } catch { /* none here */ }
|
|
61
|
+
if (stat) return { dir, isFile: stat.isFile() };
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir) return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify the git shape at/above `start`. O(depth) stats + ≤2 small reads.
|
|
71
|
+
* → { shape: 'none' | 'gitdir' | 'worktree' | 'separate' | 'unknown', dir, mainRoot, reason }
|
|
72
|
+
*/
|
|
73
|
+
function inspectGit(start) {
|
|
74
|
+
const entry = nearestGitEntry(start);
|
|
75
|
+
if (!entry) return { shape: 'none', dir: null, mainRoot: null, reason: 'no .git found' };
|
|
76
|
+
if (!entry.isFile) return { shape: 'gitdir', dir: entry.dir, mainRoot: null, reason: '.git is a directory' };
|
|
77
|
+
|
|
78
|
+
const gitPath = path.join(entry.dir, '.git');
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = fs.readFileSync(gitPath, 'utf8');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `unreadable .git file: ${e?.code || e?.message}` };
|
|
84
|
+
}
|
|
85
|
+
const firstLine = body.split('\n', 1)[0].trim();
|
|
86
|
+
if (!firstLine.startsWith('gitdir:')) {
|
|
87
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has no "gitdir:" line' };
|
|
88
|
+
}
|
|
89
|
+
const gitdirRaw = firstLine.slice('gitdir:'.length).trim();
|
|
90
|
+
if (!gitdirRaw) {
|
|
91
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has an empty gitdir' };
|
|
92
|
+
}
|
|
93
|
+
// Submodules point at a RELATIVE gitdir; resolve against the file's own dir.
|
|
94
|
+
const gitdir = path.resolve(entry.dir, gitdirRaw);
|
|
95
|
+
const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
|
|
96
|
+
if (markerIdx === -1) {
|
|
97
|
+
// Not a linked worktree: separate-git-dir / submodule. Its own project.
|
|
98
|
+
return { shape: 'separate', dir: entry.dir, mainRoot: null, reason: 'gitdir is not under .git/worktrees/ (separate-git-dir or submodule)' };
|
|
99
|
+
}
|
|
100
|
+
if (markerIdx <= 0) {
|
|
101
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir worktrees marker has no main root above it' };
|
|
102
|
+
}
|
|
103
|
+
const candidateMain = gitdir.slice(0, markerIdx);
|
|
104
|
+
const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
|
|
105
|
+
if (!worktreeName) {
|
|
106
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir names no worktree' };
|
|
107
|
+
}
|
|
108
|
+
// Round-trip: the .git file is plain text anything could have written, so it
|
|
109
|
+
// must not redirect callers to an arbitrary path. The main tree's own admin
|
|
110
|
+
// dir must point back at THIS .git file.
|
|
111
|
+
const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
|
|
112
|
+
let backRef;
|
|
113
|
+
try {
|
|
114
|
+
backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
|
|
115
|
+
} catch {
|
|
116
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference missing (${adminGitdirFile})` };
|
|
117
|
+
}
|
|
118
|
+
if (path.resolve(backRef) !== path.resolve(gitPath)) {
|
|
119
|
+
return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference points elsewhere (${adminGitdirFile})` };
|
|
120
|
+
}
|
|
121
|
+
return { shape: 'worktree', dir: entry.dir, mainRoot: candidateMain, reason: 'linked worktree, back-reference verified' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
|
|
126
|
+
* verified linked worktree, the ancestor itself when `.git` is a directory,
|
|
127
|
+
* else null (no git, separate-git-dir, or anything unprovable).
|
|
128
|
+
*/
|
|
129
|
+
function worktreeMainRootOf(cwd) {
|
|
130
|
+
const g = inspectGit(cwd);
|
|
131
|
+
if (g.shape === 'worktree') return g.mainRoot;
|
|
132
|
+
if (g.shape === 'gitdir') return g.dir;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isExactlyTmpdir(absCwd) {
|
|
137
|
+
return absCwd === path.resolve(os.tmpdir());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isUnderManagedWorktreeRoot(absCwd) {
|
|
141
|
+
return ['job', 'epic'].map((k) => path.resolve(schedulerPaths.worktreeRoot(k))).some((root) => {
|
|
142
|
+
const rel = path.relative(root, absCwd);
|
|
143
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* classifyCwd(cwd) → { kind, projectRoot, outermostOpsRoot, innermostOpsRoot, reason }
|
|
149
|
+
* See the file header. A non-string/empty cwd is `unknown` with a null root.
|
|
150
|
+
*/
|
|
151
|
+
function classifyCwd(cwd) {
|
|
152
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
153
|
+
return { kind: 'unknown', projectRoot: null, outermostOpsRoot: null, innermostOpsRoot: null, reason: 'cwd is not a non-empty string' };
|
|
154
|
+
}
|
|
155
|
+
const parts = cwd.split(path.sep);
|
|
156
|
+
const first = parts.indexOf(OPS_DIRNAME);
|
|
157
|
+
const last = parts.lastIndexOf(OPS_DIRNAME);
|
|
158
|
+
const opsAt = (i) => (i >= 0 ? (parts.slice(0, i + 1).join(path.sep) || path.sep) : null);
|
|
159
|
+
const outermostOpsRoot = opsAt(first);
|
|
160
|
+
const innermostOpsRoot = opsAt(last);
|
|
161
|
+
// Outermost truncation (the activeSessions law). i === 0 is a relative
|
|
162
|
+
// fragment, never truncated.
|
|
163
|
+
const truncated = first > 0 ? (parts.slice(0, first).join(path.sep) || path.sep) : cwd;
|
|
164
|
+
|
|
165
|
+
const g = inspectGit(truncated);
|
|
166
|
+
const facts = { outermostOpsRoot, innermostOpsRoot };
|
|
167
|
+
if (g.shape === 'worktree') {
|
|
168
|
+
return { kind: 'worktree', projectRoot: g.mainRoot, ...facts, reason: g.reason };
|
|
169
|
+
}
|
|
170
|
+
if (path.isAbsolute(cwd)) {
|
|
171
|
+
const abs = path.resolve(cwd);
|
|
172
|
+
if (isExactlyTmpdir(abs) || isUnderManagedWorktreeRoot(abs)) {
|
|
173
|
+
return { kind: 'ephemeral', projectRoot: truncated, ...facts, reason: 'tmpdir or managed job/epic worktree root' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (g.shape === 'unknown') {
|
|
177
|
+
return { kind: 'unknown', projectRoot: truncated, ...facts, reason: g.reason };
|
|
178
|
+
}
|
|
179
|
+
if (g.shape === 'none') {
|
|
180
|
+
return { kind: 'project', projectRoot: truncated, ...facts, reason: g.reason };
|
|
181
|
+
}
|
|
182
|
+
return { kind: 'project', projectRoot: g.dir, ...facts, reason: g.reason };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { classifyCwd, nearestGitEntry, worktreeMainRootOf, OPS_DIRNAME };
|