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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-tick-wedge.test.cjs — tickQueue's serialized chain (tickTail) must
|
|
3
|
+
* survive a tick body that never settles: the body is bounded by a watchdog,
|
|
4
|
+
* the chain resets, and a late-resuming stale body is fenced off (no spawn, no
|
|
5
|
+
* mutate). mutateTail is never reset.
|
|
6
|
+
*
|
|
7
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-tick-wedge.test.cjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
let tmpHome;
|
|
18
|
+
let originalHome;
|
|
19
|
+
let originalClaudeBin;
|
|
20
|
+
let originalAutofix;
|
|
21
|
+
let originalWatchdog;
|
|
22
|
+
let originalClaudeVersion;
|
|
23
|
+
let scheduler;
|
|
24
|
+
let queueStore;
|
|
25
|
+
let auditLog;
|
|
26
|
+
let spawnMarker;
|
|
27
|
+
|
|
28
|
+
function auditKinds() {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(auditLog.auditLogPath(), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l).kind);
|
|
31
|
+
} catch { return []; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
originalClaudeBin = process.env.SM_CLAUDE_BIN;
|
|
37
|
+
originalAutofix = process.env.SM_AUTOFIX_DISABLE;
|
|
38
|
+
originalWatchdog = process.env.SM_TICK_WATCHDOG_MS;
|
|
39
|
+
// This test's spawn marker is written by the stub on ANY invocation, including the
|
|
40
|
+
// `claude --version` probe — so it needs the real probe, not the sandbox's pinned version.
|
|
41
|
+
originalClaudeVersion = process.env.SM_CLAUDE_VERSION;
|
|
42
|
+
delete process.env.SM_CLAUDE_VERSION;
|
|
43
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-tick-wedge-'));
|
|
44
|
+
process.env.HOME = tmpHome;
|
|
45
|
+
process.env.SM_AUTOFIX_DISABLE = '1';
|
|
46
|
+
// Any spawn of the "claude" binary drops a marker file: proof spawnJob ran.
|
|
47
|
+
spawnMarker = path.join(tmpHome, 'claude-spawned.marker');
|
|
48
|
+
const stubPath = path.join(tmpHome, 'claude-stub.cjs');
|
|
49
|
+
const body = `require('node:fs').appendFileSync(${JSON.stringify(spawnMarker)}, 'x');\n`
|
|
50
|
+
+ `process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');\n`
|
|
51
|
+
+ 'process.exit(0);\n';
|
|
52
|
+
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}`, { mode: 0o755 });
|
|
53
|
+
process.env.SM_CLAUDE_BIN = stubPath;
|
|
54
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager'), { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.join(tmpHome, '.claude', 'projects'), { recursive: true });
|
|
56
|
+
scheduler = require('../scheduler.cjs');
|
|
57
|
+
queueStore = require('../lib/queueStore.cjs');
|
|
58
|
+
auditLog = require('../lib/auditLog.cjs');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
process.env.HOME = originalHome;
|
|
63
|
+
for (const [k, v] of [['SM_CLAUDE_BIN', originalClaudeBin], ['SM_AUTOFIX_DISABLE', originalAutofix], ['SM_TICK_WATCHDOG_MS', originalWatchdog], ['SM_CLAUDE_VERSION', originalClaudeVersion]]) {
|
|
64
|
+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
65
|
+
}
|
|
66
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a never-settling tick body is declared wedged, the chain resets, the stale body is fenced, audit fires once', async () => {
|
|
70
|
+
process.env.SM_TICK_WATCHDOG_MS = '150';
|
|
71
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-tick-wedge-project-'));
|
|
72
|
+
const projDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'projects', 'sm-tick-wedge-fake-'));
|
|
73
|
+
fs.writeFileSync(path.join(projDir, 'session.jsonl'), `${JSON.stringify({ cwd })}\n`, 'utf8');
|
|
74
|
+
const slug = `9003-tick-wedge-${process.pid}`;
|
|
75
|
+
await scheduler.writeQueue({
|
|
76
|
+
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
|
|
77
|
+
config: {},
|
|
78
|
+
paused: null,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The first readMerged (the tick body's first await) hangs until released;
|
|
82
|
+
// every later call delegates to the real reader.
|
|
83
|
+
const realRead = queueStore.readMerged;
|
|
84
|
+
let release;
|
|
85
|
+
const hung = new Promise((r) => { release = r; });
|
|
86
|
+
let calls = 0;
|
|
87
|
+
queueStore.readMerged = function patched(...args) {
|
|
88
|
+
calls += 1;
|
|
89
|
+
if (calls === 1) return hung;
|
|
90
|
+
return realRead.apply(this, args);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const wedgedResult = await scheduler.tickQueue();
|
|
95
|
+
expect(wedgedResult).toMatchObject({ fired: false, reason: 'wedged' });
|
|
96
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged')).toHaveLength(1);
|
|
97
|
+
|
|
98
|
+
// A second wedge in the same episode (no normal tick between) must not re-audit.
|
|
99
|
+
calls = 0;
|
|
100
|
+
const hung2 = new Promise(() => {});
|
|
101
|
+
queueStore.readMerged = function patched2(...args) {
|
|
102
|
+
calls += 1;
|
|
103
|
+
if (calls === 1) return hung2;
|
|
104
|
+
return realRead.apply(this, args);
|
|
105
|
+
};
|
|
106
|
+
const again = await scheduler.tickQueue();
|
|
107
|
+
expect(again).toMatchObject({ reason: 'wedged' });
|
|
108
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged')).toHaveLength(1);
|
|
109
|
+
|
|
110
|
+
// Next tick runs on a fresh chain (real reader, generous budget).
|
|
111
|
+
queueStore.readMerged = realRead;
|
|
112
|
+
process.env.SM_TICK_WATCHDOG_MS = '30000';
|
|
113
|
+
// Launch gates this case controls, so the dispatch assertion below does not
|
|
114
|
+
// depend on host state: the CPU load gate (loadavg1/cores > 0.85 -> 'load-deferred')
|
|
115
|
+
// via tickQueue's bypassLoadGate, and the memory gate ('memory-deferred', reads
|
|
116
|
+
// MemAvailable from /proc/meminfo) via a stubbed fs.readFileSync for that one path.
|
|
117
|
+
const realReadFileSync = fs.readFileSync;
|
|
118
|
+
fs.readFileSync = function patchedReadFileSync(file, ...rest) {
|
|
119
|
+
if (file === '/proc/meminfo') return 'MemAvailable: 67108864 kB\n';
|
|
120
|
+
return realReadFileSync.call(this, file, ...rest);
|
|
121
|
+
};
|
|
122
|
+
let fresh;
|
|
123
|
+
try {
|
|
124
|
+
fresh = await scheduler.tickQueue({ bypassLoadGate: true });
|
|
125
|
+
} finally {
|
|
126
|
+
fs.readFileSync = realReadFileSync;
|
|
127
|
+
}
|
|
128
|
+
expect(fresh.reason).not.toBe('wedged');
|
|
129
|
+
|
|
130
|
+
// Let the fresh tick's own spawn (if any) finish, then clear the marker.
|
|
131
|
+
// The fresh tick must have dispatched: wait for the stub's marker, then for
|
|
132
|
+
// the row to leave running/pending (spawnJob is fire-and-forget).
|
|
133
|
+
const deadline = Date.now() + 10_000;
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
const row = queueStore.readMergedSync().jobs.find((j) => j.slug === slug);
|
|
136
|
+
if (fs.existsSync(spawnMarker) && (!row || (row.status !== 'running' && row.status !== 'pending'))) break;
|
|
137
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
138
|
+
}
|
|
139
|
+
expect(fs.existsSync(spawnMarker)).toBe(true);
|
|
140
|
+
fs.rmSync(spawnMarker, { force: true });
|
|
141
|
+
|
|
142
|
+
// Late-resume the stale body with a state that WOULD dispatch a pending job.
|
|
143
|
+
const before = queueStore.readMergedSync();
|
|
144
|
+
const staleState = {
|
|
145
|
+
...before,
|
|
146
|
+
jobs: [{ slug: `9004-tick-wedge-stale-${process.pid}`, title: 'y', cwd, status: 'pending', dependsOn: [] }],
|
|
147
|
+
config: { ...(before.config || {}) },
|
|
148
|
+
};
|
|
149
|
+
release(staleState);
|
|
150
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
151
|
+
expect(fs.existsSync(spawnMarker)).toBe(false);
|
|
152
|
+
} finally {
|
|
153
|
+
queueStore.readMerged = realRead;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a slow-but-healthy tick under the budget is not flagged as wedged', async () => {
|
|
158
|
+
process.env.SM_TICK_WATCHDOG_MS = '2000';
|
|
159
|
+
const realRead = queueStore.readMerged;
|
|
160
|
+
const before = auditKinds().filter((k) => k === 'tick_wedged').length;
|
|
161
|
+
queueStore.readMerged = async function slow(...args) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
163
|
+
return realRead.apply(this, args);
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
const r = await scheduler.tickQueue();
|
|
167
|
+
expect(r?.reason).not.toBe('wedged');
|
|
168
|
+
expect(auditKinds().filter((k) => k === 'tick_wedged').length).toBe(before);
|
|
169
|
+
} finally {
|
|
170
|
+
queueStore.readMerged = realRead;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* requeue instead of burning a whole run + an auto-fix investigation on an
|
|
5
5
|
* outage that has nothing to do with the code.
|
|
6
6
|
*
|
|
7
|
-
* Run: timeout
|
|
7
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-transient-failure.test.cjs
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { test } from 'vitest';
|
|
13
13
|
const assert = require('node:assert/strict');
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const os = require('node:os');
|
|
@@ -18,6 +18,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
21
22
|
const { execFileSync } = require('node:child_process');
|
|
22
23
|
|
|
23
24
|
let tmpHome;
|
|
@@ -30,6 +31,24 @@ function git(args, cwd) {
|
|
|
30
31
|
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// A completed job's PRD is archived immediately (archiveCompletedPrd, called
|
|
35
|
+
// for EVERY job that reaches effectiveStatus:'completed' — not just a
|
|
36
|
+
// retention-aged one), and the very next reconcile() pass (spawnJob's own
|
|
37
|
+
// broadcast({flush:true}) right after finishing) then finds the row's PRD
|
|
38
|
+
// gone and backfills+drops it from queue.json into history.jsonl in that
|
|
39
|
+
// SAME pass — see scheduler-reconcile-history-backfill.test.cjs. So a job
|
|
40
|
+
// that actually completed is found in EITHER queue.json OR history.jsonl,
|
|
41
|
+
// never guaranteed to still be the former.
|
|
42
|
+
function readTerminalRow(queuePath, projectCwd, slug) {
|
|
43
|
+
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
44
|
+
const row = jobs.find((j) => j.slug === slug);
|
|
45
|
+
if (row) return row;
|
|
46
|
+
const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
|
|
47
|
+
if (!fs.existsSync(historyPath)) return undefined;
|
|
48
|
+
const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
49
|
+
return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
function initRepo(dir) {
|
|
34
53
|
fs.mkdirSync(dir, { recursive: true });
|
|
35
54
|
git(['init', '-q'], dir);
|
|
@@ -61,8 +80,9 @@ function writeProjectQueue(cwd, jobs) {
|
|
|
61
80
|
// test can prove whether — and where — it was actually spawned, then emits a
|
|
62
81
|
// clean stream-json success result.
|
|
63
82
|
function writeClaudeStub() {
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
return claudeStub.writeClaudeStub({
|
|
84
|
+
allowCommitUnder: os.tmpdir(),
|
|
85
|
+
body: `
|
|
66
86
|
const fs = require('fs');
|
|
67
87
|
const path = require('path');
|
|
68
88
|
const { execFileSync } = require('child_process');
|
|
@@ -73,14 +93,13 @@ function writeClaudeStub() {
|
|
|
73
93
|
// fallback case), where the run parks in needs_review instead, which is
|
|
74
94
|
// fine — this test only cares whether it ran in place, not its verdict.
|
|
75
95
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
runGit(['add', '-A']);
|
|
97
|
+
runGit(['commit', '-q', '-m', 'stub commit']);
|
|
78
98
|
} catch { /* not a git repo — best-effort only */ }
|
|
79
99
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');
|
|
80
100
|
process.exit(0);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return stubPath;
|
|
101
|
+
`,
|
|
102
|
+
});
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
beforeAll(() => {
|
|
@@ -111,7 +130,13 @@ test('a job held behind the worktree cap stays pending with a heldReason, is nev
|
|
|
111
130
|
registerActiveProject(projectCwd);
|
|
112
131
|
|
|
113
132
|
const slug = `1112-test-held-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
114
|
-
|
|
133
|
+
// Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
|
|
134
|
+
// swept into prds-archived/ by consolidateFlatPrds on every reconcile()
|
|
135
|
+
// pass once a row is no longer 'pending'/'running' — which would strand
|
|
136
|
+
// this test's row the moment it completes (reconcile()'s terminal-job
|
|
137
|
+
// "PRD gone -> archived on purpose" drop, see
|
|
138
|
+
// scheduler-reconcile-history-backfill.test.cjs).
|
|
139
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
115
140
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
116
141
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Held behind the worktree cap.', 'utf8');
|
|
117
142
|
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
@@ -147,8 +172,8 @@ test('a job held behind the worktree cap stays pending with a heldReason, is nev
|
|
|
147
172
|
|
|
148
173
|
await spawnJob({ slug, cwd: projectCwd }, `${runId}-2`, runDir, projectCwd);
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
row
|
|
175
|
+
row = readTerminalRow(queuePath, projectCwd, slug);
|
|
176
|
+
expect(row).toBeTruthy();
|
|
152
177
|
expect(row.status).toBe('completed');
|
|
153
178
|
expect(row.heldReason).toBeUndefined();
|
|
154
179
|
} finally {
|
|
@@ -163,7 +188,8 @@ test('a non-git cwd still falls back to running in place (unchanged behaviour
|
|
|
163
188
|
registerActiveProject(projectCwd);
|
|
164
189
|
|
|
165
190
|
const slug = `1112-test-nongit-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
166
|
-
|
|
191
|
+
// Epic-scoped — see the sibling test above for why not the flat prds/ dir.
|
|
192
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
167
193
|
fs.mkdirSync(prdsDir, { recursive: true });
|
|
168
194
|
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Non-git cwd, must run in place.', 'utf8');
|
|
169
195
|
const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
|
|
@@ -187,6 +213,20 @@ test('a non-git cwd still falls back to running in place (unchanged behaviour
|
|
|
187
213
|
expect(row.worktreeFallbackReason).toMatch(/not a git repository/);
|
|
188
214
|
// Ran IN PLACE — the marker landed directly in projectCwd, not a worktree.
|
|
189
215
|
expect(fs.existsSync(path.join(projectCwd, 'ran-here.marker'))).toBe(true);
|
|
216
|
+
|
|
217
|
+
// A job losing worktree isolation must never be a silent downgrade
|
|
218
|
+
// discoverable only by reading queue.json afterwards — it must also land
|
|
219
|
+
// as a warn line in the durable ops error log, naming the slug, cwd, and
|
|
220
|
+
// reason.
|
|
221
|
+
const { todayFile } = require('../lib/opsErrorLog.cjs');
|
|
222
|
+
const errorLogPath = todayFile(projectCwd);
|
|
223
|
+
expect(fs.existsSync(errorLogPath)).toBe(true);
|
|
224
|
+
const logLines = fs.readFileSync(errorLogPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
225
|
+
const fallbackLine = logLines.find((l) => l.scope === 'scheduler' && l.level === 'warn' && l.message.includes(slug));
|
|
226
|
+
expect(fallbackLine).toBeTruthy();
|
|
227
|
+
expect(fallbackLine.message).toMatch(/SHARED working tree/);
|
|
228
|
+
expect(fallbackLine.message).toMatch(/not a git repository/);
|
|
229
|
+
expect(fallbackLine.meta).toMatchObject({ slug, cwd: projectCwd, reason: expect.stringMatching(/not a git repository/) });
|
|
190
230
|
} finally {
|
|
191
231
|
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
192
232
|
fs.rmSync(runDir, { recursive: true, force: true });
|
|
@@ -23,6 +23,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
23
23
|
const fs = require('node:fs');
|
|
24
24
|
const os = require('node:os');
|
|
25
25
|
const path = require('node:path');
|
|
26
|
+
const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
|
|
26
27
|
|
|
27
28
|
let tmpHome;
|
|
28
29
|
let originalHome;
|
|
@@ -48,16 +49,13 @@ afterEach(() => {
|
|
|
48
49
|
// where it actually ran), captures the '-p' prompt, then emits a stream-json
|
|
49
50
|
// success result and exits 0.
|
|
50
51
|
function writeClaudeStub() {
|
|
51
|
-
|
|
52
|
-
const body = `
|
|
52
|
+
return claudeStub.writeClaudeStub({ body: `
|
|
53
53
|
const fs = require('fs');
|
|
54
54
|
const path = require('path');
|
|
55
55
|
fs.writeFileSync(path.join(process.cwd(), 'ran-here.marker'), 'yes', 'utf8');
|
|
56
56
|
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
57
57
|
process.exit(0);
|
|
58
|
-
|
|
59
|
-
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
60
|
-
return stubPath;
|
|
58
|
+
` });
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
test('executeJob spawns the child in execCwd (worktree) while resolving the PRD from job.cwd (main tree)', async () => {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usageSingleFlight.test.cjs — usage.cjs's fetchUsage() is the single owner
|
|
3
|
+
* of caching, single-flight de-duplication, and the shared usageCircuit for
|
|
4
|
+
* the /api/oauth/usage meter. Before this, the renderer's billing:fetch IPC
|
|
5
|
+
* handler owned the 30s OK-cache alone and the scheduler's pollLoop called
|
|
6
|
+
* fetchUsage() directly with no cache/dedup — two independent request
|
|
7
|
+
* streams against a rate-limiting endpoint, and a success on one path never
|
|
8
|
+
* reset the other's failure streak. This suite proves: (1) concurrent
|
|
9
|
+
* fetchUsage() callers coalesce into exactly one network fetch, (2) a
|
|
10
|
+
* success resets the shared circuit's streak regardless of which "caller"
|
|
11
|
+
* triggered the preceding failures, and (3) a 429's Retry-After suppresses
|
|
12
|
+
* the next request until it elapses.
|
|
13
|
+
*
|
|
14
|
+
* Never hits the live billing endpoint — global.fetch is stubbed throughout,
|
|
15
|
+
* and HOME is pointed at a scratch dir so no real credentials/cache file on
|
|
16
|
+
* this machine is ever read.
|
|
17
|
+
*
|
|
18
|
+
* usage.cjs (and config.cjs/credentials.cjs, which it requires) are loaded
|
|
19
|
+
* via Node's own require() — this is a .cjs test, not transformed ESM — so
|
|
20
|
+
* vi.mock()/vi.resetModules() do NOT intercept them the way they would for
|
|
21
|
+
* an ESM import graph; both also bake os.homedir() into module-level
|
|
22
|
+
* constants at first require (config.cjs's allowedRoots/WRITE_PREFIXES,
|
|
23
|
+
* usage.cjs's CACHE_PATH, credentials.cjs's CREDS_PATH). So this suite
|
|
24
|
+
* follows historyRollup.test.cjs's pattern instead: swap $HOME to a fresh
|
|
25
|
+
* tmp dir with a valid, far-from-expiry fake credentials file, purge
|
|
26
|
+
* Node's require.cache for every affected module, and re-require fresh
|
|
27
|
+
* each test.
|
|
28
|
+
*
|
|
29
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/usageSingleFlight.test.cjs
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
import { test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import os from 'node:os';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
function jsonResponse(status, body, headers = {}) {
|
|
40
|
+
return {
|
|
41
|
+
status,
|
|
42
|
+
ok: status >= 200 && status < 300,
|
|
43
|
+
text: async () => JSON.stringify(body),
|
|
44
|
+
json: async () => body,
|
|
45
|
+
headers: { get: (k) => headers[k.toLowerCase()] ?? null },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const usagePayload = { five_hour: { utilization: 10, resets_at: null }, seven_day: { utilization: 10, resets_at: null }, seven_day_sonnet: null, seven_day_opus: null, extra_usage: null };
|
|
50
|
+
|
|
51
|
+
const MODULES_TO_RELOAD = [
|
|
52
|
+
'../usage.cjs',
|
|
53
|
+
'../config.cjs',
|
|
54
|
+
'../lib/credentials.cjs',
|
|
55
|
+
'../lib/usageCircuit.cjs',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function purgeRequireCache() {
|
|
59
|
+
for (const m of MODULES_TO_RELOAD) {
|
|
60
|
+
try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let realHome;
|
|
65
|
+
let tmpHome;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
realHome = process.env.HOME;
|
|
69
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'usage-single-flight-test-'));
|
|
70
|
+
process.env.HOME = tmpHome;
|
|
71
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(tmpHome, '.claude', '.credentials.json'),
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
claudeAiOauth: {
|
|
76
|
+
accessToken: 'test-access-token',
|
|
77
|
+
refreshToken: 'test-refresh-token',
|
|
78
|
+
expiresAt: Date.now() + 1_000 * 60 * 60 * 24, // 24h out — never "expiring soon"
|
|
79
|
+
subscriptionType: 'max',
|
|
80
|
+
rateLimitTier: 'default_claude_max_20x',
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
purgeRequireCache();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
process.env.HOME = realHome;
|
|
89
|
+
vi.unstubAllGlobals();
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
purgeRequireCache();
|
|
92
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('two concurrent fetchUsage() calls produce exactly one HTTP fetch', async () => {
|
|
96
|
+
const { fetchUsage } = require('../usage.cjs');
|
|
97
|
+
let fetchCalls = 0;
|
|
98
|
+
vi.stubGlobal('fetch', vi.fn(async () => {
|
|
99
|
+
fetchCalls++;
|
|
100
|
+
// Slow enough that both callers are guaranteed to be in-flight together.
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
102
|
+
return jsonResponse(200, usagePayload);
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
const [a, b] = await Promise.all([fetchUsage(), fetchUsage()]);
|
|
106
|
+
|
|
107
|
+
expect(fetchCalls).toBe(1);
|
|
108
|
+
expect(a.kind).toBe('ok');
|
|
109
|
+
expect(b.kind).toBe('ok');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('an ok result resets the shared circuit streak built up by prior failures', async () => {
|
|
113
|
+
const usage = require('../usage.cjs');
|
|
114
|
+
const circuit = usage.__usageCircuitForTest;
|
|
115
|
+
|
|
116
|
+
let call = 0;
|
|
117
|
+
vi.stubGlobal('fetch', vi.fn(async () => {
|
|
118
|
+
call++;
|
|
119
|
+
if (call <= 3) return jsonResponse(500, { error: 'boom' });
|
|
120
|
+
return jsonResponse(200, usagePayload);
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// Three consecutive transient failures — simulating the scheduler's poller
|
|
124
|
+
// failing repeatedly — trip the circuit open.
|
|
125
|
+
await usage.fetchUsage();
|
|
126
|
+
await usage.fetchUsage();
|
|
127
|
+
await usage.fetchUsage();
|
|
128
|
+
expect(circuit.state()).toBe('open');
|
|
129
|
+
|
|
130
|
+
// A later success — as if it came from the renderer's independent
|
|
131
|
+
// billing:fetch call, but reached through the SAME shared fetchUsage() —
|
|
132
|
+
// must reset the streak the scheduler's calls built up, since both
|
|
133
|
+
// callers share one circuit instance.
|
|
134
|
+
const ok = await usage.fetchUsage();
|
|
135
|
+
expect(ok.kind).toBe('ok');
|
|
136
|
+
expect(circuit.state()).toBe('closed');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a 429 with Retry-After suppresses the next request until it elapses', async () => {
|
|
140
|
+
const usage = require('../usage.cjs');
|
|
141
|
+
|
|
142
|
+
let fetchCalls = 0;
|
|
143
|
+
vi.stubGlobal('fetch', vi.fn(async () => {
|
|
144
|
+
fetchCalls++;
|
|
145
|
+
return jsonResponse(429, { error: { type: 'rate_limit_error' } }, { 'retry-after': '60' });
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const first = await usage.fetchUsage();
|
|
149
|
+
expect(first.kind).toBe('meter_rate_limited');
|
|
150
|
+
expect(first.retryAfterMs).toBe(60_000);
|
|
151
|
+
|
|
152
|
+
const second = await usage.fetchUsage();
|
|
153
|
+
expect(second.kind).toBe('meter_rate_limited');
|
|
154
|
+
expect(second.suppressed).toBe(true);
|
|
155
|
+
// No second network call — the suppression window hasn't elapsed.
|
|
156
|
+
expect(fetchCalls).toBe(1);
|
|
157
|
+
});
|
|
@@ -46,7 +46,7 @@ test('WORK_TYPES matches the tag ids declared in tagLibrary.ts TAG_LIBRARY, in o
|
|
|
46
46
|
expect(tagMatches).toEqual(WORK_TYPES);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test('api.d.ts tag unions
|
|
49
|
+
test('api.d.ts tag unions both contain exactly the same 6 values', () => {
|
|
50
50
|
const src = fs.readFileSync(API_D_TS_PATH, 'utf8');
|
|
51
51
|
const unionMatches = [...src.matchAll(/tag\?:\s*((?:'[^']+'\s*\|?\s*)+);/g)].map((m) =>
|
|
52
52
|
m[1]
|
package/src/main/bilkoHost.cjs
CHANGED
|
@@ -46,6 +46,10 @@ const {
|
|
|
46
46
|
const WRITER = 'bilko-host';
|
|
47
47
|
const PROJECT_PAGE_LENSES = new Set(['home', 'marketing', 'feature', 'architecture']);
|
|
48
48
|
|
|
49
|
+
function homePagePath(cwd) {
|
|
50
|
+
return opsPath(cwd, 'project-pages', 'home.html');
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
function bilkoHostDir(cwd) {
|
|
50
54
|
return opsPath(cwd, 'bilko-host');
|
|
51
55
|
}
|
|
@@ -103,9 +107,10 @@ async function writeDocuments(cwd, documents) {
|
|
|
103
107
|
async function get({ cwd }) {
|
|
104
108
|
const realCwd = config.validatePath(cwd);
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
// The single Project Home page (project-pages/home.html) seeds the root
|
|
111
|
+
// document. `hasMarketingPage` is the renderer's legacy field name for
|
|
112
|
+
// "a root-document source page exists".
|
|
113
|
+
const marketingResult = await config.readText(homePagePath(realCwd));
|
|
109
114
|
const pkgResult = await config.readJson(path.join(realCwd, 'package.json'));
|
|
110
115
|
const pkg = pkgResult.exists && !pkgResult.parseError ? pkgResult.data : null;
|
|
111
116
|
|
|
@@ -142,15 +147,15 @@ async function get({ cwd }) {
|
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
/** Seeds documents.json with a single root document (the
|
|
150
|
+
/** Seeds documents.json with a single root document (the Project Home page, home.html) if it doesn't exist yet. */
|
|
146
151
|
async function ensureSeededDocuments(cwd) {
|
|
147
152
|
const existing = await readDocuments(cwd);
|
|
148
153
|
if (existing) return existing;
|
|
149
154
|
const seeded = [{
|
|
150
155
|
id: crypto.randomUUID(),
|
|
151
156
|
subpath: '',
|
|
152
|
-
title: 'Project (
|
|
153
|
-
source: { kind: 'project-page-lens', lens: '
|
|
157
|
+
title: 'Project (Home)',
|
|
158
|
+
source: { kind: 'project-page-lens', lens: 'home' },
|
|
154
159
|
addedAt: new Date().toISOString(),
|
|
155
160
|
}];
|
|
156
161
|
await writeDocuments(cwd, seeded);
|
|
@@ -215,8 +220,12 @@ async function removeDocument({ cwd, id }) {
|
|
|
215
220
|
|
|
216
221
|
async function resolveDocumentHtml(cwd, doc) {
|
|
217
222
|
if (doc.source.kind === 'project-page-lens') {
|
|
223
|
+
// Only `home` is generated now (one home.html); the other lens names
|
|
224
|
+
// are legacy and resolve to their old output/<lens>.html if one exists.
|
|
218
225
|
const result = await config.readText(
|
|
219
|
-
|
|
226
|
+
doc.source.lens === 'home'
|
|
227
|
+
? homePagePath(cwd)
|
|
228
|
+
: opsPath(cwd, 'project-pages', 'output', `${doc.source.lens}.html`),
|
|
220
229
|
);
|
|
221
230
|
if (!result.exists) {
|
|
222
231
|
throw new Error(`document "${doc.title}" points at the ${doc.source.lens} Project Page, but it hasn't been generated yet`);
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
const { spawn } = require('node:child_process');
|
|
46
46
|
const { ipcMain } = require('electron');
|
|
47
|
-
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
47
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
|
|
48
48
|
const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
|
|
49
49
|
const { recordExchange } = require('./exchanges.cjs');
|
|
50
50
|
const { classifyToolUse } = require('./lib/toolUseClassify.cjs');
|
|
@@ -530,11 +530,14 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
530
530
|
const fullPrompt = STOP_SIGNAL_INSTRUCTION + CHAT_MODE_TRUTH_INSTRUCTION + prompt;
|
|
531
531
|
|
|
532
532
|
// sessionId is the Epic's claudeSessionId for an Epic-backed tab (see
|
|
533
|
-
// comment above childEnv) — resolve that Epic's agentType persona model
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
533
|
+
// comment above childEnv) — resolve that Epic's agentType persona model,
|
|
534
|
+
// project-overlay-then-global (agentModelResolve.cjs's
|
|
535
|
+
// readOverlayAwarePersonaModel). Terminal (EpicTerminalPane.tsx) calls
|
|
536
|
+
// this SAME function via the agents:resolve-epic-model IPC, so the two
|
|
537
|
+
// views of one Epic agree by construction, not by convention. Never
|
|
538
|
+
// throws; falls back to the hardcoded 'sonnet' literal when there's no
|
|
539
|
+
// Epic, no agentType, or the persona has no model/'inherit' — --model
|
|
540
|
+
// must never be left unpinned (CLAUDE.md model-pinning rule).
|
|
538
541
|
const model = agentModelResolve.resolveEpicModel({ cwd, claudeSessionId: sessionId });
|
|
539
542
|
|
|
540
543
|
// Build argv as an array — no shell: true, no string interpolation
|
|
@@ -568,7 +571,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
568
571
|
// real-time NDJSON streaming; stderr piped for error-message capture.
|
|
569
572
|
let child;
|
|
570
573
|
try {
|
|
571
|
-
|
|
574
|
+
const target = claudeSpawnTarget('chat', sessionId, claudeBin);
|
|
575
|
+
child = spawn(target.command, args, {
|
|
576
|
+
...(target.argv0 ? { argv0: target.argv0 } : {}),
|
|
572
577
|
cwd: execCwd,
|
|
573
578
|
env: childEnv,
|
|
574
579
|
stdio: ['ignore', 'pipe', 'pipe'],
|
package/src/main/config.cjs
CHANGED
|
@@ -148,9 +148,8 @@ function validateWrite(realAbs) {
|
|
|
148
148
|
// (PRD 825; scheduler.cjs remote.writePrd)
|
|
149
149
|
// project-brief projectBrief.cjs's brief.json (PRD 837)
|
|
150
150
|
// bilko-host bilkoHost.cjs's bundle prep
|
|
151
|
-
// project-pages projectHomeAdminRoutes.cjs's
|
|
152
|
-
// (
|
|
153
|
-
// project-home-admin-routes)
|
|
151
|
+
// project-pages projectHomeAdminRoutes.cjs's project_home_write
|
|
152
|
+
// path (home.html)
|
|
154
153
|
// Paths come from opsOwnership.opsPath (PRD 1082) — the one ops-root
|
|
155
154
|
// resolver — which THROWS for an ephemeral root (a linked worktree that
|
|
156
155
|
// pty.cjs registered as an allowed root for its spawn cwd, or
|
package/src/main/docEdit.cjs
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
const { ipcMain } = require('electron');
|
|
18
18
|
const { spawn } = require('node:child_process');
|
|
19
19
|
const crypto = require('node:crypto');
|
|
20
|
-
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
20
|
+
const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
|
|
21
|
+
const { withProcRole } = require('./lib/cleanEnv.cjs');
|
|
21
22
|
const { extractJson } = require('./lib/extractJson.cjs');
|
|
22
23
|
const { assertInsideHome } = require('./lib/insideHome.cjs');
|
|
23
24
|
const { expandHome } = require('./lib/expandHome.cjs');
|
|
@@ -99,7 +100,8 @@ function runClaude(prompt, { model = 'sonnet', timeoutMs = 90_000, systemPrompt
|
|
|
99
100
|
// stdin MUST be closed ('ignore') — `claude -p` otherwise blocks waiting
|
|
100
101
|
// for piped stdin and returns empty. SM_KG_INTERNAL=1 tells the
|
|
101
102
|
// prompt-logging hook to skip this invocation.
|
|
102
|
-
const
|
|
103
|
+
const target = claudeSpawnTarget('aux', 'docedit', bin);
|
|
104
|
+
const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
|
|
103
105
|
let out = '';
|
|
104
106
|
let err = '';
|
|
105
107
|
// Cap accumulated stdout — the model output shouldn't grow `out` without
|