claude-code-session-manager 0.86.0 → 0.87.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.
Files changed (128) hide show
  1. package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
  2. package/dist/assets/{DataModel-Q4jhl24R.js → DataModel--mISIJ6h.js} +1 -1
  3. package/dist/assets/{History-Cj2FejEo.js → History-C2ahUXTg.js} +2 -2
  4. package/dist/assets/{Hooks-CaelQI6t.js → Hooks-BiC6oyR2.js} +3 -3
  5. package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-BPleEOld.js} +1 -1
  6. package/dist/assets/{Library-DgI9oCCZ.js → Library-Dc8Qst1R.js} +1 -1
  7. package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-DIXh-OLX.js} +1 -1
  8. package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
  9. package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DqcbLOLZ.js} +2 -2
  10. package/dist/assets/{Memory-C2qYp-3M.js → Memory-CW62MXlh.js} +4 -4
  11. package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-Bw1FhRuF.js} +1 -1
  12. package/dist/assets/Permissions-BcUC-5y8.js +3 -0
  13. package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-BnKx9flD.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
  15. package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-CWr0O_w-.js} +1 -1
  16. package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
  18. package/dist/assets/Settings-DluB-vN1.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-CHLSseay.js} +1 -1
  20. package/dist/assets/{Skills-C_YHkAy-.js → Skills-gNdo_HNK.js} +2 -2
  21. package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Cru05-Ia.js} +1 -1
  22. package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-DNHY0xou.js} +1 -1
  23. package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-I4lmbCgP.js} +1 -1
  24. package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-bWMHjmRh.js} +1 -1
  25. package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
  26. package/dist/assets/{index-CXFQIPhO.js → index-fc_JjdxL.js} +724 -724
  27. package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
  28. package/dist/index.html +2 -2
  29. package/package.json +14 -14
  30. package/plugins/CLAUDE.md +61 -0
  31. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  32. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  33. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  34. package/scripts/scheduler-mcp-server.cjs +7 -0
  35. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  36. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  37. package/src/main/__tests__/epicMint.test.cjs +2 -2
  38. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  39. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  40. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
  41. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
  42. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  43. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  44. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  45. package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
  46. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  47. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  48. package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
  49. package/src/main/__tests__/queueHistory.test.cjs +63 -0
  50. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  51. package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
  52. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
  53. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  54. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  55. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
  56. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  57. package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
  58. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  59. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
  60. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  61. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
  62. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +14 -4
  63. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
  64. package/src/main/chatRunner.cjs +8 -5
  65. package/src/main/health.cjs +1 -1
  66. package/src/main/historyAggregator.cjs +5 -0
  67. package/src/main/index.cjs +95 -44
  68. package/src/main/ipcSchemas.cjs +47 -0
  69. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  70. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  71. package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
  72. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  73. package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
  74. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  75. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  76. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  77. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  78. package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
  79. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
  80. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
  81. package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
  82. package/src/main/lib/agentModelResolve.cjs +65 -27
  83. package/src/main/lib/bootSelfHeal.cjs +88 -0
  84. package/src/main/lib/delegationReadiness.cjs +290 -225
  85. package/src/main/lib/effectiveModelInfo.cjs +333 -0
  86. package/src/main/lib/ephemeralCwd.cjs +1 -1
  87. package/src/main/lib/epicMint.cjs +3 -3
  88. package/src/main/lib/gitWorktree.cjs +42 -12
  89. package/src/main/lib/guardShims.cjs +156 -0
  90. package/src/main/lib/jobDirtFilter.cjs +7 -2
  91. package/src/main/lib/launchFailure.cjs +2 -1
  92. package/src/main/lib/mcpToolCatalog.cjs +4 -1
  93. package/src/main/lib/needsReviewLedger.cjs +205 -0
  94. package/src/main/lib/opsErrorLog.cjs +1 -1
  95. package/src/main/lib/opsOwnership.cjs +1 -1
  96. package/src/main/lib/prdCreate.cjs +56 -1
  97. package/src/main/lib/prdDisposition.cjs +199 -0
  98. package/src/main/lib/prdFrontmatter.cjs +8 -2
  99. package/src/main/lib/prdLocations.cjs +167 -45
  100. package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
  101. package/src/main/lib/projectPageSummarySchema.cjs +1 -1
  102. package/src/main/lib/projectRootResolve.cjs +1 -1
  103. package/src/main/lib/queueHistory.cjs +19 -1
  104. package/src/main/lib/queueStore.cjs +6 -1
  105. package/src/main/lib/reaperHelpers.cjs +181 -15
  106. package/src/main/lib/scheduleJobSchema.cjs +8 -0
  107. package/src/main/lib/scheduleJobTransitions.cjs +33 -0
  108. package/src/main/lib/schedulerConfig.cjs +24 -0
  109. package/src/main/lib/usageCircuit.cjs +159 -0
  110. package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
  111. package/src/main/scheduler/prdParser.cjs +13 -0
  112. package/src/main/scheduler.cjs +1285 -143
  113. package/src/main/templates/PRD_AUTHORING.md +50 -0
  114. package/src/main/templates/project-pages-catalog.json +1 -1
  115. package/src/main/usage.cjs +21 -3
  116. package/src/preload/api.d.ts +92 -1
  117. package/src/preload/index.cjs +10 -0
  118. package/web/README.md +41 -0
  119. package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
  120. package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
  121. package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
  122. package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
  123. package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
  124. package/dist/assets/Permissions-BiYZNGYW.js +0 -3
  125. package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
  126. package/dist/assets/Settings-Cv-pRyms.js +0 -3
  127. package/dist/assets/settingsSchema-BJVciriw.js +0 -3
  128. /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
@@ -32,6 +32,8 @@
32
32
 
33
33
  const { appendAuditEvent } = require('./auditLog.cjs');
34
34
  const telemetryCounters = require('./telemetryCounters.cjs');
35
+ const queueHistory = require('./queueHistory.cjs');
36
+ const { buildNeedsReviewEntryLine, buildNeedsReviewResolutionLine } = require('./needsReviewLedger.cjs');
35
37
 
36
38
  // A run genuinely finished when it leaves 'running' for one of these —
37
39
  // distinct from every other legal edge in LEGAL_TRANSITIONS (retries,
@@ -195,6 +197,37 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
195
197
  telemetryCounters.trackSchedulerJobFinish({ status: toStatus });
196
198
  }
197
199
 
200
+ // needs_review ledger (PRD: needs_review durability). This IS the single
201
+ // funnel every needs_review transition — entry or resolution — passes
202
+ // through (see this file's own header comment: transitionJob is the ONE
203
+ // place `status` is assigned), so hooking here, rather than any one of
204
+ // needs_review's several distinct entry call sites in scheduler.cjs,
205
+ // guarantees no path can bypass it.
206
+ //
207
+ // Fire-and-forget, exactly like every other best-effort side-channel write
208
+ // in this codebase (rcaReport.cjs, auditLog.cjs's own posture) — a ledger
209
+ // write failure must never fail or delay the transition it's recording.
210
+ if (toStatus === 'needs_review' && from !== 'needs_review') {
211
+ // ENTRY: stamp the episode's runId/enteredAt on the job itself so the
212
+ // RESOLUTION line (below) can reference the SAME runId even if the job's
213
+ // own `runId` is reassigned in between (e.g. a resume-recovery
214
+ // re-dispatch mints a fresh one before this episode resolves).
215
+ job.needsReviewEntryRunId = job.runId ?? null;
216
+ job.needsReviewEnteredAt = entry.at;
217
+ queueHistory.appendHistory([buildNeedsReviewEntryLine(job, entry)]).catch((e) => {
218
+ console.error('[scheduleJobTransitions] failed to append needs_review entry', e?.message ?? String(e));
219
+ });
220
+ } else if (from === 'needs_review' && toStatus !== 'needs_review') {
221
+ // RESOLUTION: read the episode stamped at entry, then clear it — the
222
+ // episode is over regardless of what this resolution line looks like.
223
+ const episode = { runId: job.needsReviewEntryRunId ?? null, enteredAt: job.needsReviewEnteredAt ?? null };
224
+ delete job.needsReviewEntryRunId;
225
+ delete job.needsReviewEnteredAt;
226
+ queueHistory.appendHistory([buildNeedsReviewResolutionLine(job, entry, episode)]).catch((e) => {
227
+ console.error('[scheduleJobTransitions] failed to append needs_review resolution', e?.message ?? String(e));
228
+ });
229
+ }
230
+
198
231
  return true;
199
232
  }
200
233
 
@@ -56,6 +56,16 @@ module.exports = {
56
56
  // (pause/resume, job start/finish/reap/reset) bypass this via
57
57
  // broadcast({ flush: true }).
58
58
  BROADCAST_COALESCE_MS: 200,
59
+ // reconcile() pass duration above which a single warn-level breakdown is
60
+ // logged (per-phase ms, PRD file count, resolved dir count) — the only
61
+ // evidence trail for a slow pass otherwise requires hand-timing the scan
62
+ // from a throwaway script. Baseline measured 2026-09-13 on a machine with
63
+ // ~160 PRD dirs: allProjectCwds() ~270ms x5/pass, resolvePrdsDirs()
64
+ // ~430ms, historyTerminalBySlug() ~418ms over 784 entries — a healthy pass
65
+ // today is comfortably under 2s; this should trip well before a
66
+ // regression back toward those numbers compounds into the reported
67
+ // "schedule.state timed out after 5000ms" toast.
68
+ RECONCILE_SLOW_PASS_MS: 2_000,
59
69
  // Terminal (completed/failed) jobs older than this move from queue.json's
60
70
  // hot jobs[] into the append-only history.jsonl sidecar. See
61
71
  // src/main/lib/queueHistory.cjs.
@@ -109,6 +119,20 @@ module.exports = {
109
119
  // SM_JOB_OVERRUN_FLOOR_MINUTES.
110
120
  JOB_OVERRUN_FLOOR_MS: 45 * 60_000,
111
121
 
122
+ // Wall-clock KILL budget (distinct from JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS
123
+ // above, which only ever ESCALATE — log + audit, deliberately never kill).
124
+ // This one ACTS: a RUNNING job past `budgetMs = clamp(estimateMinutes *
125
+ // JOB_BUDGET_FACTOR, JOB_BUDGET_FLOOR_MS, JOB_BUDGET_CEILING_MS)` is
126
+ // SIGTERMed and parked needs_review. Calibrated against 606 run-meta
127
+ // records since 2026-08-25: p50=13m, p90=60m, max=240m, against a
128
+ // concurrencyCap of 4 — two 4h outliers hold half the machine's dispatch
129
+ // capacity for an afternoon with nothing noticing or acting on it. Override
130
+ // with SM_JOB_BUDGET_FACTOR / SM_JOB_BUDGET_FLOOR_MINUTES /
131
+ // SM_JOB_BUDGET_CEILING_MINUTES.
132
+ JOB_BUDGET_FACTOR: 3,
133
+ JOB_BUDGET_FLOOR_MS: 45 * 60_000,
134
+ JOB_BUDGET_CEILING_MS: 180 * 60_000,
135
+
112
136
  // A 'running' row with no runtime.pid recorded (spawnJob's status:running
113
137
  // mutate at scheduler.cjs:~3524 landed, but the pid-bearing runtime={}
114
138
  // mutate that follows executeJob's spawn callback never did — the spawn
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * usageCircuit.cjs — a breaker over the /api/oauth/usage meter (PRD ss-02).
5
+ * Degraded mode is explicit, never a silent 0: repeated failures open the
6
+ * circuit with jittered backoff; degradedBudget() carries forward the last
7
+ * known-good BINDING window's utilization (never 0), pinning to 100 on a
8
+ * fresh executor-observed 429 and capping concurrency at 2. Pure Node.
9
+ */
10
+
11
+ const FLOOR_MS = 30_000;
12
+ const CAP_MS = 8 * 60 * 1000;
13
+
14
+ /** Doubles from FLOOR_MS, capped at CAP_MS. 0/falsy prevMs starts at the floor. */
15
+ function doubledBase(prevMs) {
16
+ return prevMs > 0 ? Math.min(prevMs * 2, CAP_MS) : FLOOR_MS;
17
+ }
18
+
19
+ /**
20
+ * Next backoff interval: doubles `prevMs` (floor FLOOR_MS, cap CAP_MS), then
21
+ * jitters by 0.5 + rand()*0.5 so concurrent instances don't retry in lockstep.
22
+ */
23
+ function backoffWithJitter(prevMs) {
24
+ const base = doubledBase(prevMs);
25
+ return base * (0.5 + Math.random() * 0.5);
26
+ }
27
+
28
+ /**
29
+ * A cached reset timestamp is fresh only if it's strictly in the future
30
+ * relative to `now` — null/unparseable/at-or-before-now all read as stale,
31
+ * since a passed reset no longer describes a live window.
32
+ */
33
+ function isResetFresh(iso, now) {
34
+ if (!iso) return false;
35
+ const resetMs = Date.parse(iso);
36
+ if (!Number.isFinite(resetMs)) return false;
37
+ const nowMs = now instanceof Date ? now.getTime() : Number(now);
38
+ if (!Number.isFinite(nowMs)) return false;
39
+ return resetMs > nowMs;
40
+ }
41
+
42
+ /**
43
+ * Which window is actually binding dispatch. Prefers `payload.limits[]`'s
44
+ * `is_active` entry (the richer per-window shape); falls back to the flat
45
+ * `five_hour` field when `limits[]` is absent, matching usage.cjs's shape.
46
+ */
47
+ function bindingWindow(payload) {
48
+ if (payload && Array.isArray(payload.limits) && payload.limits.length) {
49
+ const active = payload.limits.find((l) => l && l.is_active);
50
+ if (active) {
51
+ return {
52
+ name: active.type || active.name || 'unknown',
53
+ utilization: active.utilization,
54
+ resets_at: active.resets_at ?? null,
55
+ };
56
+ }
57
+ }
58
+ const fh = payload && payload.five_hour;
59
+ return {
60
+ name: 'five_hour',
61
+ utilization: fh ? fh.utilization : undefined,
62
+ resets_at: fh ? fh.resets_at : null,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Conservative budget to run on while the meter is down. utilization is
68
+ * carried forward from the last known-good BINDING window — never 0, which
69
+ * would read as "plenty of headroom" instead of "we don't know." A fresh
70
+ * executor-observed 429 (its own window not yet passed) pins utilization to
71
+ * 100 regardless of the stale cached value. concurrencyCap never exceeds 2.
72
+ */
73
+ function degradedBudget(lastGoodPayload, executorEvidence = {}) {
74
+ const window = bindingWindow(lastGoodPayload);
75
+ let utilization = Number.isFinite(window.utilization) && window.utilization > 0
76
+ ? window.utilization
77
+ : 100;
78
+
79
+ const configuredCap = Number.isFinite(executorEvidence.configuredCap)
80
+ ? executorEvidence.configuredCap
81
+ : 2;
82
+ const concurrencyCap = Math.min(configuredCap, 2);
83
+
84
+ if (executorEvidence.observed429 && isResetFresh(executorEvidence.resetsAt, executorEvidence.now)) {
85
+ utilization = 100;
86
+ }
87
+
88
+ return { utilization, concurrencyCap };
89
+ }
90
+
91
+ /** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
92
+ function singleFlight(fn) {
93
+ let inFlight = null;
94
+ return function singleFlightWrapped(...args) {
95
+ if (inFlight) return inFlight;
96
+ inFlight = Promise.resolve(fn(...args)).finally(() => { inFlight = null; });
97
+ return inFlight;
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Breaker state machine over the meter: closed -> open after 3 consecutive
103
+ * failures; open allows exactly one probe per backoff interval (half_open);
104
+ * a successful probe closes the circuit and resets backoff, a failed probe
105
+ * reopens it with backoff doubled.
106
+ */
107
+ function createUsageCircuit({ now = () => Date.now() } = {}) {
108
+ let consecutiveFailures = 0;
109
+ let currentState = 'closed';
110
+ let backoffBaseMs = 0;
111
+ let nextProbeAtMs = 0;
112
+ let probeInFlight = false;
113
+
114
+ function scheduleNextProbe() {
115
+ const intervalMs = backoffWithJitter(backoffBaseMs);
116
+ backoffBaseMs = doubledBase(backoffBaseMs);
117
+ nextProbeAtMs = now() + intervalMs;
118
+ }
119
+
120
+ function state() {
121
+ if (currentState === 'open' && !probeInFlight && now() >= nextProbeAtMs) {
122
+ currentState = 'half_open';
123
+ probeInFlight = true;
124
+ }
125
+ return currentState;
126
+ }
127
+
128
+ function recordFailure(kind) { // eslint-disable-line no-unused-vars
129
+ consecutiveFailures += 1;
130
+ if (currentState === 'half_open') {
131
+ probeInFlight = false;
132
+ currentState = 'open';
133
+ scheduleNextProbe();
134
+ return;
135
+ }
136
+ if (currentState === 'closed' && consecutiveFailures >= 3) {
137
+ currentState = 'open';
138
+ scheduleNextProbe();
139
+ }
140
+ }
141
+
142
+ function recordSuccess(payload) { // eslint-disable-line no-unused-vars
143
+ consecutiveFailures = 0;
144
+ probeInFlight = false;
145
+ backoffBaseMs = 0;
146
+ currentState = 'closed';
147
+ }
148
+
149
+ return { recordSuccess, recordFailure, state };
150
+ }
151
+
152
+ module.exports = {
153
+ createUsageCircuit,
154
+ backoffWithJitter,
155
+ isResetFresh,
156
+ bindingWindow,
157
+ degradedBudget,
158
+ singleFlight,
159
+ };
@@ -362,7 +362,7 @@ function releaseLock(lockPath) {
362
362
  * dependency fails inside maybeFinalizeHistory's try/catch, not at require
363
363
  * time for the whole watchdog script. */
364
364
  function defaultFinalizeClosedDays(opts) {
365
- const { finalizeClosedDays } = require('../../src/main/historyAggregator.cjs');
365
+ const { finalizeClosedDays } = require('../historyAggregator.cjs');
366
366
  return finalizeClosedDays(opts);
367
367
  }
368
368
 
@@ -119,10 +119,23 @@ async function parsePrdRaw(filePath) {
119
119
  // dependent follow-up PRD (scheduler-launch-as-persona) resolves this
120
120
  // into a launch decision, which this module does not do.
121
121
  agentType: fm.agentType || null,
122
+ // Wave-authoring decision (scheduler wave-disposition PRD): 'append' or
123
+ // 'new-head', set only when createPrd() actually had a disposition to
124
+ // record — see prdCreate.cjs/prdDisposition.cjs. Absent on every PRD
125
+ // authored before this field existed, and on any PRD that never needed
126
+ // the decision (first-ever PRD in its Epic, or one with its own explicit
127
+ // dependsOn).
128
+ disposition: (fm.disposition === 'append' || fm.disposition === 'new-head') ? fm.disposition : null,
122
129
  // Opt-in exclusive-lease flag (PRD 1107, quietMachineLease.cjs +
123
130
  // schedulerBatch.cjs's pickNextBatch). Only a literal `true` opts in —
124
131
  // matches prdFrontmatter.cjs's applyKey semantics for this field.
125
132
  quietMachine: fm.quietMachine === 'true',
133
+ // Explicit opt-out of the wall-clock budget kill (scheduler.cjs's budget
134
+ // watchdog) — only a literal `true` opts out, matching quietMachine's
135
+ // parse semantics above. quietMachine jobs are exempt regardless of this
136
+ // field; this is for a job that isn't quiet-machine-eligible but still
137
+ // needs no wall-clock kill ceiling.
138
+ budgetExempt: fm.budgetExempt === 'true',
126
139
  body: body.trim(),
127
140
  };
128
141
  }