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
|
@@ -18,13 +18,18 @@ const opsErrorLog = require('../lib/opsErrorLog.cjs');
|
|
|
18
18
|
const tmpDirs = [];
|
|
19
19
|
let originalHome;
|
|
20
20
|
let originalSmTelemetrySpool;
|
|
21
|
+
let originalWorktreeRoot;
|
|
21
22
|
|
|
22
23
|
afterEach(async () => {
|
|
23
24
|
if (originalHome !== undefined) process.env.HOME = originalHome;
|
|
24
25
|
if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
|
|
26
|
+
if (originalWorktreeRoot !== undefined) {
|
|
27
|
+
if (originalWorktreeRoot === null) delete process.env.SM_WORKTREE_ROOT; else process.env.SM_WORKTREE_ROOT = originalWorktreeRoot;
|
|
28
|
+
originalWorktreeRoot = undefined;
|
|
29
|
+
}
|
|
25
30
|
const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
|
|
26
31
|
delete require.cache[telemetryPath];
|
|
27
|
-
const activeSessionsPath = require.resolve('
|
|
32
|
+
const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
|
|
28
33
|
delete require.cache[activeSessionsPath];
|
|
29
34
|
while (tmpDirs.length) {
|
|
30
35
|
const d = tmpDirs.pop();
|
|
@@ -85,12 +90,23 @@ test('level "warn" routes to telemetryClient.logLine, default "error" routes to
|
|
|
85
90
|
// ─── ephemeral cwd: local refused, telemetry still fires ─────────────────
|
|
86
91
|
|
|
87
92
|
test('an ephemeral cwd yields zero local lines but one telemetry record, with a normalized projectHash', () => {
|
|
93
|
+
// Why this test depends on the process tmpdir: ephemeral classification
|
|
94
|
+
// (cwdClassify.classifyCwd, via ephemeralCwd.isEphemeralCwd) is an exact match
|
|
95
|
+
// on os.tmpdir() plus a prefix match on the managed job/epic worktree roots,
|
|
96
|
+
// and those roots are <SM_WORKTREE_ROOT || os.tmpdir()>/session-manager-*-worktrees
|
|
97
|
+
// (schedulerPaths.worktreeRoot). The sandbox globalSetup mkdtemp's that base under
|
|
98
|
+
// the tmpdir it saw at startup, so a relocated TMPDIR / prior env mutation changes
|
|
99
|
+
// where KIND_CONFIG.epic.root points. Pin SM_WORKTREE_ROOT to a fixture dir this
|
|
100
|
+
// test owns and derive the cwd from KIND_CONFIG, so expected and actual roots
|
|
101
|
+
// resolve through the same code path regardless of TMPDIR.
|
|
102
|
+
originalWorktreeRoot = process.env.SM_WORKTREE_ROOT === undefined ? null : process.env.SM_WORKTREE_ROOT;
|
|
103
|
+
process.env.SM_WORKTREE_ROOT = mkTmpProject();
|
|
88
104
|
const { KIND_CONFIG } = require('../lib/gitWorktree.cjs');
|
|
89
105
|
const worktreeCwd = path.join(KIND_CONFIG.epic.root, 'fakehash', 'fake-epic-id');
|
|
90
106
|
const realRoot = '/home/bilko/Projects/real-project';
|
|
91
107
|
|
|
92
|
-
const activeSessionsPath = require.resolve('
|
|
93
|
-
const original = require('
|
|
108
|
+
const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
|
|
109
|
+
const original = require('../lib/activeSessions.cjs');
|
|
94
110
|
require.cache[activeSessionsPath] = {
|
|
95
111
|
id: activeSessionsPath,
|
|
96
112
|
filename: activeSessionsPath,
|
|
@@ -103,7 +119,9 @@ test('an ephemeral cwd yields zero local lines but one telemetry record, with a
|
|
|
103
119
|
|
|
104
120
|
opsErrorLog.appendError({ cwd: worktreeCwd, scope: 'chatRunner', message: 'worktree err' });
|
|
105
121
|
|
|
106
|
-
|
|
122
|
+
// resolveProjectRoot now consumes cwdClassify (not the stubbed projectRootOf):
|
|
123
|
+
// an ephemeral worktree cwd is refused outright, so no local path is even built.
|
|
124
|
+
expect(() => opsErrorLog.todayFile(worktreeCwd)).toThrow(/ephemeral/);
|
|
107
125
|
expect(reportErrorCalls).toHaveLength(1);
|
|
108
126
|
expect(reportErrorCalls[0].context.cwd).toBe(realRoot);
|
|
109
127
|
expect(reportErrorCalls[0].context.cwd).not.toBe(worktreeCwd);
|
|
@@ -139,6 +157,12 @@ test('a realistic error with an absolute path and a prompt-like string never rea
|
|
|
139
157
|
message: `run failed while processing "${promptLike}" at ${absPath}`,
|
|
140
158
|
});
|
|
141
159
|
|
|
160
|
+
// The telemetry write is fire-and-forget async; poll (bounded) for the queue
|
|
161
|
+
// file instead of a fixed sleep, which raced under load.
|
|
162
|
+
const deadline = Date.now() + 5000;
|
|
163
|
+
while (!fs.existsSync(client.queuePath()) && Date.now() < deadline) {
|
|
164
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
165
|
+
}
|
|
142
166
|
await new Promise((r) => setTimeout(r, 50));
|
|
143
167
|
|
|
144
168
|
const raw = await fsp.readFile(client.queuePath(), 'utf8');
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
* whether that pass ends in an actual job launch (see classifyQueueStarvation's
|
|
17
17
|
* header for why this must be distinct from `lastRunAt`).
|
|
18
18
|
*
|
|
19
|
+
* Usage-meter circuit PRD update: pollLoop no longer fires blind at a hard-
|
|
20
|
+
* coded cachedUtilization=0 on failure — it falls back to
|
|
21
|
+
* usageCircuit.degradedBudget(), which carries forward the last known-good
|
|
22
|
+
* BINDING window's utilization (or conservatively assumes 100/no-headroom
|
|
23
|
+
* when there's no known-good reading yet at all). So each test below first
|
|
24
|
+
* drives ONE successful ('ok') poll — with the queue still empty, so it
|
|
25
|
+
* can't itself cause a dispatch — to seed that known-good low-utilization
|
|
26
|
+
* reading, THEN seeds the pending job and drives the failure. This proves
|
|
27
|
+
* dispatch proceeds under the CARRIED-FORWARD degraded budget, not a blind 0.
|
|
28
|
+
*
|
|
19
29
|
* No existing test in this repo mocks a module import (see
|
|
20
30
|
* pty-epic-worktree-spawn-cwd.test.cjs's header) — this monkey-patches
|
|
21
31
|
* usage.cjs's cached module object directly instead, same pattern.
|
|
@@ -73,10 +83,24 @@ function mkProject() {
|
|
|
73
83
|
return { cwd, slug };
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
// dependsOn deliberately references a slug that can never resolve (no such
|
|
87
|
+
// row, no history/archive entry) so pickNextBatch's dep gate holds this job
|
|
88
|
+
// forever instead of ever handing it to spawnJob. tickQueue() still reaches
|
|
89
|
+
// the picker and stamps lastDispatchAttemptAt (that stamp is unconditional —
|
|
90
|
+
// see lastDispatchAttemptAt's own header comment), which is the only thing
|
|
91
|
+
// these tests assert on. Without this, the picker actually dispatches the
|
|
92
|
+
// job: spawnJob() is fire-and-forget (scheduler.cjs's own comment: "spawnJob
|
|
93
|
+
// is fire-and-forget; it calls tickQueue() on completion") and its real
|
|
94
|
+
// child-process exit handler later calls mutate() independently of the
|
|
95
|
+
// tickTail chain flushPendingTick() drains — round-tripping whatever
|
|
96
|
+
// lastDispatchAttemptAt was in memory at ITS read time. Reproduced outside
|
|
97
|
+
// vitest: that stray write landed ~2s after this suite's own reset-to-null,
|
|
98
|
+
// clobbering the NEXT test's "still null" assertion with the previous test's
|
|
99
|
+
// stamp. A held (never-dispatched) job has no such background continuation.
|
|
76
100
|
async function seedOnePendingJob() {
|
|
77
101
|
const { cwd, slug } = mkProject();
|
|
78
102
|
await scheduler.writeQueue({
|
|
79
|
-
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
|
|
103
|
+
jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: ['unresolvable-dep-never-completes'] }],
|
|
80
104
|
config: {},
|
|
81
105
|
paused: null,
|
|
82
106
|
});
|
|
@@ -96,11 +120,23 @@ async function flushPendingTick() {
|
|
|
96
120
|
await scheduler.tickQueue();
|
|
97
121
|
}
|
|
98
122
|
|
|
99
|
-
test('pollLoop still attempts a dispatch after a transient billing failure', async () => {
|
|
123
|
+
test('pollLoop still attempts a dispatch after a transient billing failure, using the degraded budget (not utilization 0)', async () => {
|
|
124
|
+
// Seed a known-good low-utilization reading via an 'ok' poll first.
|
|
125
|
+
process.env.SM_E2E = '1';
|
|
126
|
+
process.env.SM_MOCK_BILLING_KIND = 'ok';
|
|
127
|
+
await scheduler.pollLoop();
|
|
128
|
+
// A prior test's leftover project (a REAL, still-registered project cwd —
|
|
129
|
+
// readQueue() merges every such cwd) can still hold its own permanently-
|
|
130
|
+
// pending job, so this seed poll's own maybeLaunchWhenAvailable may itself
|
|
131
|
+
// spawn a fire-and-forget tick; drain it before resetting/reseeding so
|
|
132
|
+
// that stray tick's async mutate() can't land AFTER (and clobber) the
|
|
133
|
+
// reset below.
|
|
134
|
+
await flushPendingTick();
|
|
135
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
136
|
+
|
|
100
137
|
await seedOnePendingJob();
|
|
101
138
|
expect(await lastDispatchAttemptAt()).toBeNull();
|
|
102
139
|
|
|
103
|
-
process.env.SM_E2E = '1';
|
|
104
140
|
process.env.SM_MOCK_BILLING_KIND = 'transient';
|
|
105
141
|
await scheduler.pollLoop();
|
|
106
142
|
await flushPendingTick();
|
|
@@ -108,10 +144,18 @@ test('pollLoop still attempts a dispatch after a transient billing failure', asy
|
|
|
108
144
|
expect(await lastDispatchAttemptAt()).not.toBeNull();
|
|
109
145
|
});
|
|
110
146
|
|
|
111
|
-
test('pollLoop still attempts a dispatch after the billing poll throws', async () => {
|
|
147
|
+
test('pollLoop still attempts a dispatch after the billing poll throws, using the degraded budget (not utilization 0)', async () => {
|
|
148
|
+
process.env.SM_E2E = '1';
|
|
149
|
+
process.env.SM_MOCK_BILLING_KIND = 'ok';
|
|
150
|
+
await scheduler.pollLoop(); // seed a known-good low-utilization reading
|
|
151
|
+
await flushPendingTick();
|
|
152
|
+
await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
|
|
153
|
+
|
|
112
154
|
await seedOnePendingJob();
|
|
113
155
|
expect(await lastDispatchAttemptAt()).toBeNull();
|
|
114
156
|
|
|
157
|
+
delete process.env.SM_E2E;
|
|
158
|
+
delete process.env.SM_MOCK_BILLING_KIND;
|
|
115
159
|
billing.fetchUsage = async () => { throw new Error('simulated IPC transport failure'); };
|
|
116
160
|
await scheduler.pollLoop();
|
|
117
161
|
await flushPendingTick();
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* so a collision silently merges two unrelated projects' PRDs into one
|
|
9
9
|
* group.
|
|
10
10
|
*
|
|
11
|
-
* Run: timeout
|
|
11
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/prd-group-allocator.test.cjs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import { test } from 'vitest';
|
|
17
17
|
const assert = require('node:assert/strict');
|
|
18
18
|
const fs = require('node:fs');
|
|
19
19
|
const fsp = require('node:fs/promises');
|
|
@@ -40,6 +40,8 @@ const REQUIRED_CAPABILITIES = [
|
|
|
40
40
|
{ capability: 'retag a PRD (parallelGroup/estimateMinutes)', ipcChannel: 'schedule:retag-prd', adminRoute: 'POST /admin/scheduler/retag-prd' },
|
|
41
41
|
{ capability: 'list scheduler jobs', ipcChannel: null, adminRoute: 'GET /admin/scheduler/jobs' },
|
|
42
42
|
{ capability: 'reset a stuck job', ipcChannel: 'schedule:reset-job', adminRoute: 'POST /admin/scheduler/reset-job' },
|
|
43
|
+
{ capability: 'pause new dispatch (user-initiated)', ipcChannel: 'schedule:pause', adminRoute: 'POST /admin/scheduler/pause' },
|
|
44
|
+
{ capability: 'resume a paused scheduler', ipcChannel: 'schedule:resume', adminRoute: 'POST /admin/scheduler/resume' },
|
|
43
45
|
{ capability: 'cancel a not-yet-terminal job', ipcChannel: null, adminRoute: 'POST /admin/scheduler/cancel-job' },
|
|
44
46
|
];
|
|
45
47
|
|
|
@@ -499,6 +499,12 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
|
|
|
499
499
|
bustCwdCache();
|
|
500
500
|
const slug = uniqueSlug('collision-slug');
|
|
501
501
|
const epicId = 'epic-reset-collision';
|
|
502
|
+
// Both projects' rows need a real PRD source — a 'needs_review' row whose
|
|
503
|
+
// PRD is invisible to reconcile()'s onDisk scan is silently dropped (never
|
|
504
|
+
// backfilled to history.jsonl the way a completed/failed/skipped orphan
|
|
505
|
+
// is), which would make jobA vanish for a reason unrelated to what this
|
|
506
|
+
// test actually verifies (cwd-scoped job lookup).
|
|
507
|
+
await writePrdFixture({ cwd: cwdA, epicId, slug });
|
|
502
508
|
await writePrdFixture({ cwd: cwdB, epicId, slug });
|
|
503
509
|
await scheduler.writeQueue({
|
|
504
510
|
jobs: [
|
|
@@ -518,8 +524,14 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
|
|
|
518
524
|
expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
|
|
519
525
|
|
|
520
526
|
const state = await scheduler.remote.listJobs();
|
|
521
|
-
|
|
522
|
-
|
|
527
|
+
// Filter by slug too, not just cwd: cwdA is a shared fixture directory
|
|
528
|
+
// reused by many tests in this file, each of which leaves its own
|
|
529
|
+
// never-archived PRD file behind (only the queue row is cleared in
|
|
530
|
+
// `finally`) — reconcile()'s fresh-discovery pass can quarantine one of
|
|
531
|
+
// those leftovers as a NEW row under this same cwd on any later pass, so
|
|
532
|
+
// `cwd` alone no longer uniquely identifies this test's own job.
|
|
533
|
+
const jobA = state.find((j) => j.cwd === cwdA && j.slug === slug);
|
|
534
|
+
const jobB = state.find((j) => j.cwd === cwdB && j.slug === slug);
|
|
523
535
|
expect(jobA.status).toBe('needs_review'); // untouched
|
|
524
536
|
expect(jobB.status).toBe('pending'); // the one actually targeted
|
|
525
537
|
} finally {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { seedAuthoringGuide } = require('../lib/prdAuthoringSeed.cjs');
|
|
6
|
+
|
|
7
|
+
const write = async (abs, text) => fs.writeFileSync(abs, text, 'utf8');
|
|
8
|
+
|
|
9
|
+
describe('seedAuthoringGuide', () => {
|
|
10
|
+
let dir, src, dest;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prd-seed-'));
|
|
13
|
+
src = path.join(dir, 'src.md');
|
|
14
|
+
dest = path.join(dir, 'dest.md');
|
|
15
|
+
fs.writeFileSync(src, '<!-- PRD_AUTHORING.md v3 -->\nnew body\n');
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
18
|
+
|
|
19
|
+
it('seeds an absent destination', async () => {
|
|
20
|
+
expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
|
|
21
|
+
expect(fs.readFileSync(dest, 'utf8')).toContain('new body');
|
|
22
|
+
});
|
|
23
|
+
it('overwrites a stale stamp', async () => {
|
|
24
|
+
fs.writeFileSync(dest, '<!-- PRD_AUTHORING.md v2 -->\nold\n');
|
|
25
|
+
expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
|
|
26
|
+
expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
|
|
27
|
+
});
|
|
28
|
+
it('leaves a matching stamp untouched', async () => {
|
|
29
|
+
const same = '<!-- PRD_AUTHORING.md v3 -->\nlocal edit\n';
|
|
30
|
+
fs.writeFileSync(dest, same);
|
|
31
|
+
expect(await seedAuthoringGuide({ src, dest, write })).toBe(false);
|
|
32
|
+
expect(fs.readFileSync(dest, 'utf8')).toBe(same);
|
|
33
|
+
});
|
|
34
|
+
it('overwrites a destination with no stamp', async () => {
|
|
35
|
+
fs.writeFileSync(dest, '# no stamp\n');
|
|
36
|
+
expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
|
|
37
|
+
expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdCreateDisposition.test.cjs — scheduler wave-disposition PRD:
|
|
3
|
+
* createPrd()'s authoring-time disposition gate. A PRD landing as a wave
|
|
4
|
+
* ROOT (no dependsOn of its own) in an Epic that already has incomplete
|
|
5
|
+
* PRDs must record an explicit 'append'/'new-head' choice — required for an
|
|
6
|
+
* interactive caller, defaulted (and logged) for a non-interactive one.
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdCreateDisposition.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
const fsp = require('node:fs/promises');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const config = require('../config.cjs');
|
|
18
|
+
const prdParser = require('../scheduler/prdParser.cjs');
|
|
19
|
+
const { parsePrdFile } = require('../lib/prdFrontmatter.cjs');
|
|
20
|
+
const { createPrd } = require('../lib/prdCreate.cjs');
|
|
21
|
+
|
|
22
|
+
async function mkTmpPrdsDir() {
|
|
23
|
+
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-disposition-create-prd-'));
|
|
24
|
+
config.addAllowedRoot(root);
|
|
25
|
+
const prdsDir = path.join(root, '.claude', 'prds');
|
|
26
|
+
await fsp.mkdir(prdsDir, { recursive: true });
|
|
27
|
+
return prdsDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Minimal fake `remote` — same shape as prdCreate.test.cjs's
|
|
31
|
+
* makeFakeRemoteWithPrdsDir, but listPrds also reports sourcePromptId/
|
|
32
|
+
* dependsOn (read back from each seeded file's real frontmatter) and
|
|
33
|
+
* status (from the test-supplied statusBySlug map) — the fields
|
|
34
|
+
* createPrd()'s disposition gate actually reads. */
|
|
35
|
+
function makeFakeRemote(prdsDir, statusBySlug = {}) {
|
|
36
|
+
return {
|
|
37
|
+
async allocateParallelGroup() {
|
|
38
|
+
return prdParser.allocateParallelGroup(prdsDir);
|
|
39
|
+
},
|
|
40
|
+
async readPrd(slug) {
|
|
41
|
+
try {
|
|
42
|
+
const text = await fsp.readFile(path.join(prdsDir, `${slug}.md`), 'utf8');
|
|
43
|
+
return { ok: true, text };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { ok: false, error: e?.message };
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
async writePrd(slug, body) {
|
|
49
|
+
try {
|
|
50
|
+
const filePath = path.join(prdsDir, `${slug}.md`);
|
|
51
|
+
await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
|
|
52
|
+
return { ok: true };
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return { ok: false, error: e?.message };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async listPrds() {
|
|
58
|
+
const entries = await fsp.readdir(prdsDir);
|
|
59
|
+
const prds = [];
|
|
60
|
+
for (const f of entries) {
|
|
61
|
+
if (!f.endsWith('.md')) continue;
|
|
62
|
+
const slug = f.slice(0, -3);
|
|
63
|
+
const raw = await fsp.readFile(path.join(prdsDir, f), 'utf8');
|
|
64
|
+
const { frontmatter } = parsePrdFile(raw);
|
|
65
|
+
prds.push({
|
|
66
|
+
slug,
|
|
67
|
+
sourcePromptId: frontmatter.sourcePromptId ?? null,
|
|
68
|
+
dependsOn: frontmatter.dependsOn ?? null,
|
|
69
|
+
status: statusBySlug[slug] ?? null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { prds, total: prds.length, limit: prds.length, offset: 0, hasMore: false };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Seeds a fixture PRD file directly (bypassing createPrd) at an explicit,
|
|
78
|
+
* already-NN-prefixed slug, so tests can assert dependsOn against a known
|
|
79
|
+
* name instead of a freshly-allocated one. */
|
|
80
|
+
async function seedPrd(prdsDir, slug, { sourcePromptId, dependsOn } = {}) {
|
|
81
|
+
const lines = ['---', 'title: seed', `sourcePromptId: ${sourcePromptId}`];
|
|
82
|
+
if (dependsOn && dependsOn.length) lines.push(`dependsOn: [${dependsOn.join(', ')}]`);
|
|
83
|
+
lines.push('---', '# Goal', '', 'seed body', '');
|
|
84
|
+
await fsp.writeFile(path.join(prdsDir, `${slug}.md`), lines.join('\n'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validBody(overrides = {}) {
|
|
88
|
+
return {
|
|
89
|
+
title: 'Wave 2 root',
|
|
90
|
+
cwd: os.homedir(),
|
|
91
|
+
estimateMinutes: 10,
|
|
92
|
+
goal: 'Do the follow-on work.',
|
|
93
|
+
acceptanceCriteria: ['a'],
|
|
94
|
+
implementationNotes: 'n',
|
|
95
|
+
...overrides,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// This suite's own process may itself be running as a scheduled job (this
|
|
100
|
+
// file's disposition gate keys directly off SM_SCHEDULER_JOB_SLUG — see
|
|
101
|
+
// createPrd()) — never assume it's unset going in; each test controls it
|
|
102
|
+
// explicitly.
|
|
103
|
+
const originalJobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
delete process.env.SM_SCHEDULER_JOB_SLUG;
|
|
106
|
+
});
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
if (originalJobSlug === undefined) delete process.env.SM_SCHEDULER_JOB_SLUG;
|
|
109
|
+
else process.env.SM_SCHEDULER_JOB_SLUG = originalJobSlug;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('refuses to write, without a disposition, a new root joining an Epic that already has an incomplete PRD', async () => {
|
|
113
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
114
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
|
|
115
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
|
|
116
|
+
|
|
117
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-1' }), remote);
|
|
118
|
+
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
expect(result.status).toBe(400);
|
|
121
|
+
expect(result.error).toMatch(/disposition/i);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('disposition: append attaches the new root behind the Epic\'s current terminal PRD', async () => {
|
|
125
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
126
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
|
|
127
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
|
|
128
|
+
|
|
129
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-1', disposition: 'append' }), remote);
|
|
130
|
+
|
|
131
|
+
expect(result.ok).toBe(true);
|
|
132
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
133
|
+
expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
|
|
134
|
+
expect(written).toMatch(/^disposition: append$/m);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('disposition: new-head leaves the new root independent (no dependsOn)', async () => {
|
|
138
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
139
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
|
|
140
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
|
|
141
|
+
|
|
142
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-1', disposition: 'new-head' }), remote);
|
|
143
|
+
|
|
144
|
+
expect(result.ok).toBe(true);
|
|
145
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
146
|
+
expect(written).not.toContain('dependsOn');
|
|
147
|
+
expect(written).toMatch(/^disposition: new-head$/m);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a non-interactive caller (SM_SCHEDULER_JOB_SLUG set) with no disposition defaults to append, logged as a default', async () => {
|
|
151
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
152
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
|
|
153
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
|
|
154
|
+
process.env.SM_SCHEDULER_JOB_SLUG = 'some-running-job';
|
|
155
|
+
|
|
156
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-1' }), remote);
|
|
157
|
+
|
|
158
|
+
expect(result.ok).toBe(true);
|
|
159
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
160
|
+
expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
|
|
161
|
+
expect(written).toMatch(/^disposition: append$/m);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('no disposition required for the first PRD in a brand-new Epic', async () => {
|
|
165
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
166
|
+
const remote = makeFakeRemote(prdsDir);
|
|
167
|
+
|
|
168
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-empty' }), remote);
|
|
169
|
+
|
|
170
|
+
expect(result.ok).toBe(true);
|
|
171
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
172
|
+
expect(written).not.toContain('disposition');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('no disposition required when every existing PRD in the Epic is already completed', async () => {
|
|
176
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
177
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-done' });
|
|
178
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'completed' });
|
|
179
|
+
|
|
180
|
+
const result = await createPrd(validBody({ sourcePromptId: 'epic-done' }), remote);
|
|
181
|
+
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
184
|
+
expect(written).not.toContain('disposition');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('an explicit dependsOn bypasses the disposition gate entirely', async () => {
|
|
188
|
+
const prdsDir = await mkTmpPrdsDir();
|
|
189
|
+
await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
|
|
190
|
+
const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
|
|
191
|
+
|
|
192
|
+
const result = await createPrd(
|
|
193
|
+
validBody({ sourcePromptId: 'epic-1', dependsOn: ['1-wave1-tail'] }),
|
|
194
|
+
remote,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(result.ok).toBe(true);
|
|
198
|
+
const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
|
|
199
|
+
expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
|
|
200
|
+
expect(written).not.toContain('disposition');
|
|
201
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdFrontmatterDisposition.test.cjs — scheduler wave-disposition PRD:
|
|
3
|
+
* `disposition` ('append' | 'new-head') is a recognized, patchable PRD
|
|
4
|
+
* frontmatter key, same pattern as `dependsOn` in
|
|
5
|
+
* prdFrontmatterDependsOn.test.cjs. It records the authoring-time decision
|
|
6
|
+
* for a wave's root PRD: does it extend the Epic's existing dependsOn chain
|
|
7
|
+
* (append) or start an independent, parallel-eligible chain (new-head)?
|
|
8
|
+
*
|
|
9
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdFrontmatterDisposition.test.cjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import { test, expect } from 'vitest';
|
|
15
|
+
|
|
16
|
+
const { parsePrdFile, serializePrdFile, RECOGNIZED_KEYS } = require('../lib/prdFrontmatter.cjs');
|
|
17
|
+
|
|
18
|
+
test('disposition is a recognized key', () => {
|
|
19
|
+
expect(RECOGNIZED_KEYS.has('disposition')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('parsePrdFile parses disposition: append and round-trips it byte-identically when unedited', () => {
|
|
23
|
+
const raw = [
|
|
24
|
+
'---',
|
|
25
|
+
'title: A second wave',
|
|
26
|
+
'cwd: ~/Projects/session-manager',
|
|
27
|
+
'estimateMinutes: 20',
|
|
28
|
+
'dependsOn: [widget-base]',
|
|
29
|
+
'disposition: append',
|
|
30
|
+
'---',
|
|
31
|
+
'# Goal',
|
|
32
|
+
'',
|
|
33
|
+
'Extend the chain.',
|
|
34
|
+
'',
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
const { frontmatter, body } = parsePrdFile(raw);
|
|
38
|
+
expect(frontmatter.disposition).toBe('append');
|
|
39
|
+
expect(serializePrdFile(frontmatter, body)).toBe(raw);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('parsePrdFile parses disposition: new-head', () => {
|
|
43
|
+
const raw = [
|
|
44
|
+
'---',
|
|
45
|
+
'title: A parallel wave',
|
|
46
|
+
'cwd: ~/Projects/session-manager',
|
|
47
|
+
'estimateMinutes: 20',
|
|
48
|
+
'disposition: new-head',
|
|
49
|
+
'---',
|
|
50
|
+
'body',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
const { frontmatter } = parsePrdFile(raw);
|
|
55
|
+
expect(frontmatter.disposition).toBe('new-head');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('parsePrdFile leaves disposition unset for an unrecognized value rather than throwing', () => {
|
|
59
|
+
const raw = [
|
|
60
|
+
'---',
|
|
61
|
+
'title: A hand-edited PRD',
|
|
62
|
+
'cwd: ~/Projects/session-manager',
|
|
63
|
+
'estimateMinutes: 20',
|
|
64
|
+
'disposition: sideways',
|
|
65
|
+
'---',
|
|
66
|
+
'body',
|
|
67
|
+
'',
|
|
68
|
+
].join('\n');
|
|
69
|
+
|
|
70
|
+
const { frontmatter } = parsePrdFile(raw);
|
|
71
|
+
expect(frontmatter.disposition).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('parsePrdFile leaves disposition undefined when omitted — a PRD authored before this field existed round-trips unaffected', () => {
|
|
75
|
+
const raw = [
|
|
76
|
+
'---',
|
|
77
|
+
'title: An older PRD',
|
|
78
|
+
'cwd: ~/Projects/session-manager',
|
|
79
|
+
'estimateMinutes: 10',
|
|
80
|
+
'---',
|
|
81
|
+
'# Goal',
|
|
82
|
+
'',
|
|
83
|
+
'Do the thing.',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n');
|
|
86
|
+
|
|
87
|
+
const { frontmatter, body } = parsePrdFile(raw);
|
|
88
|
+
expect(frontmatter.disposition).toBeUndefined();
|
|
89
|
+
expect(serializePrdFile(frontmatter, body)).toBe(raw);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('serializePrdFile emits a newly-set disposition onto a frontmatter object that lacked one', () => {
|
|
93
|
+
const raw = [
|
|
94
|
+
'---',
|
|
95
|
+
'title: Legacy PRD',
|
|
96
|
+
'cwd: ~/Projects/session-manager',
|
|
97
|
+
'estimateMinutes: 5',
|
|
98
|
+
'---',
|
|
99
|
+
'body',
|
|
100
|
+
'',
|
|
101
|
+
].join('\n');
|
|
102
|
+
|
|
103
|
+
const { frontmatter, body } = parsePrdFile(raw);
|
|
104
|
+
const updated = serializePrdFile({ ...frontmatter, disposition: 'new-head' }, body);
|
|
105
|
+
expect(updated).toContain('disposition: new-head');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('serializePrdFile patches an existing disposition (promoting append to new-head)', () => {
|
|
109
|
+
const raw = [
|
|
110
|
+
'---',
|
|
111
|
+
'title: A second wave',
|
|
112
|
+
'cwd: ~/Projects/session-manager',
|
|
113
|
+
'estimateMinutes: 20',
|
|
114
|
+
'dependsOn: [widget-base]',
|
|
115
|
+
'disposition: append',
|
|
116
|
+
'---',
|
|
117
|
+
'body',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n');
|
|
120
|
+
|
|
121
|
+
const { frontmatter, body } = parsePrdFile(raw);
|
|
122
|
+
const updated = serializePrdFile({ ...frontmatter, dependsOn: [], disposition: 'new-head' }, body);
|
|
123
|
+
expect(updated).toContain('disposition: new-head');
|
|
124
|
+
expect(updated).not.toContain('dependsOn');
|
|
125
|
+
});
|