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,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* projectPages.test.cjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* read as having no output at all — and the shipped-default fallback added
|
|
6
|
-
* by the "project-home-hosted-html-spec" PRD: a project with no generated
|
|
7
|
-
* output gets the build-time default home.html (isDefault: true) instead of
|
|
8
|
-
* the bare `{ output: null }` empty-state signal.
|
|
2
|
+
* projectPages.test.cjs — projectPages.cjs's read-only surface: get() of the
|
|
3
|
+
* single project-pages/home.html ({html|null, mtimeMs}) plus the refcounted
|
|
4
|
+
* watcher that pushes `project-pages:changed`.
|
|
9
5
|
*
|
|
10
6
|
* Run: timeout 120 npx vitest run src/main/__tests__/projectPages.test.cjs
|
|
11
7
|
*/
|
|
@@ -20,8 +16,6 @@ const path = require('node:path');
|
|
|
20
16
|
const config = require('../config.cjs');
|
|
21
17
|
const {
|
|
22
18
|
get,
|
|
23
|
-
DEFAULT_HOME_PATH,
|
|
24
|
-
readDefaultHomeHtml,
|
|
25
19
|
attachWindow,
|
|
26
20
|
watchOutput,
|
|
27
21
|
unwatchOutput,
|
|
@@ -31,6 +25,7 @@ const {
|
|
|
31
25
|
|
|
32
26
|
const tmpDirs = [];
|
|
33
27
|
afterEach(async () => {
|
|
28
|
+
closeAllOutputWatchers();
|
|
34
29
|
while (tmpDirs.length) {
|
|
35
30
|
const d = tmpDirs.pop();
|
|
36
31
|
await fsp.rm(d, { recursive: true, force: true });
|
|
@@ -44,179 +39,98 @@ async function mkTmpCwd() {
|
|
|
44
39
|
return dir;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
function
|
|
48
|
-
return path.join(cwd, 'session-manager-operations', 'project-pages'
|
|
42
|
+
function pagesDir(cwd) {
|
|
43
|
+
return path.join(cwd, 'session-manager-operations', 'project-pages');
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
fs.
|
|
54
|
-
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest));
|
|
55
|
-
for (const [lens, html] of Object.entries(lenses)) {
|
|
56
|
-
fs.writeFileSync(path.join(dir, `${lens}.html`), html);
|
|
57
|
-
}
|
|
46
|
+
function writeHome(cwd, html) {
|
|
47
|
+
fs.mkdirSync(pagesDir(cwd), { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(pagesDir(cwd), 'home.html'), html);
|
|
58
49
|
}
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
feature: 'FEATURE',
|
|
71
|
-
architecture: 'ARCHITECTURE',
|
|
72
|
-
brief: 'BRIEF',
|
|
73
|
-
});
|
|
74
|
-
const result = await get({ cwd });
|
|
75
|
-
expect(result.output).not.toBeNull();
|
|
76
|
-
expect(result.output.home).toBe('HOME');
|
|
77
|
-
expect(result.output.brief).toBe('BRIEF');
|
|
78
|
-
expect(result.output.generatedAt).toBe('2026-08-02T00:00:00.000Z');
|
|
79
|
-
expect(result.output.isDefault).toBe(false);
|
|
80
|
-
});
|
|
51
|
+
function makeFakeWindow(sent) {
|
|
52
|
+
return {
|
|
53
|
+
isDestroyed: () => false,
|
|
54
|
+
webContents: {
|
|
55
|
+
isDestroyed: () => false,
|
|
56
|
+
isCrashed: () => false,
|
|
57
|
+
send: (channel, payload) => sent.push({ channel, payload }),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
writeOutput(cwd, {
|
|
89
|
-
home: 'HOME',
|
|
90
|
-
marketing: 'MARKETING',
|
|
91
|
-
feature: 'FEATURE',
|
|
92
|
-
architecture: 'ARCHITECTURE',
|
|
93
|
-
});
|
|
94
|
-
const result = await get({ cwd });
|
|
95
|
-
expect(result.output).not.toBeNull();
|
|
96
|
-
expect(result.output.home).toBe('HOME');
|
|
97
|
-
expect(result.output.marketing).toBe('MARKETING');
|
|
98
|
-
expect(result.output.feature).toBe('FEATURE');
|
|
99
|
-
expect(result.output.architecture).toBe('ARCHITECTURE');
|
|
100
|
-
expect(result.output.brief).toBeUndefined();
|
|
101
|
-
expect(result.output.isDefault).toBe(false);
|
|
102
|
-
});
|
|
62
|
+
async function waitFor(predicate, ms = 5000) {
|
|
63
|
+
const deadline = Date.now() + ms;
|
|
64
|
+
while (!predicate() && Date.now() < deadline) {
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
103
68
|
|
|
104
|
-
|
|
105
|
-
test('(b) no manifest at all: get() returns the shipped default with isDefault true', async () => {
|
|
69
|
+
test('get() returns the home.html text and its mtimeMs', async () => {
|
|
106
70
|
const cwd = await mkTmpCwd();
|
|
71
|
+
writeHome(cwd, '<html>HOME</html>');
|
|
107
72
|
const result = await get({ cwd });
|
|
108
|
-
expect(result.
|
|
109
|
-
expect(result.
|
|
110
|
-
expect(result.
|
|
111
|
-
expect(result.output.home).toBe(readDefaultHomeHtml());
|
|
112
|
-
expect(result.output.marketing).toBeUndefined();
|
|
113
|
-
expect(result.output.feature).toBeUndefined();
|
|
114
|
-
expect(result.output.architecture).toBeUndefined();
|
|
73
|
+
expect(result.html).toBe('<html>HOME</html>');
|
|
74
|
+
expect(typeof result.mtimeMs).toBe('number');
|
|
75
|
+
expect(Object.keys(result).sort()).toEqual(['html', 'mtimeMs']);
|
|
115
76
|
});
|
|
116
77
|
|
|
117
|
-
test('
|
|
78
|
+
test('get() with no home.html returns {html: null, mtimeMs: null} — no shipped default', async () => {
|
|
118
79
|
const cwd = await mkTmpCwd();
|
|
119
|
-
|
|
120
|
-
home: 'HOME',
|
|
121
|
-
marketing: 'MARKETING',
|
|
122
|
-
feature: 'FEATURE',
|
|
123
|
-
// architecture.html deliberately missing
|
|
124
|
-
brief: 'BRIEF',
|
|
125
|
-
});
|
|
126
|
-
const result = await get({ cwd });
|
|
127
|
-
expect(result.output).not.toBeNull();
|
|
128
|
-
expect(result.output.isDefault).toBe(true);
|
|
129
|
-
expect(result.output.home).toBe(readDefaultHomeHtml());
|
|
80
|
+
expect(await get({ cwd })).toEqual({ html: null, mtimeMs: null });
|
|
130
81
|
});
|
|
131
82
|
|
|
132
|
-
test('
|
|
83
|
+
test('get() ignores legacy manifest/lens files — only home.html counts', async () => {
|
|
133
84
|
const cwd = await mkTmpCwd();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
139
|
-
const result = await get({ cwd });
|
|
140
|
-
expect(result.output).not.toBeNull();
|
|
141
|
-
expect(result.output.isDefault).toBe(true);
|
|
85
|
+
const out = path.join(pagesDir(cwd), 'output');
|
|
86
|
+
fs.mkdirSync(out, { recursive: true });
|
|
87
|
+
fs.writeFileSync(path.join(out, 'home.html'), 'OLD');
|
|
88
|
+
fs.writeFileSync(path.join(out, 'manifest.json'), '{"generatedAt":"2026-08-02T00:00:00.000Z"}');
|
|
89
|
+
expect(await get({ cwd })).toEqual({ html: null, mtimeMs: null });
|
|
142
90
|
});
|
|
143
91
|
|
|
144
|
-
|
|
145
|
-
test('(c) default fallback content is identical regardless of which project cwd requests it', async () => {
|
|
146
|
-
const cwdA = await mkTmpCwd();
|
|
147
|
-
const cwdB = await mkTmpCwd();
|
|
148
|
-
const resultA = await get({ cwd: cwdA });
|
|
149
|
-
const resultB = await get({ cwd: cwdB });
|
|
150
|
-
expect(resultA.output.isDefault).toBe(true);
|
|
151
|
-
expect(resultB.output.isDefault).toBe(true);
|
|
152
|
-
expect(resultA.output.home).toBe(resultB.output.home);
|
|
153
|
-
expect(resultA.output.home).toBe(readDefaultHomeHtml());
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('the shipped default is honest: no fabricated project-specific content, and prompts Generate My Project Home', () => {
|
|
157
|
-
const html = readDefaultHomeHtml();
|
|
158
|
-
expect(html).toContain('Generate My Project Home');
|
|
159
|
-
expect(html.toLowerCase()).not.toContain('lorem ipsum');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// ── ephemeral cwd: get() must fall back, never throw/reject ────────────────
|
|
163
|
-
test('get() with an ephemeral cwd (os.tmpdir() itself) returns the shipped default instead of throwing', async () => {
|
|
92
|
+
test('get() with an ephemeral cwd (os.tmpdir() itself) returns null instead of throwing', async () => {
|
|
164
93
|
config.addAllowedRoot(os.tmpdir());
|
|
165
|
-
|
|
166
|
-
expect(result.output).not.toBeNull();
|
|
167
|
-
expect(result.output.isDefault).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// ── watcher ──────────────────────────────────────────────────────────────
|
|
171
|
-
afterEach(() => {
|
|
172
|
-
closeAllOutputWatchers();
|
|
94
|
+
expect(await get({ cwd: os.tmpdir() })).toEqual({ html: null, mtimeMs: null });
|
|
173
95
|
});
|
|
174
96
|
|
|
175
97
|
test('watchOutput() on an ephemeral cwd refuses instead of crashing', async () => {
|
|
176
98
|
config.addAllowedRoot(os.tmpdir());
|
|
177
|
-
|
|
178
|
-
expect(result).toEqual({ ok: false, reason: 'ephemeral' });
|
|
99
|
+
expect(await watchOutput(os.tmpdir())).toEqual({ ok: false, reason: 'ephemeral' });
|
|
179
100
|
expect(_outputWatchers.size).toBe(0);
|
|
180
101
|
});
|
|
181
102
|
|
|
182
|
-
test('watchOutput()
|
|
103
|
+
test('watchOutput() is refcounted: two watches share one watcher, closed at the last unwatch', async () => {
|
|
104
|
+
const cwd = await mkTmpCwd();
|
|
105
|
+
expect(await watchOutput(cwd)).toEqual({ ok: true });
|
|
106
|
+
expect(await watchOutput(cwd)).toEqual({ ok: true });
|
|
107
|
+
expect(_outputWatchers.size).toBe(1);
|
|
108
|
+
unwatchOutput(cwd);
|
|
109
|
+
expect(_outputWatchers.size).toBe(1);
|
|
110
|
+
unwatchOutput(cwd);
|
|
111
|
+
expect(_outputWatchers.size).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('watchOutput() pushes {cwd, html, mtimeMs} on project-pages:changed when home.html lands, ignoring sibling files', async () => {
|
|
183
115
|
const cwd = await mkTmpCwd();
|
|
184
116
|
const sent = [];
|
|
185
|
-
|
|
186
|
-
isDestroyed: () => false,
|
|
187
|
-
webContents: {
|
|
188
|
-
isDestroyed: () => false,
|
|
189
|
-
isCrashed: () => false,
|
|
190
|
-
send: (channel, payload) => sent.push({ channel, payload }),
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
attachWindow(fakeWindow);
|
|
117
|
+
attachWindow(makeFakeWindow(sent));
|
|
194
118
|
|
|
195
|
-
|
|
196
|
-
expect(watchResult).toEqual({ ok: true });
|
|
119
|
+
expect(await watchOutput(cwd)).toEqual({ ok: true });
|
|
197
120
|
expect(_outputWatchers.size).toBe(1);
|
|
198
121
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
feature: 'FEATURE',
|
|
203
|
-
architecture: 'ARCHITECTURE',
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Bounded poll for the debounced (awaitWriteFinish) push instead of a bare
|
|
207
|
-
// sleep — chokidar's stability window is 50ms, so this gives it generous
|
|
208
|
-
// headroom without hanging the suite on a broken watcher.
|
|
209
|
-
const deadline = Date.now() + 5000;
|
|
210
|
-
while (sent.length === 0 && Date.now() < deadline) {
|
|
211
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
212
|
-
}
|
|
122
|
+
fs.writeFileSync(path.join(pagesDir(cwd), 'summary.json'), '{}');
|
|
123
|
+
writeHome(cwd, '<html>HOME</html>');
|
|
124
|
+
await waitFor(() => sent.length > 0);
|
|
213
125
|
|
|
214
126
|
expect(sent.length).toBeGreaterThan(0);
|
|
127
|
+
for (const msg of sent) expect(msg.channel).toBe('project-pages:changed');
|
|
215
128
|
const last = sent[sent.length - 1];
|
|
216
|
-
expect(last.channel).toBe('project-pages:changed');
|
|
217
129
|
expect(last.payload.cwd).toBe(fs.realpathSync(cwd));
|
|
218
|
-
expect(last.payload.
|
|
219
|
-
expect(last.payload.
|
|
130
|
+
expect(last.payload.html).toBe('<html>HOME</html>');
|
|
131
|
+
expect(typeof last.payload.mtimeMs).toBe('number');
|
|
132
|
+
// summary.json alone must not have produced a push carrying no html
|
|
133
|
+
expect(sent.every((m) => m.payload.html === '<html>HOME</html>')).toBe(true);
|
|
220
134
|
|
|
221
135
|
unwatchOutput(cwd);
|
|
222
136
|
expect(_outputWatchers.size).toBe(0);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue-health-verdict.test.cjs — classifyQueueHealth is the Scheduler
|
|
3
|
+
* page's queue-health header: the single honest read of WHY a queue looks
|
|
4
|
+
* stale (saturated slots / blocked dependencies / genuinely idle / the
|
|
5
|
+
* dispatch driver itself stalled / a decision to pause / an open launch
|
|
6
|
+
* circuit breaker), reusing classifyQueueStarvation so the header can never
|
|
7
|
+
* disagree with the starvation watchdog's own read of the same state.
|
|
8
|
+
*
|
|
9
|
+
* Run: timeout 180 npx vitest run src/main/__tests__/queue-health-verdict.test.cjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import { test, afterAll } from 'vitest';
|
|
15
|
+
const assert = require('node:assert/strict');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
// HOME before require: every state path is baked from os.homedir() at load.
|
|
21
|
+
const originalHome = process.env.HOME;
|
|
22
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'queue-health-test-'));
|
|
23
|
+
process.env.HOME = tmpHome;
|
|
24
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
26
|
+
|
|
27
|
+
const { classifyQueueHealth, QUEUE_STARVATION_MS } = require('../scheduler.cjs');
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
process.env.HOME = originalHome;
|
|
31
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const CWD = '/home/bilko/Projects/starry-night-ships';
|
|
35
|
+
const NOW = Date.parse('2026-09-13T20:00:00.000Z');
|
|
36
|
+
const LONG = QUEUE_STARVATION_MS + 60_000;
|
|
37
|
+
|
|
38
|
+
test('saturated: jobs running, zero free slots', () => {
|
|
39
|
+
const jobs = [
|
|
40
|
+
{ slug: '1-a', cwd: CWD, status: 'running', startedAt: new Date(NOW - LONG).toISOString(), dependsOn: [] },
|
|
41
|
+
{ slug: '2-b', cwd: CWD, status: 'pending', dependsOn: [] },
|
|
42
|
+
];
|
|
43
|
+
const v = classifyQueueHealth({
|
|
44
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(['1-a']),
|
|
45
|
+
freeSlots: 0, totalSlots: 4, lastRunAtMs: NOW, now: NOW, cwd: CWD,
|
|
46
|
+
});
|
|
47
|
+
assert.equal(v.kind, 'saturated');
|
|
48
|
+
assert.equal(v.runningCount, 1);
|
|
49
|
+
assert.equal(v.totalSlots, 4);
|
|
50
|
+
// Dispatchable/blocked counts stay real numbers even in a kind whose
|
|
51
|
+
// headline cause is slot saturation, not dependency shape — the header
|
|
52
|
+
// must still be able to show them alongside the verdict.
|
|
53
|
+
assert.equal(v.pending, 1);
|
|
54
|
+
assert.equal(v.dispatchable, 1);
|
|
55
|
+
assert.deepEqual(v.blockedChains, []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('blocked: every pending row terminates in a failed dependency', () => {
|
|
59
|
+
const jobs = [
|
|
60
|
+
{ slug: '1-a', cwd: CWD, status: 'failed', dependsOn: [] },
|
|
61
|
+
{ slug: '2-b', cwd: CWD, status: 'pending', dependsOn: ['1-a'] },
|
|
62
|
+
];
|
|
63
|
+
const v = classifyQueueHealth({
|
|
64
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(),
|
|
65
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - 5_000, now: NOW, cwd: CWD,
|
|
66
|
+
});
|
|
67
|
+
assert.equal(v.kind, 'blocked');
|
|
68
|
+
assert.equal(v.pending, 1);
|
|
69
|
+
assert.equal(v.dispatchable, 0);
|
|
70
|
+
assert.equal(v.blockedChains.length, 1);
|
|
71
|
+
assert.deepEqual(v.blockedChains[0].blockedBy, ['1-a']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('idle: nothing pending', () => {
|
|
75
|
+
const jobs = [
|
|
76
|
+
{ slug: '1-a', cwd: CWD, status: 'completed', dependsOn: [] },
|
|
77
|
+
];
|
|
78
|
+
const v = classifyQueueHealth({
|
|
79
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(),
|
|
80
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW, now: NOW, cwd: CWD,
|
|
81
|
+
});
|
|
82
|
+
assert.equal(v.kind, 'idle');
|
|
83
|
+
assert.equal(v.pending, 0);
|
|
84
|
+
assert.equal(v.dispatchable, 0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('stalled: dispatchable work, nothing running, driver idle past threshold', () => {
|
|
88
|
+
const jobs = [
|
|
89
|
+
{ slug: '1-a', cwd: CWD, status: 'pending', dependsOn: [] },
|
|
90
|
+
];
|
|
91
|
+
const v = classifyQueueHealth({
|
|
92
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(),
|
|
93
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - LONG, now: NOW, cwd: CWD,
|
|
94
|
+
});
|
|
95
|
+
assert.equal(v.kind, 'stalled');
|
|
96
|
+
assert.equal(v.dispatchable, 1);
|
|
97
|
+
assert.ok(v.idleMs >= QUEUE_STARVATION_MS);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('running: dispatchable work, nothing running yet, but driver has not been idle long enough to call it a stall', () => {
|
|
101
|
+
const jobs = [
|
|
102
|
+
{ slug: '1-a', cwd: CWD, status: 'pending', dependsOn: [] },
|
|
103
|
+
];
|
|
104
|
+
const v = classifyQueueHealth({
|
|
105
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(),
|
|
106
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - 5_000, now: NOW, cwd: CWD,
|
|
107
|
+
});
|
|
108
|
+
assert.equal(v.kind, 'running');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('paused: a decision, not a stall — takes priority over everything else', () => {
|
|
112
|
+
const jobs = [
|
|
113
|
+
{ slug: '1-a', cwd: CWD, status: 'pending', dependsOn: [] },
|
|
114
|
+
];
|
|
115
|
+
const v = classifyQueueHealth({
|
|
116
|
+
jobs, paused: { reason: 'manual', since: NOW }, launchBlocks: {}, runningSet: new Set(),
|
|
117
|
+
freeSlots: 0, totalSlots: 4, lastRunAtMs: NOW - LONG, now: NOW, cwd: CWD,
|
|
118
|
+
});
|
|
119
|
+
assert.equal(v.kind, 'paused');
|
|
120
|
+
assert.equal(v.reason, 'manual');
|
|
121
|
+
assert.equal(v.pending, 1);
|
|
122
|
+
assert.equal(v.dispatchable, 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('launch-blocked: an open circuit breaker for a persona a pending row actually uses', () => {
|
|
126
|
+
const jobs = [
|
|
127
|
+
{ slug: '1-a', cwd: CWD, status: 'pending', dependsOn: [], agentType: 'dev-lead' },
|
|
128
|
+
];
|
|
129
|
+
const launchBlocks = {
|
|
130
|
+
'dev-lead': { kind: 'model_config_rejected', attempts: 3, hint: 'update the CLI', until: null },
|
|
131
|
+
};
|
|
132
|
+
const v = classifyQueueHealth({
|
|
133
|
+
jobs, paused: null, launchBlocks, runningSet: new Set(),
|
|
134
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - LONG, now: NOW, cwd: CWD,
|
|
135
|
+
});
|
|
136
|
+
assert.equal(v.kind, 'launch-blocked');
|
|
137
|
+
assert.equal(v.agentType, 'dev-lead');
|
|
138
|
+
assert.equal(v.pending, 1);
|
|
139
|
+
assert.equal(v.dispatchable, 1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('launch-blocked is scoped: a breaker for a persona nothing pending here uses does not fire', () => {
|
|
143
|
+
const jobs = [
|
|
144
|
+
{ slug: '1-a', cwd: CWD, status: 'pending', dependsOn: [], agentType: 'dev-lead' },
|
|
145
|
+
];
|
|
146
|
+
const launchBlocks = {
|
|
147
|
+
'architect': { kind: 'model_config_rejected', attempts: 3, hint: 'update the CLI', until: null },
|
|
148
|
+
};
|
|
149
|
+
const v = classifyQueueHealth({
|
|
150
|
+
jobs, paused: null, launchBlocks, runningSet: new Set(),
|
|
151
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - 5_000, now: NOW, cwd: CWD,
|
|
152
|
+
});
|
|
153
|
+
assert.notEqual(v.kind, 'launch-blocked');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('needsReviewCount and cwd scoping: another project\'s rows never leak into this scope\'s counts', () => {
|
|
157
|
+
const other = '/home/bilko/Projects/session-manager';
|
|
158
|
+
const jobs = [
|
|
159
|
+
{ slug: '1-a', cwd: CWD, status: 'needs_review', dependsOn: [] },
|
|
160
|
+
{ slug: '2-b', cwd: CWD, status: 'pending', dependsOn: [] },
|
|
161
|
+
{ slug: '3-c', cwd: other, status: 'pending', dependsOn: [] },
|
|
162
|
+
{ slug: '4-d', cwd: other, status: 'needs_review', dependsOn: [] },
|
|
163
|
+
];
|
|
164
|
+
const v = classifyQueueHealth({
|
|
165
|
+
jobs, paused: null, launchBlocks: {}, runningSet: new Set(),
|
|
166
|
+
freeSlots: 4, totalSlots: 4, lastRunAtMs: NOW - 5_000, now: NOW, cwd: CWD,
|
|
167
|
+
});
|
|
168
|
+
assert.equal(v.pending, 1);
|
|
169
|
+
assert.equal(v.needsReviewCount, 1);
|
|
170
|
+
});
|
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
'use strict';
|
|
25
25
|
|
|
26
|
-
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
26
|
+
import { test, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
27
27
|
const fs = require('node:fs');
|
|
28
28
|
const os = require('node:os');
|
|
29
29
|
const path = require('node:path');
|
|
30
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
30
31
|
|
|
31
32
|
let tmpHome;
|
|
32
33
|
let originalHome;
|
|
33
34
|
let originalClaudeBin;
|
|
35
|
+
let originalAutofixDisable;
|
|
34
36
|
let scheduler;
|
|
35
37
|
let queueStore;
|
|
36
38
|
|
|
@@ -41,21 +43,26 @@ let queueStore;
|
|
|
41
43
|
// guard then makes runQueueStarvationWatchdog return null for a reason that
|
|
42
44
|
// has nothing to do with what that test is actually checking.
|
|
43
45
|
function writeClaudeStub() {
|
|
44
|
-
|
|
45
|
-
const body = `
|
|
46
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
46
47
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');
|
|
47
48
|
process.exit(0);
|
|
48
|
-
|
|
49
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
50
|
-
return stubPath;
|
|
49
|
+
` });
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
beforeAll(() => {
|
|
54
53
|
originalHome = process.env.HOME;
|
|
55
54
|
originalClaudeBin = process.env.SM_CLAUDE_BIN;
|
|
55
|
+
originalAutofixDisable = process.env.SM_AUTOFIX_DISABLE;
|
|
56
56
|
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-starve-driver-'));
|
|
57
57
|
process.env.HOME = tmpHome;
|
|
58
58
|
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
59
|
+
// The forced tick's dispatched job has no PRD file and makes no commit, so
|
|
60
|
+
// it lands 'needs_review' and (without this) immediately fires a
|
|
61
|
+
// fire-and-forget auto-fix investigation — ANOTHER real claude spawn, under
|
|
62
|
+
// its own queue row, that can still be in runningSet by the time the next
|
|
63
|
+
// test in this file reads it (see writeClaudeStub's header comment; this
|
|
64
|
+
// closes the same gap one layer further down the cascade).
|
|
65
|
+
process.env.SM_AUTOFIX_DISABLE = '1';
|
|
59
66
|
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
60
67
|
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
61
68
|
scheduler = require('../scheduler.cjs');
|
|
@@ -66,6 +73,8 @@ afterAll(() => {
|
|
|
66
73
|
process.env.HOME = originalHome;
|
|
67
74
|
if (originalClaudeBin === undefined) delete process.env.SM_CLAUDE_BIN;
|
|
68
75
|
else process.env.SM_CLAUDE_BIN = originalClaudeBin;
|
|
76
|
+
if (originalAutofixDisable === undefined) delete process.env.SM_AUTOFIX_DISABLE;
|
|
77
|
+
else process.env.SM_AUTOFIX_DISABLE = originalAutofixDisable;
|
|
69
78
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
70
79
|
});
|
|
71
80
|
|
|
@@ -81,7 +90,7 @@ function mkProject() {
|
|
|
81
90
|
return { cwd, slug };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
test('the
|
|
93
|
+
test('ticks that launch nothing do NOT refresh the clock — idleness reads lastRunAt, not lastDispatchAttemptAt', async () => {
|
|
85
94
|
const { cwd, slug } = mkProject();
|
|
86
95
|
await scheduler.writeQueue({
|
|
87
96
|
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
|
|
@@ -90,18 +99,37 @@ test('the watchdog reads idleness from lastDispatchAttemptAt, not the always-fre
|
|
|
90
99
|
});
|
|
91
100
|
|
|
92
101
|
const now = Date.now();
|
|
93
|
-
// The
|
|
94
|
-
//
|
|
95
|
-
//
|
|
102
|
+
// The structural repeat of f18e161: the loop ticks every 30 s (so
|
|
103
|
+
// lastDispatchAttemptAt is seconds old) but nothing has LAUNCHED in 40
|
|
104
|
+
// minutes (lastRunAt). The boot stamp is aged out so it can't mask it.
|
|
105
|
+
const bootedAtMs = now - 3 * 60 * 60_000;
|
|
96
106
|
const state = {
|
|
97
107
|
...queueStore.readMergedSync(),
|
|
98
|
-
lastRunAt: new Date(now -
|
|
99
|
-
lastDispatchAttemptAt: new Date(now -
|
|
108
|
+
lastRunAt: new Date(now - 40 * 60_000).toISOString(),
|
|
109
|
+
lastDispatchAttemptAt: new Date(now - 5_000).toISOString(),
|
|
100
110
|
};
|
|
101
111
|
|
|
102
|
-
const verdict = await scheduler.runQueueStarvationWatchdog(state, { now });
|
|
112
|
+
const verdict = await scheduler.runQueueStarvationWatchdog(state, { now, bootedAtMs });
|
|
103
113
|
expect(verdict).not.toBeNull();
|
|
104
114
|
expect(verdict.kind).toBe('starved');
|
|
115
|
+
|
|
116
|
+
// The watchdog's forced tick above dispatches this job for real (see
|
|
117
|
+
// writeClaudeStub's header comment) — tickQueue doesn't await the spawned
|
|
118
|
+
// process to completion, so runningSet can still hold this slug for a
|
|
119
|
+
// moment after this test's own `await` returns. Under a fast, unloaded
|
|
120
|
+
// machine the stub exits before the NEXT test's watchdog call ever reads
|
|
121
|
+
// runningSet, which is racy rather than deterministic — under a heavily
|
|
122
|
+
// loaded machine (e.g. the full test:unit run) it reliably loses,
|
|
123
|
+
// stranding a slug in runningSet and making
|
|
124
|
+
// classifyQueueStarvation's `runningCount > 0` guard return null for the
|
|
125
|
+
// next test, which has nothing to do with what that test actually checks.
|
|
126
|
+
// Wait for this job to actually leave 'running' before moving on.
|
|
127
|
+
const deadline = Date.now() + 5_000;
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
const row = queueStore.readMergedSync().jobs.find((j) => j.slug === slug);
|
|
130
|
+
if (!row || row.status !== 'running') break;
|
|
131
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
132
|
+
}
|
|
105
133
|
});
|
|
106
134
|
|
|
107
135
|
test('a wedged cancelToken does not block the watchdog from actually driving a tick', async () => {
|
|
@@ -117,21 +145,34 @@ test('a wedged cancelToken does not block the watchdog from actually driving a t
|
|
|
117
145
|
// applyPauseCleared reset (a direct state write, not the clearPause()
|
|
118
146
|
// helper) — leaving the token permanently cancelled with paused: null.
|
|
119
147
|
await scheduler.setPaused('rate_limit', null);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
148
|
+
// setPaused's own broadcast({flush:true}) and a still-draining
|
|
149
|
+
// fire-and-forget mutate() from the PRIOR test's forced dispatch (e.g.
|
|
150
|
+
// tickQueue's own finally-chained call) both write through the SAME
|
|
151
|
+
// serialized mutate() queue this raw writeQueue() call bypasses — one of
|
|
152
|
+
// them can land AFTER this write and re-persist the stale paused:{...}
|
|
153
|
+
// object it read before this clear, purely by losing the race. Re-assert
|
|
154
|
+
// paused:null until it actually sticks, rather than trusting one write to
|
|
155
|
+
// win a race against mutations this test doesn't control the timing of.
|
|
156
|
+
const pausedClearDeadline = Date.now() + 5_000;
|
|
157
|
+
do {
|
|
158
|
+
await scheduler.writeQueue({
|
|
159
|
+
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
|
|
160
|
+
config: {},
|
|
161
|
+
paused: null,
|
|
162
|
+
});
|
|
163
|
+
if (!queueStore.readMergedSync().paused) break;
|
|
164
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
165
|
+
} while (Date.now() < pausedClearDeadline);
|
|
125
166
|
|
|
126
167
|
const before = queueStore.readMergedSync().lastDispatchAttemptAt;
|
|
127
168
|
|
|
128
169
|
const now = Date.now();
|
|
129
170
|
const state = {
|
|
130
171
|
...queueStore.readMergedSync(),
|
|
131
|
-
lastRunAt:
|
|
172
|
+
lastRunAt: new Date(now - 40 * 60_000).toISOString(),
|
|
132
173
|
lastDispatchAttemptAt: new Date(now - 40 * 60_000).toISOString(),
|
|
133
174
|
};
|
|
134
|
-
const verdict = await scheduler.runQueueStarvationWatchdog(state, { now });
|
|
175
|
+
const verdict = await scheduler.runQueueStarvationWatchdog(state, { now, bootedAtMs: now - 3 * 60 * 60_000 });
|
|
135
176
|
expect(verdict.kind).toBe('starved');
|
|
136
177
|
|
|
137
178
|
// tickQueue() only stamps lastDispatchAttemptAt once it gets PAST the
|
|
@@ -141,3 +182,59 @@ test('a wedged cancelToken does not block the watchdog from actually driving a t
|
|
|
141
182
|
expect(after).not.toBe(before);
|
|
142
183
|
expect(after).not.toBeNull();
|
|
143
184
|
});
|
|
185
|
+
|
|
186
|
+
const NOW = Date.parse('2026-09-18T12:00:00.000Z');
|
|
187
|
+
const CWD = '/tmp/sm-starve-clock-project';
|
|
188
|
+
const pendingRow = (slug, extra = {}) => ({ slug, cwd: CWD, status: 'pending', dependsOn: [], ...extra });
|
|
189
|
+
const idleFor = (ms) => NOW - ms;
|
|
190
|
+
|
|
191
|
+
test('a real launch (fresh lastRunAt) refreshes the clock; so do a pause clear and a boot', () => {
|
|
192
|
+
const jobs = [pendingRow('a')];
|
|
193
|
+
const base = { jobs, paused: null, runningCount: 0, now: NOW };
|
|
194
|
+
const stale = idleFor(3 * 60 * 60_000);
|
|
195
|
+
expect(scheduler.classifyQueueStarvation({ ...base, lastRunAtMs: stale }).kind).toBe('starved');
|
|
196
|
+
expect(scheduler.classifyQueueStarvation({ ...base, lastRunAtMs: idleFor(5_000) })).toBeNull();
|
|
197
|
+
expect(scheduler.classifyQueueStarvation({ ...base, lastRunAtMs: stale, lastPauseClearedAtMs: idleFor(5_000) })).toBeNull();
|
|
198
|
+
expect(scheduler.classifyQueueStarvation({ ...base, lastRunAtMs: stale, schedulerBootedAtMs: idleFor(5_000) })).toBeNull();
|
|
199
|
+
expect(scheduler.dispatchIdleMs({ lastRunAtMs: 1000, lastPauseClearedAtMs: 4000, schedulerBootedAtMs: 2000, now: 10_000 })).toBe(6000);
|
|
200
|
+
expect(scheduler.dispatchIdleMs({ now: 10_000 })).toBe(Infinity);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('rows held by a launch breaker / lease read blocked, not starved (per project too)', () => {
|
|
204
|
+
const jobs = [pendingRow('a'), pendingRow('b')];
|
|
205
|
+
const args = { jobs, paused: null, runningCount: 0, lastRunAtMs: idleFor(3 * 60 * 60_000), now: NOW };
|
|
206
|
+
expect(scheduler.classifyQueueStarvation(args).kind).toBe('starved');
|
|
207
|
+
const partial = scheduler.classifyQueueStarvation({ ...args, heldSlugs: new Set(['a']) });
|
|
208
|
+
expect(partial.kind).toBe('starved');
|
|
209
|
+
expect(partial.dispatchable).toBe(1);
|
|
210
|
+
const all = scheduler.classifyQueueStarvation({ ...args, heldSlugs: new Map([['a', 'x'], ['b', 'y']]) });
|
|
211
|
+
expect(all.kind).toBe('blocked');
|
|
212
|
+
expect(all.dispatchable).toBe(0);
|
|
213
|
+
const byProject = scheduler.classifyQueueStarvationByProject({
|
|
214
|
+
jobs, paused: null, runningSet: new Set(), lastRunAtMs: args.lastRunAtMs, heldSlugs: new Set(['a', 'b']), now: NOW,
|
|
215
|
+
});
|
|
216
|
+
expect(byProject.map((v) => v.kind)).toEqual(['blocked']);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('the watchdog latches: one forced tick / audit per starve episode, re-armed by threshold or running-count change', async () => {
|
|
220
|
+
const cwd = '/tmp/sm-starve-latch-project';
|
|
221
|
+
const jobs = [{ slug: 'latch-a', cwd, status: 'pending', dependsOn: [] }];
|
|
222
|
+
const opts = { bootedAtMs: NOW - 3 * 60 * 60_000 };
|
|
223
|
+
const state = { jobs, paused: null, lastRunAt: new Date(NOW - 40 * 60_000).toISOString() };
|
|
224
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
225
|
+
const starvedLogs = () => warn.mock.calls.filter((c) => String(c[0]).includes('QUEUE STARVED')).length;
|
|
226
|
+
try {
|
|
227
|
+
// The row exists only in this hand-built state, never in queue.json, so
|
|
228
|
+
// the forced tick has nothing real to launch.
|
|
229
|
+
expect((await scheduler.runQueueStarvationWatchdog(state, { now: NOW, ...opts })).kind).toBe('starved');
|
|
230
|
+
expect(starvedLogs()).toBe(1);
|
|
231
|
+
// Same episode, well inside QUEUE_STARVATION_MS: still reported, not re-forced.
|
|
232
|
+
expect((await scheduler.runQueueStarvationWatchdog(state, { now: NOW + 60_000, ...opts })).kind).toBe('starved');
|
|
233
|
+
expect(starvedLogs()).toBe(1);
|
|
234
|
+
// QUEUE_STARVATION_MS elapsed again: a new episode.
|
|
235
|
+
await scheduler.runQueueStarvationWatchdog(state, { now: NOW + scheduler.QUEUE_STARVATION_MS + 1, ...opts });
|
|
236
|
+
expect(starvedLogs()).toBe(2);
|
|
237
|
+
} finally {
|
|
238
|
+
warn.mockRestore();
|
|
239
|
+
}
|
|
240
|
+
});
|