specrails-desktop 2.34.0 → 2.35.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 (209) hide show
  1. package/client/dist/assets/ActivityFeedPage-LCBJ5WiD.js +1 -0
  2. package/client/dist/assets/AgentBrowserCapture-B-xQpyZr.js +1 -0
  3. package/client/dist/assets/AgentModeAnalyticsPane-BI1Vcf3T.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-BTRwCl9h.js +2 -0
  5. package/client/dist/assets/AgentModeJobsPane-VzeBJLqP.js +1 -0
  6. package/client/dist/assets/AgentsPage-CJj1K24O.js +87 -0
  7. package/client/dist/assets/AnalyticsPage-CQpV5_fX.js +1 -0
  8. package/client/dist/assets/CodePage-9-tFh0jB.js +2 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-BcZ0_O_C.js +1 -0
  10. package/client/dist/assets/DocsDialog-BPAzDICW.js +11 -0
  11. package/client/dist/assets/DocsPage-Dqo4Hn6G.js +11 -0
  12. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +1 -0
  13. package/client/dist/assets/InteractiveJobComposer-D36fZWai.js +19 -0
  14. package/client/dist/assets/JobDetailModal-BxsktbkQ.js +1 -0
  15. package/client/dist/assets/JobDetailPage-WWyCy6Sp.js +1 -0
  16. package/client/dist/assets/JobsPage-CEbM7uxU.js +1 -0
  17. package/client/dist/assets/{LoopBuilderPage-WeiofeL6.js → LoopBuilderPage-CYsSN8io.js} +2 -2
  18. package/client/dist/assets/LoopPreviewModal-BJhgX5wY.js +1 -0
  19. package/client/dist/assets/LoopsPage-DB4OqMot.js +1 -0
  20. package/client/dist/assets/MinimizedChatsContext-CcjBHMHd.js +1 -0
  21. package/client/dist/assets/PluginsPage-CR8K4nM_.js +2 -0
  22. package/client/dist/assets/{ProjectSettingsDialog-BkCXBQWk.js → ProjectSettingsDialog-C45-qhF4.js} +1 -1
  23. package/client/dist/assets/ReviewPacketPage-Cz_QQeC3.js +1 -0
  24. package/client/dist/assets/TemplatePreviewModal-wFBd2vQP.js +1 -0
  25. package/client/dist/assets/TicketDetailModal-BvZoyDAL.js +14 -0
  26. package/client/dist/assets/{Trans-CTqw6rr9.js → Trans-DXZ-BLXb.js} +1 -1
  27. package/client/dist/assets/agent-BQgKv_2F.js +1 -0
  28. package/client/dist/assets/agent-C1NRDJqL.js +1 -0
  29. package/client/dist/assets/agent-CLNleiy6.js +1 -0
  30. package/client/dist/assets/agent-CmceArkQ.js +1 -0
  31. package/client/dist/assets/agent-D3UxQ3SR.js +1 -0
  32. package/client/dist/assets/agent-DB1JeE64.js +1 -0
  33. package/client/dist/assets/agent-DXkHcQ-D.js +1 -0
  34. package/client/dist/assets/agent-DjJJJ2M8.js +1 -0
  35. package/client/dist/assets/ban-E4EaZe3a.js +1 -0
  36. package/client/dist/assets/bot-DzuuuC5R.js +1 -0
  37. package/client/dist/assets/{browser-DxP-alBK.js → browser-BgqHDMXP.js} +1 -1
  38. package/client/dist/assets/{browser-CyR1Kp3m.js → browser-CsKJtrpv.js} +1 -1
  39. package/client/dist/assets/browser-Cu58EpZU.js +1 -0
  40. package/client/dist/assets/{browser-rRCwi7Y7.js → browser-CvXOJm-j.js} +1 -1
  41. package/client/dist/assets/{browser-CvgDySd9.js → browser-DOrFh1c7.js} +1 -1
  42. package/client/dist/assets/{browser-DwZljcRh.js → browser-DQv_Jo7x.js} +1 -1
  43. package/client/dist/assets/{browser-C25DkQiO.js → browser-LPgxvJu2.js} +1 -1
  44. package/client/dist/assets/{browser-CiJ7-L32.js → browser-eL-3MIEI.js} +1 -1
  45. package/client/dist/assets/button-CJqFnUOL.js +1 -0
  46. package/client/dist/assets/{chunk-KS7C4IRE-Cx5sqopX.js → chunk-KS7C4IRE-DEKu087k.js} +1 -1
  47. package/client/dist/assets/{commands-DyUZQ1FI.js → commands-6tYQih5m.js} +1 -1
  48. package/client/dist/assets/{commands-B9RZO4qS.js → commands-BK2QinXN.js} +1 -1
  49. package/client/dist/assets/{commands-Bbxu8kLU.js → commands-C2Nca76h.js} +1 -1
  50. package/client/dist/assets/{commands-CDU3qv68.js → commands-CKdLihNX.js} +1 -1
  51. package/client/dist/assets/{commands-DTQBF_q5.js → commands-CYIBTAlc.js} +1 -1
  52. package/client/dist/assets/{commands-hOtKuOXc.js → commands-CjUtYJIM.js} +1 -1
  53. package/client/dist/assets/{commands-BEwbAjtD.js → commands-Cq_u_8y9.js} +1 -1
  54. package/client/dist/assets/{commands-CzezgtQj.js → commands-D_ZHYQug.js} +1 -1
  55. package/client/dist/assets/core-B3mKBIh5.js +1 -0
  56. package/client/dist/assets/dashboard-B9GhC5ku.js +1 -0
  57. package/client/dist/assets/dashboard-C8WOEiPu.js +1 -0
  58. package/client/dist/assets/dashboard-CBnv6zOI.js +1 -0
  59. package/client/dist/assets/dashboard-CaP0fU87.js +1 -0
  60. package/client/dist/assets/dashboard-D4ZZ4x84.js +1 -0
  61. package/client/dist/assets/dashboard-DmuTQBX8.js +1 -0
  62. package/client/dist/assets/dashboard-DynLYj3d.js +1 -0
  63. package/client/dist/assets/dashboard-zj4gGFJ1.js +1 -0
  64. package/client/dist/assets/dialog-CdWCb02j.js +45 -0
  65. package/client/dist/assets/dist-js-CWeUdf5Z.js +1 -0
  66. package/client/dist/assets/dist-js-Rlh4FLH5.js +1 -0
  67. package/client/dist/assets/event-Bt7f3wXe.js +1 -0
  68. package/client/dist/assets/file-text-BcLCfcWZ.js +1 -0
  69. package/client/dist/assets/{formatDistanceToNow-BOuXgebD.js → formatDistanceToNow-C0ImhVKp.js} +1 -1
  70. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EjGOpn4b.js → getTimezoneOffsetInMilliseconds-owhDdTXs.js} +1 -1
  71. package/client/dist/assets/git-refresh-wTW0rO0Z.js +1 -0
  72. package/client/dist/assets/i18n-DwgvwTnx.js +2 -0
  73. package/client/dist/assets/index-DhjjFyU5.js +66 -0
  74. package/client/dist/assets/index-xBFsOEiJ.css +2 -0
  75. package/client/dist/assets/jira-api-CcZMcEN5.js +1 -0
  76. package/client/dist/assets/loops-B7YpGizO.js +1 -0
  77. package/client/dist/assets/{loops-CMMQOw2P.js → loops-BomD2J-h.js} +1 -1
  78. package/client/dist/assets/{loops-B2_97D26.js → loops-BrQHSQw3.js} +1 -1
  79. package/client/dist/assets/{loops-BwxCndZk.js → loops-BvBwvTPA.js} +1 -1
  80. package/client/dist/assets/{loops-BISOkuCu.js → loops-CODk6Slz.js} +1 -1
  81. package/client/dist/assets/{loops-CD19kEzq.js → loops-DC6k-Ym-.js} +1 -1
  82. package/client/dist/assets/{loops-aEb4RxN2.js → loops-DSEg8-uY.js} +1 -1
  83. package/client/dist/assets/{loops-Bo6w_peZ.js → loops-DhCoXD_v.js} +1 -1
  84. package/client/dist/assets/narration-4Z2VUhZw.js +1 -0
  85. package/client/dist/assets/narration-BGuiEDj3.js +1 -0
  86. package/client/dist/assets/narration-BuSoqRmv.js +1 -0
  87. package/client/dist/assets/narration-CB9t29fy.js +1 -0
  88. package/client/dist/assets/narration-CU3x8FHD.js +1 -0
  89. package/client/dist/assets/narration-CWeXhfP5.js +1 -0
  90. package/client/dist/assets/narration-DRaDnhT5.js +1 -0
  91. package/client/dist/assets/narration-DtEbQgCw.js +1 -0
  92. package/client/dist/assets/packet-B1IxaL5f.js +1 -0
  93. package/client/dist/assets/packet-Bh5kkbiB.js +1 -0
  94. package/client/dist/assets/packet-By8fqYxW.js +1 -0
  95. package/client/dist/assets/packet-CZdvA44E.js +1 -0
  96. package/client/dist/assets/packet-CrjHRQCP.js +1 -0
  97. package/client/dist/assets/packet-Dwi2pTtx.js +1 -0
  98. package/client/dist/assets/packet-_-ZIa0IR.js +1 -0
  99. package/client/dist/assets/packet-n0W6E1Hn.js +1 -0
  100. package/client/dist/assets/save-B6wGWwBR.js +1 -0
  101. package/client/dist/assets/settings-Bd0x4TBa.js +1 -0
  102. package/client/dist/assets/settings-C1-7cXzC.js +1 -0
  103. package/client/dist/assets/settings-CAFaT38b.js +1 -0
  104. package/client/dist/assets/settings-Ct3v1nW2.js +1 -0
  105. package/client/dist/assets/settings-DHh7_IF5.js +1 -0
  106. package/client/dist/assets/settings-DQXAmdcv.js +1 -0
  107. package/client/dist/assets/settings-guzlhpup.js +1 -0
  108. package/client/dist/assets/settings-sZfTPMEH.js +1 -0
  109. package/client/dist/assets/{spending-BpYPsevQ.js → spending-BQX3P7Cq.js} +1 -1
  110. package/client/dist/assets/useDesktop-BzX769yH.js +1 -0
  111. package/client/dist/index.html +29 -31
  112. package/docs/internals/embedded-browser-native-webview-evaluation.md +161 -0
  113. package/docs/internals/review-packet.md +212 -0
  114. package/package.json +1 -1
  115. package/server/dist/accept-ladder.js +61 -0
  116. package/server/dist/agent-chat-manager.js +22 -1
  117. package/server/dist/agent-defaults.js +359 -0
  118. package/server/dist/agent-operator-prompt.js +13 -5
  119. package/server/dist/browser-playwright.js +39 -2
  120. package/server/dist/db.js +38 -0
  121. package/server/dist/delivery-evidence.js +242 -0
  122. package/server/dist/desktop-router.js +23 -0
  123. package/server/dist/feature-flags.js +23 -0
  124. package/server/dist/loop-command-catalog.js +33 -0
  125. package/server/dist/loop-executors.js +12 -2
  126. package/server/dist/loop-factory.js +16 -1
  127. package/server/dist/loops-router.js +2 -0
  128. package/server/dist/mcp/tools/rails.js +16 -0
  129. package/server/dist/project-registry.js +33 -1
  130. package/server/dist/project-router-loop-runs.js +27 -2
  131. package/server/dist/providers/claude-adapter.js +60 -3
  132. package/server/dist/queue-manager.js +47 -4
  133. package/server/dist/rail-isolated-launch.js +119 -5
  134. package/server/dist/rail-pr-store.js +40 -3
  135. package/server/dist/rail-worktree-release.js +17 -1
  136. package/server/dist/rails-router.js +124 -3
  137. package/server/dist/review-packet.js +327 -0
  138. package/server/dist/revision-seed.js +73 -0
  139. package/server/dist/run-duration-stats.js +71 -0
  140. package/server/dist/stuck-run-detector.js +153 -0
  141. package/server/dist/telemetry-receiver.js +22 -0
  142. package/server/dist/worktree-node-modules.js +138 -16
  143. package/client/dist/assets/ActivityFeedPage-DIDxNsDj.js +0 -1
  144. package/client/dist/assets/AgentBrowserCapture-7pJ2G5VY.js +0 -1
  145. package/client/dist/assets/AgentModeAnalyticsPane-BSSyTnTM.js +0 -2
  146. package/client/dist/assets/AgentModeCodePane-DzW6gvDw.js +0 -2
  147. package/client/dist/assets/AgentModeJobsPane-9wJff15f.js +0 -1
  148. package/client/dist/assets/AgentsPage-CIA67psb.js +0 -87
  149. package/client/dist/assets/AnalyticsPage-C0LwYELz.js +0 -1
  150. package/client/dist/assets/CodePage-RtKvHmAO.js +0 -2
  151. package/client/dist/assets/DesktopAnalyticsPage-BU8Y_Ts1.js +0 -1
  152. package/client/dist/assets/DocsDialog-DIpoaGlQ.js +0 -11
  153. package/client/dist/assets/DocsPage-ME47KZpK.js +0 -11
  154. package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +0 -1
  155. package/client/dist/assets/InteractiveJobComposer-BGjIe9R6.js +0 -19
  156. package/client/dist/assets/JobDetailModal-1UZkqH4I.js +0 -1
  157. package/client/dist/assets/JobDetailPage-CN65sAf1.js +0 -1
  158. package/client/dist/assets/JobsPage-f-4dqys7.js +0 -1
  159. package/client/dist/assets/LoopPreviewModal-DDTnTyH0.js +0 -1
  160. package/client/dist/assets/LoopsPage-BVNchIex.js +0 -1
  161. package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +0 -1
  162. package/client/dist/assets/PluginsPage-CdFzGEfQ.js +0 -2
  163. package/client/dist/assets/TemplatePreviewModal-RDi0khFG.js +0 -1
  164. package/client/dist/assets/TicketDetailModal-MhJzrqbi.js +0 -14
  165. package/client/dist/assets/agent-Bd-q3Wqp.js +0 -1
  166. package/client/dist/assets/agent-BlvdW4Hv.js +0 -1
  167. package/client/dist/assets/agent-C-KQhxeM.js +0 -1
  168. package/client/dist/assets/agent-Ca15w5eu.js +0 -1
  169. package/client/dist/assets/agent-D6fKaoMd.js +0 -1
  170. package/client/dist/assets/agent-HFMYMTbB.js +0 -1
  171. package/client/dist/assets/agent-QIEbkBjg.js +0 -1
  172. package/client/dist/assets/agent-mTCsAbCl.js +0 -1
  173. package/client/dist/assets/api-B2M7CtDw.js +0 -1
  174. package/client/dist/assets/arrow-left-CpIR4j7G.js +0 -1
  175. package/client/dist/assets/arrow-up-right-9DoMn7fD.js +0 -1
  176. package/client/dist/assets/browser-CXUcnTMI.js +0 -1
  177. package/client/dist/assets/button-BUIIBQPT.js +0 -1
  178. package/client/dist/assets/chevron-right-CViMxucY.js +0 -1
  179. package/client/dist/assets/dashboard-BHRqsGCV.js +0 -1
  180. package/client/dist/assets/dashboard-Bfr90vOl.js +0 -1
  181. package/client/dist/assets/dashboard-CTsM8x3m.js +0 -1
  182. package/client/dist/assets/dashboard-CqC6wX0h.js +0 -1
  183. package/client/dist/assets/dashboard-CuZtAszc.js +0 -1
  184. package/client/dist/assets/dashboard-DAcqIc-n.js +0 -1
  185. package/client/dist/assets/dashboard-DekiV-86.js +0 -1
  186. package/client/dist/assets/dashboard-DoDczc5S.js +0 -1
  187. package/client/dist/assets/dialog-CzIekCw0.js +0 -45
  188. package/client/dist/assets/dist-Cez5CRkX.js +0 -1
  189. package/client/dist/assets/dist-js-BmsFWRly.js +0 -1
  190. package/client/dist/assets/dist-js-J5W_N5ng.js +0 -1
  191. package/client/dist/assets/external-link-BcU5EWZp.js +0 -1
  192. package/client/dist/assets/file-text-CFT-xXKN.js +0 -1
  193. package/client/dist/assets/i18n-nJQvD5zJ.js +0 -2
  194. package/client/dist/assets/index-C_sdsSZt.js +0 -63
  195. package/client/dist/assets/index-DCN5QbDm.css +0 -2
  196. package/client/dist/assets/jira-api-Bhixwn-d.js +0 -1
  197. package/client/dist/assets/loops-CnxqL8IL.js +0 -1
  198. package/client/dist/assets/save-28sIlCdr.js +0 -1
  199. package/client/dist/assets/settings-BveOJoaI.js +0 -1
  200. package/client/dist/assets/settings-Cj1C6F5k.js +0 -1
  201. package/client/dist/assets/settings-CuVwOpe0.js +0 -1
  202. package/client/dist/assets/settings-D5X4DfRc.js +0 -1
  203. package/client/dist/assets/settings-DXwxe-BW.js +0 -1
  204. package/client/dist/assets/settings-Di81Ba_V.js +0 -1
  205. package/client/dist/assets/settings-jbLjCYC_.js +0 -1
  206. package/client/dist/assets/settings-wIZNPFnn.js +0 -1
  207. package/client/dist/assets/useBrowserCaptureSession-BBXBN0Na.js +0 -1
  208. package/client/dist/assets/useDesktop-B6Du2N83.js +0 -1
  209. package/client/dist/assets/useSharedWebSocket-DK96ctb7.js +0 -1
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.claudeAdapter = void 0;
14
14
  exports._normaliseClaudeModel = normaliseModel;
15
+ exports._resolveClaudeSpawnModel = resolveSpawnModel;
15
16
  const child_process_1 = require("child_process");
16
17
  const path_1 = __importDefault(require("path"));
17
18
  const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
@@ -33,6 +34,7 @@ function normaliseModel(model) {
33
34
  return 'sonnet';
34
35
  case 'claude-fable-5':
35
36
  return 'fable';
37
+ case 'claude-opus-5':
36
38
  case 'claude-opus-4-8':
37
39
  case 'claude-opus-4-5':
38
40
  case 'claude-opus-4-1-20250805':
@@ -46,6 +48,24 @@ function normaliseModel(model) {
46
48
  return model || 'sonnet';
47
49
  }
48
50
  }
51
+ /**
52
+ * Catalog aliases whose GENERATION Specrails pins explicitly.
53
+ *
54
+ * The catalog value (`opus`) stays the stored, validated and displayed identity
55
+ * — profiles, project settings, conversation rows and analytics all keep using
56
+ * it, and `normaliseModel` collapses the pinned id back to it. Only the spawn
57
+ * argument is expanded, so picking "Claude Opus" is a product decision about
58
+ * WHICH Opus runs instead of delegating that to whatever generation the CLI's
59
+ * bare `opus` alias currently points at.
60
+ */
61
+ const PINNED_ALIAS_MODEL_IDS = {
62
+ opus: 'claude-opus-5',
63
+ };
64
+ /** Catalog value (or concrete id) in, spawn model id out. */
65
+ function resolveSpawnModel(model) {
66
+ const alias = normaliseModel(model);
67
+ return PINNED_ALIAS_MODEL_IDS[alias] ?? alias;
68
+ }
49
69
  /**
50
70
  * Common flags + the effective tool boundary for this spawn.
51
71
  *
@@ -95,7 +115,7 @@ function commonFlagsFor(opts) {
95
115
  }
96
116
  function buildClaudeArgs(action, opts) {
97
117
  const args = [];
98
- const model = normaliseModel(opts.model);
118
+ const model = resolveSpawnModel(opts.model);
99
119
  // Titles are unconditionally pure-output. Centralising the policy here keeps
100
120
  // both ChatManager and AgentChatManager from accidentally regaining tools.
101
121
  const commonFlags = commonFlagsFor(action === 'auto-title' ? { ...opts, toolPolicy: 'none' } : opts);
@@ -272,14 +292,51 @@ function parseClaudeStreamLine(line) {
272
292
  const tool = blocks.find((b) => b.type === 'tool_use');
273
293
  if (tool?.name) {
274
294
  const input = JSON.stringify(parsed.message?.content?.[0]?.input ?? {});
275
- return withAssistantUsage({ kind: 'tool-use', name: tool.name, inputPreview: input.slice(0, 200) }, msg);
295
+ return withAssistantUsage({
296
+ kind: 'tool-use',
297
+ name: tool.name,
298
+ inputPreview: input.slice(0, 200),
299
+ ...(typeof tool.id === 'string'
300
+ ? { toolUseId: tool.id }
301
+ : {}),
302
+ }, msg);
276
303
  }
277
304
  return withAssistantUsage({ kind: 'other', type, raw: parsed }, msg);
278
305
  }
279
306
  if (type === 'tool_use') {
280
307
  const name = parsed.name ?? '<unnamed>';
281
308
  const input = JSON.stringify(parsed.input ?? {});
282
- return { kind: 'tool-use', name, inputPreview: input.slice(0, 200) };
309
+ return {
310
+ kind: 'tool-use',
311
+ name,
312
+ inputPreview: input.slice(0, 200),
313
+ ...(typeof parsed.id === 'string' ? { toolUseId: parsed.id } : {}),
314
+ };
315
+ }
316
+ // `user`-role frames carry tool RESULTS back to the model. Surface a bounded
317
+ // text projection so activity surfaces (agent chat's execution log) can show
318
+ // what a tool returned; frames without a tool_result block stay 'other'.
319
+ if (type === 'user') {
320
+ const msg = parsed.message;
321
+ const blocks = Array.isArray(msg?.content) ? msg.content : [];
322
+ const resultBlock = blocks.find((b) => b.type === 'tool_result');
323
+ if (resultBlock) {
324
+ const content = resultBlock.content;
325
+ const text = typeof content === 'string'
326
+ ? content
327
+ : Array.isArray(content)
328
+ ? content
329
+ .filter((c) => c.type === 'text' && typeof c.text === 'string')
330
+ .map((c) => c.text)
331
+ .join('\n')
332
+ : '';
333
+ return {
334
+ kind: 'tool-result',
335
+ outputPreview: text.slice(0, 600),
336
+ ...(typeof resultBlock.tool_use_id === 'string' ? { toolUseId: resultBlock.tool_use_id } : {}),
337
+ ...(resultBlock.is_error === true ? { isError: true } : {}),
338
+ };
339
+ }
283
340
  }
284
341
  return { kind: 'other', type, raw: parsed };
285
342
  }
@@ -26,6 +26,7 @@ const result_event_1 = require("./result-event");
26
26
  const crypto_1 = require("crypto");
27
27
  const providers_1 = require("./providers");
28
28
  const runtime_1 = require("./providers/runtime");
29
+ const agent_defaults_1 = require("./agent-defaults");
29
30
  const codex_otel_bridge_1 = require("./codex-otel-bridge");
30
31
  const db_1 = require("./db");
31
32
  const interactive_job_session_1 = require("./interactive-job-session");
@@ -250,6 +251,7 @@ class QueueManager {
250
251
  * For codex it controls the catalog model used at spawn time and as the
251
252
  * fallback model name stamped onto the ai_invocations row. */
252
253
  _resolvedModel;
254
+ _agentDefaults;
253
255
  _onJobFinished;
254
256
  /** Project-owned durable effects that are part of every queue admission
255
257
  * (for example, recovery/rail ownership). Invoked after queued_jobs has
@@ -356,6 +358,7 @@ class QueueManager {
356
358
  this._getDesktopDailyBudget = options?.getDesktopDailyBudget ?? null;
357
359
  this._adapter = (0, providers_1.getAdapter)(options?.provider ?? 'claude');
358
360
  this._resolvedModel = options?.resolvedModel ?? null;
361
+ this._agentDefaults = options?.agentDefaults ?? null;
359
362
  this._onJobFinished = options?.onJobFinished ?? null;
360
363
  this._onJobAdmission = options?.onJobAdmission ?? null;
361
364
  this._onBudgetExceeded = options?.onBudgetExceeded ?? null;
@@ -2272,9 +2275,22 @@ class QueueManager {
2272
2275
  // Per-job model override (consumed once) takes precedence — used by the
2273
2276
  // freestyle model picker so the user can choose haiku/sonnet/opus per launch.
2274
2277
  const modelOverride = this._jobModelSelection.get(jobId);
2275
- const fallbackRailModel = adapter.id === 'claude' && this._db
2276
- ? (0, db_1.getProjectSettings)(this._db).orchestratorModel
2277
- : (this._resolvedModel ?? adapter.defaultModel());
2278
+ // Global Specrails Agents defaults (app Settings ▸ Specrails Agents) —
2279
+ // read AT SPAWN TIME (no restart) and layered BELOW every project-level
2280
+ // choice, ABOVE the built-in adapter default.
2281
+ let globalAgentDefaults = null;
2282
+ try {
2283
+ globalAgentDefaults = this._agentDefaults?.(adapter.id) ?? null;
2284
+ }
2285
+ catch {
2286
+ globalAgentDefaults = null;
2287
+ }
2288
+ const claudeProjectSettings = adapter.id === 'claude' && this._db ? (0, db_1.getProjectSettings)(this._db) : null;
2289
+ const fallbackRailModel = claudeProjectSettings
2290
+ ? (claudeProjectSettings.orchestratorModelExplicit
2291
+ ? claudeProjectSettings.orchestratorModel
2292
+ : globalAgentDefaults?.pipelineModel ?? claudeProjectSettings.orchestratorModel)
2293
+ : (this._resolvedModel ?? globalAgentDefaults?.pipelineModel ?? adapter.defaultModel());
2278
2294
  // Relocate-artifacts: when relocated, claude is spawned from the workspace
2279
2295
  // so add `--add-dir <repoDir>` so its tools can still reach repo files by
2280
2296
  // absolute path. (gemini/codex get env-only tweaks at spawn time below.)
@@ -2301,7 +2317,24 @@ class QueueManager {
2301
2317
  if (selection !== null && coreSupports) {
2302
2318
  // selection is string (explicit) or undefined (default resolution)
2303
2319
  const { resolveProfile, snapshotForJob, persistJobProfile, } = require('./profile-manager');
2304
- const resolved = resolveProfile(execution.cwd, selection ?? undefined, adapter.id);
2320
+ let resolved = resolveProfile(execution.cwd, selection ?? undefined, adapter.id);
2321
+ // Global agent defaults: fill per-agent model gaps in the resolved
2322
+ // profile, or synthesize a baseline profile when the project has
2323
+ // none. Explicit profile choices always win; inert when the global
2324
+ // layer is off (byte-identical legacy behaviour).
2325
+ if (globalAgentDefaults && adapter.capabilities.profiles === true) {
2326
+ if (resolved) {
2327
+ const merged = (0, agent_defaults_1.mergeProfileWithAgentDefaults)(resolved.profile, globalAgentDefaults);
2328
+ if (merged.changed)
2329
+ resolved = { name: resolved.name, profile: merged.profile };
2330
+ }
2331
+ else if (Object.keys(globalAgentDefaults.agentModels).length > 0) {
2332
+ resolved = {
2333
+ name: agent_defaults_1.GLOBAL_DEFAULTS_PROFILE_NAME,
2334
+ profile: (0, agent_defaults_1.synthesizeProfileFromDefaults)(adapter, globalAgentDefaults),
2335
+ };
2336
+ }
2337
+ }
2305
2338
  if (resolved) {
2306
2339
  profileSnapshotPath = snapshotForJob(this._projectSlug, jobId, resolved);
2307
2340
  profileName = resolved.name;
@@ -2322,10 +2355,19 @@ class QueueManager {
2322
2355
  // explicit per-job override > resolved profile orchestrator > project/default.
2323
2356
  // resolveProfile validates the profile model against this adapter's catalog.
2324
2357
  const railModel = modelOverride ?? profileOrchestratorModel ?? fallbackRailModel;
2358
+ // Pipeline-level reasoning effort from the global layer (rails never had a
2359
+ // per-job effort knob, so this is purely additive). Validated against the
2360
+ // FINAL model — claude/codex sub-agents inherit the process-level effort,
2361
+ // kimi rides its env knob via buildProviderEnv, gemini has no effort.
2362
+ const globalRailEffort = globalAgentDefaults?.pipelineEffort
2363
+ && (0, runtime_1.isReasoningEffortValidForModel)(adapter, railModel, globalAgentDefaults.pipelineEffort)
2364
+ ? globalAgentDefaults.pipelineEffort
2365
+ : undefined;
2325
2366
  const railSpawnOptions = {
2326
2367
  prompt: railPrompt,
2327
2368
  systemPrompt: systemAppend || undefined,
2328
2369
  model: railModel,
2370
+ ...(globalRailEffort ? { reasoning_effort: globalRailEffort } : {}),
2329
2371
  extraArgs: railExtraArgs.length > 0 ? railExtraArgs : undefined,
2330
2372
  };
2331
2373
  const args = adapter.buildArgs('rail-job', railSpawnOptions);
@@ -2544,6 +2586,7 @@ class QueueManager {
2544
2586
  // spawn and APPEND on top of the CLI default instead of replacing it.
2545
2587
  systemPrompt: isFreestyle ? (systemAppend || undefined) : undefined,
2546
2588
  model: railModel,
2589
+ ...(globalRailEffort ? { reasoning_effort: globalRailEffort } : {}),
2547
2590
  extraArgs: !isFreestyle && systemAppend
2548
2591
  ? ['--append-system-prompt', systemAppend, ...(railExtraArgs ?? [])]
2549
2592
  : (railExtraArgs.length > 0 ? railExtraArgs : undefined),
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.PrContinuationIsolationError = void 0;
37
+ exports.buildSpecSnapshot = buildSpecSnapshot;
37
38
  exports.launchIsolatedRail = launchIsolatedRail;
38
39
  exports.reconcileRailWorktrees = reconcileRailWorktrees;
39
40
  /**
@@ -70,6 +71,7 @@ const rail_worktrees_store_1 = require("./rail-worktrees-store");
70
71
  const pr_naming_1 = require("./pr-naming");
71
72
  const jira_db_1 = require("./jira/jira-db");
72
73
  const db_1 = require("./db");
74
+ const delivery_evidence_1 = require("./delivery-evidence");
73
75
  const integration_branch_1 = require("./integration-branch");
74
76
  const repo_lock_1 = require("./repo-lock");
75
77
  const rail_isolation_1 = require("./rail-isolation");
@@ -79,6 +81,7 @@ const rail_merge_orchestrator_1 = require("./rail-merge-orchestrator");
79
81
  const loop_executors_1 = require("./loop-executors");
80
82
  const worktree_overlay_1 = require("./worktree-overlay");
81
83
  const worktree_node_modules_1 = require("./worktree-node-modules");
84
+ const revision_seed_1 = require("./revision-seed");
82
85
  const workspace_resolution_1 = require("./workspace-resolution");
83
86
  const feature_flags_1 = require("./feature-flags");
84
87
  const file_provenance_1 = require("./file-provenance");
@@ -125,6 +128,66 @@ function unitNamingInput(ctx, ticketId) {
125
128
  }
126
129
  return { ticketId, title: spec?.title ?? null, labels: spec?.labels ?? null, jiraKey };
127
130
  }
131
+ /**
132
+ * Assemble the revision briefing from the generation being revised. Reads only
133
+ * durable rows, tolerates every one of them being absent (a legacy row carries
134
+ * no snapshot or evidence), and never throws — a thinner briefing is far better
135
+ * than a failed revision launch.
136
+ */
137
+ function buildRevisionSeedForLaunch(db, revision, ticketIds) {
138
+ try {
139
+ const previous = (0, rail_pr_store_1.getPrDelivery)(db, revision.ofDeliveryId);
140
+ const branches = previous
141
+ ? [...new Set(JSON.parse(previous.branches || '[]').map((b) => b.branch).filter(Boolean))]
142
+ : [];
143
+ // Revision depth walks the supersession chain, bounded so a corrupted
144
+ // lineage can never spin here.
145
+ let depth = 1;
146
+ let cursor = previous;
147
+ for (let hops = 0; hops < 20 && cursor?.revision_of; hops++) {
148
+ depth += 1;
149
+ cursor = (0, rail_pr_store_1.getPrDelivery)(db, cursor.revision_of) ?? undefined;
150
+ }
151
+ return (0, revision_seed_1.buildRevisionSeed)({
152
+ note: revision.note,
153
+ specSnapshot: previous ? (0, rail_pr_store_1.readSpecSnapshot)(previous.spec_snapshot) : null,
154
+ ticketIds: [...ticketIds],
155
+ branches,
156
+ baseBranch: previous?.base_branch ?? 'the integration branch',
157
+ branchDiffSummary: null,
158
+ evidence: previous ? (0, delivery_evidence_1.readSettleEvidence)(previous.settle_evidence) : null,
159
+ revisionNumber: depth,
160
+ });
161
+ }
162
+ catch {
163
+ // Absolute floor: the user's instruction still reaches the run.
164
+ return revision.note;
165
+ }
166
+ }
167
+ /**
168
+ * Launch-time freeze of every covered ticket for the delivery row's
169
+ * spec_snapshot column (migration 56). The review packet's "what you asked"
170
+ * section renders this snapshot, never the live store — a spec edited mid-run
171
+ * must not rewrite what the delivery was asked to do. Failure-tolerant: an
172
+ * unreadable ticket degrades to a bare-id entry.
173
+ */
174
+ function buildSpecSnapshot(ctx, ticketIds) {
175
+ return ticketIds.map((ticketId) => {
176
+ let spec;
177
+ try {
178
+ spec = ctx.getTicketSpec(ticketId);
179
+ }
180
+ catch {
181
+ /* tolerated — snapshot stays bare */
182
+ }
183
+ return {
184
+ ticketId,
185
+ title: spec?.title ?? null,
186
+ description: spec?.description ?? null,
187
+ labels: Array.isArray(spec?.labels) ? spec.labels : [],
188
+ };
189
+ });
190
+ }
128
191
  /**
129
192
  * Commit messages are consumed by GitHub↔Jira development panels. Branch names
130
193
  * already carry the Jira key for new work; commits must too, especially when
@@ -301,7 +364,7 @@ function branchRecords(results) {
301
364
  branchOwnership: result.run.branchOwnership,
302
365
  worktreePath: result.run.handle.worktreePath,
303
366
  overlayExcludes: result.run.overlayExcludes,
304
- overlayCleanupEvidence: result.run.overlayCleanupEvidence,
367
+ overlayCleanupEvidence: [...result.run.overlayCleanupEvidence, ...result.run.warmLinkEvidence],
305
368
  settlementIgnoredPaths: result.run.settlementIgnoredPaths ?? null,
306
369
  })));
307
370
  }
@@ -323,7 +386,18 @@ async function launchIsolatedRail(input, io = {}) {
323
386
  const baseRepo = ctx.project.path;
324
387
  const slug = ctx.project.slug;
325
388
  const worktreesRoot = path.join((0, artifact_registry_1.resolveHome)(), '.specrails', 'projects', slug, 'worktrees');
326
- const constants = (0, loop_constants_1.loadConstantMap)(ctx.desktopDb);
389
+ // A revision run is seeded with a full briefing (the instruction PLUS the
390
+ // frozen spec, the branch that carries the work and what the previous run
391
+ // actually reported) rather than the bare sentence — the run starts a FRESH
392
+ // provider session by contract, so durable context is the only thing it has.
393
+ // Per-RUN data, so it is layered over the project's constant map for this
394
+ // launch only and never persisted as a constant.
395
+ const constants = input.revision
396
+ ? {
397
+ ...(0, loop_constants_1.loadConstantMap)(ctx.desktopDb),
398
+ REVISION_REQUEST: buildRevisionSeedForLaunch(ctx.db, input.revision, ticketIds),
399
+ }
400
+ : (0, loop_constants_1.loadConstantMap)(ctx.desktopDb);
327
401
  // Capture the PR-delivery mode ONCE at launch entry so a mid-flight env flip
328
402
  // can never split one launch across the two delivery paths.
329
403
  const prMode = (0, rail_isolation_1.isRailPrDeliveryEnabled)();
@@ -582,9 +656,16 @@ async function launchIsolatedRail(input, io = {}) {
582
656
  supersedesDeliveryId: launchContinuation?.source === 'rail-pr-delivery'
583
657
  ? launchContinuation.deliveryId
584
658
  : null,
659
+ specSnapshot: buildSpecSnapshot(ctx, ticketIds),
660
+ ...(input.revision ? { revisionNote: input.revision.note, revisionOf: input.revision.ofDeliveryId } : {}),
585
661
  }, input.requiredPrContinuation
586
662
  ? { id: input.requiredPrContinuation.deliveryId, decision: input.requiredPrContinuation.decision }
587
- : null);
663
+ // A revision replaces the generation it revises, atomically, so the rail
664
+ // never shows two active deliveries and a failed revision restores the
665
+ // predecessor through the shipped rollback path.
666
+ : input.revision
667
+ ? { id: input.revision.ofDeliveryId, decision: input.revision.decision }
668
+ : null);
588
669
  prDeliveryId = generation.delivery.id;
589
670
  supersededDelivery = generation.superseded;
590
671
  input.onPrDeliveryCreated?.(prDeliveryId);
@@ -695,10 +776,17 @@ async function launchIsolatedRail(input, io = {}) {
695
776
  // into the fresh worktree (best-effort; a failure just leaves the run
696
777
  // on the legacy cold start). Linked paths join the overlay exclusion
697
778
  // list so they inherit the commit-time guarantees.
779
+ let warmLinkEvidence = [];
698
780
  try {
699
781
  const nm = (io.linkNodeModules ?? worktree_node_modules_1.linkNodeModulesIntoWorktree)(baseRepo, handle.worktreePath);
700
- if (nm.linked.length > 0)
701
- overlayExcludes = overlayExcludes.concat(nm.linked);
782
+ // `authenticated` (not `linked`) is the authorized set: it also covers
783
+ // links a previous pass created, which a resumed launch would
784
+ // otherwise leave both un-excluded from the commit and unauthorized
785
+ // for release.
786
+ const authorized = nm.authenticated ?? nm.linked ?? [];
787
+ if (authorized.length > 0)
788
+ overlayExcludes = overlayExcludes.concat(authorized);
789
+ warmLinkEvidence = nm.evidence ?? [];
702
790
  if (nm.warnings.length > 0)
703
791
  notifyOverlayDegraded(unit.ticketId, nm.warnings);
704
792
  }
@@ -732,6 +820,7 @@ async function launchIsolatedRail(input, io = {}) {
732
820
  handle,
733
821
  overlayExcludes,
734
822
  overlayCleanupEvidence,
823
+ warmLinkEvidence,
735
824
  provenanceSnapshot,
736
825
  continuationTarget,
737
826
  baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
@@ -876,6 +965,13 @@ async function launchIsolatedRail(input, io = {}) {
876
965
  instructionsFilename: overlayInstructions,
877
966
  }, a.overlayCleanupEvidence);
878
967
  }
968
+ // Warm-dependency links are re-proven against the base repo (their source),
969
+ // not the framework overlay source. A link the engine replaced with a real
970
+ // install loses its authorization here and correctly preserves the worktree.
971
+ if (a.warmLinkEvidence.length > 0) {
972
+ a.warmLinkEvidence = (0, worktree_node_modules_1.authenticateWarmNodeModulesLinks)(baseRepo, a.handle.worktreePath)
973
+ .filter((live) => a.warmLinkEvidence.some((prior) => prior.path === live.path && prior.kind === live.kind && prior.digest === live.digest));
974
+ }
879
975
  let commit;
880
976
  try {
881
977
  commit = await (0, worktree_manager_1.commitWorktreeAndVerify)(git, a.handle.worktreePath, worktreeCommitMessage(ctx, a.ticketId, a.runId, implementationOutcome === 'failed'), a.overlayExcludes);
@@ -1347,7 +1443,25 @@ async function launchIsolatedRail(input, io = {}) {
1347
1443
  const expectedHeadByBranch = (0, rail_worktree_release_1.durableBranchHeads)(settledBranchRecords);
1348
1444
  const overlayEvidenceByBranch = (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(settledBranchRecords);
1349
1445
  const settlementIgnoredByBranch = (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(settledBranchRecords);
1446
+ // Review-packet evidence harvest (deterministic, no model calls) — MUST
1447
+ // run while the reviewable worktrees are still mounted, i.e. before the
1448
+ // releaseRailWorktrees calls below. harvestDeliveryEvidence never throws
1449
+ // by contract; the belt-and-braces catch keeps settle unconditional.
1450
+ const harvest = io.harvestEvidence ?? delivery_evidence_1.harvestDeliveryEvidence;
1451
+ let settleEvidence;
1452
+ try {
1453
+ settleEvidence = harvest({ readEvents: (runId) => (0, db_1.getJobEvents)(ctx.db, runId) }, results.map((result) => ({
1454
+ ticketId: result.run.ticketId,
1455
+ runId: result.run.runId,
1456
+ worktreePath: result.run.handle.worktreePath,
1457
+ })));
1458
+ }
1459
+ catch (e) {
1460
+ console.warn(`[rail-isolated] evidence harvest failed: ${e instanceof Error ? e.message : String(e)}`);
1461
+ settleEvidence = undefined;
1462
+ }
1350
1463
  const patch = {
1464
+ settleEvidence,
1351
1465
  branches: settledBranchRecords,
1352
1466
  worktreeIds,
1353
1467
  implementationOutcome,
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PrDeliveryGenerationConflict = exports.ACTIVE_PR_DECISIONS = exports.TERMINAL_PR_DECISIONS = void 0;
3
+ exports.PrDeliveryGenerationConflict = exports.ACTIVE_PR_DECISIONS = exports.TERMINAL_PR_DECISIONS = exports.REVISION_NOTE_CAP = void 0;
4
+ exports.boundSpecSnapshot = boundSpecSnapshot;
5
+ exports.boundRevisionNote = boundRevisionNote;
6
+ exports.readSpecSnapshot = readSpecSnapshot;
4
7
  exports.isTerminalPrDecision = isTerminalPrDecision;
5
8
  exports.createPrDelivery = createPrDelivery;
6
9
  exports.createPrDeliveryGeneration = createPrDeliveryGeneration;
@@ -26,6 +29,34 @@ exports.toPrDeliverySnapshot = toPrDeliverySnapshot;
26
29
  exports.toRailPrStateMessage = toRailPrStateMessage;
27
30
  exports.toPrDecisionCardEnvelope = toPrDecisionCardEnvelope;
28
31
  const ids_1 = require("./ids");
32
+ /** Defensive per-ticket cap; contract-layer specs run long but bounded. */
33
+ const SPEC_SNAPSHOT_DESCRIPTION_CAP = 32_768;
34
+ function boundSpecSnapshot(entries) {
35
+ return entries.map((entry) => ({
36
+ ticketId: entry.ticketId,
37
+ title: entry.title,
38
+ description: entry.description == null ? null : entry.description.slice(0, SPEC_SNAPSHOT_DESCRIPTION_CAP),
39
+ labels: entry.labels.slice(0, 32),
40
+ }));
41
+ }
42
+ /** A revision instruction is one sentence of intent, not a document. Bound it so
43
+ * a pasted essay cannot bloat every snapshot that carries the row. */
44
+ exports.REVISION_NOTE_CAP = 2000;
45
+ function boundRevisionNote(note) {
46
+ return note.replace(/[\r\n\t\0]+/g, ' ').replace(/\s{2,}/g, ' ').trim().slice(0, exports.REVISION_NOTE_CAP);
47
+ }
48
+ /** Parse a persisted spec_snapshot column value (null-tolerant). */
49
+ function readSpecSnapshot(raw) {
50
+ if (!raw)
51
+ return null;
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ return Array.isArray(parsed) ? parsed : null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
29
60
  /** States after which the delivery is closed (no further decision possible). */
30
61
  exports.TERMINAL_PR_DECISIONS = new Set(['completed', 'merged', 'discarded', 'superseded']);
31
62
  /** States in which the delivery still awaits (or is executing) a user decision. */
@@ -53,9 +84,12 @@ function createPrDelivery(db, input) {
53
84
  id, rail_index, loop_id, rail_key, ticket_ids, base_branch,
54
85
  loop_name, origin_surface, origin_conversation_id,
55
86
  implementation_outcome, delivery_outcome, status_code,
56
- is_continuation, supersedes_delivery_id
87
+ is_continuation, supersedes_delivery_id, spec_snapshot,
88
+ revision_note, revision_of
57
89
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', 'pending',
58
- 'implementation_running', ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null);
90
+ 'implementation_running', ?, ?, ?, ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null, input.specSnapshot && input.specSnapshot.length > 0
91
+ ? JSON.stringify(boundSpecSnapshot(input.specSnapshot))
92
+ : null, input.revisionNote ? boundRevisionNote(input.revisionNote) : null, input.revisionOf ?? null);
59
93
  return getPrDelivery(db, id);
60
94
  }
61
95
  class PrDeliveryGenerationConflict extends Error {
@@ -345,6 +379,7 @@ const PATCH_COLUMNS = {
345
379
  operationStartedAtMs: 'operation_started_at_ms',
346
380
  cleanupWarnings: 'cleanup_warnings',
347
381
  safetyArchives: 'safety_archives',
382
+ settleEvidence: 'settle_evidence',
348
383
  };
349
384
  function patchValue(key, patch) {
350
385
  const raw = patch[key];
@@ -358,6 +393,8 @@ function patchValue(key, patch) {
358
393
  return raw == null ? null : boundPrDiagnostic(String(raw));
359
394
  if (key === 'isContinuation')
360
395
  return raw ? 1 : 0;
396
+ if (key === 'settleEvidence')
397
+ return raw == null ? null : JSON.stringify(raw);
361
398
  return raw;
362
399
  }
363
400
  /** Bounded, single-line diagnostics are safe to persist/broadcast as secondary
@@ -42,6 +42,7 @@ const worktree_manager_1 = require("./worktree-manager");
42
42
  const path = __importStar(require("path"));
43
43
  const fs = __importStar(require("fs"));
44
44
  const worktree_overlay_1 = require("./worktree-overlay");
45
+ const worktree_node_modules_1 = require("./worktree-node-modules");
45
46
  const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
46
47
  /** Collapse per-unit settlement evidence into one unambiguous immutable HEAD per
47
48
  * branch. Conflicting or malformed records intentionally yield no authorization. */
@@ -157,8 +158,23 @@ async function verifyReleaseEvidence(input, worktree) {
157
158
  if (!expected || !COMMIT_SHA_RE.test(expected)) {
158
159
  return { failure: 'no durable settled HEAD is recorded for its branch', overlayEntries: [] };
159
160
  }
160
- const overlayEntries = (input.overlayEvidenceByBranch.get(worktree.branch) ?? [])
161
+ const persistedEntries = (input.overlayEvidenceByBranch.get(worktree.branch) ?? [])
161
162
  .filter((entry) => (0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry));
163
+ // Warm-dependency links are authenticated LIVE against the base repo, on top
164
+ // of whatever was persisted. Two reasons this is derived and not persisted-
165
+ // only: a repo's ordinary `node_modules/` DIRECTORY ignore pattern does not
166
+ // ignore a SYMLINK of that name, so the link reads as untracked dirt and
167
+ // permanently blocked release (and therefore branch checkout); and worktrees
168
+ // that settled before the links carried evidence are stuck at `needs-review`
169
+ // where no persisted fix could ever reach them. The proof is identical to the
170
+ // persisted one — a symlink resolving to the base checkout's identically-
171
+ // named dependency directory — so nothing weaker is being accepted here.
172
+ const warmEntries = (input.authenticateWarmLinks ?? worktree_node_modules_1.authenticateWarmNodeModulesLinks)(input.repoDir, worktree.worktree_path);
173
+ const seenPaths = new Set(persistedEntries.map((entry) => entry.path));
174
+ const overlayEntries = [
175
+ ...persistedEntries,
176
+ ...warmEntries.filter((entry) => !seenPaths.has(entry.path)),
177
+ ];
162
178
  const overlayExcludes = overlayEntries.map((entry) => entry.path);
163
179
  try {
164
180
  const status = await input.git.run([