claude-code-session-manager 0.97.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-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
- package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
- package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
- package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
- package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
- package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
- package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
- package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
- package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
- package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
- package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
- package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
- package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
- package/dist/assets/Settings-Dc1F_sSg.js +3 -0
- package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
- package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
- package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
- package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
- package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
- package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
- package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
- package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
- package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +26 -29
- package/plugins/CLAUDE.md +6 -6
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -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 +153 -19
- 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 +33 -30
- package/src/main/lib/workTypeLibrary.cjs +7 -1
- package/src/main/promptSessionEvents.cjs +37 -13
- package/src/main/queueOps.cjs +17 -6
- package/src/main/scheduler.cjs +975 -203
- 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-nESrTRIg.js +0 -1
- package/dist/assets/Scheduler-BVYTeh39.js +0 -16
- package/dist/assets/Settings-DIhGgdfs.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__/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 -154
- 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-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 -375
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
- package/src/main/lib/kgExchangePairing.cjs +0 -75
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* anymore — both just point the headless executor at STANDARDS_PATH with an
|
|
12
12
|
* instruction to read it before starting. There's nothing to go stale: the
|
|
13
13
|
* executor always reads the live file at run time, same one-concept-one-
|
|
14
|
-
* implementation reasoning that keeps the two PRD-creation paths
|
|
15
|
-
*
|
|
14
|
+
* implementation reasoning that keeps the two PRD-creation paths (admin
|
|
15
|
+
* HTTP route, chat:create-prd IPC) in sync.
|
|
16
16
|
*/
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
@@ -32,6 +32,8 @@ const { resolveDepSlug, findNearMatches } = require('./depSlugResolve.cjs');
|
|
|
32
32
|
const { isFixPlanSlug } = require('./fixPlanSlug.cjs');
|
|
33
33
|
const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
|
|
34
34
|
const { sizingWarnings } = require('./prdSizing.cjs');
|
|
35
|
+
const { validateGate, validateFiles, renderFilesSection, renderGateSection } = require('./prdGateFiles.cjs');
|
|
36
|
+
const { resolveGate, parseChain, isNoneGate } = require('./definitionOfDone.cjs');
|
|
35
37
|
|
|
36
38
|
// A caller-supplied slug that already starts with its own `NN-` (e.g.
|
|
37
39
|
// "254-perf-x") used to silently become the double-prefixed row
|
|
@@ -68,18 +70,19 @@ function deriveSlugFromTitle(title) {
|
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* Build the full PRD markdown body (frontmatter + required sections + a
|
|
71
|
-
* pointer at the engineering standards file), matching the
|
|
72
|
-
*
|
|
73
|
-
* criteria / Implementation notes / Out of scope /
|
|
74
|
-
* in order.
|
|
73
|
+
* pointer at the engineering standards file), matching the body shape
|
|
74
|
+
* PRD_AUTHORING.md §13 documents: frontmatter, then Goal / Acceptance
|
|
75
|
+
* criteria / Files / Implementation notes / Out of scope / Gate /
|
|
76
|
+
* Engineering standards, in order.
|
|
75
77
|
*/
|
|
76
78
|
function buildPrdBody(input) {
|
|
77
79
|
const {
|
|
78
80
|
title, cwd, estimateMinutes, goal, acceptanceCriteria,
|
|
79
81
|
implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, agentType, dependsOn, quietMachine, disposition, deliverable, artifactPaths, planId,
|
|
82
|
+
gate, files,
|
|
80
83
|
} = input;
|
|
81
84
|
|
|
82
|
-
// No `parallelGroup` frontmatter key
|
|
85
|
+
// No `parallelGroup` frontmatter key (PRD_AUTHORING.md §5) — the NN-
|
|
83
86
|
// filename prefix is the single source of truth for grouping; adding a
|
|
84
87
|
// second one here would let the two drift out of sync.
|
|
85
88
|
const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}`, `estimateMinutes: ${estimateMinutes}`];
|
|
@@ -140,13 +143,22 @@ function buildPrdBody(input) {
|
|
|
140
143
|
'do not re-read the whole file every run.',
|
|
141
144
|
].join('\n');
|
|
142
145
|
|
|
146
|
+
// `# Files` sits right after Acceptance criteria; `# Gate` sits after Out
|
|
147
|
+
// of scope, before Engineering standards (gate-and-files-in-PRD-body unit).
|
|
148
|
+
// Each section appears only when its field is given. Only the MCP tool
|
|
149
|
+
// refuses a call without them; createPrd() (so the HTTP route and the IPC
|
|
150
|
+
// handler too) just warns when either is absent.
|
|
143
151
|
const bodyLines = [
|
|
144
152
|
'# Goal', '', goal, '',
|
|
145
153
|
'# Acceptance criteria', '', acLines, '',
|
|
154
|
+
];
|
|
155
|
+
if (files && files.length) bodyLines.push(...renderFilesSection(files));
|
|
156
|
+
bodyLines.push(
|
|
146
157
|
'# Implementation notes', '', implementationNotes, '',
|
|
147
158
|
'# Out of scope', '', oosLines, '',
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
);
|
|
160
|
+
if (gate && gate.length) bodyLines.push(...renderGateSection(gate));
|
|
161
|
+
bodyLines.push('## Engineering standards', '', standardsPointer, '');
|
|
150
162
|
|
|
151
163
|
return `${fmLines.join('\n')}${bodyLines.join('\n')}`;
|
|
152
164
|
}
|
|
@@ -347,6 +359,67 @@ async function createPrd(input, remote) {
|
|
|
347
359
|
}
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
// Fake-heading guard (gate-and-files-in-PRD-body unit): a caller-supplied
|
|
363
|
+
// free-text field that itself contains a "# Gate" or "# Files" heading line
|
|
364
|
+
// would, after buildPrdBody splices in the real section below, read as TWO
|
|
365
|
+
// Gate/Files sections — readExplicitGate takes the FIRST fence/heading it
|
|
366
|
+
// finds, so the fake one would silently win and the executor would see the
|
|
367
|
+
// wrong gate. Checked before NN allocation so a refusal burns no number.
|
|
368
|
+
const HEADING_LINE_RE = /^ {0,3}#{1,6}[ \t]+(gate|files)[ \t]*#*[ \t]*$/i;
|
|
369
|
+
function headingGuardError(fieldLabel, text) {
|
|
370
|
+
if (typeof text !== 'string') return null;
|
|
371
|
+
for (const line of text.split(/\r\n|\r|\n/)) {
|
|
372
|
+
if (HEADING_LINE_RE.test(line)) {
|
|
373
|
+
return `The ${fieldLabel} text has a "# Gate" or "# Files" heading. The API writes those sections ` +
|
|
374
|
+
'from the gate and files fields. Remove the heading and call again.';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
for (const [fieldLabel, text] of [['goal', input.goal], ['implementation notes', input.implementationNotes]]) {
|
|
380
|
+
const headingError = headingGuardError(fieldLabel, text);
|
|
381
|
+
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(input.acceptanceCriteria)) {
|
|
384
|
+
for (const entry of input.acceptanceCriteria) {
|
|
385
|
+
const headingError = headingGuardError('acceptance criteria', entry);
|
|
386
|
+
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (Array.isArray(input.outOfScope)) {
|
|
390
|
+
for (const entry of input.outOfScope) {
|
|
391
|
+
const headingError = headingGuardError('out of scope', entry);
|
|
392
|
+
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Gate + files validation (gate-and-files-in-PRD-body unit): fail-closed,
|
|
397
|
+
// checked BEFORE NN allocation so a refusal burns no number — same posture
|
|
398
|
+
// as the artifactPaths checks above. The actual shape rules live in
|
|
399
|
+
// prdGateFiles.cjs (pure, no I/O); this is just the accumulate-and-return glue.
|
|
400
|
+
const extraWarnings = [];
|
|
401
|
+
let gateCommands;
|
|
402
|
+
if (input.gate !== undefined) {
|
|
403
|
+
const gateValidation = validateGate(input.gate);
|
|
404
|
+
if (!gateValidation.ok) {
|
|
405
|
+
return { ok: false, status: 400, error: gateValidation.error };
|
|
406
|
+
}
|
|
407
|
+
gateCommands = gateValidation.commands;
|
|
408
|
+
extraWarnings.push(...gateValidation.warnings);
|
|
409
|
+
} else {
|
|
410
|
+
extraWarnings.push('No gate: the scheduler cannot re-check this PRD.');
|
|
411
|
+
}
|
|
412
|
+
let filesList;
|
|
413
|
+
if (input.files !== undefined) {
|
|
414
|
+
const filesValidation = validateFiles(input.files);
|
|
415
|
+
if (!filesValidation.ok) {
|
|
416
|
+
return { ok: false, status: 400, error: filesValidation.error };
|
|
417
|
+
}
|
|
418
|
+
filesList = filesValidation.files;
|
|
419
|
+
} else {
|
|
420
|
+
extraWarnings.push('No files list: the executor is not told which files it may change.');
|
|
421
|
+
}
|
|
422
|
+
|
|
350
423
|
const nn = await remote.allocateParallelGroup(input.cwd);
|
|
351
424
|
const filenameSlug = `${nn}-${slug}`;
|
|
352
425
|
|
|
@@ -463,7 +536,32 @@ async function createPrd(input, remote) {
|
|
|
463
536
|
}
|
|
464
537
|
if (!planId) planId = mintPlanId();
|
|
465
538
|
|
|
466
|
-
const body = buildPrdBody({ ...input, planId });
|
|
539
|
+
const body = buildPrdBody({ ...input, planId, gate: gateCommands, files: filesList });
|
|
540
|
+
|
|
541
|
+
// Read-back check (gate-and-files-in-PRD-body unit): the whole point of a
|
|
542
|
+
// declared gate is that the scheduler's own reader (resolveGate/parseChain
|
|
543
|
+
// in definitionOfDone.cjs) gets back exactly what was declared. A stray
|
|
544
|
+
// ```gate fence elsewhere in the body (goal/notes/criteria/out-of-scope)
|
|
545
|
+
// would otherwise win — readExplicitGate takes the FIRST fence in the
|
|
546
|
+
// text — and the scheduler would silently re-run the wrong gate. Caught
|
|
547
|
+
// here, before the write, rather than discovered later from a job log.
|
|
548
|
+
if (gateCommands) {
|
|
549
|
+
const expectedSource = gateCommands.length === 1 && isNoneGate(gateCommands[0]) ? 'none' : 'explicit';
|
|
550
|
+
const expectedSequence = expectedSource === 'none' ? [] : gateCommands.flatMap(parseChain);
|
|
551
|
+
const resolved = resolveGate(body);
|
|
552
|
+
const matches = resolved.source === expectedSource
|
|
553
|
+
&& JSON.stringify(resolved.sequence) === JSON.stringify(expectedSequence);
|
|
554
|
+
if (!matches) {
|
|
555
|
+
return {
|
|
556
|
+
ok: false,
|
|
557
|
+
status: 400,
|
|
558
|
+
error: 'The PRD text already holds a gate fence (three backticks + gate), so the scheduler would read '
|
|
559
|
+
+ 'the wrong gate. Remove it from the goal, implementation notes, acceptance criteria, out of scope '
|
|
560
|
+
+ 'or files, and call again.',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
467
565
|
const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
|
|
468
566
|
if (!writeResult?.ok) {
|
|
469
567
|
return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
@@ -480,25 +578,59 @@ async function createPrd(input, remote) {
|
|
|
480
578
|
sourceTabId: input.sourceTabId ?? null,
|
|
481
579
|
});
|
|
482
580
|
|
|
581
|
+
// PRD 1446: writePrd() succeeding used to just promise "the next scheduler
|
|
582
|
+
// reconcile pass (~1 minute)" adopts this file — but nothing reliably ran
|
|
583
|
+
// that pass (the 60s pollLoop only reaches reconcile() via
|
|
584
|
+
// maybeLaunchWhenAvailable, which returns early while zero rows are
|
|
585
|
+
// pending; the 10-minute rescheduleTimer could itself stall behind a slow
|
|
586
|
+
// billing fetch). Trigger one reconcile immediately through the EXISTING
|
|
587
|
+
// broadcast-coalescer seam (remote.requestReconcile(), same one resetJob
|
|
588
|
+
// already uses) instead of waiting on either. Best-effort: a reconcile
|
|
589
|
+
// failure here must not fail a write that already landed on disk — the
|
|
590
|
+
// PRD file is real regardless, and the next scheduled pass still adopts
|
|
591
|
+
// it if this one didn't stick.
|
|
592
|
+
let enqueued = false;
|
|
593
|
+
if (typeof remote.requestReconcile === 'function') {
|
|
594
|
+
try {
|
|
595
|
+
await remote.requestReconcile();
|
|
596
|
+
if (typeof remote.getJob === 'function') {
|
|
597
|
+
enqueued = Boolean(await remote.getJob(filenameSlug));
|
|
598
|
+
}
|
|
599
|
+
} catch (e) {
|
|
600
|
+
console.warn(`[prdCreate] immediate requestReconcile failed (non-fatal, next scheduled pass will adopt it): ${e?.message ?? e}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
483
604
|
// No `status` field here, and never one named 'queued' — that string was
|
|
484
605
|
// handed straight to callers as if it were a real ScheduleJob status, and
|
|
485
606
|
// is exactly how the 1021/1022 incident's invalid `"status": "queued"`
|
|
486
607
|
// rows got onto disk (this route's own response was the most likely
|
|
487
608
|
// source). A PRD is a queue row's SOURCE, not the row itself: writePrd
|
|
488
|
-
// only ever joins an Epic and writes the .md file — the queue row
|
|
489
|
-
// created separately, by
|
|
490
|
-
//
|
|
491
|
-
//
|
|
609
|
+
// only ever joins an Epic and writes the .md file — the queue row above
|
|
610
|
+
// is created separately, by reconcile(). `enqueued` reflects whether that
|
|
611
|
+
// reconcile pass (triggered synchronously above) actually produced the
|
|
612
|
+
// row before this function returned — it is not a fabricated status.
|
|
492
613
|
return {
|
|
493
614
|
ok: true,
|
|
494
615
|
nn,
|
|
495
616
|
filename: `${filenameSlug}.md`,
|
|
496
617
|
prdPath: writeResult.path ?? null,
|
|
497
618
|
epicId: writeResult.epicId ?? null,
|
|
498
|
-
enqueued
|
|
499
|
-
note:
|
|
500
|
-
|
|
501
|
-
|
|
619
|
+
enqueued,
|
|
620
|
+
note: enqueued
|
|
621
|
+
? 'PRD file written and adopted into the queue as a pending row'
|
|
622
|
+
: 'PRD file written; the queue row is derived by the next scheduler reconcile pass, not created here',
|
|
623
|
+
// gate-and-files-in-PRD-body unit: honest echo of what was actually
|
|
624
|
+
// validated and rendered, same spirit as `note` above — never a fabricated status.
|
|
625
|
+
gate: {
|
|
626
|
+
source: gateCommands ? (gateCommands.length === 1 && isNoneGate(gateCommands[0]) ? 'none' : 'explicit') : 'absent',
|
|
627
|
+
commands: gateCommands || [],
|
|
628
|
+
},
|
|
629
|
+
files: filesList || [],
|
|
630
|
+
// Advisory only (PRD 1403 sizing warnings + gate-and-files-in-PRD-body
|
|
631
|
+
// absent/none-gate warnings) — never blocks the write. See prdSizing.cjs
|
|
632
|
+
// and prdGateFiles.cjs.
|
|
633
|
+
warnings: [...extraWarnings, ...sizingWarnings(input)],
|
|
502
634
|
};
|
|
503
635
|
}
|
|
504
636
|
|
|
@@ -542,8 +674,10 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
542
674
|
filename: result.filename,
|
|
543
675
|
prdPath: result.prdPath ?? null,
|
|
544
676
|
epicId: result.epicId ?? null,
|
|
545
|
-
enqueued:
|
|
677
|
+
enqueued: result.enqueued,
|
|
546
678
|
note: result.note,
|
|
679
|
+
gate: result.gate,
|
|
680
|
+
files: result.files,
|
|
547
681
|
warnings: result.warnings,
|
|
548
682
|
});
|
|
549
683
|
});
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* an already-written PRD's disposition from the Scheduler UI. Uses
|
|
19
19
|
* computeDispositionRewrite() to validate the rewrite is safe before
|
|
20
20
|
* handing the new dependsOn to remote.updatePrd (which itself refuses a
|
|
21
|
-
*
|
|
21
|
+
* 'running' or 'completed' row — see scheduler.cjs's updatePrd).
|
|
22
22
|
*
|
|
23
23
|
* `rows` throughout is the same shape listPrdsInternal() already produces:
|
|
24
24
|
* { slug, status, dependsOn }. dependsOn entries are resolved against `rows`
|
|
@@ -151,7 +151,7 @@ function wouldCreateCycle(rows, slug, newDependsOn) {
|
|
|
151
151
|
* behind another chain" action). Pure — returns the new dependsOn value on
|
|
152
152
|
* success; the caller (scheduler.cjs's IPC handler) is responsible for
|
|
153
153
|
* actually persisting it via remote.updatePrd, which independently refuses
|
|
154
|
-
* a
|
|
154
|
+
* a 'running' or 'completed' row (defense in depth, not duplicated here).
|
|
155
155
|
*
|
|
156
156
|
* Refuses (ok: false) when:
|
|
157
157
|
* - `slug` isn't a known row.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* prdGateFiles.cjs — pure validate + render helpers for a PRD's `gate` and
|
|
5
|
+
* `files` fields (gate-and-files-in-PRD-body unit).
|
|
6
|
+
*
|
|
7
|
+
* Why: 74% of parked jobs had no gate the scheduler could read. The planner
|
|
8
|
+
* now declares the gate commands and the files list when it calls
|
|
9
|
+
* scheduler_create_prd; prdCreate.cjs's createPrd() validates them with the
|
|
10
|
+
* functions here, then renders them as two PRD body sections (`# Files`,
|
|
11
|
+
* `# Gate`). `resolveGate`/`parseChain` in definitionOfDone.cjs already read
|
|
12
|
+
* a fenced ```gate block back — renderGateSection's output must round-trip
|
|
13
|
+
* through them unchanged (checked by createPrd(), not here).
|
|
14
|
+
*
|
|
15
|
+
* No I/O, no scheduler imports — pure string-in, string/array-out.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { parseChain, explainChain, isNoneGate } = require('./definitionOfDone.cjs');
|
|
19
|
+
|
|
20
|
+
const GATE_MIN_ENTRIES = 1;
|
|
21
|
+
const GATE_MAX_ENTRIES = 10;
|
|
22
|
+
const GATE_ENTRY_MAX_CHARS = 500;
|
|
23
|
+
const FILES_MIN_ENTRIES = 1;
|
|
24
|
+
const FILES_MAX_ENTRIES = 50;
|
|
25
|
+
const FILES_ENTRY_MAX_CHARS = 300;
|
|
26
|
+
|
|
27
|
+
const NONE_GATE_WARNING =
|
|
28
|
+
'Gate is none: the scheduler cannot re-check this PRD. Use none only for docs or config with no runnable check.';
|
|
29
|
+
|
|
30
|
+
// Same whitespace/control-char rule explainChain's rule 1 uses
|
|
31
|
+
// (definitionOfDone.cjs), minus tab — a tab has no legitimate place in a
|
|
32
|
+
// repo-relative file path.
|
|
33
|
+
const FILES_FOREIGN_WHITESPACE_RE = /[^\S ]|[\u0000-\u0008\u000A-\u001F\u007F]/;
|
|
34
|
+
|
|
35
|
+
// Chars stripped from a gate entry's first word before comparing it to
|
|
36
|
+
// "none" — quotes, plus the punctuation a typo or trailing comment tends to
|
|
37
|
+
// add (`none.`, `"none"`, `none!`).
|
|
38
|
+
const NONE_PUNCT_STRIP_RE = /^[.,;:!?()[\]{}#'"]+|[.,;:!?()[\]{}#'"]+$/g;
|
|
39
|
+
|
|
40
|
+
/** Cut a string to 80 chars for an error message, marking truncation with "…". */
|
|
41
|
+
function cut80(s) {
|
|
42
|
+
return s.length > 80 ? `${s.slice(0, 80)}…` : s;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `<field> entry <n> ("<cut entry>"): <rule>` — the shared error-text shape. */
|
|
46
|
+
function entryError(field, index, entry, rule) {
|
|
47
|
+
return `${field} entry ${index} ("${cut80(entry)}"): ${rule}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* True when `entry` reads as a typo'd "none" opt-out (`none.`, `"none"`,
|
|
52
|
+
* `none # docs`, `NONE!`) but is not the exact opt-out itself. The exact
|
|
53
|
+
* opt-out (isNoneGate) is never a near-miss.
|
|
54
|
+
*/
|
|
55
|
+
function isNearMissNone(entry) {
|
|
56
|
+
if (isNoneGate(entry)) return false;
|
|
57
|
+
const firstWord = entry.split(/[ \t]+/)[0] ?? '';
|
|
58
|
+
return /^none$/i.test(firstWord.replace(NONE_PUNCT_STRIP_RE, ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate a PRD's `gate` field.
|
|
63
|
+
*
|
|
64
|
+
* @param {unknown} gate
|
|
65
|
+
* @returns {{ok:true, commands:string[], warnings:string[]}|{ok:false, error:string}}
|
|
66
|
+
* commands is `['none']` for the opt-out, else the trimmed entries verbatim
|
|
67
|
+
* (one command chain per entry) — the exact strings renderGateSection and
|
|
68
|
+
* createPrd's read-back check both work from.
|
|
69
|
+
*/
|
|
70
|
+
function validateGate(gate) {
|
|
71
|
+
if (!Array.isArray(gate) || gate.length < GATE_MIN_ENTRIES || gate.length > GATE_MAX_ENTRIES) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `gate must have ${GATE_MIN_ENTRIES} to ${GATE_MAX_ENTRIES} entries (got ${Array.isArray(gate) ? gate.length : 0}).`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const trimmed = gate.map((e) => (typeof e === 'string' ? e.trim() : e));
|
|
79
|
+
|
|
80
|
+
// Per-entry shape rules — run uniformly over every entry, including "none"
|
|
81
|
+
// (it trivially satisfies all of them; the none/mixed semantics are a
|
|
82
|
+
// separate check below). explainChain's result is kept per entry so the
|
|
83
|
+
// missing-timeout warning pass below never re-parses.
|
|
84
|
+
const explainedByEntry = [];
|
|
85
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
86
|
+
const entry = trimmed[i];
|
|
87
|
+
const idx = i + 1;
|
|
88
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
89
|
+
return { ok: false, error: `gate entry ${idx}: must be a non-empty string.` };
|
|
90
|
+
}
|
|
91
|
+
if (entry.length > GATE_ENTRY_MAX_CHARS) {
|
|
92
|
+
return { ok: false, error: entryError('gate', idx, entry, `must be at most ${GATE_ENTRY_MAX_CHARS} chars.`) };
|
|
93
|
+
}
|
|
94
|
+
if (/[\r\n]/.test(entry)) {
|
|
95
|
+
return { ok: false, error: entryError('gate', idx, entry, 'must not contain a newline.') };
|
|
96
|
+
}
|
|
97
|
+
if (entry.includes('```')) {
|
|
98
|
+
return { ok: false, error: entryError('gate', idx, entry, 'must not contain a triple-backtick run (breaks the fence).') };
|
|
99
|
+
}
|
|
100
|
+
if (entry.startsWith('#')) {
|
|
101
|
+
return { ok: false, error: entryError('gate', idx, entry, 'must not start with "#" — the fence parser drops lines starting with "#".') };
|
|
102
|
+
}
|
|
103
|
+
if (isNearMissNone(entry)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: entryError('gate', idx, entry, 'write exactly none, with nothing else, for a PRD with no runnable check.'),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const explained = explainChain(entry);
|
|
110
|
+
if (!explained.ok) {
|
|
111
|
+
return { ok: false, error: entryError('gate', idx, entry, explained.error) };
|
|
112
|
+
}
|
|
113
|
+
explainedByEntry.push(explained);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// "none" semantics: alone it's the opt-out; mixed with anything else it's
|
|
117
|
+
// an error (one PRD cannot be both "no gate" and "here is a gate").
|
|
118
|
+
const noneIdx = trimmed.findIndex((e) => isNoneGate(e));
|
|
119
|
+
if (noneIdx !== -1) {
|
|
120
|
+
if (trimmed.length > 1) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: entryError(
|
|
124
|
+
'gate', noneIdx + 1, trimmed[noneIdx],
|
|
125
|
+
'"none" must be the only entry when used — remove the other entries or remove "none".',
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { ok: true, commands: ['none'], warnings: [NONE_GATE_WARNING] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Missing-timeout warning (never rejects) — a step without one can hang
|
|
133
|
+
// the run.
|
|
134
|
+
const warnings = [];
|
|
135
|
+
explainedByEntry.forEach((explained, i) => {
|
|
136
|
+
explained.steps.forEach((step, stepIdx) => {
|
|
137
|
+
if (!explained.timeoutGiven[stepIdx]) {
|
|
138
|
+
warnings.push(
|
|
139
|
+
`gate entry ${i + 1} step "${cut80(step.raw)}" does not start with "timeout <seconds>" — a step without a timeout can hang the run.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { ok: true, commands: trimmed, warnings };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate a PRD's `files` field.
|
|
150
|
+
*
|
|
151
|
+
* @param {unknown} files
|
|
152
|
+
* @returns {{ok:true, files:string[]}|{ok:false, error:string}}
|
|
153
|
+
* files is deduped (exact match, order preserved) with every leading `./`
|
|
154
|
+
* stripped per entry.
|
|
155
|
+
*/
|
|
156
|
+
function validateFiles(files) {
|
|
157
|
+
if (!Array.isArray(files) || files.length < FILES_MIN_ENTRIES || files.length > FILES_MAX_ENTRIES) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: `files must have ${FILES_MIN_ENTRIES} to ${FILES_MAX_ENTRIES} entries (got ${Array.isArray(files) ? files.length : 0}).`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
const out = [];
|
|
166
|
+
for (let i = 0; i < files.length; i++) {
|
|
167
|
+
const idx = i + 1;
|
|
168
|
+
const original = files[i];
|
|
169
|
+
if (typeof original !== 'string') {
|
|
170
|
+
return { ok: false, error: `files entry ${idx}: must be a string.` };
|
|
171
|
+
}
|
|
172
|
+
const entry = original.trim();
|
|
173
|
+
if (entry.length === 0) {
|
|
174
|
+
return { ok: false, error: `files entry ${idx}: must not be empty.` };
|
|
175
|
+
}
|
|
176
|
+
if (entry.length > FILES_ENTRY_MAX_CHARS) {
|
|
177
|
+
return { ok: false, error: entryError('files', idx, entry, `must be at most ${FILES_ENTRY_MAX_CHARS} chars.`) };
|
|
178
|
+
}
|
|
179
|
+
if (/[\r\n]/.test(entry)) {
|
|
180
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain a newline.') };
|
|
181
|
+
}
|
|
182
|
+
if (FILES_FOREIGN_WHITESPACE_RE.test(entry)) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: entryError('files', idx, entry, 'has a character that is not a plain space (for example a tab, a no-break space, or a line break). Retype it.'),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (entry.includes('\\')) {
|
|
189
|
+
return { ok: false, error: entryError('files', idx, entry, 'use "/" between folders, not "\\".') };
|
|
190
|
+
}
|
|
191
|
+
if (entry.includes('`')) {
|
|
192
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain a backtick.') };
|
|
193
|
+
}
|
|
194
|
+
if (entry.startsWith('-')) {
|
|
195
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not start with "-".') };
|
|
196
|
+
}
|
|
197
|
+
if (entry.startsWith('/') || entry.startsWith('~') || /^[A-Za-z]:/.test(entry)) {
|
|
198
|
+
return { ok: false, error: entryError('files', idx, entry, 'must be repo-relative — absolute paths are not allowed.') };
|
|
199
|
+
}
|
|
200
|
+
if (entry.includes('*') || entry.includes('?')) {
|
|
201
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain "*" or "?" (no globs).') };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let normalized = entry;
|
|
205
|
+
while (normalized.startsWith('./')) normalized = normalized.slice(2);
|
|
206
|
+
if (normalized.length === 0) {
|
|
207
|
+
return { ok: false, error: entryError('files', idx, entry, 'is empty after removing "./".') };
|
|
208
|
+
}
|
|
209
|
+
const segments = normalized.split('/');
|
|
210
|
+
if (segments.includes('..')) {
|
|
211
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain a ".." path segment.') };
|
|
212
|
+
}
|
|
213
|
+
if (segments.includes('.')) {
|
|
214
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain a "." path segment.') };
|
|
215
|
+
}
|
|
216
|
+
if (normalized.includes('//')) {
|
|
217
|
+
return { ok: false, error: entryError('files', idx, entry, 'must not contain an empty path segment ("//").') };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!seen.has(normalized)) {
|
|
221
|
+
seen.add(normalized);
|
|
222
|
+
out.push(normalized);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { ok: true, files: out };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Render the `# Files` body section as an array of lines (joinable with
|
|
231
|
+
* '\n' alongside the rest of buildPrdBody's bodyLines). `files` must already
|
|
232
|
+
* be validated/normalized (validateFiles's output).
|
|
233
|
+
*
|
|
234
|
+
* @param {string[]} files
|
|
235
|
+
* @returns {string[]}
|
|
236
|
+
*/
|
|
237
|
+
function renderFilesSection(files) {
|
|
238
|
+
return [
|
|
239
|
+
'# Files', '',
|
|
240
|
+
'Change only these files. If the work needs another file, change it and say why in your report.',
|
|
241
|
+
'',
|
|
242
|
+
...files.map((f) => `- ${f}`),
|
|
243
|
+
'',
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Render the `# Gate` body section as an array of lines. `commandsOrNone`
|
|
249
|
+
* must already be validated (validateGate's `commands` output) — either
|
|
250
|
+
* `['none']` or the ordered command-chain strings to fence verbatim.
|
|
251
|
+
*
|
|
252
|
+
* @param {string[]} commandsOrNone
|
|
253
|
+
* @returns {string[]}
|
|
254
|
+
*/
|
|
255
|
+
function renderGateSection(commandsOrNone) {
|
|
256
|
+
const isNone = commandsOrNone.length === 1 && isNoneGate(commandsOrNone[0]);
|
|
257
|
+
if (isNone) {
|
|
258
|
+
return [
|
|
259
|
+
'# Gate', '',
|
|
260
|
+
'This PRD has no runnable check. Check each acceptance criterion by reading the files.',
|
|
261
|
+
'',
|
|
262
|
+
'```gate',
|
|
263
|
+
'none',
|
|
264
|
+
'```',
|
|
265
|
+
'',
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
return [
|
|
269
|
+
'# Gate', '',
|
|
270
|
+
'Run these commands last, in order. Each must exit 0.',
|
|
271
|
+
'',
|
|
272
|
+
'```gate',
|
|
273
|
+
...commandsOrNone,
|
|
274
|
+
'```',
|
|
275
|
+
'',
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
validateGate,
|
|
281
|
+
validateFiles,
|
|
282
|
+
renderFilesSection,
|
|
283
|
+
renderGateSection,
|
|
284
|
+
};
|
|
@@ -384,6 +384,64 @@ function deriveProjectCwdFromPrdPath(filePath) {
|
|
|
384
384
|
return null;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// Incident (2026-09-30): an Epic can live in a non-git PARENT folder P
|
|
388
|
+
// (P/session-manager-operations/scheduler/epics/<epicId>/prds/*.md) while
|
|
389
|
+
// its PRDs' own frontmatter `cwd` points at a SUB-REPO, e.g. P/repo. P is
|
|
390
|
+
// never itself a tracked project cwd (activeProjectCwds/allProjectCwds only
|
|
391
|
+
// know about registered project roots), so no amount of
|
|
392
|
+
// resolvePrdsDirs()/listEpicPrdDirs() enumeration ever visits it — only
|
|
393
|
+
// walking UP from the job's own cwd does. Bounded to a few hops so a
|
|
394
|
+
// same-machine but unrelated ancestor folder that happens to also contain a
|
|
395
|
+
// `session-manager-operations/` tree can't be walked into indefinitely.
|
|
396
|
+
const MAX_EPIC_ANCESTOR_HOPS = 3;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* epicId must be an exact, plain directory-name match — never a glob, never
|
|
400
|
+
* string-concatenated into a path. Mirrors resolveEpicPrdWriteDir's own
|
|
401
|
+
* guard above.
|
|
402
|
+
*/
|
|
403
|
+
function isSafeEpicId(epicId) {
|
|
404
|
+
return typeof epicId === 'string' && epicId.length > 0
|
|
405
|
+
&& !epicId.includes('/') && !epicId.includes('\\') && !epicId.includes('..');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Walk up to MAX_EPIC_ANCESTOR_HOPS parent directories ABOVE `cwd` (never
|
|
410
|
+
* including `cwd` itself — that's already covered by resolveEpicsRoot/
|
|
411
|
+
* listEpicPrdDirs) looking for an EXACT
|
|
412
|
+
* `<ancestor>/session-manager-operations/scheduler/epics/<epicId>/<leafSubpath...>`
|
|
413
|
+
* match. Built only from `path.join` segments (never string concatenation
|
|
414
|
+
* with the caller-supplied epicId/cwd), with a resolved-path containment
|
|
415
|
+
* check as defense in depth. Returns only dirs that actually exist on disk.
|
|
416
|
+
*/
|
|
417
|
+
function ancestorEpicDirs(cwd, epicId, leafSubpath) {
|
|
418
|
+
if (!cwd || typeof cwd !== 'string' || !path.isAbsolute(cwd)) return [];
|
|
419
|
+
if (!isSafeEpicId(epicId)) return [];
|
|
420
|
+
const out = [];
|
|
421
|
+
let dir = path.dirname(cwd);
|
|
422
|
+
for (let hop = 0; hop < MAX_EPIC_ANCESTOR_HOPS; hop++) {
|
|
423
|
+
const candidate = path.join(dir, OPS_ROOT_DIR, ...EPICS_SUBPATH, epicId, ...leafSubpath);
|
|
424
|
+
const resolvedDir = path.resolve(dir);
|
|
425
|
+
if (path.resolve(candidate).startsWith(resolvedDir + path.sep) && fs.existsSync(candidate)) {
|
|
426
|
+
out.push(candidate);
|
|
427
|
+
}
|
|
428
|
+
const parent = path.dirname(dir);
|
|
429
|
+
if (parent === dir) break; // reached the filesystem root
|
|
430
|
+
dir = parent;
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Ancestor-folder `epics/<epicId>/prds` dirs above `cwd` (see ancestorEpicDirs). */
|
|
436
|
+
function ancestorEpicPrdDirs(cwd, epicId) {
|
|
437
|
+
return ancestorEpicDirs(cwd, epicId, ['prds']);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Ancestor-folder `epics/<epicId>/prds-archived` dirs above `cwd` (see ancestorEpicDirs). */
|
|
441
|
+
function ancestorEpicArchivedPrdDirs(cwd, epicId) {
|
|
442
|
+
return ancestorEpicDirs(cwd, epicId, ['prds-archived']);
|
|
443
|
+
}
|
|
444
|
+
|
|
387
445
|
module.exports = {
|
|
388
446
|
resolvePrdWriteDir,
|
|
389
447
|
resolvePrdsDirs,
|
|
@@ -394,6 +452,9 @@ module.exports = {
|
|
|
394
452
|
listArchivedPrdDirs,
|
|
395
453
|
deriveEpicIdFromPrdPath,
|
|
396
454
|
deriveProjectCwdFromPrdPath,
|
|
455
|
+
ancestorEpicPrdDirs,
|
|
456
|
+
ancestorEpicArchivedPrdDirs,
|
|
457
|
+
isSafeEpicId,
|
|
397
458
|
PRD_SUBPATH,
|
|
398
459
|
EPICS_SUBPATH,
|
|
399
460
|
};
|
|
@@ -20,6 +20,7 @@ const { splitFrontmatter, parsePrdFile, serializePrdFile } = require('./prdFront
|
|
|
20
20
|
const { resolvePrdWriteDir, resolvePrdsDirs } = require('./prdLocations.cjs');
|
|
21
21
|
const { projectQueuePath } = require('./queueStore.cjs');
|
|
22
22
|
const { expandHome } = require('./expandHome.cjs');
|
|
23
|
+
const atomicFs = require('./atomicFs.cjs');
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Move every `.md` file in legacyPrdsDir whose frontmatter `cwd` resolves to
|
|
@@ -262,9 +263,7 @@ async function legacyAdoptExistingPrds() {
|
|
|
262
263
|
fm.createdVia = 'legacy-adopted';
|
|
263
264
|
fm.issuedAt = issuedAt;
|
|
264
265
|
const newRaw = serializePrdFile(fm, body);
|
|
265
|
-
|
|
266
|
-
await fsp.writeFile(tmp, newRaw, 'utf8');
|
|
267
|
-
await fsp.rename(tmp, filePath);
|
|
266
|
+
await atomicFs.writeTextAtomic(filePath, newRaw);
|
|
268
267
|
stamped += 1;
|
|
269
268
|
} catch (e) {
|
|
270
269
|
if (e?.code === 'ENOENT') continue; // raced with a concurrent mover/archiver
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* jobs finishing in <=10 min, yet nothing at the authoring gate flags an oversized PRD before
|
|
6
6
|
* it lands. `sizingWarnings` is the single rule set; prdCreate.cjs's createPrd() surfaces its
|
|
7
7
|
* output in the create-prd response, and scheduler-mcp-server.cjs's scheduler_create_prd
|
|
8
|
-
* handler prints it as a "
|
|
8
|
+
* handler prints it as part of a "Warnings:" block (merged with the gate/files warnings
|
|
9
|
+
* from prdGateFiles.cjs). Warnings never block a write.
|
|
9
10
|
*/
|
|
10
11
|
'use strict';
|
|
11
12
|
|