claude-code-session-manager 0.85.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 (147) hide show
  1. package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
  2. package/dist/assets/{DataModel-DRH-Ty20.js → DataModel--mISIJ6h.js} +1 -1
  3. package/dist/assets/{History-CfRhT1Im.js → History-C2ahUXTg.js} +2 -2
  4. package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-BiC6oyR2.js} +3 -3
  5. package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko-BPleEOld.js} +1 -1
  6. package/dist/assets/{Library-CpArQ-OJ.js → Library-Dc8Qst1R.js} +1 -1
  7. package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DIXh-OLX.js} +1 -1
  8. package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
  9. package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-DqcbLOLZ.js} +2 -2
  10. package/dist/assets/{Memory-ChMWkNd0.js → Memory-CW62MXlh.js} +4 -4
  11. package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Bw1FhRuF.js} +1 -1
  12. package/dist/assets/Permissions-BcUC-5y8.js +3 -0
  13. package/dist/assets/{Plugins-BtChISho.js → Plugins-BnKx9flD.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
  15. package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-CWr0O_w-.js} +1 -1
  16. package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
  18. package/dist/assets/Settings-DluB-vN1.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CHLSseay.js} +1 -1
  20. package/dist/assets/{Skills-DJB6-bBM.js → Skills-gNdo_HNK.js} +2 -2
  21. package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-Cru05-Ia.js} +1 -1
  22. package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-DNHY0xou.js} +1 -1
  23. package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-I4lmbCgP.js} +1 -1
  24. package/dist/assets/{Toggle-B122N0HL.js → Toggle-bWMHjmRh.js} +1 -1
  25. package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
  26. package/dist/assets/{index-DhvuQL4C.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 +15 -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__/health-starve-escalation.test.cjs +94 -0
  40. package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
  41. package/src/main/__tests__/machineProfile.test.cjs +19 -1
  42. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  43. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
  44. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
  45. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  46. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  47. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  48. package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
  49. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  50. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +96 -0
  51. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  52. package/src/main/__tests__/queue-starvation-per-project.test.cjs +147 -0
  53. package/src/main/__tests__/queueHistory.test.cjs +63 -0
  54. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  55. package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
  56. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
  57. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  58. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  59. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +121 -0
  60. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
  61. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  62. package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
  63. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +189 -0
  64. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
  65. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  66. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -0
  67. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
  68. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  69. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
  70. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +154 -0
  71. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
  72. package/src/main/__tests__/telemetryClient.test.cjs +75 -5
  73. package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
  74. package/src/main/chatRunner.cjs +8 -5
  75. package/src/main/health.cjs +76 -3
  76. package/src/main/historyAggregator.cjs +5 -0
  77. package/src/main/index.cjs +95 -44
  78. package/src/main/ipcSchemas.cjs +47 -0
  79. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  80. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  81. package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
  82. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  83. package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
  84. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  85. package/src/main/lib/__tests__/loadGate.test.cjs +103 -2
  86. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  87. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  88. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  89. package/src/main/lib/__tests__/telemetryBoot.test.cjs +11 -0
  90. package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
  91. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
  92. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
  93. package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
  94. package/src/main/lib/agentModelResolve.cjs +65 -27
  95. package/src/main/lib/bootSelfHeal.cjs +88 -0
  96. package/src/main/lib/delegationReadiness.cjs +290 -225
  97. package/src/main/lib/effectiveModelInfo.cjs +333 -0
  98. package/src/main/lib/ephemeralCwd.cjs +1 -1
  99. package/src/main/lib/epicMint.cjs +3 -3
  100. package/src/main/lib/gitWorktree.cjs +42 -12
  101. package/src/main/lib/guardShims.cjs +156 -0
  102. package/src/main/lib/jobDirtFilter.cjs +7 -2
  103. package/src/main/lib/launchFailure.cjs +2 -1
  104. package/src/main/lib/loadGate.cjs +23 -1
  105. package/src/main/lib/machineProfile.cjs +15 -0
  106. package/src/main/lib/mcpToolCatalog.cjs +4 -1
  107. package/src/main/lib/needsReviewLedger.cjs +205 -0
  108. package/src/main/lib/opsErrorLog.cjs +1 -1
  109. package/src/main/lib/opsOwnership.cjs +1 -1
  110. package/src/main/lib/prdCreate.cjs +56 -1
  111. package/src/main/lib/prdDisposition.cjs +199 -0
  112. package/src/main/lib/prdFrontmatter.cjs +8 -2
  113. package/src/main/lib/prdLocations.cjs +167 -45
  114. package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
  115. package/src/main/lib/projectPageSummarySchema.cjs +1 -1
  116. package/src/main/lib/projectRootResolve.cjs +1 -1
  117. package/src/main/lib/queueHistory.cjs +19 -1
  118. package/src/main/lib/queueStore.cjs +6 -1
  119. package/src/main/lib/reaperHelpers.cjs +181 -15
  120. package/src/main/lib/scheduleJobSchema.cjs +8 -0
  121. package/src/main/lib/scheduleJobTransitions.cjs +33 -0
  122. package/src/main/lib/schedulerBatch.cjs +12 -1
  123. package/src/main/lib/schedulerConfig.cjs +37 -0
  124. package/src/main/lib/telemetryBoot.cjs +11 -8
  125. package/src/main/lib/telemetryClient.cjs +44 -2
  126. package/src/main/lib/telemetrySettings.cjs +20 -3
  127. package/src/main/lib/usageCircuit.cjs +159 -0
  128. package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
  129. package/src/main/pty.cjs +9 -0
  130. package/src/main/scheduler/prdParser.cjs +13 -0
  131. package/src/main/scheduler.cjs +1869 -193
  132. package/src/main/templates/PRD_AUTHORING.md +50 -0
  133. package/src/main/templates/project-pages-catalog.json +1 -1
  134. package/src/main/usage.cjs +21 -3
  135. package/src/preload/api.d.ts +92 -1
  136. package/src/preload/index.cjs +10 -0
  137. package/web/README.md +41 -0
  138. package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
  139. package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
  140. package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
  141. package/dist/assets/AgentLibrary-Bkv-HcP1.js +0 -3
  142. package/dist/assets/MarkdownEditor-Xc141kjj.js +0 -1
  143. package/dist/assets/Permissions-DKoNVgzj.js +0 -3
  144. package/dist/assets/Scheduler-CbES7MC8.js +0 -14
  145. package/dist/assets/Settings-BX3FElXk.js +0 -3
  146. package/dist/assets/settingsSchema-sGoCTd7J.js +0 -3
  147. /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * effectiveModelInfo.cjs — read-only observer answering "what will this
5
+ * Epic's agentType actually run as": the persona's model alias, the
6
+ * concrete model id evidence points at, and the provenance of each. Sits
7
+ * BESIDE agentModelResolve.cjs (which decides what a launch actually passes
8
+ * to `--model`) rather than replacing it — nothing here feeds a spawn
9
+ * decision, and agentModelResolve.cjs's own resolution is unchanged.
10
+ *
11
+ * Provenance is the entire point: a caller must be able to tell "we KNOW
12
+ * this alias last ran as claude-opus-5 because a real run resolved it" from
13
+ * "we don't know; the CLI resolves this alias at launch." The two must
14
+ * never collapse into one optimistic string.
15
+ *
16
+ * Effort-level resolution is deliberately NOT here — it already exists,
17
+ * unused, in the renderer's `useEffectiveSettings.ts` (a scope-chain reader
18
+ * with a file watcher). This module supplies only the half that genuinely
19
+ * needs main-process filesystem access: persona lookup (overlay-aware) and
20
+ * concrete-model evidence (scheduler run logs, session transcripts).
21
+ *
22
+ * Never throws — every lookup failure degrades a field to null, same
23
+ * contract as agentModelResolve.cjs's resolveEpicModel.
24
+ */
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+ const os = require('node:os');
29
+ const { splitFrontmatter } = require('./prdFrontmatter.cjs');
30
+ const { encodeCwd } = require('./encodeCwd.cjs');
31
+
32
+ // Mirrors rawSessionModel.ts's RAW_MODELS — duplicated rather than imported
33
+ // because that file is a renderer ES module and this is a main-process CJS
34
+ // module (CLAUDE.md: "No CommonJS in renderer, no ES modules in main").
35
+ // Anything NOT in this set is treated as an already-concrete pinned model id
36
+ // (e.g. 'claude-opus-5'), never as an alias needing evidence.
37
+ const KNOWN_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku', 'fable']);
38
+
39
+ // Run-log evidence (scheduler.cjs's executeJob, ~line 4473) is written
40
+ // essentially at dispatch time, before any of the run's own stdout streams
41
+ // in — so a bounded HEAD read (not a tail) is both correct and cheap even
42
+ // against a run log that grows to tens of MB. O(min(size, cap)) time/space.
43
+ const MAX_RUN_LOG_HEAD_BYTES = 64 * 1024;
44
+
45
+ // Same bound epicDelegationStats.cjs's readTranscriptTail uses for a single
46
+ // pass over a transcript that can reach tens of MB. O(min(size, cap)).
47
+ const MAX_TRANSCRIPT_TAIL_BYTES = 8 * 1024 * 1024;
48
+
49
+ // Mirrors scheduler.cjs's own ROOT/RUNS_DIR constants (same convention as
50
+ // queueStore.cjs's LEGACY_QUEUE_PATH, which duplicates this literal rather
51
+ // than requiring scheduler.cjs's ~530KB module graph for one path).
52
+ function runsDir(homeDir) {
53
+ return path.join(homeDir, '.claude', 'session-manager', 'scheduled-plans', 'runs');
54
+ }
55
+
56
+ function isConcreteModelId(modelAlias) {
57
+ return Boolean(modelAlias) && !KNOWN_MODEL_ALIASES.has(modelAlias);
58
+ }
59
+
60
+ /**
61
+ * True when this app's spawn path (cleanEnv.cjs's cleanChildEnv) would strip
62
+ * an effort env var before a child ever saw it — i.e. whether the settings
63
+ * schema's env-var effort configuration path is actually reachable for an
64
+ * app-launched session. Derived by probing cleanChildEnv itself rather than
65
+ * hardcoding a boolean, so this self-corrects if that deletion list ever
66
+ * changes instead of silently drifting stale.
67
+ */
68
+ function computeEffortReachable(deps) {
69
+ try {
70
+ const { cleanChildEnv } = deps.cleanEnv || require('./cleanEnv.cjs');
71
+ const probe = cleanChildEnv({ CLAUDE_EFFORT: 'high', CLAUDE_CODE_EFFORT_LEVEL: 'high' });
72
+ return Boolean(probe.CLAUDE_EFFORT || probe.CLAUDE_CODE_EFFORT_LEVEL);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Resolves a persona's model alias, overlay-aware (agentLibrary.cjs's
80
+ * getPersonaBody: project overlay at `<cwd>/.claude/agents/<name>.md` wins
81
+ * over the global `~/.claude/agents/<name>.md` — the same precedence
82
+ * `resolvePrdPersonaForSpawn` already uses, unlike `listPersonas`/
83
+ * `readPersonaModel`, which only ever see the global directory).
84
+ *
85
+ * Never throws: a missing/unreadable persona (dangling agentType) resolves
86
+ * to `{ modelAlias: null, modelSource: 'fallback' }` without logging —
87
+ * agentModelResolve.cjs's own `logDanglingPersonaOnce`/`reportDanglingAgentTypeOnce`
88
+ * dedup-per-(cwd,agentType) already covers that diagnostic on the LAUNCH
89
+ * path; this read-only observer deliberately does not add a second logger
90
+ * for the same dangling reference.
91
+ */
92
+ async function resolvePersonaModelAlias(cwd, agentType, deps) {
93
+ const getPersonaBody = deps.getPersonaBody || require('../agentLibrary.cjs').getPersonaBody;
94
+ const resolvePersonaPaths = deps.resolvePersonaPaths || require('./epicMint.cjs').resolvePersonaPaths;
95
+
96
+ let persona;
97
+ try {
98
+ persona = await getPersonaBody({ cwd, name: agentType });
99
+ } catch {
100
+ persona = null;
101
+ }
102
+ if (!persona) return { modelAlias: null, modelSource: 'fallback' };
103
+
104
+ const { fm } = splitFrontmatter(persona.text);
105
+
106
+ let fromOverlay = false;
107
+ try {
108
+ const { projectPath } = resolvePersonaPaths(cwd, agentType);
109
+ fromOverlay = Boolean(projectPath) && path.resolve(projectPath) === path.resolve(persona.path);
110
+ } catch {
111
+ // Best-effort overlay signal only — a resolution failure here just means
112
+ // "assume global", never a reason to fail the whole lookup.
113
+ }
114
+
115
+ if (fm.model && fm.model !== 'inherit') {
116
+ return { modelAlias: fm.model, modelSource: fromOverlay ? 'persona-overlay' : 'persona' };
117
+ }
118
+ return { modelAlias: null, modelSource: 'inherit' };
119
+ }
120
+
121
+ /** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/queue.json`'s hot jobs[]. */
122
+ function readHotJobsForCwd(cwd, deps) {
123
+ const { projectQueuePath } = deps.queueStore || require('./queueStore.cjs');
124
+ try {
125
+ const data = JSON.parse(fs.readFileSync(projectQueuePath(cwd), 'utf8'));
126
+ return Array.isArray(data.jobs) ? data.jobs : [];
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
131
+
132
+ /** Best-effort read of `<cwd>/session-manager-operations/scheduler/state/history.jsonl`'s archived rows. */
133
+ function readHistoryEntriesForCwd(cwd, deps) {
134
+ const { projectHistoryPath } = deps.queueStore || require('./queueStore.cjs');
135
+ let text;
136
+ try {
137
+ text = fs.readFileSync(projectHistoryPath(cwd), 'utf8');
138
+ } catch {
139
+ return [];
140
+ }
141
+ const entries = [];
142
+ for (const line of text.split('\n')) {
143
+ if (!line.trim()) continue;
144
+ try {
145
+ const j = JSON.parse(line);
146
+ // needs_review_entry/needs_review_resolution lines (needsReviewLedger.cjs)
147
+ // share history.jsonl with terminal job rows but carry no `agentType` —
148
+ // findLatestSchedulerModel's own `j.agentType === agentType` filter
149
+ // already excludes them, but skip explicitly here too so this reader
150
+ // stays correct even if a future ledger line shape happens to pick up
151
+ // an agentType-shaped field.
152
+ if (j?.kind && j.kind !== 'terminal') continue;
153
+ entries.push(j);
154
+ } catch {
155
+ // corrupt/partial line — skip
156
+ }
157
+ }
158
+ return entries;
159
+ }
160
+
161
+ function jobTimestampMs(job) {
162
+ return Date.parse(job?.finishedAt || job?.startedAt || job?.queuedAt || 0) || 0;
163
+ }
164
+
165
+ /** Bounded HEAD read of an absolute file path; null on any read failure. */
166
+ function readHead(filePath, maxBytes, deps) {
167
+ const statSync = deps.statSync || fs.statSync;
168
+ const openSync = deps.openSync || fs.openSync;
169
+ const readSync = deps.readSync || fs.readSync;
170
+ const closeSync = deps.closeSync || fs.closeSync;
171
+ try {
172
+ const size = statSync(filePath).size;
173
+ const length = Math.min(maxBytes, size);
174
+ const buffer = Buffer.alloc(length);
175
+ const fd = openSync(filePath, 'r');
176
+ try {
177
+ readSync(fd, buffer, 0, length, 0);
178
+ } finally {
179
+ closeSync(fd);
180
+ }
181
+ return buffer.toString('utf8');
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ /** Bounded TAIL read of an absolute file path; null on any read failure. */
188
+ function readTail(filePath, maxBytes, deps) {
189
+ const statSync = deps.statSync || fs.statSync;
190
+ const openSync = deps.openSync || fs.openSync;
191
+ const readSync = deps.readSync || fs.readSync;
192
+ const closeSync = deps.closeSync || fs.closeSync;
193
+ try {
194
+ const size = statSync(filePath).size;
195
+ const start = Math.max(0, size - maxBytes);
196
+ const length = size - start;
197
+ const buffer = Buffer.alloc(length);
198
+ const fd = openSync(filePath, 'r');
199
+ try {
200
+ readSync(fd, buffer, 0, length, start);
201
+ } finally {
202
+ closeSync(fd);
203
+ }
204
+ const text = buffer.toString('utf8');
205
+ return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Evidence source 1: the scheduler already logs the persona-resolved model
213
+ * at dispatch (scheduler.cjs's executeJob, ~line 4473: `[scheduler]
214
+ * agentType=... persona=... model=...`). Scans this project's hot queue.json
215
+ * jobs[] AND its archived history.jsonl for the most recent job whose
216
+ * agentType matches, then reads that job's own run log for the line. O(J log
217
+ * J) to sort J candidate jobs, O(min(logSize, cap)) per log read.
218
+ */
219
+ function findLatestSchedulerModel(cwd, agentType, deps) {
220
+ const jobs = [...readHotJobsForCwd(cwd, deps), ...readHistoryEntriesForCwd(cwd, deps)];
221
+ const matches = jobs.filter((j) => j && j.agentType === agentType && j.runId && j.slug);
222
+ matches.sort((a, b) => jobTimestampMs(b) - jobTimestampMs(a));
223
+
224
+ const homeDir = deps.homeDir || os.homedir();
225
+ const re = /\[scheduler\] agentType=\S+ persona=.*? model=(\S+)/g;
226
+ for (const job of matches) {
227
+ const logPath = path.join(runsDir(homeDir), job.runId, `${job.slug}.log`);
228
+ const head = readHead(logPath, MAX_RUN_LOG_HEAD_BYTES, deps);
229
+ if (!head) continue;
230
+ let match;
231
+ let last = null;
232
+ while ((match = re.exec(head))) last = match[1];
233
+ if (last) return last;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /**
239
+ * Evidence source 2: the most recent Epic (active-index.json entry) created
240
+ * with this agentType, read via its own claude-session transcript's last
241
+ * `"model":"..."` occurrence. O(E log E) to sort E candidate Epics,
242
+ * O(min(transcriptSize, cap)) per transcript read.
243
+ */
244
+ function findLatestTranscriptModel(cwd, agentType, deps) {
245
+ const readActiveIndex = deps.readActiveIndex || require('./epicMint.cjs').readActiveIndex;
246
+ let sessions;
247
+ try {
248
+ ({ sessions } = readActiveIndex(cwd));
249
+ } catch {
250
+ return null;
251
+ }
252
+ const matches = Object.values(sessions || {}).filter((s) => s && s.agentType === agentType && s.claudeSessionId);
253
+ matches.sort((a, b) => (Date.parse(b.createdAt || 0) || 0) - (Date.parse(a.createdAt || 0) || 0));
254
+
255
+ const homeDir = deps.homeDir || os.homedir();
256
+ const encodeCwdFn = deps.encodeCwd || encodeCwd;
257
+ const re = /"model":"([^"]+)"/g;
258
+ for (const session of matches) {
259
+ const filePath = path.join(homeDir, '.claude', 'projects', encodeCwdFn(cwd), `${session.claudeSessionId}.jsonl`);
260
+ const tail = readTail(filePath, MAX_TRANSCRIPT_TAIL_BYTES, deps);
261
+ if (!tail) continue;
262
+ let match;
263
+ let last = null;
264
+ while ((match = re.exec(tail))) last = match[1];
265
+ if (last) return last;
266
+ }
267
+ return null;
268
+ }
269
+
270
+ /**
271
+ * resolveEffectiveModelInfo({ cwd, agentType, deps? }) →
272
+ * { agentType, modelAlias, modelSource, resolvedModelId, resolvedFrom, effortReachable }
273
+ *
274
+ * Never throws. `cwd` is normalized through the same worktree/ops-internal
275
+ * -aware resolver every ops-root reader goes through, so a job worktree cwd
276
+ * resolves to its main project tree before anything is looked up.
277
+ */
278
+ async function resolveEffectiveModelInfo({ cwd, agentType, deps = {} } = {}) {
279
+ const effortReachable = computeEffortReachable(deps);
280
+ const miss = {
281
+ agentType: agentType || null,
282
+ modelAlias: null,
283
+ modelSource: 'fallback',
284
+ resolvedModelId: null,
285
+ resolvedFrom: null,
286
+ effortReachable,
287
+ };
288
+ if (!cwd || !agentType) return miss;
289
+
290
+ try {
291
+ const { projectRootOf } = deps.activeSessions || require('./activeSessions.cjs');
292
+ const normalizedCwd = projectRootOf(path.resolve(cwd));
293
+ if (!normalizedCwd) return miss;
294
+
295
+ const { modelAlias, modelSource } = await resolvePersonaModelAlias(normalizedCwd, agentType, deps);
296
+ const result = { agentType, modelAlias, modelSource, resolvedModelId: null, resolvedFrom: null, effortReachable };
297
+
298
+ if (isConcreteModelId(modelAlias)) {
299
+ // Already a concrete pinned id — echo it back, fabricate no evidence.
300
+ result.resolvedModelId = modelAlias;
301
+ return result;
302
+ }
303
+
304
+ // Evidence is never derived via historyAggregator.cjs's resolvePricingKey
305
+ // (which buckets any unknown id to 'sonnet' with estimated:true) —
306
+ // deliberately: that is a pricing heuristic, and borrowing it here would
307
+ // turn "we don't know" into a confident wrong answer.
308
+ const schedulerHit = findLatestSchedulerModel(normalizedCwd, agentType, deps);
309
+ if (schedulerHit) {
310
+ result.resolvedModelId = schedulerHit;
311
+ result.resolvedFrom = 'scheduler-run';
312
+ return result;
313
+ }
314
+
315
+ const transcriptHit = findLatestTranscriptModel(normalizedCwd, agentType, deps);
316
+ if (transcriptHit) {
317
+ result.resolvedModelId = transcriptHit;
318
+ result.resolvedFrom = 'transcript';
319
+ return result;
320
+ }
321
+
322
+ return result;
323
+ } catch {
324
+ return miss;
325
+ }
326
+ }
327
+
328
+ module.exports = {
329
+ resolveEffectiveModelInfo,
330
+ isConcreteModelId,
331
+ computeEffortReachable,
332
+ KNOWN_MODEL_ALIASES,
333
+ };
@@ -37,7 +37,7 @@
37
37
 
38
38
  const os = require('node:os');
39
39
  const path = require('node:path');
40
- const { worktreeMainRootOf } = require('../../../scripts/lib/activeSessions.cjs');
40
+ const { worktreeMainRootOf } = require('./activeSessions.cjs');
41
41
  const { KIND_CONFIG } = require('./gitWorktree.cjs');
42
42
 
43
43
  const TMPDIR = path.resolve(os.tmpdir());
@@ -161,7 +161,7 @@ function resolvePersonaPaths(cwd, agentType, deps = {}) {
161
161
  * a readable `.md` persona file, checking the project overlay before the
162
162
  * global agents dir. Never throws (a traversal attempt or missing file is
163
163
  * treated as "doesn't exist", same fail-closed spirit as
164
- * agentModelResolve.cjs's readPersonaModel).
164
+ * agentModelResolve.cjs's readOverlayAwarePersonaModel).
165
165
  */
166
166
  function personaFileExists(cwd, agentType, deps = {}) {
167
167
  const projectDir = deps.projectDir || path.join(cwd, '.claude', 'agents');
@@ -289,8 +289,8 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
289
289
  }
290
290
 
291
291
  // WRITE-TIME FK CHECK (throw on write, report on read — see this file's
292
- // header + agentModelResolve.cjs's readPersonaModel, which is the READ
293
- // side of this same FK and stays permissive on purpose). A mint whose
292
+ // header + agentModelResolve.cjs's readOverlayAwarePersonaModel, which is
293
+ // the READ side of this same FK and stays permissive on purpose). A mint whose
294
294
  // agentType names a persona that doesn't resolve is a bug in the caller
295
295
  // (a typo, or a persona deleted between the picker loading and the mint
296
296
  // landing) — refuse it now rather than silently orphaning the Epic.
@@ -54,6 +54,7 @@ const path = require('node:path');
54
54
  const os = require('node:os');
55
55
  const crypto = require('node:crypto');
56
56
  const { execFile } = require('node:child_process');
57
+ const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
57
58
 
58
59
  // Per-kind configuration. Roots are kept OUTSIDE any project's own tree
59
60
  // (os.tmpdir(), not `<cwd>/.git/...`) so a managed worktree never shows up in
@@ -304,7 +305,7 @@ async function removeWorktreeDir(cwd, dir) {
304
305
  * <name>` segment stripped off.
305
306
  *
306
307
  * Round-trip verified before being trusted, mirroring the same defense
307
- * `scripts/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
308
+ * `src/main/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
308
309
  * `.git` pointer file is a plain text file, so its content must not be
309
310
  * trusted to redirect `git worktree remove --force` at an arbitrary path
310
311
  * without the candidate main tree's OWN admin dir
@@ -481,29 +482,58 @@ async function sweepStaleWorktreeCheckouts(kind, opts = {}) {
481
482
  * copy of the human's WIP stays in `cwd` the whole time. Nothing here ever
482
483
  * `git add`s or commits the carried paths on the job's behalf, and nothing
483
484
  * ever restores them back into `cwd` — they were never removed from it.
485
+ *
486
+ * The diff itself is captured via `git diff --output=<file>` so git writes
487
+ * the patch straight to disk — never through Node's stdout buffer. A prior
488
+ * version piped the patch through `execFile`'s default-`maxBuffer` stdout
489
+ * capture, which overflowed on a real dirty tree of ~240 changed paths
490
+ * (`stdout maxBuffer length exceeded`, job 1192) and silently fell back to
491
+ * running the job in the SHARED working tree — the exact moment isolation
492
+ * matters most. `--output` removes the buffer ceiling entirely regardless of
493
+ * how large the tree's outstanding diff is. The temp patch file lives
494
+ * inside `dir` (the job's own fresh worktree checkout, not a bare
495
+ * `os.tmpdir()` path) so it is guaranteed to be cleaned up either by the
496
+ * explicit `fsp.rm` below or, on any earlier failure, by the caller tearing
497
+ * the whole `dir` down.
484
498
  */
485
499
  async function captureAndCarryBaseDiff({ cwd, dir }) {
486
500
  let paths = [];
487
- let patch = '';
488
501
  try {
489
502
  const nameOut = await execGit(['diff', 'HEAD', '--name-only'], { cwd, timeout: 15_000 });
490
503
  paths = nameOut.split('\n').map((l) => l.trim()).filter(Boolean);
491
- patch = await execGit(['diff', 'HEAD', '--binary'], { cwd, timeout: 30_000 });
492
504
  } catch (e) {
493
- return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
505
+ return { ok: false, reason: `listing changed paths failed: ${(e && (e.stderrText || e.message)) || e}` };
494
506
  }
495
- if (!patch || !patch.trim()) return { ok: true, paths: [] };
496
507
 
497
- const patchFile = path.join(os.tmpdir(), `sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
508
+ // Everything from here on operates on a real (or attempted) file at
509
+ // `patchFile` — wrapped in one try/finally so the temp file is removed on
510
+ // EVERY exit path, including a `git diff` that errors after partially
511
+ // writing it, not just the clean-success path.
512
+ const patchFile = path.join(dir, `.sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
498
513
  try {
499
- await fsp.writeFile(patchFile, patch, 'utf8');
500
- await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 30_000 });
501
- } catch (e) {
502
- return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
514
+ try {
515
+ await execGit(['diff', 'HEAD', '--binary', `--output=${patchFile}`], { cwd, timeout: 120_000 });
516
+ } catch (e) {
517
+ return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
518
+ }
519
+
520
+ let stat;
521
+ try {
522
+ stat = await fsp.stat(patchFile);
523
+ } catch (e) {
524
+ return { ok: false, reason: `reading captured diff file failed: ${(e && e.message) || e}` };
525
+ }
526
+ if (stat.size === 0) return { ok: true, paths: [] };
527
+
528
+ try {
529
+ await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 30_000 });
530
+ } catch (e) {
531
+ return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
532
+ }
533
+ return { ok: true, paths };
503
534
  } finally {
504
535
  try { await fsp.rm(patchFile, { force: true }); } catch { /* best-effort */ }
505
536
  }
506
- return { ok: true, paths };
507
537
  }
508
538
 
509
539
  /**
@@ -1273,7 +1303,7 @@ async function reclaimTerminalJobOrphans({ cwd, terminalSlugs, isLive }) {
1273
1303
  }
1274
1304
  const dirtyPaths = statusOut.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
1275
1305
  const opsOnly = dirtyPaths.every(
1276
- (p) => p === 'session-manager-operations' || p.startsWith('session-manager-operations/')
1306
+ (p) => p === OPS_ROOT_DIR || p.startsWith(`${OPS_ROOT_DIR}/`)
1277
1307
  );
1278
1308
  if (!opsOnly) continue; // real non-ops dirty content present — leave for a human
1279
1309
 
@@ -0,0 +1,156 @@
1
+ /**
2
+ * guardShims.cjs — stable, app-upgrade-proof indirection for the three
3
+ * PreToolUse guard hooks that delegationReadiness.cjs installs into an
4
+ * adopting project's `.claude/settings.json`.
5
+ *
6
+ * Why this exists: those installers used to bake
7
+ * `path.resolve(__dirname, '../../../scripts/hooks/guard-*.cjs')` — THIS
8
+ * app's own copy of the script — directly into the hook command. Run via
9
+ * `npx claude-code-session-manager@latest` (the normal install path), that
10
+ * resolves under `~/.npm/_npx/<hash>/node_modules/claude-code-session-manager/`
11
+ * — a directory scoped to ONE npx resolution. The next `npx ...@latest` (a
12
+ * new release) or an npm cache prune creates/removes a different hash dir, so
13
+ * every previously-installed hook decays into a dead path. A PreToolUse
14
+ * command pointing at a missing script exits non-zero WITHOUT exit code 2 (a
15
+ * non-blocking error to the harness), so it silently guards nothing — while
16
+ * checkPrdWriteGuard correctly flips the readiness banner back to red. That
17
+ * decay is why "Delegation not ready" reappears across projects even after a
18
+ * successful install.
19
+ *
20
+ * Fix: install a STABLE shim under the user's home
21
+ * (`~/.claude/session-manager/hooks/guard-<name>.cjs`) that never moves
22
+ * across app upgrades. The shim contains no guard logic of its own — CLAUDE.md's
23
+ * "adopt by REFERENCE, never vendor" law — it reads a pointer file
24
+ * (`app-root.json`, rewritten on every app boot with the CURRENTLY running
25
+ * app's root) and `require()`s the real `scripts/hooks/guard-<name>.cjs` from
26
+ * there. An app upgrade (a new npx hash dir) just rewrites the pointer on the
27
+ * next boot; every already-installed project hook keeps resolving through
28
+ * the same shim path with no re-install.
29
+ *
30
+ * The guard scripts run their logic as a require-time side effect (stdin ->
31
+ * decision -> process.exit, not gated on `require.main === module`), so a
32
+ * plain `require()` from the shim is enough to invoke them.
33
+ */
34
+ 'use strict';
35
+
36
+ const fs = require('node:fs');
37
+ const path = require('node:path');
38
+ const os = require('node:os');
39
+ const { writeJson, writeTextAtomic, addAllowedRoot } = require('../config.cjs');
40
+
41
+ // src/main/lib -> app root (three levels up) — the same resolution
42
+ // delegationReadiness.cjs's *_GUARD_SCRIPT constants use.
43
+ const APP_ROOT = path.resolve(__dirname, '..', '..', '..');
44
+
45
+ const GUARD_NAMES = ['guard-prd-writes.cjs', 'guard-destructive-git.cjs', 'guard-inline-implementation.cjs', 'guard-self-schedule.cjs'];
46
+
47
+ function hooksDir(homeDir = os.homedir()) {
48
+ return path.join(homeDir, '.claude', 'session-manager', 'hooks');
49
+ }
50
+
51
+ function pointerPath(homeDir = os.homedir()) {
52
+ return path.join(hooksDir(homeDir), 'app-root.json');
53
+ }
54
+
55
+ function shimPath(guardName, homeDir = os.homedir()) {
56
+ return path.join(hooksDir(homeDir), guardName);
57
+ }
58
+
59
+ function shimBody(guardName) {
60
+ return `'use strict';
61
+ const fs = require('node:fs');
62
+ const path = require('node:path');
63
+ const ptr = JSON.parse(fs.readFileSync(path.join(__dirname, 'app-root.json'), 'utf8'));
64
+ require(path.join(ptr.appRoot, 'scripts', 'hooks', '${guardName}'));
65
+ `;
66
+ }
67
+
68
+ /**
69
+ * Write the pointer file + all three shims under `homeDir`. Idempotent: a
70
+ * re-run with the same `appRoot` produces byte-identical files; a re-run with
71
+ * a DIFFERENT `appRoot` (an app upgrade) rewrites only the pointer, which is
72
+ * exactly what lets already-installed project hooks follow the upgrade with
73
+ * no re-install. Never throws — returns `{ ok, error }` so a boot-time caller
74
+ * can fire-and-forget and an installer can refuse cleanly rather than writing
75
+ * a hook command that points at a shim that doesn't exist.
76
+ */
77
+ async function writeGuardShims({ homeDir = os.homedir(), appRoot = APP_ROOT } = {}) {
78
+ try {
79
+ addAllowedRoot(homeDir);
80
+ await writeJson(pointerPath(homeDir), { appRoot });
81
+ for (const name of GUARD_NAMES) {
82
+ await writeTextAtomic(shimPath(name, homeDir), shimBody(name));
83
+ }
84
+ return { ok: true };
85
+ } catch (err) {
86
+ return { ok: false, error: err?.message ?? String(err) };
87
+ }
88
+ }
89
+
90
+ /** True when every shim + the pointer file already exist on disk under `homeDir`. */
91
+ function guardShimsExist(homeDir = os.homedir()) {
92
+ return fs.existsSync(pointerPath(homeDir)) && GUARD_NAMES.every((name) => fs.existsSync(shimPath(name, homeDir)));
93
+ }
94
+
95
+ /**
96
+ * Given an absolute script path a hook command resolved to, determine
97
+ * whether it's actually a guard SHIM (its directory carries a sibling
98
+ * `app-root.json`) rather than a direct app-root script. A direct script has
99
+ * no pointer and is trivially "real" once `fs.existsSync` is true — the
100
+ * caller doesn't need this function for that case.
101
+ *
102
+ * For a shim, existence of the shim FILE is not enough: the shim's own
103
+ * require() target is `ptr.appRoot/scripts/hooks/<name>.cjs`, resolved only
104
+ * at hook-invocation time. If the pointer is missing, unparseable, or now
105
+ * names a script that's gone, the shim silently guards nothing the next time
106
+ * a hook actually fires — decay one hop downstream of the hook command
107
+ * itself. Returns `null` when `scriptAbsPath` is not a shim (no sibling
108
+ * pointer) — the caller's plain existence check already covers that case.
109
+ * Throws when it IS a shim and the pointer has decayed.
110
+ */
111
+ function resolveShimTarget(scriptAbsPath) {
112
+ const dir = path.dirname(scriptAbsPath);
113
+ const ptrFile = path.join(dir, 'app-root.json');
114
+ if (!fs.existsSync(ptrFile)) return null;
115
+
116
+ const guardName = path.basename(scriptAbsPath);
117
+ let appRoot;
118
+ try {
119
+ appRoot = JSON.parse(fs.readFileSync(ptrFile, 'utf8'))?.appRoot;
120
+ } catch (err) {
121
+ throw new Error(`guard shim pointer ${ptrFile} is unparseable: ${err?.message ?? err}`);
122
+ }
123
+ if (typeof appRoot !== 'string' || !appRoot) {
124
+ throw new Error(`guard shim pointer ${ptrFile} has no appRoot`);
125
+ }
126
+ const real = path.join(appRoot, 'scripts', 'hooks', guardName);
127
+ if (!fs.existsSync(real)) {
128
+ throw new Error(`guard shim pointer ${ptrFile} resolves to a script that no longer exists: ${real}`);
129
+ }
130
+ return real;
131
+ }
132
+
133
+ /**
134
+ * `writeGuardShims({ homeDir })`, but returns `null` on success and an
135
+ * install-function-shaped error object (`{ ok:false, action:'error', error }`)
136
+ * on failure — the one call every `install*Guard` in delegationReadiness.cjs
137
+ * needs before it can safely write a hook command pointing at the shim.
138
+ */
139
+ async function ensureGuardShimsOrError(homeDir) {
140
+ const result = await writeGuardShims({ homeDir });
141
+ if (result.ok) return null;
142
+ return { ok: false, action: 'error', error: `could not write the stable guard shim under ${hooksDir(homeDir)}: ${result.error}` };
143
+ }
144
+
145
+ module.exports = {
146
+ APP_ROOT,
147
+ GUARD_NAMES,
148
+ hooksDir,
149
+ pointerPath,
150
+ shimPath,
151
+ shimBody,
152
+ writeGuardShims,
153
+ guardShimsExist,
154
+ resolveShimTarget,
155
+ ensureGuardShimsOrError,
156
+ };
@@ -27,8 +27,13 @@
27
27
  * must still park.
28
28
  */
29
29
 
30
- /** The one app-owned root. Matches at any depth, and only as a path segment. */
31
- const OPS_ROOT_SEGMENT = 'session-manager-operations';
30
+ /**
31
+ * The one app-owned root. Matches at any depth, and only as a path segment.
32
+ * Sourced from opsOwnership.cjs (the single-writer law's own root name)
33
+ * rather than a second hardcoded literal — this file matches git-porcelain
34
+ * RELATIVE paths, never builds an fs path, so opsPath() itself doesn't apply.
35
+ */
36
+ const { OPS_ROOT_DIR: OPS_ROOT_SEGMENT } = require('./opsOwnership.cjs');
32
37
 
33
38
  /** True when `p` lives under an app-owned operations root. Pure. */
34
39
  function isAppOwnedChurn(p) {
@@ -309,9 +309,10 @@ function evaluateLaunchGate(block, { now, claudeVersion } = {}) {
309
309
  * closed set so operators never have to open a transcript to tell a
310
310
  * non-start from an implementation failure from a verifier downgrade.
311
311
  */
312
- function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure }) {
312
+ function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill }) {
313
313
  if (worktreeIntegrationFailure) return 'worktree_integration_failed';
314
314
  if (effectiveStatus === 'completed') return 'completed';
315
+ if (budgetKill) return 'budget_exceeded';
315
316
  if (sigtermOverride) return 'signal_kill_with_commit';
316
317
  if (exitCode === 143 || exitCode === 137) return 'signal_kill';
317
318
  if (typeof exitCode === 'number' && exitCode !== 0) return `impl_failed:exit_${exitCode}`;