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
|
@@ -16,14 +16,21 @@
|
|
|
16
16
|
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
19
|
-
import { test } from 'vitest';
|
|
19
|
+
import { test, afterAll } from 'vitest';
|
|
20
20
|
const assert = require('node:assert/strict');
|
|
21
21
|
const fs = require('node:fs');
|
|
22
22
|
const os = require('node:os');
|
|
23
23
|
const path = require('node:path');
|
|
24
24
|
|
|
25
25
|
// HOME before require: every state path is baked from os.homedir() at load.
|
|
26
|
-
|
|
26
|
+
// beforeAll runs AFTER module evaluation, so this assignment must stay at
|
|
27
|
+
// top level — see pollLoop-dispatch-on-failure.test.cjs:40-56 for the
|
|
28
|
+
// beforeAll/afterAll pattern this restore mirrors.
|
|
29
|
+
const originalHome = process.env.HOME;
|
|
30
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'starve-per-project-test-'));
|
|
31
|
+
process.env.HOME = tmpHome;
|
|
32
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
33
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
27
34
|
|
|
28
35
|
const {
|
|
29
36
|
classifyQueueStarvation,
|
|
@@ -31,6 +38,11 @@ const {
|
|
|
31
38
|
QUEUE_STARVATION_MS,
|
|
32
39
|
} = require('../scheduler.cjs');
|
|
33
40
|
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
process.env.HOME = originalHome;
|
|
43
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
34
46
|
const PROJECT_A = '/home/bilko/Projects/starry-night-ships';
|
|
35
47
|
const PROJECT_B = '/home/bilko/Projects/session-manager';
|
|
36
48
|
const NOW = Date.parse('2026-09-12T20:00:00.000Z');
|
|
@@ -53,11 +53,11 @@ beforeEach(() => {
|
|
|
53
53
|
// Fresh require per test so HISTORY_PATH's target file starts clean.
|
|
54
54
|
delete require.cache[require.resolve('../lib/queueHistory.cjs')];
|
|
55
55
|
queueHistory = require('../lib/queueHistory.cjs');
|
|
56
|
-
try { fs.unlinkSync(queueHistory.
|
|
56
|
+
try { fs.unlinkSync(queueHistory.historyPath()); } catch { /* ok if absent */ }
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
afterEach(() => {
|
|
60
|
-
try { fs.unlinkSync(queueHistory.
|
|
60
|
+
try { fs.unlinkSync(queueHistory.historyPath()); } catch { /* ok if absent */ }
|
|
61
61
|
if (prevOverride === undefined) delete process.env.SM_HISTORY_PATH_OVERRIDE;
|
|
62
62
|
else process.env.SM_HISTORY_PATH_OVERRIDE = prevOverride;
|
|
63
63
|
});
|
|
@@ -163,6 +163,25 @@ test('appendHistory dedupes by slug+runId against what is already on disk', asyn
|
|
|
163
163
|
expect(read.length).toBe(1);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
test('appendHistory: a needs_review ledger line and a terminal row sharing slug+runId do NOT dedupe against each other', async () => {
|
|
167
|
+
// An entry and its resolution line (needsReviewLedger.cjs) deliberately
|
|
168
|
+
// reference the SAME runId as each other — and a slug's terminal archive
|
|
169
|
+
// row can later reuse that same runId too (same run, different `kind`).
|
|
170
|
+
// jobKey must bucket by kind or the second/third write looks like a
|
|
171
|
+
// crash-replay of the first and silently never lands.
|
|
172
|
+
const entryLine = { kind: 'needs_review_entry', slug: 'ledger-dup', runId: 'shared-run', at: new Date(NOW).toISOString(), reason: 'uncommitted_changes', source: 'spawnJob:finalize' };
|
|
173
|
+
const resolutionLine = { kind: 'needs_review_resolution', slug: 'ledger-dup', runId: 'shared-run', at: new Date(NOW).toISOString(), resolvedTo: 'completed', ladderRung: 'reverify', source: 'reverifyNeedsReview:heal', dwellMs: 1000 };
|
|
174
|
+
const terminalLine = old({ slug: 'ledger-dup', runId: 'shared-run' });
|
|
175
|
+
const r1 = await queueHistory.appendHistory([entryLine]);
|
|
176
|
+
const r2 = await queueHistory.appendHistory([resolutionLine]);
|
|
177
|
+
const r3 = await queueHistory.appendHistory([terminalLine]);
|
|
178
|
+
expect(r1.appended).toBe(1);
|
|
179
|
+
expect(r2.appended).toBe(1);
|
|
180
|
+
expect(r3.appended).toBe(1);
|
|
181
|
+
const read = await queueHistory.readHistory({ limit: 10 });
|
|
182
|
+
expect(read.filter((j) => j.slug === 'ledger-dup')).toHaveLength(3);
|
|
183
|
+
});
|
|
184
|
+
|
|
166
185
|
test('readHistory respects limit and returns [] when file is absent', async () => {
|
|
167
186
|
const empty = await queueHistory.readHistory({ limit: 5 });
|
|
168
187
|
expect(empty).toEqual([]);
|
|
@@ -220,6 +239,18 @@ test('selectHistoryJobs: default historyEntries=[] preserves old call shape', ()
|
|
|
220
239
|
expect(merged.map((j) => j.slug)).toEqual(['01-fresh']);
|
|
221
240
|
});
|
|
222
241
|
|
|
242
|
+
test('selectHistoryJobs: needs_review ledger lines (kind !== "terminal") are excluded from the merged result', () => {
|
|
243
|
+
// The History view renders ScheduleJob-shaped rows (status, exitCode, ...)
|
|
244
|
+
// — a needs_review_entry/needs_review_resolution line (needsReviewLedger.cjs)
|
|
245
|
+
// has neither, and must not slip through as a statusless row in that table.
|
|
246
|
+
const { selectHistoryJobs } = require('../scheduler.cjs');
|
|
247
|
+
const terminalEntry = old({ slug: 'hist-1', runId: 'rhist1' });
|
|
248
|
+
const ledgerEntry = { kind: 'needs_review_entry', slug: 'ledger-1', runId: 'rledger1', at: new Date(NOW).toISOString(), reason: 'uncommitted_changes', source: 'spawnJob:finalize' };
|
|
249
|
+
const ledgerResolution = { kind: 'needs_review_resolution', slug: 'ledger-1', runId: 'rledger1', at: new Date(NOW).toISOString(), resolvedTo: 'completed', ladderRung: 'reverify', source: 'reverifyNeedsReview:heal', dwellMs: 500 };
|
|
250
|
+
const merged = selectHistoryJobs([], 10, [terminalEntry, ledgerEntry, ledgerResolution]);
|
|
251
|
+
expect(merged.map((j) => j.slug)).toEqual(['hist-1']);
|
|
252
|
+
});
|
|
253
|
+
|
|
223
254
|
// ---------- historyTerminalBySlug ----------
|
|
224
255
|
//
|
|
225
256
|
// Guards against scheduler.cjs's reconcile() resurrecting an already-
|
|
@@ -258,6 +289,22 @@ test('historyTerminalBySlug: cache invalidates after a new appendHistory call (m
|
|
|
258
289
|
expect(second.has('cache-2')).toBe(true);
|
|
259
290
|
});
|
|
260
291
|
|
|
292
|
+
test('historyTerminalBySlug: ignores needs_review ledger lines, even a LATER one for an already-terminal slug', async () => {
|
|
293
|
+
// Chronological order matters here: the terminal row lands first, and a
|
|
294
|
+
// needs_review_entry for the SAME slug (a later re-run parking again)
|
|
295
|
+
// lands after it. Without the kind guard, last-line-wins would replace
|
|
296
|
+
// the true 'completed' record with a status-less ledger row.
|
|
297
|
+
const terminal = old({ slug: 'mixed-1', status: 'completed', runId: 'rterm' });
|
|
298
|
+
await queueHistory.appendHistory([terminal]);
|
|
299
|
+
await queueHistory.appendHistory([{
|
|
300
|
+
kind: 'needs_review_entry', slug: 'mixed-1', runId: 'rlater', cwd: null,
|
|
301
|
+
at: new Date(NOW).toISOString(), reason: 'uncommitted_changes', source: 'spawnJob:finalize',
|
|
302
|
+
}]);
|
|
303
|
+
|
|
304
|
+
const map = await queueHistory.historyTerminalBySlug();
|
|
305
|
+
expect(map.get('mixed-1')).toEqual({ status: 'completed', finishedAt: terminal.finishedAt, landedCommit: null });
|
|
306
|
+
});
|
|
307
|
+
|
|
261
308
|
// ---------- completedSlugsForCwd (PRD 1122) ----------
|
|
262
309
|
//
|
|
263
310
|
// Feeds scheduler.cjs's computeDepHistorySatisfaction: a dependsOn slug with
|
|
@@ -290,3 +337,19 @@ test('completedSlugsForCwd: returns an empty Set when the project has no history
|
|
|
290
337
|
const slugs = await queueHistory.completedSlugsForCwd(freshCwd);
|
|
291
338
|
expect(slugs.size).toBe(0);
|
|
292
339
|
});
|
|
340
|
+
|
|
341
|
+
test('completedSlugsForCwd: ignores needs_review ledger lines even when they are the newest line for a completed slug', async () => {
|
|
342
|
+
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-1122-ledger-'));
|
|
343
|
+
await queueHistory.appendHistory([
|
|
344
|
+
old({ slug: '900-base', status: 'completed', runId: 'ra', cwd: projectCwd }),
|
|
345
|
+
]);
|
|
346
|
+
// A later needs_review park for a NEW run of the same slug must not blank
|
|
347
|
+
// out the earlier 'completed' record read by completedSlugsForCwd.
|
|
348
|
+
await queueHistory.appendHistory([{
|
|
349
|
+
kind: 'needs_review_entry', slug: '900-base', runId: 'rb', cwd: projectCwd,
|
|
350
|
+
at: new Date(NOW).toISOString(), reason: 'uncommitted_changes', source: 'spawnJob:finalize',
|
|
351
|
+
}]);
|
|
352
|
+
|
|
353
|
+
const slugs = await queueHistory.completedSlugsForCwd(projectCwd);
|
|
354
|
+
expect(slugs.has('900-base')).toBe(true);
|
|
355
|
+
});
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* pollLoop): the 'meter_rate_limited' branch used to never back off and
|
|
5
5
|
* never call persistSchedulerState(), letting scheduler-state.json freeze
|
|
6
6
|
* stale while the loop kept failing every POLL_INTERVAL_MS underneath it.
|
|
7
|
-
* This suite covers the
|
|
8
|
-
* curve/cap (nextBackoffMs)
|
|
9
|
-
* (shouldWarnFailureStreak)
|
|
7
|
+
* This suite covers the pure pieces of the fix: the shared backoff
|
|
8
|
+
* curve/cap (nextBackoffMs), the once-per-streak WARN gate
|
|
9
|
+
* (shouldWarnFailureStreak), and the periodic escalation ladder that fires
|
|
10
|
+
* every FAILURE_STREAK_ESCALATION_MS while a streak PERSISTS past the
|
|
11
|
+
* initial WARN, re-arming once the streak clears (shouldEscalateFailureStreak).
|
|
10
12
|
*
|
|
11
13
|
* Run: timeout 120 npx vitest run src/main/__tests__/rateLimitPollerStreak.test.cjs
|
|
12
14
|
*/
|
|
@@ -17,8 +19,10 @@ import { test, expect } from 'vitest';
|
|
|
17
19
|
const {
|
|
18
20
|
nextBackoffMs,
|
|
19
21
|
shouldWarnFailureStreak,
|
|
22
|
+
shouldEscalateFailureStreak,
|
|
20
23
|
BACKOFF_MAX_MS,
|
|
21
24
|
FAILURE_STREAK_WARN_THRESHOLD,
|
|
25
|
+
FAILURE_STREAK_ESCALATION_MS,
|
|
22
26
|
} = require('../scheduler.cjs');
|
|
23
27
|
|
|
24
28
|
test('nextBackoffMs starts at 30s from a zero/falsy backoff', () => {
|
|
@@ -77,3 +81,34 @@ test('shouldWarnFailureStreak respects a custom threshold', () => {
|
|
|
77
81
|
expect(shouldWarnFailureStreak(3, false, 3)).toBe(true);
|
|
78
82
|
expect(shouldWarnFailureStreak(2, false, 3)).toBe(false);
|
|
79
83
|
});
|
|
84
|
+
|
|
85
|
+
test('shouldEscalateFailureStreak: false below the WARN threshold, regardless of elapsed time', () => {
|
|
86
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD - 1, null, Date.now())).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('shouldEscalateFailureStreak: fires immediately the first time (lastEscalatedAtMs null) once past the WARN threshold', () => {
|
|
90
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, Date.now())).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('shouldEscalateFailureStreak: does not re-fire before FAILURE_STREAK_ESCALATION_MS has elapsed since the last one', () => {
|
|
94
|
+
const lastEscalatedAtMs = 1_000_000;
|
|
95
|
+
const tooSoon = lastEscalatedAtMs + FAILURE_STREAK_ESCALATION_MS - 1;
|
|
96
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, lastEscalatedAtMs, tooSoon)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('shouldEscalateFailureStreak: fires again once FAILURE_STREAK_ESCALATION_MS has elapsed — the persisting-streak ladder', () => {
|
|
100
|
+
const lastEscalatedAtMs = 1_000_000;
|
|
101
|
+
const dueNow = lastEscalatedAtMs + FAILURE_STREAK_ESCALATION_MS;
|
|
102
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, lastEscalatedAtMs, dueNow)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('shouldEscalateFailureStreak: re-arms after a streak clears (lastEscalatedAtMs reset to null) — a later independent streak escalates again', () => {
|
|
106
|
+
const firstStreakNow = 1_000_000;
|
|
107
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, firstStreakNow)).toBe(true);
|
|
108
|
+
// Streak clears (a success resets both failureStreakWarned and the
|
|
109
|
+
// escalation timestamp) — a LATER, independent streak crossing the WARN
|
|
110
|
+
// threshold again must escalate again immediately, not wait out a stale
|
|
111
|
+
// 30-minute window from the previous incident.
|
|
112
|
+
const laterStreakNow = firstStreakNow + 5 * 60_000; // well under 30 min later
|
|
113
|
+
expect(shouldEscalateFailureStreak(FAILURE_STREAK_WARN_THRESHOLD, null, laterStreakNow)).toBe(true);
|
|
114
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reconcileTiming.test.cjs — proves reconcile() (src/main/scheduler.cjs)
|
|
3
|
+
* records a per-phase timing breakdown and emits exactly one warn-level
|
|
4
|
+
* `logs.writeLine` when a pass exceeds RECONCILE_SLOW_PASS_MS
|
|
5
|
+
* (src/main/lib/schedulerConfig.cjs), and stays silent on a normal-speed
|
|
6
|
+
* pass. Before this, diagnosing a slow reconcile pass (the
|
|
7
|
+
* "schedule.state timed out after 5000ms" toast) required hand-timing the
|
|
8
|
+
* filesystem scans from a throwaway script.
|
|
9
|
+
*
|
|
10
|
+
* HOME-isolated (mirrors reconcileFlatPrdSweep.test.cjs): stub HOME to a
|
|
11
|
+
* mkdtemp dir before requiring scheduler.cjs, and register the fixture
|
|
12
|
+
* project as "active" via a fake ~/.claude/projects transcript — reconcile's
|
|
13
|
+
* PRD discovery (prdLocations.cjs) only scans cwds it can find that way.
|
|
14
|
+
*
|
|
15
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/reconcileTiming.test.cjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
import { test, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
let tmpHome;
|
|
26
|
+
let originalHome;
|
|
27
|
+
let scheduler;
|
|
28
|
+
let queueHistory;
|
|
29
|
+
let logs;
|
|
30
|
+
let RECONCILE_SLOW_PASS_MS;
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
originalHome = process.env.HOME;
|
|
34
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reconcile-timing-home-'));
|
|
35
|
+
process.env.HOME = tmpHome;
|
|
36
|
+
|
|
37
|
+
scheduler = require('../scheduler.cjs');
|
|
38
|
+
queueHistory = require('../lib/queueHistory.cjs');
|
|
39
|
+
logs = require('../logs.cjs');
|
|
40
|
+
({ RECONCILE_SLOW_PASS_MS } = require('../lib/schedulerConfig.cjs'));
|
|
41
|
+
|
|
42
|
+
if (!scheduler.PRDS_DIR.startsWith(tmpHome)) {
|
|
43
|
+
throw new Error(`refusing to run: PRDS_DIR (${scheduler.PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
process.env.HOME = originalHome;
|
|
49
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function registerActiveProject(cwd) {
|
|
57
|
+
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
58
|
+
const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
|
|
59
|
+
fs.mkdirSync(slugDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function makeFixtureProject(prefix) {
|
|
64
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
65
|
+
registerActiveProject(cwd);
|
|
66
|
+
return cwd;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeCanonicalPrd(cwd, epicId, slug) {
|
|
70
|
+
const epicPrdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
|
|
71
|
+
fs.mkdirSync(epicPrdsDir, { recursive: true });
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(epicPrdsDir, `${slug}.md`),
|
|
74
|
+
`---\ntitle: Timing fixture\ncwd: ${cwd}\nestimateMinutes: 10\nsourcePromptId: ${epicId}\ncreatedVia: scheduler-api\nissuedAt: 2026-08-07T00:00:00.000Z\n---\n\n# Goal\n\nSomething.\n`,
|
|
75
|
+
'utf8',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function warnCallsMatching(spy) {
|
|
80
|
+
return spy.mock.calls
|
|
81
|
+
.map(([payload]) => payload)
|
|
82
|
+
.filter((payload) => payload?.level === 'warn' && payload?.scope === 'scheduler' && /reconcile\(\) pass took/.test(payload?.message ?? ''));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('a normal-speed reconcile() pass logs nothing', async () => {
|
|
86
|
+
const cwd = makeFixtureProject('sm-reconcile-timing-fast-proj-');
|
|
87
|
+
writeCanonicalPrd(cwd, 'epic-fast', '1600-fast');
|
|
88
|
+
|
|
89
|
+
const writeLineSpy = vi.spyOn(logs, 'writeLine');
|
|
90
|
+
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
91
|
+
await scheduler.reconcile(state);
|
|
92
|
+
|
|
93
|
+
expect(warnCallsMatching(writeLineSpy)).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('a slow reconcile() pass emits exactly one warn with the per-phase breakdown', async () => {
|
|
97
|
+
const cwd = makeFixtureProject('sm-reconcile-timing-slow-proj-');
|
|
98
|
+
writeCanonicalPrd(cwd, 'epic-slow', '1601-slow');
|
|
99
|
+
|
|
100
|
+
const realHistoryTerminalBySlug = queueHistory.historyTerminalBySlug.bind(queueHistory);
|
|
101
|
+
vi.spyOn(queueHistory, 'historyTerminalBySlug').mockImplementation(async (...args) => {
|
|
102
|
+
await new Promise((resolve) => { setTimeout(resolve, RECONCILE_SLOW_PASS_MS + 200); });
|
|
103
|
+
return realHistoryTerminalBySlug(...args);
|
|
104
|
+
});
|
|
105
|
+
const writeLineSpy = vi.spyOn(logs, 'writeLine');
|
|
106
|
+
|
|
107
|
+
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
108
|
+
await scheduler.reconcile(state);
|
|
109
|
+
|
|
110
|
+
const warnCalls = warnCallsMatching(writeLineSpy);
|
|
111
|
+
expect(warnCalls).toHaveLength(1);
|
|
112
|
+
const { meta } = warnCalls[0];
|
|
113
|
+
expect(meta.totalMs).toBeGreaterThan(RECONCILE_SLOW_PASS_MS);
|
|
114
|
+
expect(meta.phaseMs).toHaveProperty('flatPrdSweep');
|
|
115
|
+
expect(meta.phaseMs).toHaveProperty('prdDirResolve');
|
|
116
|
+
expect(meta.phaseMs).toHaveProperty('parseLoop');
|
|
117
|
+
expect(meta.phaseMs).toHaveProperty('historyLookup');
|
|
118
|
+
expect(meta.phaseMs).toHaveProperty('queueWrite');
|
|
119
|
+
expect(meta.phaseMs.historyLookup).toBeGreaterThanOrEqual(RECONCILE_SLOW_PASS_MS);
|
|
120
|
+
expect(typeof meta.prdFileCount).toBe('number');
|
|
121
|
+
expect(typeof meta.resolvedDirCount).toBe('number');
|
|
122
|
+
expect(meta.prdFileCount).toBeGreaterThanOrEqual(1);
|
|
123
|
+
}, 15_000);
|
|
124
|
+
|
|
125
|
+
test('reconcile() surfaces its own error and does not swallow it while timing', async () => {
|
|
126
|
+
vi.spyOn(queueHistory, 'partitionJobs').mockImplementation(() => {
|
|
127
|
+
throw new Error('boom: injected failure for timing-swallow test');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const cwd = makeFixtureProject('sm-reconcile-timing-throw-proj-');
|
|
131
|
+
writeCanonicalPrd(cwd, 'epic-throw', '1602-throw');
|
|
132
|
+
|
|
133
|
+
const state = { jobs: [], invalidJobs: [], paused: null };
|
|
134
|
+
await expect(scheduler.reconcile(state)).rejects.toThrow('boom: injected failure for timing-swallow test');
|
|
135
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runVerify-landed-commit-outranks.test.cjs — ground truth (exit 0 + a commit
|
|
3
|
+
* landed THIS run + no explicit FAIL sentinel) outranks the transcript-
|
|
4
|
+
* pattern heuristics (final-20% is_error, FAIL/Traceback,
|
|
5
|
+
* ModuleNotFoundError), so a self-recovered tool error no longer parks a
|
|
6
|
+
* green, committed run.
|
|
7
|
+
*
|
|
8
|
+
* Fixture: the REAL run log for 1218-fo-01-move-scripts-lib-into-src-main-lib
|
|
9
|
+
* (session-manager-operations/reviews/2026-09-13-scheduler-stability-
|
|
10
|
+
* investigation.md), copied verbatim from
|
|
11
|
+
* ~/.claude/session-manager/scheduled-plans/runs/2026-09-13T21-52-51-192Z/ —
|
|
12
|
+
* a `git apply` "patch does not apply" tool error at event 179/209 (inside
|
|
13
|
+
* the final 20% of the transcript) that the agent retried and resolved 3
|
|
14
|
+
* events later, in a run that landed commit d1edf15 and was STILL parked
|
|
15
|
+
* needs_review (verdicts.json: verdict transcript_errors, reason
|
|
16
|
+
* "is_error=true in final 20% of transcript (event 179/209)"), spawning a
|
|
17
|
+
* $3.74 fix-plan investigation into work that needed no fix.
|
|
18
|
+
*
|
|
19
|
+
* The real run's own SCHEDULER_VERDICT sentinel is an explicit FAIL — an
|
|
20
|
+
* honest, UNRELATED report that test:unit was red for proven pre-existing/
|
|
21
|
+
* environmental reasons (shared /tmp contention), not a regression from this
|
|
22
|
+
* PRD's move. Per this PRD's own AC, an explicit FAIL sentinel must keep
|
|
23
|
+
* today's behavior byte-for-byte, so scenarios (a)/(b)/(d) below use a
|
|
24
|
+
* variant of the fixture with that trailing SCHEDULER_VERDICT line stripped
|
|
25
|
+
* (sentinel === null) — the majority real-world park shape this rule targets
|
|
26
|
+
* (the findings report measured 77 of 212 parks as transcript_errors with NO
|
|
27
|
+
* sentinel at all, vs. this one specific row's own additional, unrelated
|
|
28
|
+
* FAIL claim). Scenario (c) reuses the pristine, byte-for-byte-unmodified
|
|
29
|
+
* fixture to prove the real, still-present FAIL sentinel is honored exactly
|
|
30
|
+
* as before this change.
|
|
31
|
+
*
|
|
32
|
+
* Run standalone: timeout 120 npx vitest run src/main/__tests__/runVerify-landed-commit-outranks.test.cjs
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { test } from 'vitest';
|
|
36
|
+
const assert = require('node:assert/strict');
|
|
37
|
+
const os = require('node:os');
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const { execFileSync } = require('node:child_process');
|
|
41
|
+
const { verifyRun } = require('../runVerify.cjs');
|
|
42
|
+
|
|
43
|
+
const SLUG = '1218-fo-01-move-scripts-lib-into-src-main-lib';
|
|
44
|
+
const FIXTURE_PATH = path.join(__dirname, 'fixtures', `${SLUG}.log`);
|
|
45
|
+
const LANDED_COMMIT = 'd1edf15be50a783f08a55e6de328780f0feb3cb0';
|
|
46
|
+
|
|
47
|
+
function makeTmpDir() {
|
|
48
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-landed-commit-outranks-test-'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rmdir(dir) {
|
|
52
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeTmpGitRepo() {
|
|
56
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'run-verify-landed-commit-outranks-repo-'));
|
|
57
|
+
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
58
|
+
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: dir });
|
|
59
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: dir });
|
|
60
|
+
fs.writeFileSync(path.join(dir, 'README.md'), 'init\n');
|
|
61
|
+
execFileSync('git', ['add', 'README.md'], { cwd: dir });
|
|
62
|
+
execFileSync('git', ['commit', '-q', '-m', 'init'], { cwd: dir });
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writePrd(dir, slug) {
|
|
67
|
+
const text = '---\ntitle: Test PRD\ncwd: /tmp\nestimateMinutes: 30\n---\n# Move scripts/lib into src/main/lib\n';
|
|
68
|
+
fs.writeFileSync(path.join(dir, `${slug}.md`), text);
|
|
69
|
+
return path.join(dir, `${slug}.md`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load the real fixture log, optionally stripping the trailing
|
|
74
|
+
* `SCHEDULER_VERDICT: FAIL ...` line from the final `{"type":"result",...}`
|
|
75
|
+
* event's `result` field so the sentinel scan sees no sentinel at all.
|
|
76
|
+
*/
|
|
77
|
+
function loadFixture({ stripFailSentinel }) {
|
|
78
|
+
const raw = fs.readFileSync(FIXTURE_PATH, 'utf8');
|
|
79
|
+
if (!stripFailSentinel) return raw;
|
|
80
|
+
const lines = raw.split('\n');
|
|
81
|
+
const idx = lines.findIndex((l) => l.includes('"type":"result"'));
|
|
82
|
+
assert.ok(idx !== -1, 'fixture must contain a type:result line');
|
|
83
|
+
const obj = JSON.parse(lines[idx]);
|
|
84
|
+
assert.ok(/SCHEDULER_VERDICT:\s*FAIL/.test(obj.result), 'fixture result must carry the real FAIL sentinel before stripping');
|
|
85
|
+
obj.result = obj.result.replace(/\n?SCHEDULER_VERDICT:\s*FAIL[^\n]*$/, '');
|
|
86
|
+
assert.ok(!/SCHEDULER_VERDICT/.test(obj.result), 'sentinel must be fully removed from the stripped variant');
|
|
87
|
+
lines[idx] = JSON.stringify(obj);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeLog(dir, slug, text) {
|
|
92
|
+
fs.writeFileSync(path.join(dir, `${slug}.log`), text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// (a) exit 0 + commit landed THIS run + no sentinel at all → clean, demoted to annotation.
|
|
96
|
+
test('exit 0 + landed commit this run + no FAIL sentinel → clean with annotation (1218-fo-01)', async () => {
|
|
97
|
+
const tmp = makeTmpDir();
|
|
98
|
+
const repo = makeTmpGitRepo();
|
|
99
|
+
try {
|
|
100
|
+
writeLog(tmp, SLUG, loadFixture({ stripFailSentinel: true }));
|
|
101
|
+
const prdPath = writePrd(tmp, SLUG);
|
|
102
|
+
const verdict = await verifyRun({
|
|
103
|
+
runDir: tmp,
|
|
104
|
+
prdPath,
|
|
105
|
+
queueEntry: { slug: SLUG, status: 'running', cwd: repo },
|
|
106
|
+
allJobs: [],
|
|
107
|
+
committedDuringRun: false,
|
|
108
|
+
jobLandedCommitThisRun: LANDED_COMMIT,
|
|
109
|
+
exitCode: 0,
|
|
110
|
+
});
|
|
111
|
+
assert.equal(verdict.verdict, 'clean', `expected clean, got ${verdict.verdict}: ${verdict.reason}`);
|
|
112
|
+
assert.equal(verdict.downgradeTo, null);
|
|
113
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(tmp, `${SLUG}.verdicts.json`), 'utf8'));
|
|
114
|
+
assert.ok(Array.isArray(sidecar.annotations) && sidecar.annotations.length > 0, 'the demoted is_error hit must be recorded as an annotation');
|
|
115
|
+
assert.ok(sidecar.annotations.some((a) => a.verdict === 'transcript_errors'), 'annotation must carry the original transcript_errors verdict');
|
|
116
|
+
} finally { rmdir(tmp); rmdir(repo); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// (b) the same (stripped-sentinel) log, but exitCode !== 0 → keeps today's behavior.
|
|
120
|
+
test('non-zero exit → transcript_errors (evidence never applies to a failing run)', async () => {
|
|
121
|
+
const tmp = makeTmpDir();
|
|
122
|
+
const repo = makeTmpGitRepo();
|
|
123
|
+
try {
|
|
124
|
+
writeLog(tmp, SLUG, loadFixture({ stripFailSentinel: true }));
|
|
125
|
+
const prdPath = writePrd(tmp, SLUG);
|
|
126
|
+
const verdict = await verifyRun({
|
|
127
|
+
runDir: tmp,
|
|
128
|
+
prdPath,
|
|
129
|
+
queueEntry: { slug: SLUG, status: 'running', cwd: repo },
|
|
130
|
+
allJobs: [],
|
|
131
|
+
committedDuringRun: false,
|
|
132
|
+
jobLandedCommitThisRun: LANDED_COMMIT,
|
|
133
|
+
exitCode: 1,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(verdict.verdict, 'transcript_errors', `expected transcript_errors, got ${verdict.verdict}: ${verdict.reason}`);
|
|
136
|
+
assert.equal(verdict.downgradeTo, 'needs_review');
|
|
137
|
+
} finally { rmdir(tmp); rmdir(repo); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// (c) the pristine, byte-for-byte-unmodified fixture — its own real FAIL sentinel — keeps today's behavior.
|
|
141
|
+
test('explicit FAIL sentinel (the real, unmodified fixture) → transcript_errors byte-for-byte', async () => {
|
|
142
|
+
const tmp = makeTmpDir();
|
|
143
|
+
const repo = makeTmpGitRepo();
|
|
144
|
+
try {
|
|
145
|
+
writeLog(tmp, SLUG, loadFixture({ stripFailSentinel: false }));
|
|
146
|
+
const prdPath = writePrd(tmp, SLUG);
|
|
147
|
+
const verdict = await verifyRun({
|
|
148
|
+
runDir: tmp,
|
|
149
|
+
prdPath,
|
|
150
|
+
queueEntry: { slug: SLUG, status: 'running', cwd: repo },
|
|
151
|
+
allJobs: [],
|
|
152
|
+
committedDuringRun: false,
|
|
153
|
+
jobLandedCommitThisRun: LANDED_COMMIT,
|
|
154
|
+
exitCode: 0,
|
|
155
|
+
});
|
|
156
|
+
assert.equal(verdict.verdict, 'transcript_errors', `expected transcript_errors, got ${verdict.verdict}: ${verdict.reason}`);
|
|
157
|
+
assert.equal(verdict.downgradeTo, 'needs_review');
|
|
158
|
+
assert.equal(verdict.sentinel, 'fail');
|
|
159
|
+
} finally { rmdir(tmp); rmdir(repo); }
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// (d) no landed-commit evidence at all → transcript_errors (evidence gate requires attribution).
|
|
163
|
+
test('no landed commit evidence → transcript_errors (evidence gate requires attribution)', async () => {
|
|
164
|
+
const tmp = makeTmpDir();
|
|
165
|
+
const repo = makeTmpGitRepo();
|
|
166
|
+
try {
|
|
167
|
+
writeLog(tmp, SLUG, loadFixture({ stripFailSentinel: true }));
|
|
168
|
+
const prdPath = writePrd(tmp, SLUG);
|
|
169
|
+
const verdict = await verifyRun({
|
|
170
|
+
runDir: tmp,
|
|
171
|
+
prdPath,
|
|
172
|
+
queueEntry: { slug: SLUG, status: 'running', cwd: repo },
|
|
173
|
+
allJobs: [],
|
|
174
|
+
committedDuringRun: false,
|
|
175
|
+
jobLandedCommitThisRun: null,
|
|
176
|
+
exitCode: 0,
|
|
177
|
+
});
|
|
178
|
+
assert.equal(verdict.verdict, 'transcript_errors', `expected transcript_errors, got ${verdict.verdict}: ${verdict.reason}`);
|
|
179
|
+
assert.equal(verdict.downgradeTo, 'needs_review');
|
|
180
|
+
} finally { rmdir(tmp); rmdir(repo); }
|
|
181
|
+
});
|
|
@@ -1140,7 +1140,7 @@ function noOpRunEvents(resultText) {
|
|
|
1140
1140
|
test('no_verdict_sentinel: no tool_result issues, no sentinel, no commit → needs_review', async () => {
|
|
1141
1141
|
const tmp = makeTmpDir();
|
|
1142
1142
|
try {
|
|
1143
|
-
const slug = '406-
|
|
1143
|
+
const slug = '406-capture-panel-ui';
|
|
1144
1144
|
writeLog(tmp, slug, noOpRunEvents('I need clarification on which capture API to target before proceeding.'));
|
|
1145
1145
|
const prdPath = writePrd(tmp, slug, '# Browser capture panel UI');
|
|
1146
1146
|
const verdict = await verifyRun({
|
|
@@ -1160,7 +1160,7 @@ test('no_verdict_sentinel: no tool_result issues, no sentinel, no commit → nee
|
|
|
1160
1160
|
test('no_verdict_sentinel guard: same run but committedDuringRun:true → stays clean', async () => {
|
|
1161
1161
|
const tmp = makeTmpDir();
|
|
1162
1162
|
try {
|
|
1163
|
-
const slug = '406-
|
|
1163
|
+
const slug = '406-capture-panel-ui-committed';
|
|
1164
1164
|
writeLog(tmp, slug, noOpRunEvents('I need clarification on which capture API to target before proceeding.'));
|
|
1165
1165
|
const prdPath = writePrd(tmp, slug, '# Browser capture panel UI');
|
|
1166
1166
|
const verdict = await verifyRun({
|
|
@@ -1180,7 +1180,7 @@ test('no_verdict_sentinel guard: same run but committedDuringRun:true → stays
|
|
|
1180
1180
|
test('no_verdict_sentinel guard: same run but with SCHEDULER_VERDICT: PASS sentinel + a commit → stays clean', async () => {
|
|
1181
1181
|
const tmp = makeTmpDir();
|
|
1182
1182
|
try {
|
|
1183
|
-
const slug = '406-
|
|
1183
|
+
const slug = '406-capture-panel-ui-sentinel';
|
|
1184
1184
|
writeLog(tmp, slug, noOpRunEvents('All acceptance criteria verified.\nSCHEDULER_VERDICT: PASS'));
|
|
1185
1185
|
const prdPath = writePrd(tmp, slug, '# Browser capture panel UI');
|
|
1186
1186
|
const verdict = await verifyRun({
|
|
@@ -1634,7 +1634,7 @@ test('isMergeMainSlug matches the NN-prXXX-merge-main and NN-fix-prXXX-merge-mai
|
|
|
1634
1634
|
assert.equal(isMergeMainSlug('565-pr188-merge-main'), true);
|
|
1635
1635
|
assert.equal(isMergeMainSlug('571-fix-pr141-merge-main'), true);
|
|
1636
1636
|
assert.equal(isMergeMainSlug('523-fix-bounded-fix-plan-retry'), false);
|
|
1637
|
-
assert.equal(isMergeMainSlug('406-
|
|
1637
|
+
assert.equal(isMergeMainSlug('406-capture-panel-ui'), false);
|
|
1638
1638
|
assert.equal(isMergeMainSlug(''), false);
|
|
1639
1639
|
assert.equal(isMergeMainSlug(undefined), false);
|
|
1640
1640
|
});
|
|
@@ -1727,7 +1727,7 @@ test('merge-main exemption: gh call throws → falls through to pass_no_commit,
|
|
|
1727
1727
|
test('merge-main exemption: non-merge-main slug never attempts a gh call', async () => {
|
|
1728
1728
|
const tmp = makeTmpDir();
|
|
1729
1729
|
try {
|
|
1730
|
-
const slug = '406-
|
|
1730
|
+
const slug = '406-capture-panel-ui';
|
|
1731
1731
|
writeLog(tmp, slug, noOpRunEvents('All acceptance criteria verified.\nSCHEDULER_VERDICT: PASS'));
|
|
1732
1732
|
const prdPath = writePrd(tmp, slug, '# Browser capture panel UI');
|
|
1733
1733
|
let ghCalls = 0;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* is types-only (erased at build time, nothing to import at test runtime), so
|
|
7
7
|
* each consumer keeps its own hand-written mirror of the status enum:
|
|
8
8
|
* - src/preload/api.d.ts's `ScheduleJobStatus` union
|
|
9
|
-
* - src/renderer/
|
|
9
|
+
* - src/renderer/lib/jobStatus.ts's `JobStatus` union
|
|
10
10
|
* - src/renderer/components/SchedulePanel.tsx's `FilterStatus` union
|
|
11
11
|
* (which also carries the UI-only 'all' meta-value)
|
|
12
12
|
*
|
|
@@ -52,8 +52,8 @@ test('src/preload/api.d.ts ScheduleJobStatus matches JOB_STATUSES', () => {
|
|
|
52
52
|
expect(sorted(values)).toEqual(sorted(JOB_STATUSES));
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
test('src/renderer/
|
|
56
|
-
const source = readSource('src/renderer/
|
|
55
|
+
test('src/renderer/lib/jobStatus.ts JobStatus matches JOB_STATUSES', () => {
|
|
56
|
+
const source = readSource('src/renderer/lib/jobStatus.ts');
|
|
57
57
|
const values = extractQuotedList(source, /export type JobStatus = ([^\n]+)/);
|
|
58
58
|
expect(sorted(values)).toEqual(sorted(JOB_STATUSES));
|
|
59
59
|
});
|