claude-code-session-manager 0.86.0 → 0.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -53
- package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
- package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
- package/dist/assets/Permissions-m0KAnccF.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
- package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
- package/dist/assets/Settings-BHSGGFEc.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
- package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
- package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
- package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
- package/dist/index.html +2 -2
- package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
- package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
- package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
- package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
- package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
- package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
- package/package.json +14 -17
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/README.md +59 -0
- package/scripts/audit-ops-hygiene.cjs +350 -0
- package/scripts/hooks/guard-destructive-git.cjs +10 -34
- package/scripts/hooks/guard-inline-implementation.cjs +14 -8
- package/scripts/hooks/guard-prd-writes.cjs +7 -36
- package/scripts/hooks/guard-self-schedule.cjs +175 -0
- package/scripts/ops-sweep.cjs +355 -0
- package/scripts/scheduler-mcp-server.cjs +35 -71
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
- package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
- package/src/main/__tests__/chat-queue.test.cjs +2 -2
- package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
- package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
- package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
- package/src/main/__tests__/dod-report.test.cjs +2 -2
- package/src/main/__tests__/dod-reverify.test.cjs +2 -2
- package/src/main/__tests__/epicMint.test.cjs +4 -4
- package/src/main/__tests__/exchanges.test.cjs +2 -2
- package/src/main/__tests__/extractJson.test.cjs +2 -2
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
- package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
- package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
- package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
- package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
- package/src/main/__tests__/historyRollup.test.cjs +2 -2
- package/src/main/__tests__/kg-augment.test.cjs +2 -2
- package/src/main/__tests__/mcpStatus.test.cjs +2 -2
- package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
- package/src/main/__tests__/memoryStale.test.cjs +1 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
- package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/proc-role-env.test.cjs +125 -0
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
- package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
- package/src/main/__tests__/projectPages.test.cjs +63 -149
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +65 -2
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
- package/src/main/__tests__/runVerify.test.cjs +5 -5
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
- package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
- package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
- package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
- package/src/main/bilkoHost.cjs +16 -7
- package/src/main/build-info.json +8 -0
- package/src/main/chatRunner.cjs +12 -7
- package/src/main/config.cjs +2 -3
- package/src/main/docEdit.cjs +4 -2
- package/src/main/health.cjs +268 -52
- package/src/main/heapSnapshot.cjs +2 -2
- package/src/main/historyAggregator.cjs +6 -1
- package/src/main/index.cjs +152 -50
- package/src/main/ipcSchemas.cjs +53 -17
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
- package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gateFixtures.json +20 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
- package/src/main/lib/__tests__/procName.test.cjs +92 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
- package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
- package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
- package/src/main/lib/activeIndexRebuild.cjs +1 -3
- package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
- package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
- package/src/main/lib/agentModelResolve.cjs +94 -24
- package/src/main/lib/agentPersonaSchema.cjs +2 -2
- package/src/main/lib/auditLog.cjs +30 -5
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/buildIdentity.cjs +113 -0
- package/src/main/lib/classifyPromptTicket.cjs +3 -2
- package/src/main/lib/claudeBin.cjs +37 -2
- package/src/main/lib/cleanEnv.cjs +27 -1
- package/src/main/lib/credentials.cjs +4 -2
- package/src/main/lib/cwdClassify.cjs +185 -0
- package/src/main/lib/definitionOfDone.cjs +296 -52
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/dispatchLoop.cjs +40 -0
- package/src/main/lib/effectiveModelInfo.cjs +332 -0
- package/src/main/lib/ephemeralCwd.cjs +7 -28
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +136 -19
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/historyRollup.cjs +6 -7
- package/src/main/lib/instanceLock.cjs +32 -5
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/jobSupervisorRecord.cjs +147 -0
- package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/localAdminHttp.cjs +9 -27
- package/src/main/lib/mcpToolCatalog.cjs +33 -78
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +27 -19
- package/src/main/lib/prdAuthoringSeed.cjs +36 -0
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +214 -45
- package/src/main/lib/procIdentity.cjs +126 -0
- package/src/main/lib/procName.cjs +98 -0
- package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +27 -8
- package/src/main/lib/queueStore.cjs +63 -38
- package/src/main/lib/quietMachineLease.cjs +19 -2
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/reservationExpiry.cjs +30 -0
- package/src/main/lib/runClaudeP.cjs +4 -2
- package/src/main/lib/runLogRetention.cjs +3 -2
- package/src/main/lib/scheduleJobSchema.cjs +10 -2
- package/src/main/lib/scheduleJobTransitions.cjs +60 -0
- package/src/main/lib/schedulerBatch.cjs +27 -2
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/schedulerPaths.cjs +175 -0
- package/src/main/lib/schedulerRuntimeState.cjs +59 -0
- package/src/main/lib/sessionSlots.cjs +47 -9
- package/src/main/lib/smProcNames.cjs +51 -0
- package/src/main/lib/upgradeDrain.cjs +188 -0
- package/src/main/lib/usageCircuit.cjs +206 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
- package/src/main/lib/withTimeout.cjs +33 -0
- package/src/main/mcpStatus.cjs +4 -2
- package/src/main/pluginInstall.cjs +6 -2
- package/src/main/projectPages.cjs +41 -145
- package/src/main/pty.cjs +8 -1
- package/src/main/queueOps.cjs +10 -10
- package/src/main/runVerify.cjs +69 -3
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +2841 -482
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +7 -6
- package/src/main/seedStatus.cjs +1 -1
- package/src/main/supervisor.cjs +5 -3
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/usage.cjs +140 -58
- package/src/preload/api.d.ts +110 -65
- package/src/preload/index.cjs +12 -4
- package/src/seed/agents/project-home-builder.md +31 -48
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- package/screenshots/.gitkeep +0 -0
- package/screenshots/README-screenshots.md +0 -13
- package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
- package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
- package/scripts/render-project-pages.cjs +0 -70
- package/scripts/validate-project-pages-summary.cjs +0 -62
- package/src/main/lib/projectPageSummarySchema.cjs +0 -181
- package/src/main/teams.cjs +0 -95
- package/src/main/templates/project-pages-catalog.json +0 -741
- package/src/main/templates/project-pages-default-home.html +0 -123
- package/src/main/templates/project-pages-pipeline.md +0 -417
- package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
- package/src/seed/prompts/code-review/correctness-only.md +0 -8
- package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
- package/src/seed/prompts/code-review/hallucination-check.md +0 -8
- package/src/seed/prompts/code-review/public-api-compat.md +0 -8
- package/src/seed/prompts/code-review/readability-naming.md +0 -8
- package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
- package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
- package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
- package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
- package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
- package/src/seed/prompts/documentation/adr-from-change.md +0 -8
- package/src/seed/prompts/documentation/module-readme.md +0 -8
- package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
- package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
- package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
- package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
- package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
- package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
- package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
- package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
- package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
- package/src/seed/prompts/performance/complexity-audit.md +0 -8
- package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
- package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
- package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
- package/src/seed/prompts/qa/api-contract-tests.md +0 -8
- package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
- package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
- package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
- package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
- package/src/seed/prompts/qa/tdd-red-first.md +0 -8
- package/src/seed/prompts/qa/visual-regression-review.md +0 -8
- package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
- package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
- package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
- package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
- package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
- package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
- package/src/seed/prompts/security/authz-audit.md +0 -8
- package/src/seed/prompts/security/crypto-correctness.md +0 -8
- package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
- package/src/seed/prompts/security/dependency-audit.md +0 -8
- package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
- package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
- package/src/seed/prompts/security/secret-credential-scan.md +0 -8
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-broadcast-reconcile.test.cjs — broadcast() used to do its own bare
|
|
3
|
+
* `readQueue() -> reconcile(state) -> writeQueue(state)` BEFORE ever touching
|
|
4
|
+
* the broadcastCoalescer, so a burst of N broadcast() calls inside one
|
|
5
|
+
* BROADCAST_COALESCE_MS window cost N full reconcile passes + N queue writes
|
|
6
|
+
* to produce ONE coalesced payload — and did so outside mutate()'s lock, so a
|
|
7
|
+
* concurrent mutation's update could be lost. The fix moves the
|
|
8
|
+
* reconcile+write pair inside the coalescer's `getPayload`, run through
|
|
9
|
+
* `mutate()`, so N broadcast() calls in one window cost exactly one reconcile
|
|
10
|
+
* and one write.
|
|
11
|
+
*
|
|
12
|
+
* `reconcile` is a bare local call inside getPayload's mutate() callback, so
|
|
13
|
+
* — matching this file's existing testable-seam convention (module.exports.
|
|
14
|
+
* stashList / evaluateSharedTreeGuard / committedInWindow) — it is invoked as
|
|
15
|
+
* `module.exports.reconcile(state)` specifically so tests can spy on it here.
|
|
16
|
+
* `queueStore.readMerged`/`writeSplit` are genuine cross-module property
|
|
17
|
+
* calls already, so they're spied on directly with no source change needed.
|
|
18
|
+
*
|
|
19
|
+
* HOME-isolated (mirrors reconcileFlatPrdSweep.test.cjs): stub HOME to a
|
|
20
|
+
* mkdtemp dir before requiring scheduler.cjs so `ensureDirs()` (called by the
|
|
21
|
+
* real `writeQueue()` wrapper before the now-mocked `queueStore.writeSplit`)
|
|
22
|
+
* never touches the real machine.
|
|
23
|
+
*
|
|
24
|
+
* Run: timeout 60 npx vitest run src/main/__tests__/scheduler-broadcast-reconcile.test.cjs
|
|
25
|
+
*/
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
import { test, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
let tmpHome;
|
|
34
|
+
let originalHome;
|
|
35
|
+
let scheduler;
|
|
36
|
+
let queueStore;
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
originalHome = process.env.HOME;
|
|
40
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-broadcast-reconcile-home-'));
|
|
41
|
+
process.env.HOME = tmpHome;
|
|
42
|
+
|
|
43
|
+
scheduler = require('../scheduler.cjs');
|
|
44
|
+
queueStore = require('../lib/queueStore.cjs');
|
|
45
|
+
|
|
46
|
+
if (!scheduler.PRDS_DIR.startsWith(tmpHome)) {
|
|
47
|
+
throw new Error(`refusing to run: PRDS_DIR (${scheduler.PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(() => {
|
|
52
|
+
process.env.HOME = originalHome;
|
|
53
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function freshState() {
|
|
57
|
+
return { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, paused: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fakeWindow() {
|
|
61
|
+
return {
|
|
62
|
+
isDestroyed: () => false,
|
|
63
|
+
webContents: { isDestroyed: () => false, isCrashed: () => false, send: vi.fn() },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function wait(ms) {
|
|
68
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
scheduler.attachWindow(null);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('10 broadcast() calls inside one coalesce window trigger exactly one reconcile and one queue write', async () => {
|
|
77
|
+
vi.spyOn(queueStore, 'readMerged').mockImplementation(async () => freshState());
|
|
78
|
+
const writeSpy = vi.spyOn(queueStore, 'writeSplit').mockResolvedValue(undefined);
|
|
79
|
+
const reconcileSpy = vi.spyOn(scheduler, 'reconcile').mockResolvedValue(undefined);
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < 10; i += 1) scheduler.broadcast();
|
|
82
|
+
|
|
83
|
+
// BROADCAST_COALESCE_MS is 200ms; give the trailing-edge timer room to fire.
|
|
84
|
+
await wait(400);
|
|
85
|
+
|
|
86
|
+
expect(reconcileSpy).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a window-less (flush) broadcast() still reconciles, even with no window attached', async () => {
|
|
91
|
+
scheduler.attachWindow(null);
|
|
92
|
+
vi.spyOn(queueStore, 'readMerged').mockImplementation(async () => freshState());
|
|
93
|
+
const writeSpy = vi.spyOn(queueStore, 'writeSplit').mockResolvedValue(undefined);
|
|
94
|
+
const reconcileSpy = vi.spyOn(scheduler, 'reconcile').mockResolvedValue(undefined);
|
|
95
|
+
|
|
96
|
+
await scheduler.broadcast({ flush: true });
|
|
97
|
+
|
|
98
|
+
expect(reconcileSpy).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a reconcile() that throws does not wedge the coalescer — the next broadcast() still sends', async () => {
|
|
103
|
+
const win = fakeWindow();
|
|
104
|
+
scheduler.attachWindow(win);
|
|
105
|
+
vi.spyOn(queueStore, 'readMerged').mockImplementation(async () => freshState());
|
|
106
|
+
vi.spyOn(queueStore, 'writeSplit').mockResolvedValue(undefined);
|
|
107
|
+
const reconcileSpy = vi.spyOn(scheduler, 'reconcile')
|
|
108
|
+
.mockRejectedValueOnce(new Error('boom'))
|
|
109
|
+
.mockResolvedValue(undefined);
|
|
110
|
+
|
|
111
|
+
scheduler.broadcast();
|
|
112
|
+
await wait(400);
|
|
113
|
+
expect(win.webContents.send).not.toHaveBeenCalled();
|
|
114
|
+
|
|
115
|
+
scheduler.broadcast();
|
|
116
|
+
await wait(400);
|
|
117
|
+
|
|
118
|
+
expect(reconcileSpy).toHaveBeenCalledTimes(2);
|
|
119
|
+
expect(win.webContents.send).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(win.webContents.send).toHaveBeenCalledWith('schedule:state', expect.any(Object));
|
|
121
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Ported from the retired top-level test-root directory (a node:test suite,
|
|
4
|
+
// never runnable by this repo's vitest-only setup — see CLAUDE.md's Commands
|
|
5
|
+
// section). Pins the two basic cross-project pickNextBatch contracts that
|
|
6
|
+
// were asserted there but not literally exercised elsewhere: N independent
|
|
7
|
+
// projects each with a single pending job all fire together when the global
|
|
8
|
+
// slot pool is generous, and a global cap BELOW the total pending count
|
|
9
|
+
// across projects admits exactly that many, not more.
|
|
10
|
+
//
|
|
11
|
+
// Run: timeout 120 npx vitest run src/main/__tests__/scheduler-cross-project-batch.test.cjs
|
|
12
|
+
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const { pickNextBatch } = require('../lib/schedulerBatch.cjs');
|
|
15
|
+
|
|
16
|
+
function job(slug, status, cwd, parallelGroup) {
|
|
17
|
+
return { slug, status, cwd, parallelGroup, dependsOn: [] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('3 independent projects, 1 pending each, generous cap (6) → all 3 fire', () => {
|
|
21
|
+
const jobs = [
|
|
22
|
+
job('a-job', 'pending', '/a', 5),
|
|
23
|
+
job('b-job', 'pending', '/b', 5),
|
|
24
|
+
job('c-job', 'pending', '/c', 5),
|
|
25
|
+
];
|
|
26
|
+
const { batch, reason } = pickNextBatch(jobs, new Set(), 6);
|
|
27
|
+
assert.equal(batch.length, 3);
|
|
28
|
+
assert.equal(reason, null);
|
|
29
|
+
const slugs = new Set(batch.map((j) => j.slug));
|
|
30
|
+
assert.ok(slugs.has('a-job'));
|
|
31
|
+
assert.ok(slugs.has('b-job'));
|
|
32
|
+
assert.ok(slugs.has('c-job'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('3 independent projects, 1 pending each, cap 2 (below total pending) → exactly 2 fire', () => {
|
|
36
|
+
const jobs = [
|
|
37
|
+
job('a-job', 'pending', '/a', 5),
|
|
38
|
+
job('b-job', 'pending', '/b', 5),
|
|
39
|
+
job('c-job', 'pending', '/c', 5),
|
|
40
|
+
];
|
|
41
|
+
const { batch } = pickNextBatch(jobs, new Set(), 2);
|
|
42
|
+
assert.equal(batch.length, 2);
|
|
43
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-default-eligible-heal.test.cjs — the periodic heal ladder is
|
|
3
|
+
* default-ELIGIBLE (2026-09-18, 1229-fo-03 parked shared_tree_reverted and
|
|
4
|
+
* blocked 19 of 20 pending rows because that verdict was on no allow-list).
|
|
5
|
+
*
|
|
6
|
+
* Covers: any needs_review verdict is a candidate unless named in
|
|
7
|
+
* RESCAN_EXCLUDED_VERDICTS; the evidence rung is bounded; a row that cannot
|
|
8
|
+
* heal terminates under the bounded ladder; health names a silent blocker.
|
|
9
|
+
*
|
|
10
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-default-eligible-heal.test.cjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
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
|
+
process.env.HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'default-eligible-heal-test-'));
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
isRescanCandidate,
|
|
24
|
+
isTranscriptRescannable,
|
|
25
|
+
isStrandedAutoFixPark,
|
|
26
|
+
isEligibleForNeedsReviewAutoResolve,
|
|
27
|
+
shouldRunPeriodicReverify,
|
|
28
|
+
selectEvidenceScanTargets,
|
|
29
|
+
selectAutoFixTargets,
|
|
30
|
+
selectMechanicalRecoveryTarget,
|
|
31
|
+
selectResumeRecoveryTarget,
|
|
32
|
+
applyNeedsReviewAutoResolve,
|
|
33
|
+
RESCAN_EXCLUDED_VERDICTS,
|
|
34
|
+
RESCANNABLE_VERDICTS,
|
|
35
|
+
EVIDENCE_SCAN_MAX_PER_PASS,
|
|
36
|
+
EVIDENCE_SCAN_MIN_INTERVAL_MS,
|
|
37
|
+
REVERIFY_INTERVAL_MS,
|
|
38
|
+
NEEDS_REVIEW_RESOLVE_CAP,
|
|
39
|
+
} = require('../scheduler.cjs');
|
|
40
|
+
const { evaluateBlockingParkHealth } = require('../health.cjs');
|
|
41
|
+
|
|
42
|
+
const row = (o = {}) => ({ slug: '10-x', status: 'needs_review', runId: 'r1', cwd: '/p', ...o });
|
|
43
|
+
|
|
44
|
+
test('shared_tree_reverted is a rescan candidate without being on any list', () => {
|
|
45
|
+
assert.equal(RESCANNABLE_VERDICTS.has('shared_tree_reverted'), false);
|
|
46
|
+
assert.equal(isRescanCandidate(row({ verifierVerdict: 'shared_tree_reverted' })), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('an invented verdict name the code has never seen is a rescan candidate', () => {
|
|
50
|
+
const j = row({ verifierVerdict: 'some_future_guard_verdict' });
|
|
51
|
+
assert.equal(isRescanCandidate(j), true);
|
|
52
|
+
assert.equal(shouldRunPeriodicReverify([j]), true);
|
|
53
|
+
// Not transcript-rescannable: it gets the evidence rung, never a verifyRun heal.
|
|
54
|
+
assert.equal(isTranscriptRescannable(j), false);
|
|
55
|
+
assert.deepEqual(selectEvidenceScanTargets([j]).map((x) => x.slug), ['10-x']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('a needs_review row with no runId and no verdict is still a candidate', () => {
|
|
59
|
+
assert.equal(isRescanCandidate({ slug: 's', status: 'needs_review' }), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('only the named exclusions are refused, each verdict in the set', () => {
|
|
63
|
+
for (const v of RESCAN_EXCLUDED_VERDICTS) assert.equal(isRescanCandidate(row({ verifierVerdict: v })), false, v);
|
|
64
|
+
assert.deepEqual([...RESCAN_EXCLUDED_VERDICTS].sort(), ['budget_exceeded', 'uncommitted_changes', 'worktree_integration_failed']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('rescannable verdict keeps the transcript rung as a hint', () => {
|
|
68
|
+
assert.equal(isTranscriptRescannable(row({ verifierVerdict: 'transcript_errors' })), true);
|
|
69
|
+
assert.deepEqual(selectEvidenceScanTargets([row({ verifierVerdict: 'transcript_errors' })]), []);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('1229-fo-03 shape: stranded auto-fix park is an auto-resolve door AND an evidence-scan target', () => {
|
|
73
|
+
const j = row({ verifierVerdict: 'shared_tree_reverted', autoFixAttempted: true });
|
|
74
|
+
assert.equal(isStrandedAutoFixPark(j, [j]), true);
|
|
75
|
+
assert.equal(isEligibleForNeedsReviewAutoResolve(j, [j]), true);
|
|
76
|
+
assert.equal(selectEvidenceScanTargets([j]).length, 1, 'looksDone evidence is now computed for it, so the door can fire');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a live auto-fix history that is not an auto-resolve door gets no evidence scan (PRD 1136)', () => {
|
|
80
|
+
const j = row({ verifierVerdict: 'some_future_guard_verdict', autoFixAttempted: true, autoFixOutcome: 'plan' });
|
|
81
|
+
assert.equal(selectEvidenceScanTargets([j]).length, 0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('cost bound: per-pass cap, looksDone skip, and per-row re-scan throttle', () => {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const many = Array.from({ length: 100 }, (_, i) => row({ slug: `${i}-p`, verifierVerdict: 'v_new' }));
|
|
87
|
+
assert.equal(selectEvidenceScanTargets(many, now).length, EVIDENCE_SCAN_MAX_PER_PASS);
|
|
88
|
+
const done = many.map((j) => ({ ...j, looksDone: { commits: ['a'] } }));
|
|
89
|
+
assert.equal(selectEvidenceScanTargets(done, now).length, 0);
|
|
90
|
+
const fresh = many.map((j) => ({ ...j, evidenceScannedAt: new Date(now - EVIDENCE_SCAN_MIN_INTERVAL_MS + 1000).toISOString() }));
|
|
91
|
+
assert.equal(selectEvidenceScanTargets(fresh, now).length, 0);
|
|
92
|
+
// never-scanned rows go before stale-scanned ones
|
|
93
|
+
const mixed = [
|
|
94
|
+
row({ slug: 'old', verifierVerdict: 'v', evidenceScannedAt: new Date(now - 10 * EVIDENCE_SCAN_MIN_INTERVAL_MS).toISOString() }),
|
|
95
|
+
row({ slug: 'never', verifierVerdict: 'v' }),
|
|
96
|
+
];
|
|
97
|
+
assert.equal(selectEvidenceScanTargets(mixed, now)[0].slug, 'never');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('unhealable row terminates: N passes yield a bounded number of recovery attempts, reason intact', () => {
|
|
101
|
+
let now = Date.now();
|
|
102
|
+
const job = row({ slug: '20-stuck', verifierVerdict: 'some_future_guard_verdict' });
|
|
103
|
+
const jobs = [job];
|
|
104
|
+
let evidenceScans = 0;
|
|
105
|
+
let autoFixDispatches = 0;
|
|
106
|
+
let resumeOrMechanical = 0;
|
|
107
|
+
const PASSES = 500;
|
|
108
|
+
for (let n = 0; n < PASSES; n++) {
|
|
109
|
+
now += REVERIFY_INTERVAL_MS;
|
|
110
|
+
for (const t of selectEvidenceScanTargets(jobs, now)) { evidenceScans++; t.evidenceScannedAt = new Date(now).toISOString(); }
|
|
111
|
+
resumeOrMechanical += jobs.filter((j) => selectMechanicalRecoveryTarget(j) || selectResumeRecoveryTarget(j)).length;
|
|
112
|
+
for (const t of selectAutoFixTargets(jobs, { fixSlugExists: () => false, resolveJobRunId: () => 'r1' })) {
|
|
113
|
+
autoFixDispatches++;
|
|
114
|
+
// what reverifyNeedsReview stamps before spawning
|
|
115
|
+
t.autoFixRetries = t.autoFixAttempted === true ? (t.autoFixRetries ?? 0) + 1 : t.autoFixRetries;
|
|
116
|
+
t.autoFixAttempted = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
assert.ok(autoFixDispatches <= 2, `auto-fix dispatched ${autoFixDispatches}x`);
|
|
120
|
+
assert.equal(resumeOrMechanical, 0);
|
|
121
|
+
// one scan per throttle window, not one per pass
|
|
122
|
+
assert.ok(evidenceScans <= Math.ceil(PASSES * REVERIFY_INTERVAL_MS / EVIDENCE_SCAN_MIN_INTERVAL_MS), `evidence scans ${evidenceScans}`);
|
|
123
|
+
assert.ok(evidenceScans < PASSES / 2);
|
|
124
|
+
assert.equal(job.status, 'needs_review');
|
|
125
|
+
assert.equal(job.verifierVerdict, 'some_future_guard_verdict');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('auto-resolve ladder is capped: requeue attempts stop at NEEDS_REVIEW_RESOLVE_CAP then skip', () => {
|
|
129
|
+
const j = row({ slug: '30-cap', verifierVerdict: 'x', autoFixAttempted: true });
|
|
130
|
+
const outcomes = [];
|
|
131
|
+
for (let i = 0; i < NEEDS_REVIEW_RESOLVE_CAP + 3; i++) {
|
|
132
|
+
const o = applyNeedsReviewAutoResolve(j, [j]);
|
|
133
|
+
if (!o) break;
|
|
134
|
+
outcomes.push(o);
|
|
135
|
+
if (o === 'requeued') j.status = 'needs_review';
|
|
136
|
+
if (o === 'skipped') break;
|
|
137
|
+
}
|
|
138
|
+
assert.equal(outcomes.filter((o) => o === 'requeued').length, NEEDS_REVIEW_RESOLVE_CAP);
|
|
139
|
+
assert.equal(outcomes.at(-1), 'skipped');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('evaluateBlockingParkHealth', () => {
|
|
143
|
+
const now = Date.parse('2026-09-18T20:00:00Z');
|
|
144
|
+
const parkedAgo = (ms) => [{ to: 'needs_review', at: new Date(now - ms).toISOString() }];
|
|
145
|
+
const park = (ms, o = {}) => ({ slug: '5-park', status: 'needs_review', cwd: '/p', verifierVerdict: 'shared_tree_reverted', statusHistory: parkedAgo(ms), ...o });
|
|
146
|
+
const dep = (slug, dependsOn) => ({ slug, status: 'pending', cwd: '/p', dependsOn });
|
|
147
|
+
|
|
148
|
+
test('parked past one interval with pending dependents (transitive) is non-green, naming row + count', () => {
|
|
149
|
+
const r = evaluateBlockingParkHealth([park(REVERIFY_INTERVAL_MS + 1000), dep('6-a', ['5-park']), dep('7-b', ['6-a']), dep('8-c', [])], now);
|
|
150
|
+
assert.equal(r.ok, false);
|
|
151
|
+
assert.equal(r.parks[0].slug, '5-park');
|
|
152
|
+
assert.equal(r.parks[0].dependents, 2);
|
|
153
|
+
assert.match(r.message, /5-park/);
|
|
154
|
+
assert.match(r.message, /blocking 2 pending dependent/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('young park, or park with no dependents, is healthy', () => {
|
|
158
|
+
assert.equal(evaluateBlockingParkHealth([park(1000), dep('6-a', ['5-park'])], now).ok, true);
|
|
159
|
+
assert.equal(evaluateBlockingParkHealth([park(REVERIFY_INTERVAL_MS * 5), dep('6-a', [])], now).ok, true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-dispatch-loop.test.cjs — dispatch cadence is the loop's, not the billing
|
|
3
|
+
* poller's: with the meter failing (backoff at 480 s) a pending dispatchable row still
|
|
4
|
+
* gets a tickQueue within 30 s.
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-dispatch-loop.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
import { test, expect, vi, beforeAll, afterAll } from 'vitest';
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
let tmpHome;
|
|
16
|
+
let originalHome;
|
|
17
|
+
let scheduler;
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
originalHome = process.env.HOME;
|
|
21
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-dispatch-loop-'));
|
|
22
|
+
process.env.HOME = tmpHome;
|
|
23
|
+
process.env.SM_AUTOFIX_DISABLE = '1';
|
|
24
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
26
|
+
scheduler = require('../scheduler.cjs');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
process.env.HOME = originalHome;
|
|
31
|
+
delete process.env.SM_AUTOFIX_DISABLE;
|
|
32
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('with the billing poll backed off to 480 s, a pending row is still ticked within 30 s', async () => {
|
|
36
|
+
const { startDispatchLoop } = require('../lib/dispatchLoop.cjs');
|
|
37
|
+
await scheduler.writeQueue({
|
|
38
|
+
jobs: [{ slug: `9004-dispatch-loop-${process.pid}`, title: 'x', cwd: tmpHome, status: 'pending', dependsOn: [] }],
|
|
39
|
+
config: {},
|
|
40
|
+
paused: null,
|
|
41
|
+
});
|
|
42
|
+
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] });
|
|
43
|
+
const seen = [];
|
|
44
|
+
const h = startDispatchLoop({
|
|
45
|
+
// Same wiring init() uses (tick: () => tickQueue()), observed.
|
|
46
|
+
tick: async () => { seen.push(await scheduler.tickQueue()); },
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
// Billing poll is at 480 s backoff: nothing else fires. The loop's own 30 s cadence does.
|
|
50
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
51
|
+
// tickQueue does real file I/O; wait for the (real-time) pass to settle.
|
|
52
|
+
await vi.waitFor(() => expect(seen.length).toBe(1), { timeout: 10_000 });
|
|
53
|
+
expect(seen[0]).toBeTruthy(); // tickQueue ran and recorded an outcome for the pending row
|
|
54
|
+
} finally {
|
|
55
|
+
h.stop();
|
|
56
|
+
vi.useRealTimers();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -19,6 +19,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const path = require('node:path');
|
|
22
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
22
23
|
|
|
23
24
|
let tmpHome;
|
|
24
25
|
let originalHome;
|
|
@@ -51,17 +52,14 @@ afterEach(() => {
|
|
|
51
52
|
// Stub `claude` binary: captures the '-p' prompt argv to `capturePath`, then
|
|
52
53
|
// emits one stream-json result line and exits 0.
|
|
53
54
|
function writeClaudeStub(capturePath) {
|
|
54
|
-
|
|
55
|
-
const body = `
|
|
55
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
56
56
|
const fs = require('fs');
|
|
57
57
|
const idx = process.argv.indexOf('-p');
|
|
58
58
|
const prompt = idx >= 0 ? process.argv[idx + 1] : '';
|
|
59
59
|
fs.writeFileSync(${JSON.stringify(capturePath)}, prompt, 'utf8');
|
|
60
60
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
61
61
|
process.exit(0);
|
|
62
|
-
|
|
63
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
64
|
-
return stubPath;
|
|
62
|
+
` });
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
function setupProject(prefix) {
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* against a manual force-tick click), the message must say so instead of
|
|
9
9
|
* flatly claiming nothing is pending.
|
|
10
10
|
*
|
|
11
|
-
* Run: timeout
|
|
11
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-force-tick-outcome.test.cjs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { test } from 'vitest';
|
|
17
17
|
const assert = require('node:assert/strict');
|
|
18
18
|
const { forceTickOutcome } = require('../scheduler.cjs');
|
|
19
19
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-gate-shadow.test.cjs — the shadow gate records `gateShadow` on the
|
|
3
|
+
* verdicts sidecar and the row, and NEVER changes a status.
|
|
4
|
+
*
|
|
5
|
+
* HOME is overridden BEFORE requiring scheduler.cjs (top-level consts bake in
|
|
6
|
+
* os.homedir() — see scheduler-looks-done.test.cjs).
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 180 npx vitest run src/main/__tests__/scheduler-gate-shadow.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const assert = require('node:assert/strict');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { execFileSync } = require('node:child_process');
|
|
17
|
+
|
|
18
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gate-shadow-test-'));
|
|
19
|
+
process.env.HOME = tmpHome;
|
|
20
|
+
|
|
21
|
+
const { runGateShadow } = require('../scheduler.cjs');
|
|
22
|
+
const { runGateSequence } = require('../lib/definitionOfDone.cjs');
|
|
23
|
+
const { resolveEpicPrdWriteDir } = require('../lib/prdLocations.cjs');
|
|
24
|
+
const { bustCwdCache } = require('../lib/queueStore.cjs');
|
|
25
|
+
|
|
26
|
+
function git(args, cwd) { return execFileSync('git', args, { cwd, encoding: 'utf8' }); }
|
|
27
|
+
|
|
28
|
+
function setup(name, gateBody) {
|
|
29
|
+
const cwd = path.join(tmpHome, name);
|
|
30
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
31
|
+
git(['init', '-q'], cwd);
|
|
32
|
+
git(['config', 'user.email', 't@example.com'], cwd);
|
|
33
|
+
git(['config', 'user.name', 'T'], cwd);
|
|
34
|
+
fs.writeFileSync(path.join(cwd, 'README.md'), 'x\n');
|
|
35
|
+
git(['add', '-A'], cwd);
|
|
36
|
+
git(['commit', '-q', '-m', 'init'], cwd);
|
|
37
|
+
const slugDir = path.join(tmpHome, '.claude', 'projects', `${name}-slug`);
|
|
38
|
+
fs.mkdirSync(slugDir, { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(slugDir, 't.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
40
|
+
bustCwdCache();
|
|
41
|
+
const prdDir = resolveEpicPrdWriteDir(cwd, 'gate-epic');
|
|
42
|
+
fs.mkdirSync(prdDir, { recursive: true });
|
|
43
|
+
const slug = `01-${name}`;
|
|
44
|
+
fs.writeFileSync(path.join(prdDir, `${slug}.md`), `---\ntitle: T\ncwd: ${cwd}\nestimateMinutes: 5\n---\n${gateBody}\n`);
|
|
45
|
+
const runId = `run-${name}`;
|
|
46
|
+
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
47
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
48
|
+
const verdictsPath = path.join(runDir, `${slug}.verdicts.json`);
|
|
49
|
+
fs.writeFileSync(verdictsPath, JSON.stringify({ verdict: 'transcript_errors', reason: 'r' }));
|
|
50
|
+
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
51
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
52
|
+
const queuePath = path.join(stateDir, 'queue.json');
|
|
53
|
+
const job = { slug, status: 'needs_review', cwd, runId, verifierVerdict: 'transcript_errors', startedAt: new Date().toISOString(), finishedAt: new Date().toISOString() };
|
|
54
|
+
fs.writeFileSync(queuePath, JSON.stringify({ jobs: [job] }, null, 2));
|
|
55
|
+
return { cwd, slug, job, verdictsPath, queuePath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ac = (cmd) => `# Acceptance criteria\n\n- [ ] Gate: \`${cmd}\`\n`;
|
|
59
|
+
|
|
60
|
+
test('green gate: recorded on sidecar + row, status untouched, verdict keys preserved', async () => {
|
|
61
|
+
const t = setup('green', ac('timeout 30 node -e "process.exit(0)" && timeout 30 node -e "process.exit(0)"'));
|
|
62
|
+
const rec = await runGateShadow(t.job);
|
|
63
|
+
assert.equal(rec.status, 'green');
|
|
64
|
+
assert.equal(rec.results.length, 2);
|
|
65
|
+
const sidecar = JSON.parse(fs.readFileSync(t.verdictsPath, 'utf8'));
|
|
66
|
+
assert.equal(sidecar.verdict, 'transcript_errors', 'read-merge keeps runVerify keys');
|
|
67
|
+
assert.equal(sidecar.gateShadow.status, 'green');
|
|
68
|
+
const row = JSON.parse(fs.readFileSync(t.queuePath, 'utf8')).jobs.find((j) => j.slug === t.slug);
|
|
69
|
+
assert.equal(row.status, 'needs_review', 'shadow never transitions');
|
|
70
|
+
assert.equal(row.gateShadow.status, 'green');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('red gate stops at the first failure and records red; status still untouched', async () => {
|
|
74
|
+
const t = setup('red', ac('timeout 30 node -e "process.exit(3)" && timeout 30 node -e "process.exit(0)"'));
|
|
75
|
+
const rec = await runGateShadow(t.job);
|
|
76
|
+
assert.equal(rec.status, 'red');
|
|
77
|
+
assert.equal(rec.results.length, 1);
|
|
78
|
+
assert.equal(rec.results[0].code, 3);
|
|
79
|
+
const row = JSON.parse(fs.readFileSync(t.queuePath, 'utf8')).jobs.find((j) => j.slug === t.slug);
|
|
80
|
+
assert.equal(row.status, 'needs_review');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('the gate runs with CI=1 and an isolated TMPDIR', async () => {
|
|
84
|
+
const t = setup('env', ac('timeout 30 node -e "process.exit(process.env.CI===\'1\'&&process.env.TMPDIR.includes(\'sm-gate-shadow-\')?0:9)"'));
|
|
85
|
+
assert.equal((await runGateShadow(t.job)).status, 'green');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('gate: none is recorded as an explicit opt-out (unavailable), nothing spawned', async () => {
|
|
89
|
+
const t = setup('optout', '# Acceptance criteria\n\n- [ ] `timeout 30 node -e "process.exit(9)"`\n');
|
|
90
|
+
t.job.slug = t.slug;
|
|
91
|
+
fs.writeFileSync(path.join(resolveEpicPrdWriteDir(t.cwd, 'gate-epic'), `${t.slug}.md`), `---\ntitle: T\ncwd: ${t.cwd}\ngate: none\n---\nbody\n`);
|
|
92
|
+
const rec = await runGateShadow(t.job);
|
|
93
|
+
assert.equal(rec.status, 'unavailable');
|
|
94
|
+
assert.equal(rec.reason, 'gate-opt-out');
|
|
95
|
+
assert.deepEqual(rec.results, []);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('no parseable gate → unavailable', async () => {
|
|
99
|
+
const t = setup('nogate', '# Acceptance criteria\n\n- [ ] just prose\n');
|
|
100
|
+
const rec = await runGateShadow(t.job);
|
|
101
|
+
assert.equal(rec.status, 'unavailable');
|
|
102
|
+
assert.equal(rec.reason, 'no-parseable-gate');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('at most one shadow gate in flight: a concurrent run reports busy and runs nothing', async () => {
|
|
106
|
+
const slow = [{ argv: ['node', '-e', 'setTimeout(()=>{},400)'], timeoutMs: 10_000, env: {} }];
|
|
107
|
+
const first = runGateSequence(slow, { cwd: tmpHome });
|
|
108
|
+
const second = await runGateSequence(slow, { cwd: tmpHome });
|
|
109
|
+
assert.equal(second.status, 'busy');
|
|
110
|
+
assert.deepEqual(second.results, []);
|
|
111
|
+
assert.equal((await first).status, 'green');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a hung command is killed at its timeout and the gate is red', async () => {
|
|
115
|
+
const hang = [{ argv: ['node', '-e', 'setTimeout(()=>{},60000)'], timeoutMs: 300, env: {} }];
|
|
116
|
+
const r = await runGateSequence(hang, { cwd: tmpHome });
|
|
117
|
+
assert.equal(r.status, 'red');
|
|
118
|
+
assert.equal(r.results[0].timedOut, true);
|
|
119
|
+
});
|