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
|
@@ -1,17 +1,49 @@
|
|
|
1
1
|
// PRD 834 — machine-wide scheduler-ownership lock. Secondary instances must
|
|
2
2
|
// go scheduler-passive instead of running boot reconciliation / admin server
|
|
3
3
|
// against the owner's queue state (live incident 2026-07-31).
|
|
4
|
-
|
|
4
|
+
//
|
|
5
|
+
// HOME is stubbed to a mkdtemp dir BEFORE requiring instanceLock.cjs (mirrors
|
|
6
|
+
// scheduler-reconcile-invalid-repair.test.cjs): instanceLock.cjs pulls in
|
|
7
|
+
// auditLog.cjs, whose audit log path resolves from
|
|
8
|
+
// os.homedir() — so real ~/.claude/session-manager state must never be
|
|
9
|
+
// touched by this suite.
|
|
10
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
5
11
|
import fs from 'node:fs';
|
|
6
12
|
import os from 'node:os';
|
|
7
13
|
import path from 'node:path';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
let originalHome;
|
|
16
|
+
let tmpHome;
|
|
17
|
+
let instanceLock;
|
|
18
|
+
let auditLogPath;
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
originalHome = process.env.HOME;
|
|
22
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-instance-lock-home-'));
|
|
23
|
+
process.env.HOME = tmpHome;
|
|
24
|
+
instanceLock = require('../instanceLock.cjs');
|
|
25
|
+
({ auditLogPath } = require('../auditLog.cjs'));
|
|
26
|
+
if (!auditLogPath().startsWith(tmpHome)) {
|
|
27
|
+
throw new Error(`refusing to run: auditLogPath() (${auditLogPath()}) is not under the temp HOME (${tmpHome})`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
process.env.HOME = originalHome;
|
|
33
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Thin pass-throughs so every test body below can keep calling these as
|
|
37
|
+
// bare names, resolved lazily against the module loaded in beforeAll.
|
|
38
|
+
const acquireSchedulerOwnership = (...args) => instanceLock.acquireSchedulerOwnership(...args);
|
|
39
|
+
const releaseSchedulerOwnership = (...args) => instanceLock.releaseSchedulerOwnership(...args);
|
|
40
|
+
const pidAlive = (...args) => instanceLock.pidAlive(...args);
|
|
41
|
+
const lockPath = (...args) => instanceLock.lockPath(...args);
|
|
42
|
+
|
|
43
|
+
function readAuditEvents() {
|
|
44
|
+
if (!fs.existsSync(auditLogPath())) return [];
|
|
45
|
+
return fs.readFileSync(auditLogPath(), 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
let dir;
|
|
17
49
|
|
|
@@ -22,7 +54,9 @@ beforeEach(() => {
|
|
|
22
54
|
|
|
23
55
|
afterEach(() => {
|
|
24
56
|
delete process.env.SM_SCHEDULER_LOCK_PATH;
|
|
57
|
+
delete process.env.SM_PROC_ROOT;
|
|
25
58
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
59
|
+
if (fs.existsSync(auditLogPath())) fs.rmSync(auditLogPath(), { force: true });
|
|
26
60
|
});
|
|
27
61
|
|
|
28
62
|
describe('instanceLock (PRD 834)', () => {
|
|
@@ -84,4 +118,56 @@ describe('instanceLock (PRD 834)', () => {
|
|
|
84
118
|
expect(pidAlive(-5)).toBe(false);
|
|
85
119
|
expect(pidAlive(0)).toBe(false);
|
|
86
120
|
});
|
|
121
|
+
|
|
122
|
+
it('a legacy lock ({pid, startedAt} only, no identity) is respected while its pid is alive', () => {
|
|
123
|
+
// Mirrors the on-disk shape of every lock written before this PRD.
|
|
124
|
+
fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
|
|
125
|
+
fs.writeFileSync(lockPath(), JSON.stringify({ pid: 1, startedAt: 'x' }));
|
|
126
|
+
const res = acquireSchedulerOwnership();
|
|
127
|
+
expect(res.owner).toBe(false);
|
|
128
|
+
expect(res.holderPid).toBe(1);
|
|
129
|
+
expect(JSON.parse(fs.readFileSync(lockPath(), 'utf8')).pid).toBe(1);
|
|
130
|
+
// No identity information to compare, so no stale-lock-broken audit event.
|
|
131
|
+
expect(readAuditEvents().some((e) => e.kind === 'stale_lock_broken')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('a recorded identity that no longer matches the live process at that (recycled) pid is broken, and audited', () => {
|
|
135
|
+
// process.ppid is guaranteed alive for the lifetime of this test (the
|
|
136
|
+
// vitest worker's own parent) — stand-in for "a hard crash left a lock
|
|
137
|
+
// whose pid was later reused by an unrelated live process".
|
|
138
|
+
const recycledPid = process.ppid;
|
|
139
|
+
const procRoot = path.join(dir, 'proc');
|
|
140
|
+
fs.mkdirSync(path.join(procRoot, String(recycledPid)), { recursive: true });
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(procRoot, String(recycledPid), 'stat'),
|
|
143
|
+
`${recycledPid} (bash) S 1 ${recycledPid} ${recycledPid} 0 -1 4194304 0 0 0 0 0 0 0 0 20 0 1 0 222222 0 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0\n`,
|
|
144
|
+
);
|
|
145
|
+
fs.writeFileSync(path.join(procRoot, String(recycledPid), 'cmdline'), 'bash\0');
|
|
146
|
+
process.env.SM_PROC_ROOT = procRoot;
|
|
147
|
+
|
|
148
|
+
fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
|
|
149
|
+
fs.writeFileSync(lockPath(), JSON.stringify({
|
|
150
|
+
pid: recycledPid,
|
|
151
|
+
startedAt: 'x',
|
|
152
|
+
// The identity the crashed scheduler instance actually recorded — a
|
|
153
|
+
// different cmdline/startTicks than the process now holding this pid.
|
|
154
|
+
identity: { pid: recycledPid, startTicks: 111111, cmdline: 'claude --session-id old', complete: true },
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
const res = acquireSchedulerOwnership();
|
|
158
|
+
expect(res.owner).toBe(true);
|
|
159
|
+
expect(JSON.parse(fs.readFileSync(lockPath(), 'utf8')).pid).toBe(process.pid);
|
|
160
|
+
|
|
161
|
+
const auditEvents = readAuditEvents().filter((e) => e.kind === 'stale_lock_broken');
|
|
162
|
+
expect(auditEvents.length).toBe(1);
|
|
163
|
+
expect(auditEvents[0].recordedPid).toBe(recycledPid);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('writeLockExclusive (via acquire) records a complete identity for this process', () => {
|
|
167
|
+
acquireSchedulerOwnership();
|
|
168
|
+
const onDisk = JSON.parse(fs.readFileSync(lockPath(), 'utf8'));
|
|
169
|
+
expect(onDisk.identity).toBeTruthy();
|
|
170
|
+
expect(onDisk.identity.pid).toBe(process.pid);
|
|
171
|
+
expect(onDisk.identity.complete).toBe(true);
|
|
172
|
+
});
|
|
87
173
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jobSupervisorRecord.test.cjs — record round-trip, live listing, retention
|
|
3
|
+
* claim, and the pure classifyAdoption matrix.
|
|
4
|
+
*
|
|
5
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/jobSupervisorRecord.test.cjs
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const rec = require('../jobSupervisorRecord.cjs');
|
|
14
|
+
|
|
15
|
+
let root;
|
|
16
|
+
beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-supervisor-')); });
|
|
17
|
+
afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); });
|
|
18
|
+
|
|
19
|
+
const base = (over = {}) => ({
|
|
20
|
+
runDir: path.join(root, 'run-1'), slug: 'p1', cwd: '/x', runId: 'run-1', pid: 42, pgid: 42,
|
|
21
|
+
identity: { pid: 42, startTicks: 7, cmdline: 'claude -p', complete: true },
|
|
22
|
+
execCwd: '/wt', worktreeDir: '/wt', worktreeBranch: 'sm-job/p1', sessionId: 's',
|
|
23
|
+
startedAt: 1_000, budgetMs: 10_000, maxDurationMs: null, idleKillMs: 5_000, schedulerPid: 1, codeSha: 'abc', ...over,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('write → read round-trips every field at <runDir>/<slug>.supervisor.json', () => {
|
|
27
|
+
const input = base();
|
|
28
|
+
rec.writeSupervisorRecord(input);
|
|
29
|
+
expect(fs.existsSync(path.join(input.runDir, 'p1.supervisor.json'))).toBe(true);
|
|
30
|
+
const back = rec.readSupervisorRecord(input.runDir, 'p1');
|
|
31
|
+
const { runDir, ...persisted } = input;
|
|
32
|
+
expect(back).toMatchObject({ ...persisted, kind: 'job' });
|
|
33
|
+
expect(back.runDir).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('readSupervisorRecord returns null for absent or torn files', () => {
|
|
37
|
+
expect(rec.readSupervisorRecord(path.join(root, 'nope'), 'p1')).toBeNull();
|
|
38
|
+
fs.mkdirSync(path.join(root, 'r'));
|
|
39
|
+
fs.writeFileSync(path.join(root, 'r', 'p1.supervisor.json'), '{"pid":');
|
|
40
|
+
expect(rec.readSupervisorRecord(path.join(root, 'r'), 'p1')).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('listLiveSupervisorRecords skips exited (meta.json) and stale records', () => {
|
|
44
|
+
rec.writeSupervisorRecord(base({ slug: 'live' }));
|
|
45
|
+
rec.writeSupervisorRecord(base({ slug: 'done' }));
|
|
46
|
+
fs.writeFileSync(path.join(root, 'run-1', 'done.meta.json'), '{}');
|
|
47
|
+
const live = rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 });
|
|
48
|
+
expect(live.map((r) => r.slug)).toEqual(['live']);
|
|
49
|
+
expect(live[0].runDir).toBe(path.join(root, 'run-1'));
|
|
50
|
+
expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 1, now: Date.now() + 10_000 })).toEqual([]);
|
|
51
|
+
expect(rec.listLiveSupervisorRecords(path.join(root, 'missing'), { maxAgeMs: 1 })).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('investigation records use the .investigation.log exit line as their marker', () => {
|
|
55
|
+
const runDir = path.join(root, 'run-1');
|
|
56
|
+
rec.writeSupervisorRecord(base({ slug: 'p1.investigation', kind: 'investigation' }));
|
|
57
|
+
fs.writeFileSync(path.join(runDir, 'p1.investigation.log'), 'working\n');
|
|
58
|
+
expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 })).toHaveLength(1);
|
|
59
|
+
fs.appendFileSync(path.join(runDir, 'p1.investigation.log'), '\n[scheduler] investigation exit code=0\n');
|
|
60
|
+
expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 })).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('classifyAdoption matrix', () => {
|
|
64
|
+
const r = base();
|
|
65
|
+
const same = { pid: 42, startTicks: 7, cmdline: 'claude -p', complete: true };
|
|
66
|
+
const other = { pid: 42, startTicks: 99, cmdline: 'claude -p', complete: true };
|
|
67
|
+
const now = 2_000;
|
|
68
|
+
const c = (o) => rec.classifyAdoption(r, { identity: same, pidAlive: true, logMtimeMs: now, exitMarker: false, now, ...o });
|
|
69
|
+
expect(c({})).toBe('adopt');
|
|
70
|
+
expect(c({ pidAlive: false })).toBe('dead');
|
|
71
|
+
expect(c({ pidAlive: false, exitMarker: true })).toBe('exited');
|
|
72
|
+
expect(c({ identity: other })).toBe('foreign-pid');
|
|
73
|
+
expect(c({ identity: { pid: 42, startTicks: null, cmdline: null, complete: false } })).toBe('adopt'); // fail-closed
|
|
74
|
+
expect(c({ now: 1_000 + 10_001, logMtimeMs: 1_000 + 10_000 })).toBe('over-budget');
|
|
75
|
+
expect(c({ now: 1_000 + 5_001, logMtimeMs: 1_000 })).toBe('over-budget'); // log stalled past idleKillMs
|
|
76
|
+
expect(rec.classifyAdoption(base({ budgetMs: null, maxDurationMs: 100 }), { identity: same, pidAlive: true, logMtimeMs: 1_050, now: 1_200 })).toBe('over-budget');
|
|
77
|
+
expect(rec.classifyAdoption(base({ budgetMs: null, idleKillMs: null }), { identity: same, pidAlive: true, now: 9e9 })).toBe('adopt');
|
|
78
|
+
});
|
|
@@ -69,3 +69,14 @@ test('a precomputed cwdHolders set is forwarded through to hasLiveHolder unchang
|
|
|
69
69
|
isLive('job-d', { worktree: '/tmp/checkout-d' });
|
|
70
70
|
expect(hasLiveHolder).toHaveBeenCalledWith('/tmp/checkout-d', holders);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
test('rowPid (the record/log ladder) is consulted instead of runtime.pid alone', () => {
|
|
74
|
+
const claudePidAlive = vi.fn((pid) => pid === 777);
|
|
75
|
+
const isLive = buildJobWorktreeIsLive({
|
|
76
|
+
bootJobs: [{ slug: 'job-e', status: 'running' }],
|
|
77
|
+
claudePidAlive,
|
|
78
|
+
hasLiveHolder: () => false,
|
|
79
|
+
rowPid: () => 777,
|
|
80
|
+
});
|
|
81
|
+
expect(isLive('job-e', { worktree: '/tmp/checkout-e' })).toBe(true);
|
|
82
|
+
});
|
|
@@ -173,7 +173,7 @@ test('with neither SM_DEV nor SM_E2E set, resolves to the original admin-api.jso
|
|
|
173
173
|
delete process.env.SM_E2E;
|
|
174
174
|
delete process.env.SM_ADMIN_TOKEN_PATH;
|
|
175
175
|
try {
|
|
176
|
-
expect(resolveTokenPath()).toBe(
|
|
176
|
+
expect(resolveTokenPath()).toBe(path.join(require('../schedulerPaths.cjs').schedulerHome(), 'admin-api.json'));
|
|
177
177
|
} finally {
|
|
178
178
|
if (prevDev === undefined) delete process.env.SM_DEV; else process.env.SM_DEV = prevDev;
|
|
179
179
|
if (prevE2e === undefined) delete process.env.SM_E2E; else process.env.SM_E2E = prevE2e;
|
|
@@ -78,12 +78,17 @@ test.each(MCP_TOOL_CATALOG)('$name composeDescription joins purpose/whenToUse/wh
|
|
|
78
78
|
expect(composeDescription(entry).length).toBeGreaterThan(0);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test('project-home group is exactly the single project_home_write tool', () => {
|
|
82
|
+
expect(MCP_TOOL_CATALOG.filter((e) => e.group === 'project-home').map((e) => e.name)).toEqual(['project_home_write']);
|
|
83
|
+
});
|
|
84
|
+
|
|
81
85
|
test('MCP_RECIPES covers queue-work, unstick-needs-review, and hand-off-to-another-project', () => {
|
|
82
86
|
const ids = MCP_RECIPES.map((r) => r.id);
|
|
83
87
|
expect(ids).toContain('queue-work-via-develop');
|
|
84
88
|
expect(ids).toContain('unstick-needs-review-job');
|
|
85
89
|
expect(ids).toContain('hand-finding-to-another-project');
|
|
86
|
-
|
|
90
|
+
// the single-tool project_home_write needs no recipe; the old 4-step one is gone
|
|
91
|
+
expect(ids).not.toContain('generate-project-home');
|
|
87
92
|
for (const recipe of MCP_RECIPES) {
|
|
88
93
|
expect(recipe.title).toBeTruthy();
|
|
89
94
|
expect(Array.isArray(recipe.steps)).toBe(true);
|
|
@@ -29,7 +29,7 @@ const fsp = require('node:fs/promises');
|
|
|
29
29
|
const os = require('node:os');
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const queueStore = require('../queueStore.cjs');
|
|
32
|
-
const { activeProjectCwds } = require('
|
|
32
|
+
const { activeProjectCwds } = require('../activeSessions.cjs');
|
|
33
33
|
|
|
34
34
|
const tmpDirs = [];
|
|
35
35
|
|
|
@@ -149,7 +149,7 @@ test('a transcript cwd inside an ops root is normalized to the project root, not
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
test('normalization keeps the active-project signal rather than dropping the row', () => {
|
|
152
|
-
const { projectRootOf } = require('
|
|
152
|
+
const { projectRootOf } = require('../activeSessions.cjs');
|
|
153
153
|
expect(projectRootOf('/p/session-manager-operations/prompt-sessions')).toBe('/p');
|
|
154
154
|
expect(projectRootOf('/p/session-manager-operations')).toBe('/p');
|
|
155
155
|
expect(projectRootOf('/p/session-manager-operations/scheduler/state')).toBe('/p');
|
|
@@ -185,7 +185,7 @@ test('projectStateDir fails closed on an ops-internal cwd, naming the offending
|
|
|
185
185
|
// list at all. `worktreeMainRootOf` reverses a linked worktree's `.git` FILE
|
|
186
186
|
// (`gitdir: <main>/.git/worktrees/<name>`) back to `<main>`.
|
|
187
187
|
|
|
188
|
-
const { worktreeMainRootOf } = require('
|
|
188
|
+
const { worktreeMainRootOf } = require('../activeSessions.cjs');
|
|
189
189
|
const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('../gitWorktree.cjs');
|
|
190
190
|
|
|
191
191
|
// A worktree's MAIN tree root must live outside os.tmpdir() for these fixtures
|
|
@@ -285,13 +285,13 @@ test('worktreeMainRootOf returns null for a plain non-git directory', async () =
|
|
|
285
285
|
|
|
286
286
|
test('projectRootOf resolves a worktree cwd to the main tree root', async () => {
|
|
287
287
|
const { main, worktree } = await makeWorktreeFixture();
|
|
288
|
-
const { projectRootOf } = require('
|
|
288
|
+
const { projectRootOf } = require('../activeSessions.cjs');
|
|
289
289
|
expect(projectRootOf(worktree)).toBe(main);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
test('projectRootOf resolves a cwd nested inside a worktree\'s OWN ops tree to the main root', async () => {
|
|
293
293
|
const { main, worktree } = await makeWorktreeFixture();
|
|
294
|
-
const { projectRootOf } = require('
|
|
294
|
+
const { projectRootOf } = require('../activeSessions.cjs');
|
|
295
295
|
const nestedOps = path.join(worktree, 'session-manager-operations', 'scheduler', 'state');
|
|
296
296
|
fs.mkdirSync(nestedOps, { recursive: true });
|
|
297
297
|
expect(projectRootOf(nestedOps)).toBe(main);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdDisposition.test.cjs — pure logic behind the scheduler wave-disposition
|
|
3
|
+
* PRD: does a new PRD wave extend an Epic's existing dependsOn chain
|
|
4
|
+
* ('append', gaining a dependsOn on the chain's current terminal PRD(s)) or
|
|
5
|
+
* start an independent, parallel-eligible root ('new-head')? And, after the
|
|
6
|
+
* fact, can a human safely rewrite that choice from the Scheduler UI?
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/prdDisposition.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect } from 'vitest';
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
isIncomplete,
|
|
17
|
+
resolveChainTerminals,
|
|
18
|
+
wouldCreateCycle,
|
|
19
|
+
computeDispositionRewrite,
|
|
20
|
+
} = require('../prdDisposition.cjs');
|
|
21
|
+
|
|
22
|
+
function row(overrides) {
|
|
23
|
+
return { slug: 'x', status: 'pending', dependsOn: [], ...overrides };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────── isIncomplete
|
|
27
|
+
|
|
28
|
+
test('isIncomplete: completed is the only complete status', () => {
|
|
29
|
+
expect(isIncomplete('completed')).toBe(false);
|
|
30
|
+
for (const status of [null, undefined, 'pending', 'running', 'needs_review', 'failed', 'quarantined']) {
|
|
31
|
+
expect(isIncomplete(status)).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────── resolveChainTerminals
|
|
36
|
+
|
|
37
|
+
test('resolveChainTerminals: a lone root is its own terminal', () => {
|
|
38
|
+
const rows = [row({ slug: 'a' })];
|
|
39
|
+
expect(resolveChainTerminals(rows)).toEqual(['a']);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('resolveChainTerminals: a linear chain terminates at its tail', () => {
|
|
43
|
+
const rows = [
|
|
44
|
+
row({ slug: 'a' }),
|
|
45
|
+
row({ slug: 'b', dependsOn: ['a'] }),
|
|
46
|
+
row({ slug: 'c', dependsOn: ['b'] }),
|
|
47
|
+
];
|
|
48
|
+
expect(resolveChainTerminals(rows)).toEqual(['c']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('resolveChainTerminals: a fork yields both leaves', () => {
|
|
52
|
+
const rows = [
|
|
53
|
+
row({ slug: 'a' }),
|
|
54
|
+
row({ slug: 'b', dependsOn: ['a'] }),
|
|
55
|
+
row({ slug: 'c', dependsOn: ['a'] }),
|
|
56
|
+
];
|
|
57
|
+
expect(resolveChainTerminals(rows).sort()).toEqual(['b', 'c']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('resolveChainTerminals: two independent roots are both terminals (existing multi-head Epic)', () => {
|
|
61
|
+
const rows = [row({ slug: 'a' }), row({ slug: 'z' })];
|
|
62
|
+
expect(resolveChainTerminals(rows).sort()).toEqual(['a', 'z']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('resolveChainTerminals: a dependsOn cycle contributes no terminal', () => {
|
|
66
|
+
const rows = [
|
|
67
|
+
row({ slug: 'a', dependsOn: ['b'] }),
|
|
68
|
+
row({ slug: 'b', dependsOn: ['a'] }),
|
|
69
|
+
];
|
|
70
|
+
expect(resolveChainTerminals(rows)).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('resolveChainTerminals: empty input yields no terminals', () => {
|
|
74
|
+
expect(resolveChainTerminals([])).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─────────────────────────────────────────── wouldCreateCycle
|
|
78
|
+
|
|
79
|
+
test('wouldCreateCycle: false for a fresh, unrelated edge', () => {
|
|
80
|
+
const rows = [row({ slug: 'a' }), row({ slug: 'b' })];
|
|
81
|
+
expect(wouldCreateCycle(rows, 'b', ['a'])).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('wouldCreateCycle: true when the target already (transitively) depends on the source', () => {
|
|
85
|
+
// a depends on b; proposing b depends on a closes the loop.
|
|
86
|
+
const rows = [
|
|
87
|
+
row({ slug: 'a', dependsOn: ['b'] }),
|
|
88
|
+
row({ slug: 'b' }),
|
|
89
|
+
];
|
|
90
|
+
expect(wouldCreateCycle(rows, 'b', ['a'])).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('wouldCreateCycle: true for a direct self-reference', () => {
|
|
94
|
+
const rows = [row({ slug: 'a' })];
|
|
95
|
+
expect(wouldCreateCycle(rows, 'a', ['a'])).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('wouldCreateCycle: false through an unrelated third row', () => {
|
|
99
|
+
const rows = [
|
|
100
|
+
row({ slug: 'a' }),
|
|
101
|
+
row({ slug: 'b', dependsOn: ['a'] }),
|
|
102
|
+
row({ slug: 'c' }),
|
|
103
|
+
];
|
|
104
|
+
expect(wouldCreateCycle(rows, 'c', ['b'])).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────── computeDispositionRewrite
|
|
108
|
+
|
|
109
|
+
test('append sets the expected edges', () => {
|
|
110
|
+
const rows = [
|
|
111
|
+
row({ slug: 'wave1-tail' }),
|
|
112
|
+
row({ slug: 'wave2-root' }),
|
|
113
|
+
];
|
|
114
|
+
const result = computeDispositionRewrite({
|
|
115
|
+
slug: 'wave2-root',
|
|
116
|
+
disposition: 'append',
|
|
117
|
+
dependsOn: ['wave1-tail'],
|
|
118
|
+
rows,
|
|
119
|
+
});
|
|
120
|
+
expect(result).toEqual({ ok: true, dependsOn: ['wave1-tail'] });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('new-head sets none', () => {
|
|
124
|
+
const rows = [
|
|
125
|
+
row({ slug: 'wave1-tail' }),
|
|
126
|
+
row({ slug: 'wave2-root', dependsOn: ['wave1-tail'] }),
|
|
127
|
+
];
|
|
128
|
+
const result = computeDispositionRewrite({
|
|
129
|
+
slug: 'wave2-root',
|
|
130
|
+
disposition: 'new-head',
|
|
131
|
+
dependsOn: [],
|
|
132
|
+
rows,
|
|
133
|
+
});
|
|
134
|
+
expect(result).toEqual({ ok: true, dependsOn: [] });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('promoting a wave to a head removes exactly its root edges', () => {
|
|
138
|
+
const rows = [
|
|
139
|
+
row({ slug: 'wave1-tail' }),
|
|
140
|
+
row({ slug: 'other-head' }),
|
|
141
|
+
row({ slug: 'wave2-root', dependsOn: ['wave1-tail'] }),
|
|
142
|
+
];
|
|
143
|
+
const result = computeDispositionRewrite({
|
|
144
|
+
slug: 'wave2-root',
|
|
145
|
+
disposition: 'new-head',
|
|
146
|
+
dependsOn: [],
|
|
147
|
+
rows,
|
|
148
|
+
});
|
|
149
|
+
expect(result.ok).toBe(true);
|
|
150
|
+
expect(result.dependsOn).toEqual([]);
|
|
151
|
+
// The unrelated head's own row is untouched — this function only ever
|
|
152
|
+
// returns the target row's new dependsOn, never mutates `rows`.
|
|
153
|
+
expect(rows.find((r) => r.slug === 'other-head').dependsOn).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a change that would create a cycle is rejected', () => {
|
|
157
|
+
const rows = [
|
|
158
|
+
row({ slug: 'a', dependsOn: ['b'] }),
|
|
159
|
+
row({ slug: 'b' }),
|
|
160
|
+
];
|
|
161
|
+
const result = computeDispositionRewrite({
|
|
162
|
+
slug: 'b',
|
|
163
|
+
disposition: 'append',
|
|
164
|
+
dependsOn: ['a'],
|
|
165
|
+
rows,
|
|
166
|
+
});
|
|
167
|
+
expect(result.ok).toBe(false);
|
|
168
|
+
expect(result.error).toMatch(/cycle/i);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('running rows are untouched', () => {
|
|
172
|
+
const rows = [
|
|
173
|
+
row({ slug: 'wave1-tail' }),
|
|
174
|
+
row({ slug: 'wave2-root', status: 'running', dependsOn: ['wave1-tail'] }),
|
|
175
|
+
];
|
|
176
|
+
const result = computeDispositionRewrite({
|
|
177
|
+
slug: 'wave2-root',
|
|
178
|
+
disposition: 'new-head',
|
|
179
|
+
dependsOn: [],
|
|
180
|
+
rows,
|
|
181
|
+
});
|
|
182
|
+
expect(result.ok).toBe(false);
|
|
183
|
+
expect(result.error).toMatch(/running/i);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('completed rows are untouched', () => {
|
|
187
|
+
const rows = [
|
|
188
|
+
row({ slug: 'wave1-tail' }),
|
|
189
|
+
row({ slug: 'wave2-root', status: 'completed', dependsOn: ['wave1-tail'] }),
|
|
190
|
+
];
|
|
191
|
+
const result = computeDispositionRewrite({
|
|
192
|
+
slug: 'wave2-root',
|
|
193
|
+
disposition: 'append',
|
|
194
|
+
dependsOn: ['wave1-tail'],
|
|
195
|
+
rows,
|
|
196
|
+
});
|
|
197
|
+
expect(result.ok).toBe(false);
|
|
198
|
+
expect(result.error).toMatch(/completed/i);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('refuses to rewrite a row whose existing blocker has already completed', () => {
|
|
202
|
+
const rows = [
|
|
203
|
+
row({ slug: 'blocker', status: 'completed' }),
|
|
204
|
+
row({ slug: 'dependent', status: 'pending', dependsOn: ['blocker'] }),
|
|
205
|
+
];
|
|
206
|
+
const result = computeDispositionRewrite({
|
|
207
|
+
slug: 'dependent',
|
|
208
|
+
disposition: 'new-head',
|
|
209
|
+
dependsOn: [],
|
|
210
|
+
rows,
|
|
211
|
+
});
|
|
212
|
+
expect(result.ok).toBe(false);
|
|
213
|
+
expect(result.error).toMatch(/already completed|satisfied/i);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('unknown slug is refused, not silently ignored', () => {
|
|
217
|
+
const result = computeDispositionRewrite({
|
|
218
|
+
slug: 'ghost',
|
|
219
|
+
disposition: 'new-head',
|
|
220
|
+
dependsOn: [],
|
|
221
|
+
rows: [row({ slug: 'a' })],
|
|
222
|
+
});
|
|
223
|
+
expect(result.ok).toBe(false);
|
|
224
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// PRD: fail-closed process identity so a recycled pid can never pin
|
|
2
|
+
// instanceLock ownership or dodge killOrphanClaudePid's veto. Stat parsing is
|
|
3
|
+
// exercised against fixture files under SM_PROC_ROOT (never real /proc) so a
|
|
4
|
+
// synthetic `comm` containing parens/spaces can be asserted precisely.
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const { identity, isDifferentProcess } = require('../procIdentity.cjs');
|
|
11
|
+
|
|
12
|
+
let dir;
|
|
13
|
+
|
|
14
|
+
function writeProcEntry(root, pid, { stat, cmdline }) {
|
|
15
|
+
const entryDir = path.join(root, String(pid));
|
|
16
|
+
fs.mkdirSync(entryDir, { recursive: true });
|
|
17
|
+
if (stat !== undefined) fs.writeFileSync(path.join(entryDir, 'stat'), stat);
|
|
18
|
+
if (cmdline !== undefined) fs.writeFileSync(path.join(entryDir, 'cmdline'), cmdline);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Real /proc/<pid>/stat line, comm replaced with one containing a space and
|
|
22
|
+
// an embedded ')' — field 22 (starttime) is `124686`, at the position AFTER
|
|
23
|
+
// the LAST ')', not the first.
|
|
24
|
+
function statLine(comm, startTicks) {
|
|
25
|
+
return `12345 (${comm}) R 1 12345 12345 0 -1 4194304 118 0 0 0 0 0 0 0 32 12 1 0 ${startTicks} 18698240 440 0 0 0 0 0 0 0 0 0 0 0 17 7 0 0 0 0 0 0 0 0 0 0 0 0\n`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-proc-identity-'));
|
|
30
|
+
process.env.SM_PROC_ROOT = dir;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
delete process.env.SM_PROC_ROOT;
|
|
35
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('procIdentity.identity — /proc parsing', () => {
|
|
39
|
+
it('parses field 22 located after the LAST ")" when comm contains a paren and a space', () => {
|
|
40
|
+
writeProcEntry(dir, 4242, {
|
|
41
|
+
stat: statLine('claude (worker)', 999888),
|
|
42
|
+
cmdline: 'node\0claude\0--session-id\0abc\0',
|
|
43
|
+
});
|
|
44
|
+
const id = identity(4242);
|
|
45
|
+
expect(id.pid).toBe(4242);
|
|
46
|
+
expect(id.startTicks).toBe(999888);
|
|
47
|
+
expect(id.cmdline).toBe('node claude --session-id abc');
|
|
48
|
+
expect(id.complete).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('reads the live node process itself from real /proc (sanity check against production code path)', () => {
|
|
52
|
+
delete process.env.SM_PROC_ROOT;
|
|
53
|
+
const id = identity(process.pid);
|
|
54
|
+
expect(id.pid).toBe(process.pid);
|
|
55
|
+
expect(typeof id.startTicks).toBe('number');
|
|
56
|
+
expect(id.startTicks).toBeGreaterThan(0);
|
|
57
|
+
expect(typeof id.cmdline).toBe('string');
|
|
58
|
+
expect(id.cmdline.length).toBeGreaterThan(0);
|
|
59
|
+
expect(id.complete).toBe(true);
|
|
60
|
+
process.env.SM_PROC_ROOT = dir; // restore for afterEach
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('tri-state: missing stat file → incomplete (startTicks null) even though cmdline is readable', () => {
|
|
64
|
+
writeProcEntry(dir, 5001, { cmdline: 'node\0claude\0' });
|
|
65
|
+
const id = identity(5001);
|
|
66
|
+
expect(id.startTicks).toBeNull();
|
|
67
|
+
expect(id.cmdline).toBe('node claude');
|
|
68
|
+
expect(id.complete).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('tri-state: missing cmdline file → incomplete (cmdline null) even though stat is readable', () => {
|
|
72
|
+
writeProcEntry(dir, 5002, { stat: statLine('claude', 111) });
|
|
73
|
+
const id = identity(5002);
|
|
74
|
+
expect(id.startTicks).toBe(111);
|
|
75
|
+
expect(id.cmdline).toBeNull();
|
|
76
|
+
expect(id.complete).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('tri-state: neither file exists (pid never existed / already reaped) → incomplete, never throws', () => {
|
|
80
|
+
const id = identity(999999);
|
|
81
|
+
expect(id.startTicks).toBeNull();
|
|
82
|
+
expect(id.cmdline).toBeNull();
|
|
83
|
+
expect(id.complete).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects a non-positive-integer pid without touching the filesystem', () => {
|
|
87
|
+
expect(identity(-1).complete).toBe(false);
|
|
88
|
+
expect(identity(0).complete).toBe(false);
|
|
89
|
+
expect(identity(NaN).complete).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('procIdentity.isDifferentProcess — fail-closed decision', () => {
|
|
94
|
+
const complete = (over = {}) => ({ pid: 100, startTicks: 555, cmdline: 'node claude', complete: true, ...over });
|
|
95
|
+
|
|
96
|
+
it('true only when both sides are complete AND a field differs', () => {
|
|
97
|
+
expect(isDifferentProcess(complete(), complete({ startTicks: 999 }))).toBe(true);
|
|
98
|
+
expect(isDifferentProcess(complete(), complete({ cmdline: 'node other' }))).toBe(true);
|
|
99
|
+
expect(isDifferentProcess(complete(), complete())).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('an incomplete recorded or live side never reads as different (fail closed)', () => {
|
|
103
|
+
const incomplete = { pid: 100, startTicks: null, cmdline: null, complete: false };
|
|
104
|
+
expect(isDifferentProcess(incomplete, complete())).toBe(false);
|
|
105
|
+
expect(isDifferentProcess(complete(), incomplete)).toBe(false);
|
|
106
|
+
expect(isDifferentProcess(incomplete, incomplete)).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('a null/undefined probe (unreadable /proc, process gone) never reads as different', () => {
|
|
110
|
+
expect(isDifferentProcess(complete(), null)).toBe(false);
|
|
111
|
+
expect(isDifferentProcess(null, complete())).toBe(false);
|
|
112
|
+
expect(isDifferentProcess(undefined, undefined)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('a legacy record with no identity fields at all never reads as different', () => {
|
|
116
|
+
const legacy = { pid: 100 }; // e.g. an old {pid, startedAt}-only lock body
|
|
117
|
+
expect(isDifferentProcess(legacy, complete())).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|