specrails-desktop 2.34.1 → 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 (207) 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-B1GJJ0QZ.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-D6Vxjpv5.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 +39 -2
  132. package/server/dist/queue-manager.js +47 -4
  133. package/server/dist/rail-isolated-launch.js +101 -2
  134. package/server/dist/rail-pr-store.js +40 -3
  135. package/server/dist/rails-router.js +124 -3
  136. package/server/dist/review-packet.js +327 -0
  137. package/server/dist/revision-seed.js +73 -0
  138. package/server/dist/run-duration-stats.js +71 -0
  139. package/server/dist/stuck-run-detector.js +153 -0
  140. package/server/dist/telemetry-receiver.js +22 -0
  141. package/client/dist/assets/ActivityFeedPage-B4yaoZsD.js +0 -1
  142. package/client/dist/assets/AgentBrowserCapture-Bhykbzog.js +0 -1
  143. package/client/dist/assets/AgentModeAnalyticsPane-prvv23b7.js +0 -2
  144. package/client/dist/assets/AgentModeCodePane-BmxySwPP.js +0 -2
  145. package/client/dist/assets/AgentModeJobsPane-BvW6tTmU.js +0 -1
  146. package/client/dist/assets/AgentsPage-DinMMQPm.js +0 -87
  147. package/client/dist/assets/AnalyticsPage-d5GmoZY2.js +0 -1
  148. package/client/dist/assets/CodePage-Cgbj4lpK.js +0 -2
  149. package/client/dist/assets/DesktopAnalyticsPage-DbbZi6fw.js +0 -1
  150. package/client/dist/assets/DocsDialog-B87oF3aC.js +0 -11
  151. package/client/dist/assets/DocsPage-CqJ-yRkh.js +0 -11
  152. package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +0 -1
  153. package/client/dist/assets/InteractiveJobComposer-nPGDKuCk.js +0 -19
  154. package/client/dist/assets/JobDetailModal-duouGMzi.js +0 -1
  155. package/client/dist/assets/JobDetailPage-Cw3Hu579.js +0 -1
  156. package/client/dist/assets/JobsPage-DYyV-qWT.js +0 -1
  157. package/client/dist/assets/LoopPreviewModal-Md6KF7rU.js +0 -1
  158. package/client/dist/assets/LoopsPage-BKNiwsy1.js +0 -1
  159. package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +0 -1
  160. package/client/dist/assets/PluginsPage-r682uE6n.js +0 -2
  161. package/client/dist/assets/TemplatePreviewModal-DcgWnn23.js +0 -1
  162. package/client/dist/assets/TicketDetailModal-BPaqyvrs.js +0 -14
  163. package/client/dist/assets/agent-Bd-q3Wqp.js +0 -1
  164. package/client/dist/assets/agent-BlvdW4Hv.js +0 -1
  165. package/client/dist/assets/agent-C-KQhxeM.js +0 -1
  166. package/client/dist/assets/agent-Ca15w5eu.js +0 -1
  167. package/client/dist/assets/agent-D6fKaoMd.js +0 -1
  168. package/client/dist/assets/agent-HFMYMTbB.js +0 -1
  169. package/client/dist/assets/agent-QIEbkBjg.js +0 -1
  170. package/client/dist/assets/agent-mTCsAbCl.js +0 -1
  171. package/client/dist/assets/api-B2M7CtDw.js +0 -1
  172. package/client/dist/assets/arrow-left-CpIR4j7G.js +0 -1
  173. package/client/dist/assets/arrow-up-right-9DoMn7fD.js +0 -1
  174. package/client/dist/assets/browser-CXUcnTMI.js +0 -1
  175. package/client/dist/assets/button-BUIIBQPT.js +0 -1
  176. package/client/dist/assets/chevron-right-CViMxucY.js +0 -1
  177. package/client/dist/assets/dashboard-BHRqsGCV.js +0 -1
  178. package/client/dist/assets/dashboard-Bfr90vOl.js +0 -1
  179. package/client/dist/assets/dashboard-CTsM8x3m.js +0 -1
  180. package/client/dist/assets/dashboard-CqC6wX0h.js +0 -1
  181. package/client/dist/assets/dashboard-CuZtAszc.js +0 -1
  182. package/client/dist/assets/dashboard-DAcqIc-n.js +0 -1
  183. package/client/dist/assets/dashboard-DekiV-86.js +0 -1
  184. package/client/dist/assets/dashboard-DoDczc5S.js +0 -1
  185. package/client/dist/assets/dialog-CzIekCw0.js +0 -45
  186. package/client/dist/assets/dist-Cez5CRkX.js +0 -1
  187. package/client/dist/assets/dist-js-B1s_CM_v.js +0 -1
  188. package/client/dist/assets/dist-js-BNETKUa_.js +0 -1
  189. package/client/dist/assets/external-link-BcU5EWZp.js +0 -1
  190. package/client/dist/assets/file-text-CFT-xXKN.js +0 -1
  191. package/client/dist/assets/i18n-nJQvD5zJ.js +0 -2
  192. package/client/dist/assets/index-DCN5QbDm.css +0 -2
  193. package/client/dist/assets/index-DdC_faQv.js +0 -63
  194. package/client/dist/assets/jira-api-Bhixwn-d.js +0 -1
  195. package/client/dist/assets/loops-CnxqL8IL.js +0 -1
  196. package/client/dist/assets/save-28sIlCdr.js +0 -1
  197. package/client/dist/assets/settings-BveOJoaI.js +0 -1
  198. package/client/dist/assets/settings-Cj1C6F5k.js +0 -1
  199. package/client/dist/assets/settings-CuVwOpe0.js +0 -1
  200. package/client/dist/assets/settings-D5X4DfRc.js +0 -1
  201. package/client/dist/assets/settings-DXwxe-BW.js +0 -1
  202. package/client/dist/assets/settings-Di81Ba_V.js +0 -1
  203. package/client/dist/assets/settings-jbLjCYC_.js +0 -1
  204. package/client/dist/assets/settings-wIZNPFnn.js +0 -1
  205. package/client/dist/assets/useBrowserCaptureSession-BBXBN0Na.js +0 -1
  206. package/client/dist/assets/useDesktop-B6Du2N83.js +0 -1
  207. package/client/dist/assets/useSharedWebSocket-DK96ctb7.js +0 -1
@@ -292,14 +292,51 @@ function parseClaudeStreamLine(line) {
292
292
  const tool = blocks.find((b) => b.type === 'tool_use');
293
293
  if (tool?.name) {
294
294
  const input = JSON.stringify(parsed.message?.content?.[0]?.input ?? {});
295
- 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);
296
303
  }
297
304
  return withAssistantUsage({ kind: 'other', type, raw: parsed }, msg);
298
305
  }
299
306
  if (type === 'tool_use') {
300
307
  const name = parsed.name ?? '<unnamed>';
301
308
  const input = JSON.stringify(parsed.input ?? {});
302
- 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
+ }
303
340
  }
304
341
  return { kind: 'other', type, raw: parsed };
305
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
@@ -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);
@@ -1362,7 +1443,25 @@ async function launchIsolatedRail(input, io = {}) {
1362
1443
  const expectedHeadByBranch = (0, rail_worktree_release_1.durableBranchHeads)(settledBranchRecords);
1363
1444
  const overlayEvidenceByBranch = (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(settledBranchRecords);
1364
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
+ }
1365
1463
  const patch = {
1464
+ settleEvidence,
1366
1465
  branches: settledBranchRecords,
1367
1466
  worktreeIds,
1368
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
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prDeliveryRevisionAllowed = prDeliveryRevisionAllowed;
3
4
  exports.createRailsRouter = createRailsRouter;
4
5
  const express_1 = require("express");
5
6
  const rails_store_1 = require("./rails-store");
@@ -11,6 +12,8 @@ const file_story_1 = require("./file-story");
11
12
  const loops_store_1 = require("./loops-store");
12
13
  const loop_runs_store_1 = require("./loop-runs-store");
13
14
  const providers_1 = require("./providers");
15
+ const runtime_1 = require("./providers/runtime");
16
+ const agent_defaults_1 = require("./agent-defaults");
14
17
  const spec_models_1 = require("./spec-models");
15
18
  const workspace_resolution_1 = require("./workspace-resolution");
16
19
  const loop_factory_1 = require("./loop-factory");
@@ -20,6 +23,8 @@ const loop_graph_1 = require("./loop-graph");
20
23
  const rail_isolation_1 = require("./rail-isolation");
21
24
  const rail_pr_store_1 = require("./rail-pr-store");
22
25
  const loop_effect_1 = require("./loop-effect");
26
+ const review_packet_1 = require("./review-packet");
27
+ const accept_ladder_1 = require("./accept-ladder");
23
28
  const rail_pr_decision_1 = require("./rail-pr-decision");
24
29
  const active_pr_continuation_1 = require("./active-pr-continuation");
25
30
  const rail_isolated_launch_1 = require("./rail-isolated-launch");
@@ -41,6 +46,22 @@ function prDeliveryContinuesTickets(delivery, ticketIds) {
41
46
  const requested = new Set(ticketIds);
42
47
  return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
43
48
  }
49
+ /**
50
+ * Whether a launch may revise an undecided delivery. Deliberately strict:
51
+ * the named id must BE the rail's active generation, that generation must still
52
+ * be awaiting a decision (a terminal one is history, not revisable), and the
53
+ * rail must still carry exactly the delivery's ticket set — a subset would
54
+ * revise work the user never asked about while leaving the rest undecided.
55
+ */
56
+ function prDeliveryRevisionAllowed(delivery, revisionOfDeliveryId, ticketIds) {
57
+ if (delivery.id !== revisionOfDeliveryId)
58
+ return false;
59
+ if ((0, rail_pr_store_1.isTerminalPrDecision)(delivery.decision))
60
+ return false;
61
+ const covered = new Set(delivery.ticketIds);
62
+ const requested = new Set(ticketIds);
63
+ return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
64
+ }
44
65
  function prDeliveryCheckoutBlock(delivery) {
45
66
  if (delivery.deliveryOutcome === 'blocked') {
46
67
  return 'delivery is blocked and has no safely checkoutable result';
@@ -381,7 +402,20 @@ function createRailsRouter() {
381
402
  return;
382
403
  }
383
404
  let { mode = 'implement' } = req.body ?? {};
384
- const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber } = req.body ?? {};
405
+ const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber, revisionOfDeliveryId, revisionNote } = req.body ?? {};
406
+ // Revision launch (nontech-review-experience Wave 3): the user asked for a
407
+ // change to a delivery that is already awaiting their decision. Shape is
408
+ // validated here; the narrow guard exemption is enforced below.
409
+ if (revisionOfDeliveryId !== undefined && revisionOfDeliveryId !== null) {
410
+ if (typeof revisionOfDeliveryId !== 'string' || !/^[A-Za-z0-9-]{1,64}$/.test(revisionOfDeliveryId)) {
411
+ res.status(400).json({ error: 'invalid_revision_target', detail: 'revisionOfDeliveryId must be a delivery id' });
412
+ return;
413
+ }
414
+ if (typeof revisionNote !== 'string' || revisionNote.trim().length === 0) {
415
+ res.status(400).json({ error: 'revision_note_required', detail: 'revisionNote must describe the change to make' });
416
+ return;
417
+ }
418
+ }
385
419
  // Explicit target PR (deliver-rail-into-existing-pr): the user names an
386
420
  // existing open PR as the delivery destination. Shape-validated here;
387
421
  // resolved and verified inside the isolated launch, fail-closed.
@@ -414,6 +448,18 @@ function createRailsRouter() {
414
448
  res.status(404).json({ error: 'Factory loop not found' });
415
449
  return;
416
450
  }
451
+ // The revision loop is meaningless without a revision to apply: its prompt
452
+ // consumes {{const:REVISION_REQUEST}}, and an unresolved constant renders
453
+ // as an EMPTY string (loop-constants), so naming it directly would spawn a
454
+ // run whose central instruction is silently blank. It is reachable by id
455
+ // (fork/preview resolve it), so the launch door has to refuse it.
456
+ if (loopId === loop_factory_1.FACTORY_REVISION_LOOP_ID && !revisionOfDeliveryId) {
457
+ res.status(400).json({
458
+ error: 'revision_loop_requires_revision',
459
+ detail: 'factory:revision only runs as a revision — pass revisionOfDeliveryId and revisionNote instead of naming the loop',
460
+ });
461
+ return;
462
+ }
417
463
  mode = fmode;
418
464
  }
419
465
  if (!VALID_MODES.has(mode)) {
@@ -429,6 +475,17 @@ function createRailsRouter() {
429
475
  if ((0, feature_flags_1.isLoopsEnabled)() && (typeof loopId !== 'string' || !loopId) && mode !== 'loop') {
430
476
  loopId = (0, loop_factory_1.factoryLoopForMode)(mode)?.id;
431
477
  }
478
+ // A revision must run the Architect-LESS revision loop, whatever the rail's
479
+ // stored mode is. Without this every one-sentence tweak would re-run the
480
+ // full implement pipeline — the exact cost the feature exists to avoid.
481
+ // An explicit loopId is respected (a caller naming a loop means it).
482
+ if (revisionOfDeliveryId && (0, feature_flags_1.isLoopsEnabled)() && (typeof loopId !== 'string' || !loopId || (0, loop_factory_1.isFactoryLoopId)(loopId))) {
483
+ const revisionLoop = (0, loop_factory_1.getFactoryLoop)(loop_factory_1.FACTORY_REVISION_LOOP_ID);
484
+ if (revisionLoop) {
485
+ loopId = revisionLoop.id;
486
+ mode = revisionLoop.mode;
487
+ }
488
+ }
432
489
  if (mode === 'loop' && !(0, feature_flags_1.isLoopsEnabled)()) {
433
490
  res.status(403).json({ error: 'Loops are disabled on this server' });
434
491
  return;
@@ -599,7 +656,14 @@ function createRailsRouter() {
599
656
  res.status(400).json({ error: `model is not valid for provider "${loopProvider}"`, allowed: (0, spec_models_1.getModelsForProvider)(loopProvider) });
600
657
  return;
601
658
  }
602
- const loopModel = typeof model === 'string' && model ? model : loopAdapter.defaultModel();
659
+ // Global Specrails Agents defaults (Settings ▸ Specrails Agents): fill
660
+ // model/effort ONLY when the launch body carries no explicit choice —
661
+ // dashboard factory launches send neither. Read at launch time, so a
662
+ // settings change applies to the next launch without restart.
663
+ const globalAgentDefaults = (0, agent_defaults_1.resolveAgentDefaults)(c.desktopDb, loopProvider);
664
+ const loopModel = typeof model === 'string' && model
665
+ ? model
666
+ : (globalAgentDefaults?.pipelineModel ?? loopAdapter.defaultModel());
603
667
  let effort;
604
668
  if (reasoning_effort !== undefined && reasoning_effort !== null) {
605
669
  const allowed = (0, providers_1.reasoningEffortsForModel)(loopAdapter, loopModel);
@@ -613,6 +677,10 @@ function createRailsRouter() {
613
677
  }
614
678
  effort = reasoning_effort;
615
679
  }
680
+ else if (globalAgentDefaults?.pipelineEffort
681
+ && (0, runtime_1.isReasoningEffortValidForModel)(loopAdapter, loopModel, globalAgentDefaults.pipelineEffort)) {
682
+ effort = globalAgentDefaults.pipelineEffort;
683
+ }
616
684
  // The loop's command(s) declare ticket scope + required capabilities.
617
685
  const promptsText = loopGraph.nodes
618
686
  .filter((n) => n.type === 'ai-step')
@@ -634,6 +702,7 @@ function createRailsRouter() {
634
702
  // cards) is diagnosable from the UI, not just the server log.
635
703
  let isolationUnavailableDetail;
636
704
  let continuablePrDelivery = null;
705
+ let revisionRequest = null;
637
706
  // Read-only vs mutating is DERIVED from the loop's nodes (see loop-effect),
638
707
  // not a user flag — a content-read-only loop (no ai-step/shell) never writes,
639
708
  // so it is not isolated; anything that can write is.
@@ -644,10 +713,32 @@ function createRailsRouter() {
644
713
  if ((0, rail_isolation_1.isRailPrDeliveryEnabled)()) {
645
714
  const pending = (0, rail_pr_store_1.getActivePrDeliveryByRail)(c.db, railIndex);
646
715
  const pendingSnapshot = pending ? (0, rail_pr_store_1.toPrDeliverySnapshot)(pending) : null;
647
- if (pendingSnapshot && !prDeliveryContinuesTickets(pendingSnapshot, rail.ticketIds)) {
716
+ // A revision is the ONE launch allowed to proceed against an
717
+ // undecided delivery, and only on the exact terms below: the caller
718
+ // names that delivery AND covers its full ticket set. Anything else
719
+ // would silently append work to branches the user has not judged.
720
+ const revisionsEnabled = (0, feature_flags_1.areDeliveryRevisionsEnabled)();
721
+ const revisionOfPending = Boolean(revisionsEnabled && pendingSnapshot && revisionOfDeliveryId
722
+ && prDeliveryRevisionAllowed(pendingSnapshot, revisionOfDeliveryId, rail.ticketIds));
723
+ if (revisionsEnabled && pendingSnapshot && revisionOfDeliveryId && !revisionOfPending) {
724
+ res.status(409).json({
725
+ error: 'invalid_revision_target',
726
+ prDeliveryId: pendingSnapshot.id,
727
+ detail: 'a revision must target the rail\'s active delivery and cover all of its specs',
728
+ });
729
+ return;
730
+ }
731
+ if (pendingSnapshot && !revisionOfPending && !prDeliveryContinuesTickets(pendingSnapshot, rail.ticketIds)) {
648
732
  res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
649
733
  return;
650
734
  }
735
+ if (revisionOfPending && pendingSnapshot) {
736
+ revisionRequest = {
737
+ ofDeliveryId: pendingSnapshot.id,
738
+ decision: pendingSnapshot.decision,
739
+ note: revisionNote,
740
+ };
741
+ }
651
742
  // Explicit target vs an undecided continuable delivery: the slot's
652
743
  // active generation owns its PR. A DIFFERENT explicit target would
653
744
  // append to the undecided branches → 409; the SAME PR is redundant
@@ -720,6 +811,9 @@ function createRailsRouter() {
720
811
  ...(typeof targetPrNumber === 'number' && !continuablePrDelivery
721
812
  ? { explicitPrTarget: { prNumber: targetPrNumber } }
722
813
  : {}),
814
+ // Revision of an undecided delivery: the guard above proved the
815
+ // exemption, so pass the contract through for supersession.
816
+ ...(revisionRequest ? { revision: revisionRequest } : {}),
723
817
  });
724
818
  res.status(202).json({ loopRunIds: ids, railIndex, mode, isolated: true });
725
819
  return;
@@ -999,6 +1093,33 @@ function createRailsRouter() {
999
1093
  }
1000
1094
  res.json({ ok: true, jobIds: targetJobIds, loopRunIds: targetLoopRunIds, canceled: canceledCount });
1001
1095
  });
1096
+ // GET /rails/pr-deliveries/:id/packet — the plain-language review packet for a
1097
+ // settled delivery: composed server-side from durable rows only (no model
1098
+ // calls, no live-store reads) plus the pre-resolved Accept ladder, so the
1099
+ // client never has to ask the user to choose between git actions. Read-only.
1100
+ router.get('/pr-deliveries/:prDeliveryId/packet', async (req, res) => {
1101
+ if (!(0, feature_flags_1.isReviewPacketEnabled)()) {
1102
+ res.status(404).json({ error: 'Not Found' });
1103
+ return;
1104
+ }
1105
+ const c = ctx(req);
1106
+ const row = (0, rail_pr_store_1.getPrDelivery)(c.db, req.params.prDeliveryId);
1107
+ if (!row) {
1108
+ res.status(404).json({ error: 'Delivery not found' });
1109
+ return;
1110
+ }
1111
+ try {
1112
+ const packet = (0, review_packet_1.composeReviewPacket)({ db: c.db, row });
1113
+ // Capability probes are read-only and offline; a failure degrades to the
1114
+ // confirm-gated local path rather than blocking the packet.
1115
+ const acceptCapability = await (0, accept_ladder_1.resolveAcceptCapability)({ repoDir: c.project.path, exec: pr_publisher_1.defaultExec });
1116
+ res.json({ packet, acceptCapability, snapshot: (0, rail_pr_store_1.toPrDeliverySnapshot)(row) });
1117
+ }
1118
+ catch (err) {
1119
+ console.error('[rails-router] packet composition failed:', err);
1120
+ res.status(500).json({ error: 'packet composition failed', detail: err.message });
1121
+ }
1122
+ });
1002
1123
  // POST /rails/pr-decision — the ONE decision action (safe-pr-review-flow) both
1003
1124
  // surfaces (dashboard rail row + agent-chat card) call on a rail_pr_deliveries
1004
1125
  // row: create-pr | publish | discard/dismiss | poll/reopen | merge-local |