claude-code-session-manager 0.98.0 → 0.100.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 +3 -3
- package/bin/cli.cjs +16 -4
- package/bin/node-floor.cjs +41 -0
- package/dist/assets/{DataModel-DCEgMTjH.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-Bfdoywlh.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-7akfgy67.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-D4lZtXof.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-CeI8G1w5.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-BGleLja4.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BFczzI1B.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-BxZ9_fCR.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-Cd98Q1lD.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +25 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
- package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
- package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
- package/scripts/README.md +3 -11
- package/scripts/audit-ops-hygiene.cjs +3 -3
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
- package/scripts/mint-epic.cjs +2 -3
- package/scripts/scheduler-mcp-server.cjs +36 -7
- package/src/main/agentLibrary.cjs +22 -2
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +41 -38
- package/src/main/docEdit.cjs +4 -1
- package/src/main/files.cjs +2 -5
- package/src/main/health.cjs +1 -1
- package/src/main/index.cjs +23 -10
- package/src/main/ipcSchemas.cjs +28 -35
- package/src/main/lib/agentPersonaSchema.cjs +13 -2
- package/src/main/lib/atomicFs.cjs +116 -0
- package/src/main/lib/branchSweep.cjs +13 -12
- package/src/main/lib/buildTarget.cjs +3 -3
- package/src/main/lib/claudeCliCaps.cjs +129 -0
- package/src/main/lib/credentials.cjs +2 -4
- package/src/main/lib/crossProjectFeedback.cjs +3 -3
- package/src/main/lib/cwdClassify.cjs +15 -3
- package/src/main/lib/definitionOfDone.cjs +207 -125
- package/src/main/lib/dodDrainHook.cjs +11 -5
- package/src/main/lib/epicMint.cjs +2 -4
- package/src/main/lib/epicStatusMirror.cjs +8 -10
- package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
- package/src/main/lib/gateAuthority.cjs +96 -0
- package/src/main/lib/gitExec.cjs +69 -0
- package/src/main/lib/gitWorktree.cjs +656 -92
- package/src/main/lib/instanceLock.cjs +5 -8
- package/src/main/lib/launchFailure.cjs +2 -3
- package/src/main/lib/macroLibrary.cjs +386 -0
- package/src/main/lib/mcpToolCatalog.cjs +26 -17
- package/src/main/lib/needsReviewLedger.cjs +1 -1
- package/src/main/lib/opsErrorLog.cjs +11 -1
- package/src/main/lib/opsOwnership.cjs +0 -5
- package/src/main/lib/pidAlive.cjs +32 -0
- package/src/main/lib/prdCreate.cjs +121 -12
- package/src/main/lib/prdDisposition.cjs +2 -2
- package/src/main/lib/prdGateFiles.cjs +284 -0
- package/src/main/lib/prdLocations.cjs +61 -0
- package/src/main/lib/prdMigration.cjs +2 -3
- package/src/main/lib/prdSizing.cjs +2 -1
- package/src/main/lib/promptSessionSchema.cjs +10 -2
- package/src/main/lib/rcaReport.cjs +4 -6
- package/src/main/lib/reaperHelpers.cjs +8 -1
- package/src/main/lib/reservationExpiry.cjs +6 -1
- package/src/main/lib/reviewNotice.cjs +263 -0
- package/src/main/lib/runClaudeP.cjs +4 -1
- package/src/main/lib/schedulerPaths.cjs +22 -2
- package/src/main/lib/sessionSlots.cjs +2 -4
- package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
- package/src/main/lib/timeoutShim.cjs +123 -0
- package/src/main/lib/timeoutShimScript.cjs +275 -0
- package/src/main/lib/upgradeDrain.cjs +4 -10
- package/src/main/lib/watchdogHelpers.cjs +12 -25
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/scheduler.cjs +910 -198
- package/src/main/seedAgentPersonas.cjs +278 -23
- package/src/main/supervisor.cjs +4 -1
- package/src/main/templates/PRD_AUTHORING.md +126 -406
- package/src/preload/__tests__/preload-surface.test.cjs +68 -0
- package/src/preload/api.d.ts +41 -94
- package/src/preload/index.cjs +14 -9
- package/src/seed/agents/architect.md +1 -0
- package/src/seed/agents/dev-lead.md +17 -18
- package/src/seed/agents/project-home-builder.md +1 -0
- package/src/seed/agents/validator.md +10 -4
- package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
- package/dist/assets/Scheduler-C_phpeUS.js +0 -16
- package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
- package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
- package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
- package/src/main/__tests__/agentLibrary.test.cjs +0 -276
- package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
- package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
- package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
- package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
- package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
- package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
- package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
- package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
- package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
- package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
- package/src/main/__tests__/chat-queue.test.cjs +0 -97
- package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
- package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
- package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
- package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
- package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
- package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
- package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
- package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
- package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
- package/src/main/__tests__/docEdit.test.cjs +0 -244
- package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
- package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
- package/src/main/__tests__/dod-report.test.cjs +0 -304
- package/src/main/__tests__/dod-reverify.test.cjs +0 -285
- package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
- package/src/main/__tests__/epicMint.test.cjs +0 -332
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
- package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
- package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
- package/src/main/__tests__/exchanges.test.cjs +0 -122
- package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
- package/src/main/__tests__/extractJson.test.cjs +0 -51
- package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
- package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
- package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
- package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
- package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
- package/src/main/__tests__/health-credentials.test.cjs +0 -81
- package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
- package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
- package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
- package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
- package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
- package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
- package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
- package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
- package/src/main/__tests__/historyDashboard.test.cjs +0 -163
- package/src/main/__tests__/historyRollup.test.cjs +0 -333
- package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
- package/src/main/__tests__/kg-augment.test.cjs +0 -195
- package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
- package/src/main/__tests__/machineProfile.test.cjs +0 -152
- package/src/main/__tests__/mcpStatus.test.cjs +0 -61
- package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
- package/src/main/__tests__/memoryStale.test.cjs +0 -88
- package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
- package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
- package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
- package/src/main/__tests__/personaMerge.test.cjs +0 -169
- package/src/main/__tests__/planValidator.test.cjs +0 -125
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
- package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
- package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
- package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
- package/src/main/__tests__/prdAgentType.test.cjs +0 -103
- package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
- package/src/main/__tests__/prdCreate.test.cjs +0 -979
- package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
- package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
- package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
- package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
- package/src/main/__tests__/prdLocations.test.cjs +0 -195
- package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
- package/src/main/__tests__/prdMigration.test.cjs +0 -349
- package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
- package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
- package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
- package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
- package/src/main/__tests__/prdSizing.test.cjs +0 -106
- package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
- package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
- package/src/main/__tests__/proc-role-env.test.cjs +0 -125
- package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
- package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
- package/src/main/__tests__/projectPages.test.cjs +0 -137
- package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
- package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
- package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
- package/src/main/__tests__/pty-write-result.test.cjs +0 -46
- package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
- package/src/main/__tests__/queueHistory.test.cjs +0 -355
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
- package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
- package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
- package/src/main/__tests__/rcaReport.test.cjs +0 -266
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
- package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
- package/src/main/__tests__/runLogRetention.test.cjs +0 -489
- package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
- package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
- package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
- package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
- package/src/main/__tests__/runVerify.test.cjs +0 -1784
- package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
- package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
- package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
- package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
- package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
- package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
- package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
- package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
- package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
- package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
- package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
- package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
- package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
- package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
- package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
- package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
- package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
- package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
- package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
- package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
- package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
- package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
- package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
- package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
- package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
- package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
- package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
- package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
- package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
- package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
- package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
- package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
- package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
- package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
- package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
- package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
- package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
- package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
- package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
- package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
- package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
- package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
- package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
- package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
- package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
- package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
- package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
- package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
- package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
- package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
- package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
- package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
- package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
- package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
- package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
- package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
- package/src/main/__tests__/seedStatus.test.cjs +0 -100
- package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
- package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
- package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
- package/src/main/__tests__/telemetryContract.test.cjs +0 -930
- package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
- package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
- package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
- package/src/main/__tests__/validationSentinels.test.cjs +0 -84
- package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
- package/src/main/bilkoHost.cjs +0 -314
- package/src/main/bilkoHostCore.cjs +0 -89
- package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
- package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
- package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
- package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
- package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
- package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
- package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
- package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
- package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
- package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
- package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
- package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
- package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
- package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
- package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
- package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
- package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
- package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
- package/src/main/lib/__tests__/gateFixtures.json +0 -20
- package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
- package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
- package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
- package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
- package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
- package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
- package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
- package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
- package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
- package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
- package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
- package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
- package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
- package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
- package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
- package/src/main/lib/__tests__/procName.test.cjs +0 -92
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
- package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
- package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
- package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
- package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
- package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
- package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
- package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
- package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
- package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
- package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
- package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- package/src/main/lib/kgExchangePairing.cjs +0 -75
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* scheduler-reap-dead-running-jobs.test.cjs — regression cover for PRD 935
|
|
3
|
-
* (reapDeadRunningJobs runningSet desync).
|
|
4
|
-
*
|
|
5
|
-
* spawnJob()'s completion path can write terminal run artifacts to disk and
|
|
6
|
-
* have its child process fully exit, yet still throw between executeJob()
|
|
7
|
-
* resolving and the completion mutate() finishing (e.g. writeQueue's
|
|
8
|
-
* unreadable guard). That throw is swallowed by spawnJob()'s catch, but its
|
|
9
|
-
* `finally` unconditionally deletes the job's slug from the in-memory
|
|
10
|
-
* runningSet — leaving queue.json stuck at status:"running" with a dead pid
|
|
11
|
-
* while runningSet no longer names it. reapDeadRunningJobs() used to gate its
|
|
12
|
-
* entire body on `runningSet.size === 0`, so this exact state made it return
|
|
13
|
-
* before ever reading queue.json — permanently invisible to reconciliation.
|
|
14
|
-
*
|
|
15
|
-
* This test reproduces that desync directly: a project queue.json job row
|
|
16
|
-
* with status:"running" and a dead pid, while runningSet (freshly loaded,
|
|
17
|
-
* never populated) does not contain its slug. It asserts the job still gets
|
|
18
|
-
* reconciled to a terminal status.
|
|
19
|
-
*
|
|
20
|
-
* HOME is overridden to a tmp dir BEFORE requiring scheduler.cjs, since every
|
|
21
|
-
* path this code touches (queueStore's MACHINE_STATE_PATH, activeSessions'
|
|
22
|
-
* project scan root, scheduler.cjs's ROOT/RUNS_DIR, schedulerBatch's
|
|
23
|
-
* DEFAULT_PROJECT_CWD) is baked into a top-level const from os.homedir() at
|
|
24
|
-
* require time — this test must never be able to read or write real state.
|
|
25
|
-
*
|
|
26
|
-
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
'use strict';
|
|
30
|
-
|
|
31
|
-
const assert = require('node:assert/strict');
|
|
32
|
-
const fs = require('node:fs');
|
|
33
|
-
const os = require('node:os');
|
|
34
|
-
const path = require('node:path');
|
|
35
|
-
const { execFileSync } = require('node:child_process');
|
|
36
|
-
|
|
37
|
-
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'reap-dead-running-jobs-test-'));
|
|
38
|
-
process.env.HOME = tmpHome;
|
|
39
|
-
|
|
40
|
-
const { reapDeadRunningJobs, PIDLESS_SPAWN_GRACE_MS } = require('../scheduler.cjs');
|
|
41
|
-
const { auditLogPath } = require('../lib/auditLog.cjs');
|
|
42
|
-
// queueStore's cwd discovery is cached for 30s (queueStore.cjs's CACHE_MS) —
|
|
43
|
-
// a project registered by THIS test after an earlier test already populated
|
|
44
|
-
// that cache (and didn't itself bust it, e.g. because it found nothing
|
|
45
|
-
// reapable and returned before ever calling writeQueue) would otherwise be
|
|
46
|
-
// invisible to readQueue() for the rest of that window. Bust explicitly
|
|
47
|
-
// after registering a new project so each test sees its own fixture.
|
|
48
|
-
const { bustCwdCache } = require('../lib/queueStore.cjs');
|
|
49
|
-
|
|
50
|
-
function git(args, cwd) {
|
|
51
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function initRepo(dir) {
|
|
55
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
-
git(['init', '-q'], dir);
|
|
57
|
-
git(['config', 'user.email', 'test@example.com'], dir);
|
|
58
|
-
git(['config', 'user.name', 'Test'], dir);
|
|
59
|
-
fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
|
|
60
|
-
git(['add', '-A'], dir);
|
|
61
|
-
git(['commit', '-q', '-m', 'initial'], dir);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function registerActiveProject(cwd) {
|
|
65
|
-
const projectsDir = path.join(tmpHome, '.claude', 'projects');
|
|
66
|
-
const slugDir = path.join(projectsDir, 'fake-project-slug');
|
|
67
|
-
fs.mkdirSync(slugDir, { recursive: true });
|
|
68
|
-
fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Every real scheduler row is born from a real PRD .md file — reconcile()'s
|
|
72
|
-
// auto-archive-drop path (a terminal job whose slug has no on-disk PRD
|
|
73
|
-
// anywhere is treated as "archived on purpose": dropped from queue.json and
|
|
74
|
-
// backfilled to history.jsonl, see scheduler-reconcile-history-backfill.
|
|
75
|
-
// test.cjs) fires the very next time broadcast({flush:true}) reconciles —
|
|
76
|
-
// which reapDeadRunningJobs() itself does on every reap. Without a matching
|
|
77
|
-
// PRD source, a row this test just reaped to a terminal status vanishes from
|
|
78
|
-
// queue.json before the assertions below ever read it back. Epic-scoped (not
|
|
79
|
-
// the legacy flat prds/ dir) so it's never swept by consolidateFlatPrds
|
|
80
|
-
// either.
|
|
81
|
-
function writeFixturePrd(cwd, slug) {
|
|
82
|
-
const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
|
|
83
|
-
fs.mkdirSync(prdsDir, { recursive: true });
|
|
84
|
-
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Test fixture PRD.', 'utf8');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function writeProjectQueue(cwd, jobs) {
|
|
88
|
-
const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
|
|
89
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
90
|
-
fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
|
|
91
|
-
for (const job of jobs) {
|
|
92
|
-
if (job && job.slug) writeFixturePrd(job.cwd || cwd, job.slug);
|
|
93
|
-
}
|
|
94
|
-
return path.join(stateDir, 'queue.json');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function writeRunLog(runId, slug, lines) {
|
|
98
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
99
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
100
|
-
fs.writeFileSync(path.join(runDir, `${slug}.log`), lines.join('\n') + '\n');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
test('reapDeadRunningJobs reconciles a queue.json row stuck at status:running with a dead pid, even when runningSet does not name its slug (desync repro)', async () => {
|
|
104
|
-
const projectCwd = path.join(tmpHome, 'a-project');
|
|
105
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
106
|
-
registerActiveProject(projectCwd);
|
|
107
|
-
|
|
108
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
109
|
-
{
|
|
110
|
-
slug: 'desynced-job',
|
|
111
|
-
status: 'running',
|
|
112
|
-
cwd: projectCwd,
|
|
113
|
-
runId: 'run-desynced',
|
|
114
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid (see reaperHelpers tests)
|
|
115
|
-
},
|
|
116
|
-
]);
|
|
117
|
-
writeRunLog('run-desynced', 'desynced-job', [
|
|
118
|
-
'{"type":"result","subtype":"success","result":"done","is_error":false}',
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
// runningSet is a fresh module-level Set with nothing in it — this is the
|
|
122
|
-
// exact desynced state spawnJob()'s finally block can leave behind.
|
|
123
|
-
await reapDeadRunningJobs();
|
|
124
|
-
|
|
125
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
126
|
-
assert.equal(jobs.length, 1);
|
|
127
|
-
assert.equal(jobs[0].status, 'completed', 'job must be reconciled from disk state, not skipped via the runningSet gate');
|
|
128
|
-
assert.equal(jobs[0].exitCode, 0);
|
|
129
|
-
assert.equal(jobs[0].runtime, undefined);
|
|
130
|
-
assert.equal(jobs[0].gateOutcome, 'passed', 'a success result event maps to gateOutcome:passed');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('reapDeadRunningJobs reaps a pidless row older than PIDLESS_SPAWN_GRACE_MS with an empty run dir as failed, not completed, and audits it', async () => {
|
|
134
|
-
const projectCwd = path.join(tmpHome, 'c-project');
|
|
135
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
136
|
-
registerActiveProject(projectCwd);
|
|
137
|
-
|
|
138
|
-
const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
|
|
139
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
140
|
-
{
|
|
141
|
-
slug: 'pidless-zombie',
|
|
142
|
-
status: 'running',
|
|
143
|
-
cwd: projectCwd,
|
|
144
|
-
runId: 'run-pidless-zombie',
|
|
145
|
-
startedAt: staleStartedAt,
|
|
146
|
-
estimateMinutes: 24,
|
|
147
|
-
// no runtime key at all — the spawn never got far enough to record one
|
|
148
|
-
},
|
|
149
|
-
]);
|
|
150
|
-
// Empty run dir: created, but never written to (the exact 2026-09-01 repro).
|
|
151
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', 'run-pidless-zombie'), { recursive: true });
|
|
152
|
-
|
|
153
|
-
const auditSizeBefore = fs.existsSync(auditLogPath()) ? fs.statSync(auditLogPath()).size : 0;
|
|
154
|
-
|
|
155
|
-
await reapDeadRunningJobs();
|
|
156
|
-
|
|
157
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
158
|
-
assert.equal(jobs.length, 1);
|
|
159
|
-
assert.equal(jobs[0].status, 'failed', 'an empty run dir must never be reaped as completed');
|
|
160
|
-
assert.match(jobs[0].error, /no runtime\.pid recorded/);
|
|
161
|
-
assert.equal(jobs[0].runtime, undefined);
|
|
162
|
-
assert.equal(jobs[0].gateOutcome, 'never_ran', 'a pidless reap means the gate never had a chance to run');
|
|
163
|
-
|
|
164
|
-
const auditText = fs.readFileSync(auditLogPath(), 'utf8').slice(auditSizeBefore);
|
|
165
|
-
const auditLines = auditText.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
166
|
-
const pidlessEvent = auditLines.find((e) => e.kind === 'job_reaped_pidless' && e.slug === 'pidless-zombie');
|
|
167
|
-
assert.ok(pidlessEvent, 'reaping a pidless row must leave an audit trace');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('reapDeadRunningJobs: a pidless row past grace with a landedCommit already recorded is routed to needs_review, not failed, naming the commit sha (PRD 1173)', async () => {
|
|
171
|
-
const projectCwd = path.join(tmpHome, 'c3-project-pidless-with-landed-commit');
|
|
172
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
173
|
-
registerActiveProject(projectCwd);
|
|
174
|
-
|
|
175
|
-
const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
|
|
176
|
-
const landedSha = 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f';
|
|
177
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
178
|
-
{
|
|
179
|
-
slug: 'shipped-but-pidless',
|
|
180
|
-
status: 'running',
|
|
181
|
-
cwd: projectCwd,
|
|
182
|
-
runId: 'run-shipped-but-pidless',
|
|
183
|
-
startedAt: staleStartedAt,
|
|
184
|
-
landedCommit: landedSha,
|
|
185
|
-
// no runtime key at all — the spawn never got far enough to record one
|
|
186
|
-
},
|
|
187
|
-
]);
|
|
188
|
-
// Empty run dir, same shape as the sibling 'failed' test below — the
|
|
189
|
-
// landedCommit evidence must be what changes the outcome, not the log.
|
|
190
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', 'run-shipped-but-pidless'), { recursive: true });
|
|
191
|
-
|
|
192
|
-
await reapDeadRunningJobs();
|
|
193
|
-
|
|
194
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
195
|
-
const row = jobs.find((j) => j.slug === 'shipped-but-pidless');
|
|
196
|
-
assert.equal(row.status, 'needs_review', 'a row with proof of landed work must never be transitioned to failed by the pidless branch');
|
|
197
|
-
assert.equal(row.landedCommit, landedSha, 'the pre-existing landedCommit must be preserved on the row');
|
|
198
|
-
assert.match(row.error, new RegExp(landedSha), 'the reason must name the recorded commit sha');
|
|
199
|
-
assert.equal(row.verifierVerdict, 'pidless_reap_with_landed_commit');
|
|
200
|
-
|
|
201
|
-
// Scan the whole audit log for THIS slug's event rather than slicing by a
|
|
202
|
-
// byte offset from statSync — a prior test's audit entries can contain
|
|
203
|
-
// multi-byte characters (e.g. an em dash in a reaper reason string), and
|
|
204
|
-
// slicing a utf8-decoded JS string (UTF-16 code units) at a byte offset
|
|
205
|
-
// then silently corrupts the boundary. The slug is unique to this test.
|
|
206
|
-
const auditLines = fs.readFileSync(auditLogPath(), 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
207
|
-
const pidlessEvent = auditLines.find((e) => e.kind === 'job_reaped_pidless' && e.slug === 'shipped-but-pidless');
|
|
208
|
-
assert.ok(pidlessEvent, 'reaping this row must still leave an audit trace');
|
|
209
|
-
assert.equal(pidlessEvent.landedCommit, landedSha, 'the audit event must carry the landed-commit evidence so the decision is reconstructable from the audit log alone');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test('reapDeadRunningJobs clears runId on a pidless reap whose run dir holds only a sibling slug\'s files', async () => {
|
|
213
|
-
const projectCwd = path.join(tmpHome, 'c2-project-batch-sibling');
|
|
214
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
215
|
-
registerActiveProject(projectCwd);
|
|
216
|
-
|
|
217
|
-
const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
|
|
218
|
-
// Both jobs were dispatched into the SAME batch runId dir (pickRunDir's
|
|
219
|
-
// header: "tickQueue hands ONE shared batch dir to every spawnJob in the
|
|
220
|
-
// batch"). 'sibling-that-ran' actually spawned and wrote its own log;
|
|
221
|
-
// 'never-spawned' never got a pid and never wrote anything of its own.
|
|
222
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
223
|
-
{
|
|
224
|
-
slug: 'zzq8712-pidless-batch-row',
|
|
225
|
-
status: 'running',
|
|
226
|
-
cwd: projectCwd,
|
|
227
|
-
runId: 'run-shared-batch',
|
|
228
|
-
startedAt: staleStartedAt,
|
|
229
|
-
// no runtime key at all — the spawn never got far enough to record one
|
|
230
|
-
},
|
|
231
|
-
]);
|
|
232
|
-
// Only the sibling's log exists in the shared batch dir.
|
|
233
|
-
writeRunLog('run-shared-batch', 'zzq8712-sibling-that-ran', [
|
|
234
|
-
'{"type":"result","subtype":"success","result":"done","is_error":false}',
|
|
235
|
-
]);
|
|
236
|
-
|
|
237
|
-
await reapDeadRunningJobs();
|
|
238
|
-
|
|
239
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
240
|
-
const row = jobs.find((j) => j.slug === 'zzq8712-pidless-batch-row');
|
|
241
|
-
assert.equal(row.status, 'failed');
|
|
242
|
-
assert.equal(row.runId, null, 'a runId whose dir holds no artifact for this slug must not survive the reap');
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test('reapDeadRunningJobs leaves a pidless row alone while it is still within the grace window', async () => {
|
|
246
|
-
const projectCwd = path.join(tmpHome, 'd-project');
|
|
247
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
248
|
-
registerActiveProject(projectCwd);
|
|
249
|
-
|
|
250
|
-
const freshStartedAt = new Date(Date.now() - 60_000).toISOString(); // 1 minute ago
|
|
251
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
252
|
-
{
|
|
253
|
-
slug: 'mid-flight-spawn',
|
|
254
|
-
status: 'running',
|
|
255
|
-
cwd: projectCwd,
|
|
256
|
-
runId: 'run-mid-flight',
|
|
257
|
-
startedAt: freshStartedAt,
|
|
258
|
-
},
|
|
259
|
-
]);
|
|
260
|
-
|
|
261
|
-
await reapDeadRunningJobs();
|
|
262
|
-
|
|
263
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
264
|
-
assert.equal(jobs[0].status, 'running', 'a genuinely mid-flight spawn must not be reaped');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test('reapDeadRunningJobs is a no-op when no job is actually running in queue.json', async () => {
|
|
268
|
-
const projectCwd = path.join(tmpHome, 'b-project');
|
|
269
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
270
|
-
registerActiveProject(projectCwd);
|
|
271
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
272
|
-
{ slug: 'already-done', status: 'completed', cwd: projectCwd, exitCode: 0 },
|
|
273
|
-
]);
|
|
274
|
-
|
|
275
|
-
await assert.doesNotReject(() => reapDeadRunningJobs());
|
|
276
|
-
|
|
277
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
278
|
-
assert.equal(jobs.length, 1);
|
|
279
|
-
assert.equal(jobs[0].status, 'completed');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('reapDeadRunningJobs salvages a delta-scoped in-place patch (PRD 1098) when the row carries a persisted guardBaseline', async () => {
|
|
283
|
-
const projectCwd = path.join(tmpHome, 'e-project');
|
|
284
|
-
initRepo(projectCwd);
|
|
285
|
-
registerActiveProject(projectCwd);
|
|
286
|
-
|
|
287
|
-
// Pre-existing WIP present at the recorded pre-run baseline — must never
|
|
288
|
-
// appear in the salvaged patch.
|
|
289
|
-
fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human wip\n', 'utf8');
|
|
290
|
-
|
|
291
|
-
const runId = 'run-vanished-inplace';
|
|
292
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
293
|
-
{
|
|
294
|
-
slug: 'vanished-inplace',
|
|
295
|
-
status: 'running',
|
|
296
|
-
cwd: projectCwd,
|
|
297
|
-
runId,
|
|
298
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid
|
|
299
|
-
// Includes the untracked session-manager-operations/ dir itself (git
|
|
300
|
-
// status --porcelain reports an untracked DIRECTORY, not the file
|
|
301
|
-
// inside it) — writeProjectQueue below writes queue.json INSIDE this
|
|
302
|
-
// same git repo, so it's part of the true pre-run baseline exactly
|
|
303
|
-
// like human-wip.txt.
|
|
304
|
-
guardBaseline: ['human-wip.txt', 'session-manager-operations/'],
|
|
305
|
-
},
|
|
306
|
-
]);
|
|
307
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
308
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
309
|
-
fs.writeFileSync(path.join(runDir, 'vanished-inplace.log'), '{"type":"result","subtype":"success","result":"done","is_error":false}\n');
|
|
310
|
-
|
|
311
|
-
// The job's own delta, dirtied before its process vanished.
|
|
312
|
-
fs.writeFileSync(path.join(projectCwd, 'README.md'), 'hello\nedited by job\n', 'utf8');
|
|
313
|
-
fs.writeFileSync(path.join(projectCwd, 'job-output.txt'), 'job work\n', 'utf8');
|
|
314
|
-
|
|
315
|
-
bustCwdCache();
|
|
316
|
-
await reapDeadRunningJobs();
|
|
317
|
-
|
|
318
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
319
|
-
const row = jobs.find((j) => j.slug === 'vanished-inplace');
|
|
320
|
-
// PRD 1133: a successful-looking reap that left everything uncommitted
|
|
321
|
-
// (no branch, HEAD never moved) must never read as 'completed' — this is
|
|
322
|
-
// exactly the dangerous shape the reaper now proves against.
|
|
323
|
-
assert.equal(row.status, 'needs_review');
|
|
324
|
-
assert.equal(row.verifierVerdict, 'reaped_without_integration');
|
|
325
|
-
assert.ok(row.salvagePatch, 'a salvage patch must be recorded on the row');
|
|
326
|
-
assert.ok(fs.existsSync(row.salvagePatch));
|
|
327
|
-
const patch = fs.readFileSync(row.salvagePatch, 'utf8');
|
|
328
|
-
assert.match(patch, /README\.md/);
|
|
329
|
-
assert.match(patch, /edited by job/);
|
|
330
|
-
assert.match(patch, /job-output\.txt/);
|
|
331
|
-
assert.doesNotMatch(patch, /human wip/, 'baseline WIP must never leak into the salvaged patch');
|
|
332
|
-
assert.equal(row.leftoverCount, 2, 'the reaper must attribute the job\'s own delta (README.md + job-output.txt), never the baseline WIP');
|
|
333
|
-
assert.deepEqual(new Set(row.leftoverPaths), new Set(['README.md', 'job-output.txt']));
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test('reapDeadRunningJobs: a failed reap with a persisted guardBaseline names the leftover count in its transition reason and error', async () => {
|
|
337
|
-
const projectCwd = path.join(tmpHome, 'g-project');
|
|
338
|
-
initRepo(projectCwd);
|
|
339
|
-
registerActiveProject(projectCwd);
|
|
340
|
-
|
|
341
|
-
const runId = 'run-vanished-failed';
|
|
342
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
343
|
-
{
|
|
344
|
-
slug: 'vanished-failed',
|
|
345
|
-
status: 'running',
|
|
346
|
-
cwd: projectCwd,
|
|
347
|
-
runId,
|
|
348
|
-
runtime: { pid: 999999 },
|
|
349
|
-
// Includes the untracked session-manager-operations/ dir itself — see
|
|
350
|
-
// the comment on the salvage test above for why.
|
|
351
|
-
guardBaseline: ['session-manager-operations/'],
|
|
352
|
-
},
|
|
353
|
-
]);
|
|
354
|
-
// Empty run dir → classifyRunOutcome finds no result event → 'no_result' → failed.
|
|
355
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
|
|
356
|
-
|
|
357
|
-
fs.writeFileSync(path.join(projectCwd, 'partial-work.txt'), 'unfinished\n', 'utf8');
|
|
358
|
-
|
|
359
|
-
bustCwdCache();
|
|
360
|
-
await reapDeadRunningJobs();
|
|
361
|
-
|
|
362
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
363
|
-
const row = jobs.find((j) => j.slug === 'vanished-failed');
|
|
364
|
-
assert.equal(row.status, 'failed');
|
|
365
|
-
assert.equal(row.leftoverCount, 1);
|
|
366
|
-
assert.deepEqual(row.leftoverPaths, ['partial-work.txt']);
|
|
367
|
-
assert.match(row.error, /left 1 files uncommitted/, 'the leftover count must be visible in the error string, not just a separate field');
|
|
368
|
-
const lastTransition = row.statusHistory[row.statusHistory.length - 1];
|
|
369
|
-
assert.match(lastTransition.reason, /left 1 files uncommitted/);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('reapDeadRunningJobs: a rate-limited death (api_error_status:429) is reset to pending, not stamped failed, and the reason names the rate limit (PRD 1117)', async () => {
|
|
373
|
-
const projectCwd = path.join(tmpHome, 'h-project');
|
|
374
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
375
|
-
registerActiveProject(projectCwd);
|
|
376
|
-
|
|
377
|
-
const runId = 'run-rate-limited';
|
|
378
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
379
|
-
{
|
|
380
|
-
slug: 'rate-limited-job',
|
|
381
|
-
status: 'running',
|
|
382
|
-
cwd: projectCwd,
|
|
383
|
-
runId,
|
|
384
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid — the reaper won the race
|
|
385
|
-
},
|
|
386
|
-
]);
|
|
387
|
-
// Real-shape tail: a 429 result event, the exact signal the reaper
|
|
388
|
-
// previously had no branch for and stamped terminal 'failed'.
|
|
389
|
-
writeRunLog(runId, 'rate-limited-job', [
|
|
390
|
-
'{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"rateLimitType":"seven_day","result":"You\'ve reached your Fable limit.","uuid":"x"}',
|
|
391
|
-
]);
|
|
392
|
-
|
|
393
|
-
await reapDeadRunningJobs();
|
|
394
|
-
|
|
395
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
396
|
-
const row = jobs.find((j) => j.slug === 'rate-limited-job');
|
|
397
|
-
assert.equal(row.status, 'pending', 'a rate-limited reap must be retryable, never terminal failed');
|
|
398
|
-
assert.match(row.error, /rate limit/i, 'the reset reason must name the rate limit, not read as a generic reap failure');
|
|
399
|
-
assert.equal(row.runtime, undefined);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test('reapDeadRunningJobs skips in-place salvage (no whole-tree dump) when the row has no persisted guardBaseline', async () => {
|
|
403
|
-
const projectCwd = path.join(tmpHome, 'f-project');
|
|
404
|
-
initRepo(projectCwd);
|
|
405
|
-
registerActiveProject(projectCwd);
|
|
406
|
-
|
|
407
|
-
fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human wip\n', 'utf8');
|
|
408
|
-
fs.writeFileSync(path.join(projectCwd, 'README.md'), 'hello\nedited by job\n', 'utf8');
|
|
409
|
-
|
|
410
|
-
const runId = 'run-vanished-no-baseline';
|
|
411
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
412
|
-
{
|
|
413
|
-
slug: 'vanished-no-baseline',
|
|
414
|
-
status: 'running',
|
|
415
|
-
cwd: projectCwd,
|
|
416
|
-
runId,
|
|
417
|
-
runtime: { pid: 999999 },
|
|
418
|
-
// No guardBaseline field — simulates a row whose owning process
|
|
419
|
-
// vanished before spawnJob's dispatch-time persist-mutate ever ran, so
|
|
420
|
-
// there's no safe way to distinguish this job's own dirt from the
|
|
421
|
-
// human's pre-existing WIP above.
|
|
422
|
-
},
|
|
423
|
-
]);
|
|
424
|
-
const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
|
|
425
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
426
|
-
fs.writeFileSync(path.join(runDir, 'vanished-no-baseline.log'), '{"type":"result","subtype":"success","result":"done","is_error":false}\n');
|
|
427
|
-
|
|
428
|
-
bustCwdCache();
|
|
429
|
-
await reapDeadRunningJobs();
|
|
430
|
-
|
|
431
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
432
|
-
const row = jobs.find((j) => j.slug === 'vanished-no-baseline');
|
|
433
|
-
// PRD 1133: no commit landed (HEAD never moved) — must not read as 'completed'
|
|
434
|
-
// just because the log's own result event looked clean.
|
|
435
|
-
assert.equal(row.status, 'needs_review');
|
|
436
|
-
assert.equal(row.verifierVerdict, 'reaped_without_integration');
|
|
437
|
-
assert.equal(row.salvagePatch, undefined, 'must skip salvage entirely rather than ever dumping the whole tree');
|
|
438
|
-
assert.equal(row.leftoverPaths, undefined, 'no baseline means no safe attribution — must not guess');
|
|
439
|
-
assert.equal(row.leftoverCount, undefined);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// ---------- PRD 1133: reaper must prove integration before 'completed' ----------
|
|
443
|
-
|
|
444
|
-
test('reapDeadRunningJobs: a worktree job shaped like PRD 1118 (branch un-integrated, landedCommit absent) comes out needs_review, naming the branch, and never merges it', async () => {
|
|
445
|
-
const projectCwd = path.join(tmpHome, 'i-project-1118-shape');
|
|
446
|
-
initRepo(projectCwd);
|
|
447
|
-
registerActiveProject(projectCwd);
|
|
448
|
-
|
|
449
|
-
const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
450
|
-
const slug = '1118-pause-until-the-binding-rate-limit-window';
|
|
451
|
-
const branch = `sm-job/${slug}`;
|
|
452
|
-
// Simulate the job's own worktree branch: one real commit, never merged
|
|
453
|
-
// back into the main tree (exactly the live 1118 shape — "one real commit,
|
|
454
|
-
// +301", stranded on sm-job/1118-... with landedCommit undefined).
|
|
455
|
-
git(['checkout', '-b', branch], projectCwd);
|
|
456
|
-
fs.writeFileSync(path.join(projectCwd, 'rateLimitWindow.cjs'), 'module.exports = {};\n', 'utf8');
|
|
457
|
-
git(['add', '-A'], projectCwd);
|
|
458
|
-
git(['commit', '-q', '-m', 'add rateLimitWindow.cjs'], projectCwd);
|
|
459
|
-
// Back to the original tip (detached is fine — reap only reads guardCwd's
|
|
460
|
-
// current HEAD, it never needs a named branch checked out).
|
|
461
|
-
git(['checkout', headBefore], projectCwd);
|
|
462
|
-
|
|
463
|
-
const runId = `run-${slug}`;
|
|
464
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
465
|
-
{
|
|
466
|
-
slug,
|
|
467
|
-
status: 'running',
|
|
468
|
-
cwd: projectCwd,
|
|
469
|
-
runId,
|
|
470
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid
|
|
471
|
-
guardHeadBefore: headBefore,
|
|
472
|
-
startedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
473
|
-
},
|
|
474
|
-
]);
|
|
475
|
-
writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
|
|
476
|
-
|
|
477
|
-
bustCwdCache();
|
|
478
|
-
await reapDeadRunningJobs();
|
|
479
|
-
|
|
480
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
481
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
482
|
-
assert.equal(row.status, 'needs_review', 'a successful-looking result event must not be enough on its own');
|
|
483
|
-
assert.equal(row.verifierVerdict, 'reaped_without_integration');
|
|
484
|
-
assert.match(row.error, new RegExp(branch.replace('/', '\\/')), 'the parked reason must name the branch that still holds the work');
|
|
485
|
-
assert.equal(row.landedCommit, undefined, 'nothing was actually merged, so landedCommit must stay unset');
|
|
486
|
-
|
|
487
|
-
// The branch itself must survive untouched — this reap must never attempt
|
|
488
|
-
// (let alone perform) the merge.
|
|
489
|
-
const branchStillExists = git(['rev-parse', '--verify', branch], projectCwd).trim();
|
|
490
|
-
assert.ok(branchStillExists, 'the un-integrated branch must be preserved, not merged or deleted, by a mere reap');
|
|
491
|
-
assert.equal(git(['rev-parse', 'HEAD'], projectCwd).trim(), headBefore, 'guardCwd HEAD must not have moved — no merge was performed');
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
test('reapDeadRunningJobs: a worktree job whose branch is already fully integrated into HEAD comes out completed, with landedCommit stamped', async () => {
|
|
495
|
-
const projectCwd = path.join(tmpHome, 'j-project-real-success');
|
|
496
|
-
initRepo(projectCwd);
|
|
497
|
-
registerActiveProject(projectCwd);
|
|
498
|
-
|
|
499
|
-
const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
500
|
-
const slug = 'real-successful-worktree-run';
|
|
501
|
-
const branch = `sm-job/${slug}`;
|
|
502
|
-
git(['checkout', '-b', branch], projectCwd);
|
|
503
|
-
fs.writeFileSync(path.join(projectCwd, 'feature.txt'), 'shipped\n', 'utf8');
|
|
504
|
-
git(['add', '-A'], projectCwd);
|
|
505
|
-
git(['commit', '-q', '-m', 'ship feature'], projectCwd);
|
|
506
|
-
git(['checkout', headBefore], projectCwd); // back to the pre-branch tip (detached)
|
|
507
|
-
// Simulate a normal completion pass having already integrated the branch
|
|
508
|
-
// (integrateJobBranch's own fast-forward path) BEFORE the process vanished
|
|
509
|
-
// between that merge and the queue write landing.
|
|
510
|
-
git(['merge', '--ff-only', branch], projectCwd);
|
|
511
|
-
const headAfterMerge = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
512
|
-
|
|
513
|
-
const runId = `run-${slug}`;
|
|
514
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
515
|
-
{
|
|
516
|
-
slug,
|
|
517
|
-
status: 'running',
|
|
518
|
-
cwd: projectCwd,
|
|
519
|
-
runId,
|
|
520
|
-
runtime: { pid: 999999 },
|
|
521
|
-
guardHeadBefore: headBefore,
|
|
522
|
-
startedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
523
|
-
},
|
|
524
|
-
]);
|
|
525
|
-
writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
|
|
526
|
-
|
|
527
|
-
bustCwdCache();
|
|
528
|
-
await reapDeadRunningJobs();
|
|
529
|
-
|
|
530
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
531
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
532
|
-
assert.equal(row.status, 'completed', 'the branch is already an ancestor of HEAD — the work genuinely landed');
|
|
533
|
-
assert.equal(row.verifierVerdict, undefined);
|
|
534
|
-
assert.equal(row.landedCommit, headAfterMerge, 'HEAD advanced during the run window — landedCommit must be stamped');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test('reapDeadRunningJobs: a worktree job whose branch never diverged from its base (legitimate no-op) still completes, not punished', async () => {
|
|
538
|
-
const projectCwd = path.join(tmpHome, 'k-project-noop-worktree');
|
|
539
|
-
initRepo(projectCwd);
|
|
540
|
-
registerActiveProject(projectCwd);
|
|
541
|
-
|
|
542
|
-
const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
543
|
-
const slug = 'noop-worktree-job';
|
|
544
|
-
const branch = `sm-job/${slug}`;
|
|
545
|
-
// Branch created but nothing ever committed on it — a genuine no-op run.
|
|
546
|
-
git(['branch', branch], projectCwd);
|
|
547
|
-
|
|
548
|
-
const runId = `run-${slug}`;
|
|
549
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
550
|
-
{
|
|
551
|
-
slug,
|
|
552
|
-
status: 'running',
|
|
553
|
-
cwd: projectCwd,
|
|
554
|
-
runId,
|
|
555
|
-
runtime: { pid: 999999 },
|
|
556
|
-
guardHeadBefore: headBefore,
|
|
557
|
-
startedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
558
|
-
},
|
|
559
|
-
]);
|
|
560
|
-
writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
|
|
561
|
-
|
|
562
|
-
bustCwdCache();
|
|
563
|
-
await reapDeadRunningJobs();
|
|
564
|
-
|
|
565
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
566
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
567
|
-
assert.equal(row.status, 'completed', 'a branch with no new commits is a legitimate no-op, not a finish-protocol violation');
|
|
568
|
-
assert.equal(row.verifierVerdict, undefined);
|
|
569
|
-
assert.equal(row.landedCommit, undefined, 'HEAD never moved — nothing to stamp');
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
test('reapDeadRunningJobs: an in-place run in a git repo whose HEAD genuinely advanced during the run window completes, with landedCommit stamped', async () => {
|
|
573
|
-
const projectCwd = path.join(tmpHome, 'l-project-inplace-success');
|
|
574
|
-
initRepo(projectCwd);
|
|
575
|
-
registerActiveProject(projectCwd);
|
|
576
|
-
|
|
577
|
-
const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
578
|
-
const startedAt = new Date(Date.now() - 60_000).toISOString();
|
|
579
|
-
fs.writeFileSync(path.join(projectCwd, 'shipped.txt'), 'done\n', 'utf8');
|
|
580
|
-
git(['add', '-A'], projectCwd);
|
|
581
|
-
git(['commit', '-q', '-m', 'in-place job commit'], projectCwd);
|
|
582
|
-
const headAfter = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
583
|
-
|
|
584
|
-
const slug = 'inplace-job-real-commit';
|
|
585
|
-
const runId = `run-${slug}`;
|
|
586
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
587
|
-
{
|
|
588
|
-
slug,
|
|
589
|
-
status: 'running',
|
|
590
|
-
cwd: projectCwd,
|
|
591
|
-
runId,
|
|
592
|
-
runtime: { pid: 999999 },
|
|
593
|
-
guardHeadBefore: headBefore,
|
|
594
|
-
startedAt,
|
|
595
|
-
},
|
|
596
|
-
]);
|
|
597
|
-
writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
|
|
598
|
-
|
|
599
|
-
bustCwdCache();
|
|
600
|
-
await reapDeadRunningJobs();
|
|
601
|
-
|
|
602
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
603
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
604
|
-
assert.equal(row.status, 'completed');
|
|
605
|
-
assert.equal(row.verifierVerdict, undefined);
|
|
606
|
-
assert.equal(row.landedCommit, headAfter);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
test('reapDeadRunningJobs: a non-git cwd skips the integration check entirely, preserving today\'s behaviour', async () => {
|
|
610
|
-
const projectCwd = path.join(tmpHome, 'm-project-non-git');
|
|
611
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
612
|
-
registerActiveProject(projectCwd);
|
|
613
|
-
|
|
614
|
-
const slug = 'non-git-cwd-job';
|
|
615
|
-
const runId = `run-${slug}`;
|
|
616
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
617
|
-
{
|
|
618
|
-
slug,
|
|
619
|
-
status: 'running',
|
|
620
|
-
cwd: projectCwd,
|
|
621
|
-
runId,
|
|
622
|
-
runtime: { pid: 999999 },
|
|
623
|
-
// No git repo at all — the integration/HEAD-advance check must be
|
|
624
|
-
// skipped, never treated as a failure to prove landing.
|
|
625
|
-
},
|
|
626
|
-
]);
|
|
627
|
-
writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
|
|
628
|
-
|
|
629
|
-
bustCwdCache();
|
|
630
|
-
await reapDeadRunningJobs();
|
|
631
|
-
|
|
632
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
633
|
-
const row = jobs.find((j) => j.slug === slug);
|
|
634
|
-
assert.equal(row.status, 'completed', 'a non-git cwd must keep today\'s behaviour — the check is meaningless there');
|
|
635
|
-
assert.equal(row.verifierVerdict, undefined);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// ---------- Reaper landed-commit evidence gate (job 1192 incident) ----------
|
|
639
|
-
//
|
|
640
|
-
// job 1192 shipped a real 3-file commit (7bf5fa3f...) and was still
|
|
641
|
-
// stamped 'failed' by reapDeadRunningJobs because a `landedCommit` field
|
|
642
|
-
// being present was never actually verified against the repo. These cover
|
|
643
|
-
// the dead-pid branch specifically (runtime.pid recorded, process
|
|
644
|
-
// confirmed dead) rather than the pidless branch: a pidless row with a
|
|
645
|
-
// non-empty landedCommit is already fully owned by PRD 1173's
|
|
646
|
-
// resolvePidlessFailureOverride (routes to needs_review, tested above) —
|
|
647
|
-
// this PRD's gate exists for the shape 1173 doesn't cover, where the
|
|
648
|
-
// reaper would otherwise fall straight through to 'failed' with no
|
|
649
|
-
// evidence check of any kind.
|
|
650
|
-
|
|
651
|
-
test('reapDeadRunningJobs: a dead-pid reap with a git-resolvable landedCommit transitions to completed, naming the reap cause and the evidence, source reapDeadRunningJobs:landed', async () => {
|
|
652
|
-
const projectCwd = path.join(tmpHome, 'n-project-landed-commit-evidence');
|
|
653
|
-
initRepo(projectCwd);
|
|
654
|
-
registerActiveProject(projectCwd);
|
|
655
|
-
|
|
656
|
-
fs.writeFileSync(path.join(projectCwd, 'shipped.txt'), 'done\n', 'utf8');
|
|
657
|
-
git(['add', '-A'], projectCwd);
|
|
658
|
-
git(['commit', '-q', '-m', 'ship the PRD work'], projectCwd);
|
|
659
|
-
const landedSha = git(['rev-parse', 'HEAD'], projectCwd).trim();
|
|
660
|
-
|
|
661
|
-
const runId = 'run-landed-commit-evidence';
|
|
662
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
663
|
-
{
|
|
664
|
-
slug: 'landed-but-reaped',
|
|
665
|
-
status: 'running',
|
|
666
|
-
cwd: projectCwd,
|
|
667
|
-
runId,
|
|
668
|
-
runtime: { pid: 999999 }, // guaranteed-dead pid
|
|
669
|
-
landedCommit: landedSha,
|
|
670
|
-
},
|
|
671
|
-
]);
|
|
672
|
-
// Empty run dir → classifyRunOutcome finds no result event → 'no_result'
|
|
673
|
-
// → without this PRD's gate this would stamp 'failed' despite the real
|
|
674
|
-
// commit already sitting on HEAD.
|
|
675
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
|
|
676
|
-
|
|
677
|
-
bustCwdCache();
|
|
678
|
-
await reapDeadRunningJobs();
|
|
679
|
-
|
|
680
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
681
|
-
const row = jobs.find((j) => j.slug === 'landed-but-reaped');
|
|
682
|
-
assert.equal(row.status, 'completed', 'a resolvable landedCommit must promote the reap to completed, not failed');
|
|
683
|
-
assert.equal(row.landedCommit, landedSha);
|
|
684
|
-
assert.equal(row.exitCode, 0);
|
|
685
|
-
assert.equal(row.error, null);
|
|
686
|
-
const lastTransition = row.statusHistory[row.statusHistory.length - 1];
|
|
687
|
-
assert.equal(lastTransition.source, 'reapDeadRunningJobs:landed');
|
|
688
|
-
assert.match(lastTransition.reason, /process gone/, 'the reap cause must still be named');
|
|
689
|
-
assert.match(lastTransition.reason, new RegExp(landedSha), 'the evidence sha must be named');
|
|
690
|
-
assert.match(lastTransition.reason, /resolves/);
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
test('reapDeadRunningJobs: a dead-pid reap with an unresolvable (stale) landedCommit keeps today\'s behaviour exactly — failed, same reason shape, same source', async () => {
|
|
694
|
-
const projectCwd = path.join(tmpHome, 'o-project-stale-landed-commit');
|
|
695
|
-
initRepo(projectCwd);
|
|
696
|
-
registerActiveProject(projectCwd);
|
|
697
|
-
|
|
698
|
-
const staleSha = '0123456789abcdef0123456789abcdef01234567'; // well-formed sha, never committed anywhere
|
|
699
|
-
const runId = 'run-stale-landed-commit';
|
|
700
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
701
|
-
{
|
|
702
|
-
slug: 'stale-landed-commit',
|
|
703
|
-
status: 'running',
|
|
704
|
-
cwd: projectCwd,
|
|
705
|
-
runId,
|
|
706
|
-
runtime: { pid: 999999 },
|
|
707
|
-
landedCommit: staleSha,
|
|
708
|
-
},
|
|
709
|
-
]);
|
|
710
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
|
|
711
|
-
|
|
712
|
-
bustCwdCache();
|
|
713
|
-
await reapDeadRunningJobs();
|
|
714
|
-
|
|
715
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
716
|
-
const row = jobs.find((j) => j.slug === 'stale-landed-commit');
|
|
717
|
-
assert.equal(row.status, 'failed', 'a landedCommit that does not resolve in the repo must never be trusted as evidence');
|
|
718
|
-
assert.equal(row.landedCommit, staleSha, 'the stale field is left on the row untouched, never cleared');
|
|
719
|
-
assert.match(row.error, /reaped: process gone \(outcome=no_result\)/);
|
|
720
|
-
assert.doesNotMatch(row.error, /resolves/);
|
|
721
|
-
const lastTransition = row.statusHistory[row.statusHistory.length - 1];
|
|
722
|
-
assert.equal(lastTransition.source, 'reapDeadRunningJobs', 'no evidence found — source must stay the plain reaper source, not :landed');
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
test('reapDeadRunningJobs: a landedCommit that cannot be git-resolved because the row cwd is not a usable git repo falls back to failed without throwing out of the reaper', async () => {
|
|
726
|
-
const projectCwd = path.join(tmpHome, 'p-project-unresolvable-cwd');
|
|
727
|
-
// Deliberately NOT initRepo(projectCwd) — no `.git` at all, the same
|
|
728
|
-
// "cwd git resolution cannot possibly succeed" shape a deleted/torn-down
|
|
729
|
-
// worktree checkout would produce (git itself errors identically for
|
|
730
|
-
// "not a git repository" and "no such directory" — both must be caught
|
|
731
|
-
// and treated as no-evidence, never thrown out of the reaper).
|
|
732
|
-
fs.mkdirSync(projectCwd, { recursive: true });
|
|
733
|
-
registerActiveProject(projectCwd);
|
|
734
|
-
|
|
735
|
-
const runId = 'run-unresolvable-cwd';
|
|
736
|
-
const queuePath = writeProjectQueue(projectCwd, [
|
|
737
|
-
{
|
|
738
|
-
slug: 'unresolvable-cwd-job',
|
|
739
|
-
status: 'running',
|
|
740
|
-
cwd: projectCwd,
|
|
741
|
-
runId,
|
|
742
|
-
runtime: { pid: 999999 },
|
|
743
|
-
landedCommit: '89abcdef89abcdef89abcdef89abcdef89abcdef',
|
|
744
|
-
},
|
|
745
|
-
]);
|
|
746
|
-
fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
|
|
747
|
-
|
|
748
|
-
bustCwdCache();
|
|
749
|
-
await assert.doesNotReject(() => reapDeadRunningJobs(), 'a bounded git spawn failure/timeout must never escape the reaper as an unhandled rejection');
|
|
750
|
-
|
|
751
|
-
const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
|
|
752
|
-
const row = jobs.find((j) => j.slug === 'unresolvable-cwd-job');
|
|
753
|
-
assert.equal(row.status, 'failed', 'an unresolvable cwd must fall back to failed, exactly like no evidence at all');
|
|
754
|
-
});
|