claude-code-session-manager 0.104.1 → 0.105.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 (192) hide show
  1. package/README.md +15 -3
  2. package/bin/cli.cjs +0 -5
  3. package/dist/assets/AgentLibrary-BDtr4oI-.js +2 -0
  4. package/dist/assets/Button-CuXBCQDp.js +1 -0
  5. package/dist/assets/DataModel-DmvRipT5.js +1 -0
  6. package/dist/assets/{History-BP6KKda_.js → History-B-v9pwwr.js} +2 -2
  7. package/dist/assets/Hooks-S_0oHHlr.js +3 -0
  8. package/dist/assets/{Library-BprXNJCG.js → Library-BcbGJxQ3.js} +5 -5
  9. package/dist/assets/ListDetail-DtqHqJHt.js +1 -0
  10. package/dist/assets/{MarkdownEditor-1KlkRAqT.js → MarkdownEditor-B56sPp_l.js} +1 -1
  11. package/dist/assets/{McpServers-FUDN7EnU.js → McpServers-D5VnmxAn.js} +2 -2
  12. package/dist/assets/{Memory-DryJweXu.js → Memory-0yGZLyRP.js} +6 -6
  13. package/dist/assets/Panel-NoAADJwx.js +1 -0
  14. package/dist/assets/Permissions-D2Dh-ee8.js +3 -0
  15. package/dist/assets/Plugins-kGoUgS9b.js +2 -0
  16. package/dist/assets/{ProvenanceBadge-6Hj42klQ.js → ProvenanceBadge-CHwDFTX7.js} +1 -1
  17. package/dist/assets/{SaveBar-xSfjUPiu.js → SaveBar-BtKNMIYK.js} +1 -1
  18. package/dist/assets/{Scheduler-Beh3Vj-Z.js → Scheduler-B0_kMMDs.js} +7 -7
  19. package/dist/assets/{ScopeSwitcher-9R1ANwns.js → ScopeSwitcher-BusohWZ5.js} +1 -1
  20. package/dist/assets/Settings-DuRJntaE.js +3 -0
  21. package/dist/assets/{SkillReferenceGraph-CYLTGabR.js → SkillReferenceGraph-BHOdIpsy.js} +1 -1
  22. package/dist/assets/Skills-DSTLZimI.js +3 -0
  23. package/dist/assets/SystemPrompt-BG7wFWdb.js +1 -0
  24. package/dist/assets/TagLibrary-Bx27aE3_.js +1 -0
  25. package/dist/assets/{TiptapBody-l-BMtrPd.js → TiptapBody-j0O8J7YI.js} +1 -1
  26. package/dist/assets/{Toggle-Bj2Ia7AE.js → Toggle-DzVZ6rC7.js} +1 -1
  27. package/dist/assets/ViewTabs-BizjuynT.js +1 -0
  28. package/dist/assets/claude-settings-schema-COigsajx.js +3 -0
  29. package/dist/assets/{index-D89EUizB.css → index-BLVzrc4k.css} +1 -1
  30. package/dist/assets/index-CLIaYljt.js +3073 -0
  31. package/dist/assets/monaco-editor-BsIlgG7c.js +917 -0
  32. package/dist/assets/monaco-editor-Cu5D__1H.css +1 -0
  33. package/dist/assets/settingsSchema-7MRAxaJk.js +1 -0
  34. package/dist/assets/useAgentsDirWatch-BUwfU2Kq.js +1 -0
  35. package/dist/assets/useConfigDirWatch-ot0VYwQC.js +1 -0
  36. package/dist/index.html +4 -4
  37. package/package.json +12 -14
  38. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +12 -0
  39. package/plugins/session-manager-dev/skills/develop/SKILL.md +4 -2
  40. package/scripts/scheduler-mcp-server.cjs +7 -17
  41. package/src/main/build-info.json +4 -4
  42. package/src/main/chatRunner.cjs +4 -4
  43. package/src/main/historyAggregator.cjs +7 -1
  44. package/src/main/index.cjs +34 -1
  45. package/src/main/lib/activeSessions.cjs +263 -71
  46. package/src/main/lib/agentPersonaSchema.cjs +8 -1
  47. package/src/main/lib/appRuntime.cjs +57 -0
  48. package/src/main/lib/childWithLog.cjs +10 -10
  49. package/src/main/lib/claudeBin.cjs +40 -16
  50. package/src/main/lib/cleanEnv.cjs +29 -6
  51. package/src/main/lib/defaultShell.cjs +36 -0
  52. package/src/main/lib/definitionOfDone.cjs +6 -2
  53. package/src/main/lib/delegationReadiness.cjs +13 -5
  54. package/src/main/lib/gitWorktree.cjs +6 -3
  55. package/src/main/lib/insideHome.cjs +15 -4
  56. package/src/main/lib/killTree.cjs +80 -0
  57. package/src/main/lib/loadGate.cjs +16 -0
  58. package/src/main/lib/loginShellPath.cjs +82 -0
  59. package/src/main/lib/machineProfile.cjs +4 -2
  60. package/src/main/lib/mcpStdioServer.cjs +127 -0
  61. package/src/main/lib/openExternalApp.cjs +62 -11
  62. package/src/main/lib/otlpTraceExporter.cjs +147 -0
  63. package/src/main/lib/prdMigration.cjs +64 -1
  64. package/src/main/lib/prereqs.cjs +145 -0
  65. package/src/main/lib/procIdentity.cjs +37 -2
  66. package/src/main/lib/queueHistory.cjs +97 -26
  67. package/src/main/lib/rcaReport.cjs +1 -0
  68. package/src/main/lib/reaperHelpers.cjs +5 -2
  69. package/src/main/lib/shippedPersonaSeeds.cjs +10 -0
  70. package/src/main/lib/timeoutShimScript.cjs +14 -2
  71. package/src/main/lib/updateCheck.cjs +41 -11
  72. package/src/main/lib/watchdogHelpers.cjs +23 -3
  73. package/src/main/lib/winSpawn.cjs +67 -0
  74. package/src/main/otel.cjs +18 -62
  75. package/src/main/pty.cjs +82 -12
  76. package/src/main/runVerify.cjs +42 -2
  77. package/src/main/scheduler.cjs +105 -20
  78. package/src/main/seedSchedulerMcp.cjs +11 -2
  79. package/src/main/usage.cjs +4 -1
  80. package/src/preload/api.d.ts +14 -0
  81. package/src/preload/index.cjs +2 -0
  82. package/src/seed/agents/validator.md +2 -2
  83. package/dist/assets/DataModel-BHcPa2nA.js +0 -1
  84. package/dist/assets/Hooks-CnJnddP6.js +0 -3
  85. package/dist/assets/Permissions-Df-LWvdk.js +0 -3
  86. package/dist/assets/Plugins-CIUOw9B2.js +0 -2
  87. package/dist/assets/Settings-gkp0WfEm.js +0 -3
  88. package/dist/assets/Skills-Dw5Am-f-.js +0 -3
  89. package/dist/assets/SystemPrompt-B3GwTLqh.js +0 -1
  90. package/dist/assets/TagLibrary-Dj46gOG2.js +0 -1
  91. package/dist/assets/ViewTabs-BQ_HbQFq.js +0 -1
  92. package/dist/assets/abap-DLDM7-KI.js +0 -1
  93. package/dist/assets/apex-DNDY2TF8.js +0 -1
  94. package/dist/assets/azcli-Y6nb8tq_.js +0 -1
  95. package/dist/assets/bat-BwHxbl9M.js +0 -1
  96. package/dist/assets/bicep-CFznDFnq.js +0 -2
  97. package/dist/assets/cameligo-Bf6VGUru.js +0 -1
  98. package/dist/assets/clojure-Dnu-v4kV.js +0 -1
  99. package/dist/assets/coffee-Bd8akH9Z.js +0 -1
  100. package/dist/assets/cpp-BbWJElDN.js +0 -1
  101. package/dist/assets/csharp-Co3qMtFm.js +0 -1
  102. package/dist/assets/csp-D-4FJmMZ.js +0 -1
  103. package/dist/assets/css-DdJfP1eB.js +0 -3
  104. package/dist/assets/css.worker-B4z49cGk.js +0 -93
  105. package/dist/assets/cssMode-DfqZGMQs.js +0 -1
  106. package/dist/assets/cypher-cTPe9QuQ.js +0 -1
  107. package/dist/assets/dart-BOtBlQCF.js +0 -1
  108. package/dist/assets/dockerfile-BG73LgW2.js +0 -1
  109. package/dist/assets/ecl-BEgZUVRK.js +0 -1
  110. package/dist/assets/elixir-BkW5O-1t.js +0 -1
  111. package/dist/assets/flow9-BeJ5waoc.js +0 -1
  112. package/dist/assets/freemarker2-XTPYh37h.js +0 -3
  113. package/dist/assets/fsharp-PahG7c26.js +0 -1
  114. package/dist/assets/go-acbASCJo.js +0 -1
  115. package/dist/assets/graphql-BxJiqAUM.js +0 -1
  116. package/dist/assets/handlebars-DKUF5VyH.js +0 -1
  117. package/dist/assets/hcl-DtV1sZF8.js +0 -1
  118. package/dist/assets/html-uqoqsIeI.js +0 -1
  119. package/dist/assets/html.worker-DtiGdgqp.js +0 -470
  120. package/dist/assets/htmlMode-aMTQs1su.js +0 -1
  121. package/dist/assets/index-DbR91Z5j.js +0 -3076
  122. package/dist/assets/ini-Kd9XrMLS.js +0 -1
  123. package/dist/assets/java-CXBNlu9o.js +0 -1
  124. package/dist/assets/javascript-BVxRZMds.js +0 -1
  125. package/dist/assets/jsonMode-D04xP2s5.js +0 -7
  126. package/dist/assets/julia-cl7-CwDS.js +0 -1
  127. package/dist/assets/kotlin-s7OhZKlX.js +0 -1
  128. package/dist/assets/less-9HpZscsL.js +0 -2
  129. package/dist/assets/lexon-OrD6JF1K.js +0 -1
  130. package/dist/assets/liquid-BkQHTH2P.js +0 -1
  131. package/dist/assets/lspLanguageFeatures-By9uLznH.js +0 -4
  132. package/dist/assets/lua-Cyyb5UIc.js +0 -1
  133. package/dist/assets/m3-B8OfTtLu.js +0 -1
  134. package/dist/assets/markdown-BFxVWTOG.js +0 -1
  135. package/dist/assets/mdx-Du1IlbjV.js +0 -1
  136. package/dist/assets/mips-CiqrrVzr.js +0 -1
  137. package/dist/assets/monaco-editor-BTnBOi8r.css +0 -1
  138. package/dist/assets/monaco-editor-BW5C4Iv1.js +0 -908
  139. package/dist/assets/msdax-DmeGPVcC.js +0 -1
  140. package/dist/assets/mysql-C_tMU-Nz.js +0 -1
  141. package/dist/assets/objective-c-BDtDVThU.js +0 -1
  142. package/dist/assets/pascal-vHIfCaH5.js +0 -1
  143. package/dist/assets/pascaligo-DtZ0uQbO.js +0 -1
  144. package/dist/assets/perl-Ub6l9XKa.js +0 -1
  145. package/dist/assets/pgsql-BlNEE0v7.js +0 -1
  146. package/dist/assets/php-BBUBE1dy.js +0 -1
  147. package/dist/assets/pla-DSh2-awV.js +0 -1
  148. package/dist/assets/postiats-CocnycG-.js +0 -1
  149. package/dist/assets/powerquery-tScXyioY.js +0 -1
  150. package/dist/assets/powershell-COWaemsV.js +0 -1
  151. package/dist/assets/protobuf-Brw8urJB.js +0 -2
  152. package/dist/assets/pug-8SOpv6rk.js +0 -1
  153. package/dist/assets/python-DSlImqXd.js +0 -1
  154. package/dist/assets/qsharp-Bw9ernYp.js +0 -1
  155. package/dist/assets/r-j7ic8hl3.js +0 -1
  156. package/dist/assets/razor-BmUVyvSK.js +0 -1
  157. package/dist/assets/redis-Bu5POkcn.js +0 -1
  158. package/dist/assets/redshift-Bs9aos_-.js +0 -1
  159. package/dist/assets/restructuredtext-CqXO7rUv.js +0 -1
  160. package/dist/assets/ruby-zBfavPgS.js +0 -1
  161. package/dist/assets/rust-BzKRNQWT.js +0 -1
  162. package/dist/assets/sb-BBc9UKZt.js +0 -1
  163. package/dist/assets/scala-D9hQfWCl.js +0 -1
  164. package/dist/assets/scheme-BPhDTwHR.js +0 -1
  165. package/dist/assets/scss-CBJaRo0y.js +0 -3
  166. package/dist/assets/settingsSchema-cR833AB2.js +0 -1
  167. package/dist/assets/shell-DiJ1NA_G.js +0 -1
  168. package/dist/assets/solidity-Db0IVjzk.js +0 -1
  169. package/dist/assets/sophia-CnS9iZB_.js +0 -1
  170. package/dist/assets/sparql-CJmd_6j2.js +0 -1
  171. package/dist/assets/sql-ClhHkBeG.js +0 -1
  172. package/dist/assets/st-CHwy0fLd.js +0 -1
  173. package/dist/assets/swift-Bqt4WxQ4.js +0 -3
  174. package/dist/assets/systemverilog-Bs9z6M-B.js +0 -1
  175. package/dist/assets/tcl-Dm6ycUr_.js +0 -1
  176. package/dist/assets/ts.worker-59MjiAqk.js +0 -67731
  177. package/dist/assets/tsMode-Btj0TTH7.js +0 -11
  178. package/dist/assets/twig-Csy3S7wG.js +0 -1
  179. package/dist/assets/typescript-Bzelq9vO.js +0 -1
  180. package/dist/assets/typespec-Btyra-wh.js +0 -1
  181. package/dist/assets/vb-Db0cS2oM.js +0 -1
  182. package/dist/assets/wgsl-BTesnYfV.js +0 -298
  183. package/dist/assets/xml-Whd9EaSd.js +0 -1
  184. package/dist/assets/yaml-QYf0-IN8.js +0 -1
  185. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +0 -112
  186. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  187. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +0 -106
  188. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  189. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +0 -107
  190. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  191. package/scripts/postinstall.cjs +0 -52
  192. package/src/preload/__tests__/preload-surface.test.cjs +0 -68
package/src/main/pty.cjs CHANGED
@@ -20,6 +20,7 @@ const { addAllowedRoot } = require('./config.cjs');
20
20
  const { aliasedShellBin } = require('./lib/smProcNames.cjs');
21
21
  const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
22
22
  const { checkInsideHome } = require('./lib/insideHome.cjs');
23
+ const { defaultShell } = require('./lib/defaultShell.cjs');
23
24
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
24
25
  const opsErrorLog = require('./lib/opsErrorLog.cjs');
25
26
  const { planEpicSpawn } = require('./lib/epicSpawnPlan.cjs');
@@ -35,6 +36,15 @@ const PKG_DIR = path.join(__dirname, '..', '..');
35
36
  // chatty sessions can't balloon main-process memory.
36
37
  const REPLAY_BUFFER_MAX = 256 * 1024;
37
38
 
39
+ // Coalescing window for pty:data IPC sends. A busy terminal emits thousands
40
+ // of tiny node-pty onData chunks/sec; each used to cost its own main->renderer
41
+ // IPC serialization + xterm write task. Buffering for a few ms and flushing
42
+ // once collapses that into a handful of sends/sec with no visible latency.
43
+ const PTY_FLUSH_MS = 8;
44
+ // Immediate-flush size cap so a single burst (e.g. `cat` on a big file) can't
45
+ // grow the buffer unbounded while waiting for the timer.
46
+ const PTY_FLUSH_MAX_BYTES = 64 * 1024;
47
+
38
48
  /**
39
49
  * ANSI-formatted terminal text explaining a native-module / immediate-exit
40
50
  * failure and exactly how to fix it. Written straight into the tab's output so
@@ -42,20 +52,39 @@ const REPLAY_BUFFER_MAX = 256 * 1024;
42
52
  */
43
53
  function nativeModuleHelp(reason) {
44
54
  const mac = process.platform === 'darwin';
45
- const lines = [
55
+ const head = [
46
56
  '',
47
57
  `\x1b[1;33m[session-manager] Terminal could not start.\x1b[0m`,
48
58
  `\x1b[33m${reason}\x1b[0m`,
49
59
  '',
50
- `This is almost always the node-pty native module not matching this`,
51
- `Electron build. Rebuild it once:`,
60
+ ];
61
+ if (process.platform === 'win32') {
62
+ return (
63
+ [
64
+ ...head,
65
+ `This is almost always a damaged install (the node-pty native module`,
66
+ `is missing or does not match this build). Reinstall session-manager:`,
67
+ '',
68
+ ` - Installed app: run the latest installer again.`,
69
+ ` - npm package: \x1b[36mnpm install -g claude-code-session-manager@latest\x1b[0m`,
70
+ '',
71
+ `Then quit and reopen session-manager.`,
72
+ '',
73
+ ].join('\r\n') + '\r\n'
74
+ );
75
+ }
76
+ const lines = [
77
+ ...head,
78
+ `node-pty ships prebuilt binaries for macOS, Linux and Windows on x64 and`,
79
+ `arm64. This machine is \x1b[36m${process.platform}-${process.arch}\x1b[0m. If the module failed to load, the`,
80
+ `install is probably damaged. Reinstall session-manager:`,
81
+ '',
82
+ ` \x1b[36mnpm i -g claude-code-session-manager@latest\x1b[0m`,
83
+ ` (or re-run the npx command)`,
52
84
  '',
53
- ` \x1b[36mcd ${PKG_DIR}\x1b[0m`,
54
- ` \x1b[36mnpx electron-rebuild -f -w node-pty\x1b[0m`,
85
+ `On any other platform node-pty compiles from source and needs build tools:`,
86
+ mac ? ` macOS: \x1b[36mxcode-select --install\x1b[0m` : ` Linux: build-essential + python3`,
55
87
  '',
56
- mac ? `macOS: if the rebuild fails, install the compiler first:` : '',
57
- mac ? ` \x1b[36mxcode-select --install\x1b[0m` : '',
58
- mac ? '' : '',
59
88
  `Then quit and reopen session-manager.`,
60
89
  '',
61
90
  ].filter((l) => l !== '');
@@ -77,9 +106,35 @@ class PtyManager {
77
106
  // reload — would read as the session having been wiped. Bounded ring so a
78
107
  // long-lived session can't grow this without limit.
79
108
  this.buffers = new Map();
109
+ // tabId -> pending un-sent output, flushed on a timer or size cap (see
110
+ // PTY_FLUSH_MS / PTY_FLUSH_MAX_BYTES / #flushOutput below).
111
+ this.outBuffers = new Map();
112
+ this.flushTimers = new Map(); // tabId -> Timeout
80
113
  this.window = null;
81
114
  }
82
115
 
116
+ /** Send any pending buffered output for `tabId` now, and clear its timer. */
117
+ #flushOutput(tabId) {
118
+ const timer = this.flushTimers.get(tabId);
119
+ if (timer) {
120
+ clearTimeout(timer);
121
+ this.flushTimers.delete(tabId);
122
+ }
123
+ const pending = this.outBuffers.get(tabId);
124
+ if (pending) {
125
+ this.outBuffers.delete(tabId);
126
+ sendIfAlive(this.window, `pty:data:${tabId}`, pending);
127
+ }
128
+ }
129
+
130
+ /** Discard pending buffered output for `tabId` without sending (kill/dispose). */
131
+ #dropOutput(tabId) {
132
+ const timer = this.flushTimers.get(tabId);
133
+ if (timer) clearTimeout(timer);
134
+ this.flushTimers.delete(tabId);
135
+ this.outBuffers.delete(tabId);
136
+ }
137
+
83
138
  attachWindow(window) {
84
139
  this.window = window;
85
140
  }
@@ -175,8 +230,8 @@ class PtyManager {
175
230
  SM_PROJECT_ROOT: cwd,
176
231
  });
177
232
 
178
- const shell = process.env.SHELL || '/bin/bash';
179
- console.log('[pty] spawning shell', shell);
233
+ const shell = defaultShell({ platform: process.platform, env: process.env, exists: fs.existsSync });
234
+ console.log('[pty] spawning shell', shell.file);
180
235
  // Tab ID = claudeSessionId (see CLAUDE.md), and Chat/Terminal are two views over ONE session,
181
236
  // so this asks the same decision function chatRunner.cjs does (epicSpawnPlan.cjs). `cwd`
182
237
  // itself is NEVER repointed; only this actual PTY spawn option is (epicSpawnCwd.cjs's
@@ -220,7 +275,8 @@ class PtyManager {
220
275
  // job's descendants; no shell in a job's process tree is ever spawned
221
276
  // through here, so that detection is unaffected. Non-bash/zsh shells and
222
277
  // any alias failure fall back to the real path.
223
- proc = pty.spawn(aliasedShellBin(shell), ['-il'], {
278
+ const shellFile = process.platform === 'win32' ? shell.file : aliasedShellBin(shell.file);
279
+ proc = pty.spawn(shellFile, shell.args, {
224
280
  name: 'xterm-256color',
225
281
  cols,
226
282
  rows,
@@ -251,7 +307,16 @@ class PtyManager {
251
307
  proc.onData((data) => {
252
308
  gotData = true;
253
309
  this.#appendReplay(tabId, data);
254
- sendIfAlive(this.window, `pty:data:${tabId}`, data);
310
+ const next = (this.outBuffers.get(tabId) ?? '') + data;
311
+ if (next.length >= PTY_FLUSH_MAX_BYTES) {
312
+ this.outBuffers.set(tabId, next);
313
+ this.#flushOutput(tabId);
314
+ } else {
315
+ this.outBuffers.set(tabId, next);
316
+ if (!this.flushTimers.has(tabId)) {
317
+ this.flushTimers.set(tabId, setTimeout(() => this.#flushOutput(tabId), PTY_FLUSH_MS));
318
+ }
319
+ }
255
320
  });
256
321
 
257
322
  proc.onExit(({ exitCode, signal }) => {
@@ -262,8 +327,12 @@ class PtyManager {
262
327
  console.log('[pty] suppressed exit broadcast for killed tabId=', tabId);
263
328
  this.sessions.delete(tabId);
264
329
  this.buffers.delete(tabId);
330
+ this.#dropOutput(tabId);
265
331
  return;
266
332
  }
333
+ // Flush any still-buffered output BEFORE the exit event so the renderer
334
+ // sees every byte the process produced, in order.
335
+ this.#flushOutput(tabId);
267
336
  // Fast-exit detector: a shell that dies in <1.2s with a non-zero status
268
337
  // and never printed anything almost certainly couldn't exec (broken
269
338
  // node-pty spawn-helper / ABI on macOS). Explain it in the tab instead of
@@ -341,6 +410,7 @@ class PtyManager {
341
410
  }
342
411
  this.sessions.delete(tabId);
343
412
  this.buffers.delete(tabId);
413
+ this.#dropOutput(tabId);
344
414
  }
345
415
  }
346
416
 
@@ -168,13 +168,14 @@ function parseLog(logPath) {
168
168
  try {
169
169
  text = fs.readFileSync(logPath, 'utf8');
170
170
  } catch (e) {
171
- return { events: [], resultEvent: null, error: e.message };
171
+ return { events: [], resultEvent: null, error: e.message, transcriptCommitShas: [] };
172
172
  }
173
173
 
174
174
  const events = [];
175
175
  let resultEvent = null;
176
176
  let seq = 0;
177
177
  let transcriptCommitLanded = false;
178
+ const transcriptCommitShaSet = new Set();
178
179
 
179
180
  for (const line of text.split('\n')) {
180
181
  const trimmed = line.trim();
@@ -194,6 +195,8 @@ function parseLog(logPath) {
194
195
  }
195
196
  if (obj.tool_use_result?.gitOperation?.commit?.kind === 'committed') {
196
197
  transcriptCommitLanded = true;
198
+ const sha = obj.tool_use_result.gitOperation.commit.sha;
199
+ if (typeof sha === 'string' && sha) transcriptCommitShaSet.add(sha);
197
200
  }
198
201
 
199
202
  // Final result event.
@@ -245,7 +248,7 @@ function parseLog(logPath) {
245
248
  }
246
249
  }
247
250
 
248
- return { events, resultEvent, error: null, transcriptCommitLanded };
251
+ return { events, resultEvent, error: null, transcriptCommitLanded, transcriptCommitShas: [...transcriptCommitShaSet] };
249
252
  }
250
253
 
251
254
  // Stable, narrow substrings of the harness's auto-background tool_result text
@@ -549,6 +552,28 @@ function scanSentinel(resultEvent, events) {
549
552
  return null;
550
553
  }
551
554
 
555
+ /**
556
+ * One-line reason after `SCHEDULER_VERDICT: FAIL` (same two-source scan order
557
+ * as scanSentinel). Trimmed, capped at 300 chars; '' when absent.
558
+ */
559
+ function scanSentinelReason(resultEvent, events) {
560
+ const RE = /^SCHEDULER_VERDICT:\s*FAIL\b[ \t]*([^\n]*)/m;
561
+ const clip = (m) => m[1].trim().slice(0, 300);
562
+ if (resultEvent && resultEvent.resultText) {
563
+ const m = resultEvent.resultText.match(RE);
564
+ if (m) return clip(m);
565
+ }
566
+ let lastToolResult = null;
567
+ for (const ev of events) {
568
+ if (ev.kind === 'tool_result') lastToolResult = ev;
569
+ }
570
+ if (lastToolResult && lastToolResult.content) {
571
+ const m = lastToolResult.content.match(RE);
572
+ if (m) return clip(m);
573
+ }
574
+ return '';
575
+ }
576
+
552
577
  /**
553
578
  * Scan for a `FOREIGN_WIP_PATHS: <comma-separated paths>` evidence line —
554
579
  * the mandatory companion to a `SCHEDULER_VERDICT: BLOCKED_BY_FOREIGN_WIP`
@@ -1079,6 +1104,20 @@ async function verifyRun({
1079
1104
  // Scan for the SCHEDULER_VERDICT sentinel emitted by the finish protocol.
1080
1105
  const sentinel = scanSentinel(resultEvent, events);
1081
1106
 
1107
+ // An explicit executor FAIL is always blocking — outranks every other
1108
+ // verdict and every override below (early return). Transcript issues are
1109
+ // kept as annotations so nothing is lost.
1110
+ if (sentinel === 'fail') {
1111
+ const failReason = scanSentinelReason(resultEvent, events);
1112
+ const kept = [...annotations, ...issues.map((i) => ({ verdict: i.verdict, reason: i.reason }))];
1113
+ return conclude(
1114
+ 'sentinel_fail',
1115
+ 'executor reported SCHEDULER_VERDICT: FAIL' + (failReason ? ': ' + failReason : ''),
1116
+ 'needs_review',
1117
+ { sentinel, ...(kept.length ? { annotations: kept } : {}) },
1118
+ );
1119
+ }
1120
+
1082
1121
  // Ground truth outranks heuristics: an exit-0 run that landed a commit
1083
1122
  // ATTRIBUTABLE TO THIS RUN (spawnJob's own guardHeadBefore/headAtExit
1084
1123
  // delta, or the repo-wide committedDuringRun window scan) and never
@@ -1377,6 +1416,7 @@ module.exports = {
1377
1416
  checkDeps,
1378
1417
  parseLog,
1379
1418
  scanSentinel,
1419
+ scanSentinelReason,
1380
1420
  scanForeignWipPathsClaim,
1381
1421
  isMergeMainSlug,
1382
1422
  extractMergeMainPrNumber,
@@ -82,6 +82,7 @@ const { isResetFresh, bindingWindow, degradedBudget } = require('./lib/usageCirc
82
82
  const { computeQueueHealth } = require('./lib/queueHealth.cjs');
83
83
  const { createLoadGate, topCpuConsumers } = require('./lib/loadGate.cjs');
84
84
  const { openLog, withChildAndLog } = require('./lib/childWithLog.cjs');
85
+ const { killTree, detachedSpawnOpts } = require('./lib/killTree.cjs');
85
86
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
86
87
  const { createBroadcastCoalescer } = require('./lib/broadcastCoalescer.cjs');
87
88
  const prdParser = require('./scheduler/prdParser.cjs');
@@ -846,6 +847,51 @@ async function computeCommittedDuringRun(cwd, headBefore, headAfter, startedAt,
846
847
  return module.exports.committedInWindow(cwd, startedAt, untilIso);
847
848
  }
848
849
 
850
+ /**
851
+ * Which commit does this run get credit for? A worktree-isolated run shares the
852
+ * main checkout with concurrent jobs, so a HEAD move there may be someone else's
853
+ * integration — credit comes only from this run's own integration result. An
854
+ * in-place run keeps the HEAD-delta / time-window behaviour.
855
+ */
856
+ function resolveRunCommitAttribution({
857
+ ranInWorktree, integratedSha, headBefore, headAtExit, committedInWindow,
858
+ rangeCommits, transcriptCommitShas, siblingOverlap,
859
+ }) {
860
+ if (ranInWorktree) {
861
+ const landedCommit = integratedSha || null;
862
+ return { landedCommit, committedDuringRun: Boolean(landedCommit) };
863
+ }
864
+ const shas = Array.isArray(transcriptCommitShas) ? transcriptCommitShas.filter(Boolean) : [];
865
+ if (shas.length === 0 && !siblingOverlap) {
866
+ const headMoved = Boolean(headBefore && headAtExit && headBefore !== headAtExit);
867
+ return {
868
+ landedCommit: headMoved ? headAtExit : null,
869
+ committedDuringRun: headMoved || Boolean(committedInWindow),
870
+ };
871
+ }
872
+ // Shared tree with possible siblings (or transcript evidence): credit only a
873
+ // commit in the range that this run's own transcript reported.
874
+ const own = (Array.isArray(rangeCommits) ? rangeCommits : [])
875
+ .find((c) => shas.some((sha) => c.startsWith(sha)));
876
+ const landedCommit = own || null;
877
+ return { landedCommit, committedDuringRun: Boolean(landedCommit) };
878
+ }
879
+
880
+ /** Full shas in `before..after` (newest first); [] on any error. Never throws. */
881
+ function gitRangeCommits(cwd, before, after) {
882
+ return new Promise((resolve) => {
883
+ if (!cwd || !before || !after || before === after) { resolve([]); return; }
884
+ execFile(
885
+ 'git', ['-C', cwd, 'rev-list', `${before}..${after}`],
886
+ { timeout: 10_000, windowsHide: true },
887
+ (err, out) => {
888
+ if (err) { resolve([]); return; }
889
+ resolve(String(out || '').split('\n').map((l) => l.trim()).filter(Boolean));
890
+ },
891
+ );
892
+ });
893
+ }
894
+
849
895
  /**
850
896
  * Read-only check: is a job's `sm-job/<slug>` worktree branch already fully
851
897
  * integrated into `cwd`'s current HEAD — i.e. every commit on the branch is
@@ -2769,6 +2815,8 @@ function killOrphanClaudePid(pid, recordedIdentity = null) {
2769
2815
  && isDifferentProcess(recordedIdentity, procIdentityOf(pid))) {
2770
2816
  return 'mismatch';
2771
2817
  }
2818
+ // win32 has no /proc or `ps`: the identity heuristic can't run, so never reap.
2819
+ if (process.platform === 'win32') return 'unknown';
2772
2820
  let cmdline = '';
2773
2821
  try {
2774
2822
  cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
@@ -2779,12 +2827,10 @@ function killOrphanClaudePid(pid, recordedIdentity = null) {
2779
2827
  } catch { return 'unknown'; }
2780
2828
  }
2781
2829
  if (!/\bclaude\b/.test(cmdline)) return 'mismatch';
2782
- try { process.kill(-pid, 'SIGTERM'); }
2783
- catch { try { process.kill(pid, 'SIGTERM'); } catch { /* race: died between checks */ } }
2830
+ killTree(pid, 'SIGTERM');
2784
2831
  setTimeout(() => {
2785
2832
  try { process.kill(pid, 0); } catch { return; /* already gone */ }
2786
- try { process.kill(-pid, 'SIGKILL'); }
2787
- catch { try { process.kill(pid, 'SIGKILL'); } catch { /* race */ } }
2833
+ killTree(pid, 'SIGKILL');
2788
2834
  }, 5000).unref?.();
2789
2835
  return 'killed';
2790
2836
  }
@@ -6302,10 +6348,10 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
6302
6348
  ...(jobSpawn.argv0 ? { argv0: jobSpawn.argv0 } : {}),
6303
6349
  // detached:true puts the child in its own process group so we can kill
6304
6350
  // the entire descendant tree (including any stray background bashes the
6305
- // agent spawned) with `process.kill(-pid)`. Without this, child.kill()
6351
+ // agent spawned) with killTree(pid). Without this, child.kill()
6306
6352
  // only kills the immediate `claude` process, leaving orphaned subprocs
6307
6353
  // that keep the parent alive (the 2026-05-10 cellar-publish hang).
6308
- detached: true,
6354
+ ...detachedSpawnOpts(),
6309
6355
  },
6310
6356
  },
6311
6357
  watchdogs: [resultTailWatchdog, deadmanWatchdog, idleTailWatchdog, budgetWatchdog],
@@ -7249,6 +7295,7 @@ async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPat
7249
7295
  let worktreeIntegrationDetail = null;
7250
7296
  let mergeAutoResolved = null;
7251
7297
  let mergeAutoResolvedPaths = null;
7298
+ let integratedSha = null;
7252
7299
  try {
7253
7300
  worktreeLeftoverDirty = (await uncommitted(worktree.dir)) || [];
7254
7301
  // Salvage the worktree's full diff (tracked + untracked) to the run
@@ -7273,6 +7320,7 @@ async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPat
7273
7320
  worktreeIntegrationDetail = integration;
7274
7321
  console.error(`[scheduler] ${job.slug}: worktree branch integration FAILED (${integration.reason}) — branch ${worktree.branch} preserved in ${guardCwd} for manual recovery`);
7275
7322
  } else if (integration.integrated) {
7323
+ integratedSha = integration.sha || null;
7276
7324
  console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} integrated into ${guardCwd}${integration.mergeCommit ? ' (merge commit)' : ' (fast-forward)'}`);
7277
7325
  if (integration.autoResolved) {
7278
7326
  mergeAutoResolved = integration.autoResolved;
@@ -7294,7 +7342,7 @@ async function finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPat
7294
7342
  await jw.cleanupJobWorktree({ cwd: guardCwd, dir: worktree.dir, branch: worktree.branch, keepBranch: true });
7295
7343
  } catch { /* already reported above */ }
7296
7344
  }
7297
- return { worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths };
7345
+ return { worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha };
7298
7346
  }
7299
7347
 
7300
7348
  /**
@@ -7686,6 +7734,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
7686
7734
  // self-healed rather than silently looking like an ordinary merge.
7687
7735
  let mergeAutoResolved = null;
7688
7736
  let mergeAutoResolvedPaths = null;
7737
+ let integratedSha = null;
7689
7738
  // A job's uncommitted-work patch, whichever isolation mode produced it —
7690
7739
  // set by EITHER branch below, never both (worktree.ok picks exactly one
7691
7740
  // shape for the whole run). Named generically (not "worktree...") because
@@ -7721,7 +7770,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
7721
7770
  }, reviewInRun);
7722
7771
  } finally {
7723
7772
  if (worktree.ok) {
7724
- ({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths } =
7773
+ ({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha } =
7725
7774
  await finalizeJobWorktree({ job, runDir, worktree, guardCwd, carriedPaths }));
7726
7775
  } else {
7727
7776
  // In-place run (non-git cwd, cap reached, env-disabled, or a carry-over
@@ -7873,15 +7922,50 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
7873
7922
  // Used by the sentinel override: SCHEDULER_VERDICT: PASS + a landed
7874
7923
  // commit together override incidental transcript noise verdicts.
7875
7924
  const headAtExit = await gitHead(guardCwd);
7876
- const committedDuringRun = await computeCommittedDuringRun(
7877
- guardCwd,
7878
- guardHeadBefore,
7925
+ // Worktree runs: attribution comes only from this run's own integration
7926
+ // (a concurrent job's commit moving shared HEAD must not count).
7927
+ const committedInWindow = worktree.ok
7928
+ ? false
7929
+ : await computeCommittedDuringRun(
7930
+ guardCwd,
7931
+ guardHeadBefore,
7932
+ headAtExit,
7933
+ job.startedAt,
7934
+ new Date().toISOString(),
7935
+ );
7936
+ let rangeCommits = [];
7937
+ let transcriptCommitShas = [];
7938
+ let siblingOverlap = false;
7939
+ if (!worktree.ok) {
7940
+ rangeCommits = await gitRangeCommits(guardCwd, guardHeadBefore, headAtExit);
7941
+ transcriptCommitShas = parseLog(path.join(runDir, `${job.slug}.log`)).transcriptCommitShas || [];
7942
+ const overlapState = await readQueue().catch(() => ({ jobs: [] }));
7943
+ const myStart = Date.parse(job.startedAt) || 0;
7944
+ const myEnd = Date.now();
7945
+ siblingOverlap = (overlapState.jobs || []).some((j) => {
7946
+ if (j.slug === job.slug || (j.cwd || defaultCwd) !== guardCwd) return false;
7947
+ if (j.status === 'running') return true;
7948
+ const jStart = Date.parse(j.startedAt);
7949
+ const jEnd = Date.parse(j.finishedAt);
7950
+ if (!Number.isFinite(jStart) && !Number.isFinite(jEnd)) return false;
7951
+ const s0 = Number.isFinite(jStart) ? jStart : jEnd;
7952
+ const e0 = Number.isFinite(jEnd) ? jEnd : jStart;
7953
+ return s0 <= myEnd && e0 >= myStart;
7954
+ });
7955
+ }
7956
+ const attribution = resolveRunCommitAttribution({
7957
+ ranInWorktree: Boolean(worktree.ok),
7958
+ integratedSha,
7959
+ headBefore: guardHeadBefore,
7879
7960
  headAtExit,
7880
- job.startedAt,
7881
- new Date().toISOString(),
7882
- );
7883
- if (guardHeadBefore && headAtExit && headAtExit !== guardHeadBefore) {
7884
- jobLandedCommitThisRun = headAtExit;
7961
+ committedInWindow,
7962
+ rangeCommits,
7963
+ transcriptCommitShas,
7964
+ siblingOverlap,
7965
+ });
7966
+ const committedDuringRun = attribution.committedDuringRun;
7967
+ if (attribution.landedCommit) {
7968
+ jobLandedCommitThisRun = attribution.landedCommit;
7885
7969
  }
7886
7970
 
7887
7971
  const prdPath = (await resolveVerifyPrdPath(job)) ?? archivedPrdPathForJob(job);
@@ -7955,7 +8039,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
7955
8039
  (j) => j.slug !== job.slug && j.status === 'running' && (j.cwd || defaultCwd) === guardCwd,
7956
8040
  );
7957
8041
  const guardHeadAfter = await gitHead(guardCwd);
7958
- const jobSelfCommitted = guardHeadBefore && guardHeadAfter && guardHeadAfter !== guardHeadBefore;
8042
+ const jobSelfCommitted = worktree.ok
8043
+ ? Boolean(jobLandedCommitThisRun)
8044
+ : guardHeadBefore && guardHeadAfter && guardHeadAfter !== guardHeadBefore;
7959
8045
  const guardVerdict = commitGuardVerdict({
7960
8046
  newlyDirty,
7961
8047
  siblingRunning,
@@ -9703,9 +9789,7 @@ function bootRowPid(j, logPathOf) {
9703
9789
  }
9704
9790
 
9705
9791
  function signalAdoptedGroup(pgid, signal, pid) {
9706
- try { process.kill(-pgid, signal); } catch {
9707
- try { process.kill(pid, signal); } catch { /* already dead */ }
9708
- }
9792
+ if (!killTree(pgid, signal)) killTree(pid, signal);
9709
9793
  }
9710
9794
 
9711
9795
  async function superviseAdoptedRunsPass(jobs) {
@@ -13595,6 +13679,7 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
13595
13679
  module.exports = {
13596
13680
  reportSchedulerError,
13597
13681
  finalizeJobWorktree,
13682
+ resolveRunCommitAttribution,
13598
13683
  worktreeIntegrationVerdict,
13599
13684
  guardedTick,
13600
13685
  rescheduleIntervalTick,
@@ -38,6 +38,7 @@ const { spawn } = require('node:child_process');
38
38
  const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
39
39
  const { cleanChildEnv, pathWithUserBins } = require('./lib/cleanEnv.cjs');
40
40
  const { writeJsonSync } = require('./config.cjs');
41
+ const { nodeSpawnSpec } = require('./lib/appRuntime.cjs');
41
42
 
42
43
  const SERVER_NAME = 'session-manager-scheduler';
43
44
  const MAX_ATTEMPTS = 3; // give a transient first-boot failure a few chances
@@ -102,6 +103,14 @@ function scriptPathMissing() {
102
103
  }
103
104
  }
104
105
 
106
+ /** argv for `claude mcp add`: system `node` in dev/npx, the app binary with
107
+ * ELECTRON_RUN_AS_NODE=1 when packaged (an installer user may have no Node). */
108
+ function mcpAddArgv(serverPath) {
109
+ const spec = nodeSpawnSpec(serverPath);
110
+ const envFlags = Object.entries(spec.env).flatMap(([k, v]) => ['-e', `${k}=${v}`]);
111
+ return ['mcp', 'add', SERVER_NAME, '--scope', 'user', ...envFlags, '--', spec.command, ...spec.args];
112
+ }
113
+
105
114
  /** Run `claude mcp add session-manager-scheduler --scope user -- node <serverPath>`.
106
115
  * No shell:true — argv array passed directly to spawn. */
107
116
  function runClaudeMcpAdd(serverPath) {
@@ -111,7 +120,7 @@ function runClaudeMcpAdd(serverPath) {
111
120
  try {
112
121
  proc = spawn(
113
122
  target.command,
114
- ['mcp', 'add', SERVER_NAME, '--scope', 'user', '--', 'node', serverPath],
123
+ mcpAddArgv(serverPath),
115
124
  {
116
125
  ...(target.argv0 ? { argv0: target.argv0 } : {}),
117
126
  cwd: os.homedir(),
@@ -200,4 +209,4 @@ async function seedSchedulerMcp({ logger = console, addFn = runClaudeMcpAdd, rem
200
209
  }
201
210
  }
202
211
 
203
- module.exports = { seedSchedulerMcp, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
212
+ module.exports = { seedSchedulerMcp, mcpAddArgv, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
@@ -244,7 +244,10 @@ async function networkFetchUsage() {
244
244
 
245
245
  if (result.kind === 'ok') {
246
246
  cache = { data: result.data, fetchedAt: Date.now(), sourceCredsExpiresAt: result.data.credentialsExpiresAt };
247
- persistCache(cache).catch(() => {});
247
+ // Awaited so fetchUsage() never resolves while this write is still
248
+ // in-flight — a fire-and-forget write here outlived its test's afterEach
249
+ // rmSync of HOME, causing a flaky ENOTEMPTY under CPU load.
250
+ await persistCache(cache).catch(() => {});
248
251
  retryNotBeforeMs = 0;
249
252
  lastRateLimitedResult = null;
250
253
  circuit.recordSuccess(result.data);
@@ -1,5 +1,15 @@
1
1
  import type { PromptSession } from '../renderer/state/promptSessions';
2
2
 
3
+ /** Mirrors the item shape returned by main/lib/prereqs.cjs checkPrereqs(). */
4
+ export interface PrereqItem {
5
+ id: string;
6
+ label: string;
7
+ ok: boolean;
8
+ version: string | null;
9
+ detail: string | null;
10
+ fix: { command: string; shell: 'sh' | 'powershell'; url?: string } | null;
11
+ }
12
+
3
13
  export interface ActionOutcome {
4
14
  ok: boolean;
5
15
  kind?: 'info' | 'warn' | 'error';
@@ -1550,6 +1560,10 @@ export interface SessionManagerAPI {
1550
1560
  /** Boot diagnostic — resolved claude binary path + whether it was found
1551
1561
  * on disk (false means spawn will rely on PATH and may ENOENT). */
1552
1562
  claudeBinStatus: () => Promise<{ resolved: string; foundOnDisk: boolean }>;
1563
+ /** Fresh prerequisite probe (git, claude CLI, auth, Git Bash on Windows). */
1564
+ prereqs: () => Promise<PrereqItem[]>;
1565
+ /** Opens the OS terminal running the item's official install command (looked up main-side by id). */
1566
+ prereqsRunFix: (id: string) => Promise<{ ok: true; opener: string } | { ok: false; error: string }>;
1553
1567
  /** Boot diagnostic — assertCwdInsideHome(os.homedir()) result. ok=false
1554
1568
  * on macOS symlinked-/Users mismatch and blocks all session spawns. */
1555
1569
  homeSelfCheck: () => Promise<{ ok: boolean; error?: string; realCwd?: string }>;
@@ -20,6 +20,8 @@ contextBridge.exposeInMainWorld('api', {
20
20
  // Boot diagnostics (v0.10.1) — renderer polls at mount to surface
21
21
  // missing-claude-bin / home-symlink-mismatch as toasts.
22
22
  claudeBinStatus: () => ipcRenderer.invoke('app:claude-bin-status'),
23
+ prereqs: () => ipcRenderer.invoke('app:prereqs'),
24
+ prereqsRunFix: (id) => ipcRenderer.invoke('app:prereqs-run-fix', id),
23
25
  homeSelfCheck: () => ipcRenderer.invoke('app:home-self-check'),
24
26
  seedStatus: () => ipcRenderer.invoke('app:seed-status'),
25
27
  delegationReadiness: (cwd) => ipcRenderer.invoke('app:delegation-readiness', { cwd }),
@@ -4,14 +4,14 @@ description: Validates a finished PLAN (the PRDs its validate PRD lists) once, a
4
4
  tools: Read, Grep, Glob, Bash
5
5
  model: sonnet
6
6
  title: Engineering — Plan Validator
7
- seedVersion: 2
7
+ seedVersion: 3
8
8
  ---
9
9
 
10
10
  You are validator. You judge whether a plan's PRDs actually landed what they promised. You do not fix anything, you do not re-implement, you do not queue PRDs — your output is evidence and verdicts, read by the architect who owns the plan.
11
11
 
12
12
  ## Inputs
13
13
 
14
- Your PRD's `# Acceptance criteria` lists the plan's PRD slugs and where each PRD file lives (`session-manager-operations/scheduler/epics/<epic>/prds/` while queued, `prds-archived/` once terminal — locate by slug in either). Everything else comes from the working tree and `git log`.
14
+ Your PRD's `# Acceptance criteria` lists the plan's PRD slugs. PRD files live under `$SM_PROJECT_ROOT/session-manager-operations/scheduler/epics/<epic>/` — `prds/` while queued, `prds-archived/` (or a timestamped subfolder of it) once terminal; find by slug, e.g. `find "$SM_PROJECT_ROOT/session-manager-operations/scheduler/epics/<epic>" -name '*<slug>.md'`. This folder is gitignored, so it is never present in the job worktree — don't look for it there. If a PRD file is missing, fall back to `$SM_PROJECT_ROOT/session-manager-operations/scheduler/state/queue.json` / `history.jsonl` for that slug's `landedCommit`, and record the missing file as a Minor (not Important) finding. Everything else comes from the working tree and `git log`.
15
15
 
16
16
  ## Procedure (in this order)
17
17
 
@@ -1 +0,0 @@
1
- import{ax as d,i as j,aj as $,a2 as g,ai as e,v as E,m as N,al as S,a3 as b,aB as T,a as p,_ as D,j as v,E as R}from"./index-DbR91Z5j.js";import{V}from"./ViewTabs-BQ_HbQFq.js";import"./monaco-editor-BW5C4Iv1.js";const _=[{key:"diagram",label:"Diagram"},{key:"tables",label:"Tables"}];function C(){const[s,t]=d.useState("diagram"),[a,i]=d.useState(null),[l,n]=d.useState(()=>new Set),c=d.useMemo(()=>j.filter(r=>!l.has(r.group)),[l]),m=d.useMemo(()=>$(c),[c]),o=r=>{n(x=>{const h=new Set(x);return h.has(r)?h.delete(r):h.add(r),h}),i(null)},u=a?g(a):null,y=r=>c.some(x=>x.id===r);return e.jsx(E,{toolbar:e.jsxs(e.Fragment,{children:[e.jsx(V,{options:_,active:s,onChange:t}),e.jsx(G,{hiddenGroups:l,onToggle:o}),e.jsxs("span",{className:"ml-auto text-fg-faint",children:[j.length," entities"]})]}),children:s==="diagram"?e.jsxs("div",{className:"h-full flex",children:[e.jsx("div",{className:"flex-1 min-w-0 overflow-auto",children:e.jsx(M,{layout:m,selectedId:a,onSelect:r=>i(x=>x===r?null:r)})}),e.jsx("div",{className:"w-96 shrink-0 border-l border-line overflow-y-auto",children:u?e.jsx(B,{entity:u,onSelectRelated:r=>y(r)?i(r):void 0}):e.jsx(N,{title:"select an entity",hint:"click a box in the diagram to see its storage, fields, and relations"})})]}):e.jsx(F,{})})}const L=d.memo(C);function G({hiddenGroups:s,onToggle:t}){return e.jsx("div",{className:"flex items-center gap-1.5 flex-wrap",children:v.map(a=>{const i=s.has(a.id);return e.jsxs("button",{onClick:()=>t(a.id),title:`${a.blurb} (${a.realm})`,className:`flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10.5px] ${i?"border-line text-fg-faint opacity-50":"border-line text-fg-dim hover:text-fg"}`,children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full ${a.tone.dot}`}),a.label,e.jsx("span",{className:"text-fg-faint",children:a.realm})]},a.id)})})}function M({layout:s,selectedId:t,onSelect:a}){const i=d.useMemo(()=>t?new Set(S(t)):null,[t]),l=n=>!!t&&(n.relation.from===t||n.relation.to===t);return s.boxes.length===0?e.jsx(N,{title:"no entities in view",hint:"toggle a group in the legend to show its entities"}):e.jsxs("div",{className:"relative p-4",style:{width:s.width+32,height:s.height+32},children:[e.jsxs("svg",{width:s.width,height:s.height,className:"absolute left-4 top-4 overflow-visible",children:[s.edges.map(n=>{const c=l(n);return e.jsx("path",{"data-testid":`erd-edge-${n.relation.from}-${n.relation.to}`,d:n.d,fill:"none",className:c?"stroke-accent":"stroke-line",strokeWidth:c?2:1,opacity:t&&!c?.25:1},`${n.relation.from}-${n.relation.to}-${n.relation.label}`)}),s.edges.map(n=>l(n)?e.jsx("text",{x:n.mx,y:n.my,textAnchor:"middle",className:"fill-fg-dim text-[10px]",children:n.relation.label},`label-${n.relation.from}-${n.relation.to}-${n.relation.label}`):null)]}),s.boxes.map(n=>e.jsx(O,{box:n,selected:n.id===t,dimmed:!!t&&n.id!==t&&!(i!=null&&i.has(n.id)),onClick:()=>a(n.id)},n.id))]})}function O({box:s,selected:t,dimmed:a,onClick:i}){const l=g(s.id),n=b(s.group),c=l.fields.slice(0,R),m=l.fields.length-c.length;return e.jsxs("button",{type:"button","data-testid":`erd-box-${s.id}`,onClick:i,className:`absolute text-left rounded border overflow-hidden bg-bg-hi ${t?"border-accent ring-1 ring-accent":"border-line"} ${a?"opacity-30":""}`,style:{left:s.x,top:s.y,width:s.w,height:s.h},children:[e.jsxs("div",{className:"px-2 py-1 border-b border-line flex items-center gap-1.5",children:[e.jsx("span",{className:`w-1.5 h-1.5 rounded-full shrink-0 ${n.tone.dot}`}),e.jsx("span",{className:"text-[11px] font-semibold text-fg truncate",children:l.name})]}),l.aka&&e.jsx("div",{className:"px-2 pt-0.5 text-[9.5px] text-fg-faint truncate",children:l.aka}),e.jsxs("div",{className:"px-2 py-0.5",children:[c.map(o=>e.jsxs("div",{className:"text-[9.5px] font-mono text-fg-dim truncate flex items-center gap-1",children:[o.key&&e.jsx("span",{className:"text-fg-faint",children:o.key}),e.jsx("span",{className:"truncate",children:o.name})]},o.name)),m>0&&e.jsxs("div",{className:"text-[9.5px] text-fg-faint italic",children:["+",m," more"]})]})]})}function w({fields:s}){return e.jsxs("table",{className:"w-full text-[11px] font-mono",children:[e.jsx("thead",{className:"text-fg-faint uppercase tracking-wide text-[9.5px]",children:e.jsxs("tr",{className:"border-b border-line",children:[e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"name"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"type"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"key"}),e.jsx("th",{className:"text-left font-normal px-2 py-1",children:"note"})]})}),e.jsx("tbody",{children:s.map(t=>e.jsxs("tr",{className:"border-b border-line/50",children:[e.jsxs("td",{className:"px-2 py-1 text-fg",children:[t.name,t.optional&&e.jsx("span",{className:"text-fg-faint",children:"?"})]}),e.jsx("td",{className:"px-2 py-1 text-fg-dim",children:t.type}),e.jsx("td",{className:"px-2 py-1",children:t.key&&e.jsxs(p,{tone:t.key==="pk"?"accent":"dim",children:[t.key,t.ref?` → ${t.ref}`:""]})}),e.jsx("td",{className:"px-2 py-1 text-fg-faint",children:t.note})]},t.name))})]})}function k({entity:s}){return e.jsxs("div",{className:"space-y-1 text-[11px]",children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"path"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.path})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"format"}),e.jsx("span",{className:"font-mono text-fg-dim",children:s.store.format})]}),s.store.writer&&e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"writer"}),e.jsxs(p,{tone:"accent",children:["single-writer: ",s.store.writer]})]}),e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:"text-fg-faint w-20 shrink-0",children:"definedIn"}),e.jsx("span",{className:"font-mono text-fg-dim break-all",children:s.store.definedIn})]})]})}function B({entity:s,onSelectRelated:t}){const a=b(s.group),i=T(s.id);return e.jsxs("div",{className:"p-3 space-y-3",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${a.tone.dot}`}),e.jsx("span",{className:"text-sm font-semibold text-fg",children:s.name})]}),s.aka&&e.jsx("div",{className:"text-[10.5px] text-fg-faint",children:s.aka}),e.jsx(p,{tone:"dim",className:"mt-1",children:a.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:s.summary}),e.jsxs("div",{children:[e.jsx(f,{children:"storage"}),e.jsx(k,{entity:s})]}),e.jsxs("div",{children:[e.jsx(f,{children:"fields"}),e.jsx(w,{fields:s.fields})]}),s.notes&&s.notes.length>0&&e.jsxs("div",{children:[e.jsx(f,{children:"notes"}),e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:s.notes.map(l=>e.jsx("li",{children:l},l))})]}),e.jsxs("div",{children:[e.jsx(f,{children:"relations"}),i.length===0?e.jsx("div",{className:"text-xs text-fg-faint italic",children:"no relations"}):e.jsx("ul",{className:"space-y-1",children:i.map(l=>{const n=l.from===s.id?l.to:l.from,c=g(n);return e.jsxs("li",{className:"flex items-center gap-1.5 text-xs",children:[e.jsx("span",{className:"font-mono text-fg-faint w-10 shrink-0",children:D(l.cardinality)}),e.jsx("span",{className:"text-fg-dim",children:l.label}),l.via&&e.jsxs("span",{className:"font-mono text-[10px] text-fg-faint",children:["via ",l.via]}),e.jsx("button",{type:"button",onClick:()=>t(n),className:"ml-auto text-accent hover:underline font-mono",children:c.name})]},`${l.from}-${l.to}-${l.label}`)})})]})]})}function f({children:s}){return e.jsx("div",{className:"text-[10.5px] uppercase tracking-wide text-fg-faint mb-1 font-mono",children:s})}function F(){return e.jsx("div",{className:"p-4 space-y-6",children:v.map(s=>{const t=j.filter(a=>a.group===s.id);return t.length===0?null:e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-1.5 mb-2",children:[e.jsx("span",{className:`w-2 h-2 rounded-full ${s.tone.dot}`}),e.jsx("span",{className:"text-xs font-semibold text-fg",children:s.label}),e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:s.realm})]}),e.jsx("div",{className:"grid grid-cols-1 xl:grid-cols-2 gap-3",children:t.map(a=>e.jsxs("div",{className:"border border-line rounded bg-bg-hi p-3 space-y-2",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-sm font-semibold text-fg",children:a.name}),a.aka&&e.jsx("span",{className:"text-[10.5px] text-fg-faint",children:a.aka})]}),e.jsx(p,{tone:"dim",className:"mt-1",children:s.label})]}),e.jsx("div",{className:"text-xs text-fg-dim leading-relaxed",children:a.summary}),e.jsx(k,{entity:a}),e.jsx(w,{fields:a.fields}),a.notes&&a.notes.length>0&&e.jsx("ul",{className:"list-disc pl-4 space-y-0.5 text-xs text-fg-dim",children:a.notes.map(i=>e.jsx("li",{children:i},i))})]},a.id))})]},s.id)})})}export{L as DataModel};