specrails-desktop 2.34.1 → 2.36.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 (219) hide show
  1. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +1 -0
  2. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +1 -0
  3. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +2 -0
  5. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +1 -0
  6. package/client/dist/assets/AgentsPage-C8J_42sm.js +87 -0
  7. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +1 -0
  8. package/client/dist/assets/CodePage-BZ2MwdCu.js +2 -0
  9. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +1 -0
  10. package/client/dist/assets/DocsDialog-C76VdBJC.js +11 -0
  11. package/client/dist/assets/DocsPage-ziMlj6B6.js +11 -0
  12. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +1 -0
  13. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +19 -0
  14. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +1 -0
  15. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +1 -0
  16. package/client/dist/assets/JobsPage-z5M_HVJG.js +1 -0
  17. package/client/dist/assets/{LoopBuilderPage-B1GJJ0QZ.js → LoopBuilderPage-j1FQS47c.js} +2 -2
  18. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +1 -0
  19. package/client/dist/assets/LoopsPage-Ll7zORZb.js +1 -0
  20. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +1 -0
  21. package/client/dist/assets/PluginsPage-DN4kGvJt.js +2 -0
  22. package/client/dist/assets/{ProjectSettingsDialog-D6Vxjpv5.js → ProjectSettingsDialog-OgsgxjzF.js} +1 -1
  23. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +1 -0
  24. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +1 -0
  25. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +14 -0
  26. package/client/dist/assets/{Trans-CTqw6rr9.js → Trans-Bsz9_gyq.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-DQCvMgUc.js} +1 -1
  70. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EjGOpn4b.js → getTimezoneOffsetInMilliseconds-C16cxSe1.js} +1 -1
  71. package/client/dist/assets/git-refresh-wTW0rO0Z.js +1 -0
  72. package/client/dist/assets/i18n-hnX2DPmo.js +2 -0
  73. package/client/dist/assets/index-DIhMEAVw.js +66 -0
  74. package/client/dist/assets/index-DwygcU7s.css +2 -0
  75. package/client/dist/assets/jira-api-B1-MUehg.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/{mcp-DsTwmjST.js → mcp-B8X8wBLu.js} +1 -1
  85. package/client/dist/assets/{mcp-SMkn8boG.js → mcp-C2AojZZN.js} +1 -1
  86. package/client/dist/assets/mcp-D8BT78_L.js +1 -0
  87. package/client/dist/assets/{mcp-D4ZjtIx3.js → mcp-Df5RWBNk.js} +1 -1
  88. package/client/dist/assets/{mcp-DIfBUv2D.js → mcp-Dj7BgMQW.js} +1 -1
  89. package/client/dist/assets/{mcp-D2pc5GGx.js → mcp-Q7iT7rcz.js} +1 -1
  90. package/client/dist/assets/{mcp-fSfiVYAc.js → mcp-YlvNvJbW.js} +1 -1
  91. package/client/dist/assets/{mcp-Czd_iyp0.js → mcp-mKFpTSRb.js} +1 -1
  92. package/client/dist/assets/narration-4Z2VUhZw.js +1 -0
  93. package/client/dist/assets/narration-BGuiEDj3.js +1 -0
  94. package/client/dist/assets/narration-BuSoqRmv.js +1 -0
  95. package/client/dist/assets/narration-CB9t29fy.js +1 -0
  96. package/client/dist/assets/narration-CU3x8FHD.js +1 -0
  97. package/client/dist/assets/narration-CWeXhfP5.js +1 -0
  98. package/client/dist/assets/narration-DRaDnhT5.js +1 -0
  99. package/client/dist/assets/narration-DtEbQgCw.js +1 -0
  100. package/client/dist/assets/packet-B1IxaL5f.js +1 -0
  101. package/client/dist/assets/packet-Bh5kkbiB.js +1 -0
  102. package/client/dist/assets/packet-By8fqYxW.js +1 -0
  103. package/client/dist/assets/packet-CZdvA44E.js +1 -0
  104. package/client/dist/assets/packet-CrjHRQCP.js +1 -0
  105. package/client/dist/assets/packet-Dwi2pTtx.js +1 -0
  106. package/client/dist/assets/packet-_-ZIa0IR.js +1 -0
  107. package/client/dist/assets/packet-n0W6E1Hn.js +1 -0
  108. package/client/dist/assets/save-B6wGWwBR.js +1 -0
  109. package/client/dist/assets/settings-Bd0x4TBa.js +1 -0
  110. package/client/dist/assets/settings-C1-7cXzC.js +1 -0
  111. package/client/dist/assets/settings-CAFaT38b.js +1 -0
  112. package/client/dist/assets/settings-Ct3v1nW2.js +1 -0
  113. package/client/dist/assets/settings-DHh7_IF5.js +1 -0
  114. package/client/dist/assets/settings-DQXAmdcv.js +1 -0
  115. package/client/dist/assets/settings-guzlhpup.js +1 -0
  116. package/client/dist/assets/settings-sZfTPMEH.js +1 -0
  117. package/client/dist/assets/{spending-BpYPsevQ.js → spending-EC9xqio-.js} +1 -1
  118. package/client/dist/assets/useDesktop-BQVt8lFd.js +1 -0
  119. package/client/dist/index.html +30 -32
  120. package/docs/internals/embedded-browser-native-webview-evaluation.md +161 -0
  121. package/docs/internals/review-packet.md +212 -0
  122. package/docs/mcp.md +16 -0
  123. package/package.json +1 -1
  124. package/server/dist/accept-ladder.js +61 -0
  125. package/server/dist/agent-chat-manager.js +24 -1
  126. package/server/dist/agent-defaults.js +359 -0
  127. package/server/dist/agent-mcp-config.js +94 -2
  128. package/server/dist/agent-operator-prompt.js +17 -5
  129. package/server/dist/browser-playwright.js +39 -2
  130. package/server/dist/db.js +38 -0
  131. package/server/dist/delivery-evidence.js +242 -0
  132. package/server/dist/desktop-router.js +45 -0
  133. package/server/dist/external-mcp.js +370 -0
  134. package/server/dist/feature-flags.js +23 -0
  135. package/server/dist/loop-command-catalog.js +33 -0
  136. package/server/dist/loop-executors.js +12 -2
  137. package/server/dist/loop-factory.js +16 -1
  138. package/server/dist/loops-router.js +2 -0
  139. package/server/dist/mcp/tools/rails.js +16 -0
  140. package/server/dist/project-registry.js +33 -1
  141. package/server/dist/project-router-loop-runs.js +27 -2
  142. package/server/dist/providers/claude-adapter.js +39 -2
  143. package/server/dist/queue-manager.js +47 -4
  144. package/server/dist/rail-isolated-launch.js +101 -2
  145. package/server/dist/rail-pr-store.js +40 -3
  146. package/server/dist/rails-router.js +124 -3
  147. package/server/dist/review-packet.js +327 -0
  148. package/server/dist/revision-seed.js +73 -0
  149. package/server/dist/run-duration-stats.js +71 -0
  150. package/server/dist/stuck-run-detector.js +153 -0
  151. package/server/dist/telemetry-receiver.js +22 -0
  152. package/client/dist/assets/ActivityFeedPage-B4yaoZsD.js +0 -1
  153. package/client/dist/assets/AgentBrowserCapture-Bhykbzog.js +0 -1
  154. package/client/dist/assets/AgentModeAnalyticsPane-prvv23b7.js +0 -2
  155. package/client/dist/assets/AgentModeCodePane-BmxySwPP.js +0 -2
  156. package/client/dist/assets/AgentModeJobsPane-BvW6tTmU.js +0 -1
  157. package/client/dist/assets/AgentsPage-DinMMQPm.js +0 -87
  158. package/client/dist/assets/AnalyticsPage-d5GmoZY2.js +0 -1
  159. package/client/dist/assets/CodePage-Cgbj4lpK.js +0 -2
  160. package/client/dist/assets/DesktopAnalyticsPage-DbbZi6fw.js +0 -1
  161. package/client/dist/assets/DocsDialog-B87oF3aC.js +0 -11
  162. package/client/dist/assets/DocsPage-CqJ-yRkh.js +0 -11
  163. package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +0 -1
  164. package/client/dist/assets/InteractiveJobComposer-nPGDKuCk.js +0 -19
  165. package/client/dist/assets/JobDetailModal-duouGMzi.js +0 -1
  166. package/client/dist/assets/JobDetailPage-Cw3Hu579.js +0 -1
  167. package/client/dist/assets/JobsPage-DYyV-qWT.js +0 -1
  168. package/client/dist/assets/LoopPreviewModal-Md6KF7rU.js +0 -1
  169. package/client/dist/assets/LoopsPage-BKNiwsy1.js +0 -1
  170. package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +0 -1
  171. package/client/dist/assets/PluginsPage-r682uE6n.js +0 -2
  172. package/client/dist/assets/TemplatePreviewModal-DcgWnn23.js +0 -1
  173. package/client/dist/assets/TicketDetailModal-BPaqyvrs.js +0 -14
  174. package/client/dist/assets/agent-Bd-q3Wqp.js +0 -1
  175. package/client/dist/assets/agent-BlvdW4Hv.js +0 -1
  176. package/client/dist/assets/agent-C-KQhxeM.js +0 -1
  177. package/client/dist/assets/agent-Ca15w5eu.js +0 -1
  178. package/client/dist/assets/agent-D6fKaoMd.js +0 -1
  179. package/client/dist/assets/agent-HFMYMTbB.js +0 -1
  180. package/client/dist/assets/agent-QIEbkBjg.js +0 -1
  181. package/client/dist/assets/agent-mTCsAbCl.js +0 -1
  182. package/client/dist/assets/api-B2M7CtDw.js +0 -1
  183. package/client/dist/assets/arrow-left-CpIR4j7G.js +0 -1
  184. package/client/dist/assets/arrow-up-right-9DoMn7fD.js +0 -1
  185. package/client/dist/assets/browser-CXUcnTMI.js +0 -1
  186. package/client/dist/assets/button-BUIIBQPT.js +0 -1
  187. package/client/dist/assets/chevron-right-CViMxucY.js +0 -1
  188. package/client/dist/assets/dashboard-BHRqsGCV.js +0 -1
  189. package/client/dist/assets/dashboard-Bfr90vOl.js +0 -1
  190. package/client/dist/assets/dashboard-CTsM8x3m.js +0 -1
  191. package/client/dist/assets/dashboard-CqC6wX0h.js +0 -1
  192. package/client/dist/assets/dashboard-CuZtAszc.js +0 -1
  193. package/client/dist/assets/dashboard-DAcqIc-n.js +0 -1
  194. package/client/dist/assets/dashboard-DekiV-86.js +0 -1
  195. package/client/dist/assets/dashboard-DoDczc5S.js +0 -1
  196. package/client/dist/assets/dialog-CzIekCw0.js +0 -45
  197. package/client/dist/assets/dist-Cez5CRkX.js +0 -1
  198. package/client/dist/assets/dist-js-B1s_CM_v.js +0 -1
  199. package/client/dist/assets/dist-js-BNETKUa_.js +0 -1
  200. package/client/dist/assets/external-link-BcU5EWZp.js +0 -1
  201. package/client/dist/assets/file-text-CFT-xXKN.js +0 -1
  202. package/client/dist/assets/i18n-nJQvD5zJ.js +0 -2
  203. package/client/dist/assets/index-DCN5QbDm.css +0 -2
  204. package/client/dist/assets/index-DdC_faQv.js +0 -63
  205. package/client/dist/assets/jira-api-Bhixwn-d.js +0 -1
  206. package/client/dist/assets/loops-CnxqL8IL.js +0 -1
  207. package/client/dist/assets/mcp-uDkXYu4L.js +0 -1
  208. package/client/dist/assets/save-28sIlCdr.js +0 -1
  209. package/client/dist/assets/settings-BveOJoaI.js +0 -1
  210. package/client/dist/assets/settings-Cj1C6F5k.js +0 -1
  211. package/client/dist/assets/settings-CuVwOpe0.js +0 -1
  212. package/client/dist/assets/settings-D5X4DfRc.js +0 -1
  213. package/client/dist/assets/settings-DXwxe-BW.js +0 -1
  214. package/client/dist/assets/settings-Di81Ba_V.js +0 -1
  215. package/client/dist/assets/settings-jbLjCYC_.js +0 -1
  216. package/client/dist/assets/settings-wIZNPFnn.js +0 -1
  217. package/client/dist/assets/useBrowserCaptureSession-BBXBN0Na.js +0 -1
  218. package/client/dist/assets/useDesktop-B6Du2N83.js +0 -1
  219. package/client/dist/assets/useSharedWebSocket-DK96ctb7.js +0 -1
@@ -5,6 +5,8 @@ exports.isBrowserCaptureEnabled = isBrowserCaptureEnabled;
5
5
  exports.isJiraEnabled = isJiraEnabled;
6
6
  exports.isInteractiveJobsEnabled = isInteractiveJobsEnabled;
7
7
  exports.isLoopsEnabled = isLoopsEnabled;
8
+ exports.isReviewPacketEnabled = isReviewPacketEnabled;
9
+ exports.areDeliveryRevisionsEnabled = areDeliveryRevisionsEnabled;
8
10
  function isCodeExplorerEnabled() {
9
11
  return process.env.SPECRAILS_CODE_EXPLORER !== 'false';
10
12
  }
@@ -54,3 +56,24 @@ function isInteractiveJobsEnabled() {
54
56
  function isLoopsEnabled() {
55
57
  return process.env.SPECRAILS_LOOPS_SECTION !== 'false';
56
58
  }
59
+ /**
60
+ * Review packet (nontech-review-experience): the plain-language what-changed +
61
+ * proof surface a non-technical user decides on, replacing raw git vocabulary
62
+ * at the review moment. Server-side default ON; set
63
+ * SPECRAILS_REVIEW_PACKET="false" to 404 the packet route (emergency
64
+ * rollback — the existing decision strip keeps working untouched). The client
65
+ * gates separately on VITE_FEATURE_REVIEW_PACKET.
66
+ */
67
+ function isReviewPacketEnabled() {
68
+ return process.env.SPECRAILS_REVIEW_PACKET !== 'false';
69
+ }
70
+ /**
71
+ * Delivery revisions (nontech-review-experience Wave 3): the ability to launch a
72
+ * change against a delivery that is still awaiting the user's decision. Default
73
+ * ON; set SPECRAILS_DELIVERY_REVISIONS="false" to restore the byte-identical
74
+ * legacy guard — every launch against an undecided delivery 409s again and the
75
+ * packet's "Ask for changes" cannot be satisfied.
76
+ */
77
+ function areDeliveryRevisionsEnabled() {
78
+ return process.env.SPECRAILS_DELIVERY_REVISIONS !== 'false';
79
+ }
@@ -78,6 +78,39 @@ exports.LOOP_COMMANDS = [
78
78
  requiredCapability: 'freestyle',
79
79
  ticketScope: 'per-ticket',
80
80
  },
81
+ {
82
+ name: 'revise',
83
+ label: 'revise',
84
+ description: 'Revision step (nontech-review-experience): apply the ONE change the user asked for on top of work already delivered, then have the reviewer re-grade it. No re-planning, no re-implementing.',
85
+ ticketScope: 'all',
86
+ // Deliberately NOT the implement pipeline: the plan and the code already
87
+ // exist, so an Architect pass would re-derive both and pay the full cost of
88
+ // a first run for a one-sentence tweak. The reviewer pass is orchestrated
89
+ // from HERE rather than added to core, because the sr-* agents are already
90
+ // present in the run's worktree via the overlay — that keeps the loop a
91
+ // pure desktop concern with zero specrails-core coupling, and it is what
92
+ // keeps the review packet's reviewer tier populated for revisions.
93
+ template: [
94
+ 'A previous run already delivered work for this spec, and the user has now asked for ONE specific change to it. Apply exactly that change.',
95
+ '',
96
+ 'What the user asked to change:',
97
+ '{{const:REVISION_REQUEST}}',
98
+ '',
99
+ 'If the section above is EMPTY, no change was actually requested: do not',
100
+ 'guess, do not re-implement anything, and do not touch the branch. Report',
101
+ 'that no revision instruction was provided and stop.',
102
+ '',
103
+ 'Rules:',
104
+ '- Start from the work that is ALREADY on this branch. Read it first (git diff against the base branch) so you extend it instead of redoing it.',
105
+ '- Make the SMALLEST change that satisfies the request. Do not refactor, re-plan, or re-implement anything the user did not ask about.',
106
+ '- If the request is ambiguous, choose the most conservative reading and say which one you chose.',
107
+ '- If the request cannot be done without breaking something the spec requires, stop and report that instead of forcing it.',
108
+ '',
109
+ 'Then re-grade the result: run the `sr-reviewer` agent over the resulting diff so its review report and `confidence-score.json` reflect THIS revision, not the previous run.',
110
+ '',
111
+ '{{const:GUARDRAILS}}',
112
+ ].join('\n'),
113
+ },
81
114
  {
82
115
  name: 'fix',
83
116
  label: 'fix',
@@ -121,6 +121,16 @@ function aiStepExtraArgs(adapter, cwd, repoDir) {
121
121
  }
122
122
  function createLoopExecutors(opts = {}) {
123
123
  const resolveEnv = () => typeof opts.env === 'function' ? opts.env() : opts.env ?? process.env;
124
+ // Deciders deliberately excluded: they run no pipeline agents.
125
+ const withProfileEnv = (env, provider) => {
126
+ try {
127
+ const profilePath = opts.profilePathFor?.(provider) ?? null;
128
+ return profilePath ? { ...env, SPECRAILS_PROFILE_PATH: profilePath } : env;
129
+ }
130
+ catch {
131
+ return env;
132
+ }
133
+ };
124
134
  return {
125
135
  // Cheap working-tree fingerprint for the engine's non-convergence guard:
126
136
  // HEAD sha + the porcelain dirty set, hashed. Two identical fingerprints
@@ -150,7 +160,7 @@ function createLoopExecutors(opts = {}) {
150
160
  // for the pipeline's I/O exactly like QueueManager: SPECRAILS_REPO_DIR +
151
161
  // claude `--add-dir <repoDir>` (see the shared helpers above). Best-effort
152
162
  // agent self-heal on Windows.
153
- const stepEnv = aiStepEnv(resolveEnv(), repoDir);
163
+ const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
154
164
  const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
155
165
  const effectivePrompt = (0, runtime_1.formatProviderCommand)(adapter, prompt, cwd, sessionId ?? undefined);
156
166
  const buildOpts = {
@@ -294,7 +304,7 @@ function createLoopExecutors(opts = {}) {
294
304
  const adapter = (0, providers_1.getAdapter)(provider);
295
305
  if (!adapter.capabilities.persistentStdin)
296
306
  return null;
297
- const stepEnv = aiStepEnv(resolveEnv(), repoDir);
307
+ const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
298
308
  const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
299
309
  if (repoDir) {
300
310
  try {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FACTORY_LOOPS = void 0;
3
+ exports.FACTORY_LOOPS = exports.FACTORY_REVISION_LOOP_ID = void 0;
4
4
  exports.getFactoryLoop = getFactoryLoop;
5
5
  exports.isFactoryLoopId = isFactoryLoopId;
6
6
  exports.factoryLoopMode = factoryLoopMode;
@@ -23,6 +23,8 @@ const SDD_QUICK_OPENSPEC_FACTORY = {
23
23
  mode: 'loop',
24
24
  graph: (0, loop_templates_1.opsxLifecycleGraph)(),
25
25
  };
26
+ /** The Architect-less revision loop every "ask for a change" launch runs. */
27
+ exports.FACTORY_REVISION_LOOP_ID = 'factory:revision';
26
28
  exports.FACTORY_LOOPS = [
27
29
  {
28
30
  id: 'factory:implement',
@@ -49,6 +51,19 @@ exports.FACTORY_LOOPS = [
49
51
  requiredCapability: 'freestyle',
50
52
  graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:freestyle}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
51
53
  },
54
+ {
55
+ id: exports.FACTORY_REVISION_LOOP_ID,
56
+ name: 'Revision',
57
+ // Listed so the platform's behaviour is discoverable, but `launchable:false`:
58
+ // the app runs this itself when a user asks for a change to a delivery. Its
59
+ // prompt consumes `{{const:REVISION_REQUEST}}`, and an unresolved constant
60
+ // renders as an EMPTY string, so a hand-launched run would have a blank
61
+ // central instruction. The launch route refuses it; the prompt guards too.
62
+ launchable: false,
63
+ description: "Apply the ONE change the user asked for on top of work already delivered, re-grade it with the reviewer, then verify + refine until green. No re-planning. Runs automatically when you ask for changes on a review — it is not started by hand.",
64
+ mode: 'loop',
65
+ graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:revise}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
66
+ },
52
67
  SDD_QUICK_OPENSPEC_FACTORY,
53
68
  ];
54
69
  const FACTORY_ALIASES = new Map([
@@ -65,6 +65,8 @@ function registerLoopsRoutes(router, deps) {
65
65
  description: f.description,
66
66
  mode: f.mode,
67
67
  requiredCapability: f.requiredCapability ?? null,
68
+ // Absent means launchable (every pre-existing factory loop is).
69
+ launchable: f.launchable !== false,
68
70
  graph: f.graph,
69
71
  })),
70
72
  });
@@ -122,6 +122,14 @@ function railsTools() {
122
122
  .min(1)
123
123
  .optional()
124
124
  .describe('Deliver INTO an existing open PR instead of creating a new one (launch). When the user names an existing PR ("extend PR #151"), pass its number here — the rail then works on that PR\'s head branch and settle pushes to it; NEVER launch without it in that case (a plain launch would create a duplicate PR). The PR must be open and same-repo (fork PRs are rejected with target_pr_fork).'),
125
+ revisionOfDeliveryId: zod_1.z
126
+ .string()
127
+ .optional()
128
+ .describe('Revise a delivery that is ALREADY awaiting the user\'s decision (launch). This is the only way to launch against an undecided delivery: pass its prDeliveryId (from rails.prDeliveries) together with revisionNote. Use it whenever the user asks for a change to work you already delivered — never publish/discard/merge first. The rail must still carry exactly that delivery\'s specs.'),
129
+ revisionNote: zod_1.z
130
+ .string()
131
+ .optional()
132
+ .describe('What to change, in the user\'s own words (required with revisionOfDeliveryId). It is injected into the revision run and shown on the updated review packet as "what you asked to change".'),
125
133
  },
126
134
  async handler(ctx, args) {
127
135
  const base = `${(0, types_1.projectPath)(ctx, args.projectId)}/rails`;
@@ -210,6 +218,14 @@ function railsTools() {
210
218
  body.reasoning_effort = args.reasoning_effort;
211
219
  if (args.targetPrNumber !== undefined)
212
220
  body.targetPrNumber = args.targetPrNumber;
221
+ // Revision of a delivery already awaiting the user's decision: the
222
+ // ONE launch allowed against an undecided delivery. The route
223
+ // re-validates the exemption (must be the rail's active delivery and
224
+ // cover its full spec set), so a wrong id fails closed there.
225
+ if (args.revisionOfDeliveryId !== undefined)
226
+ body.revisionOfDeliveryId = args.revisionOfDeliveryId;
227
+ if (args.revisionNote !== undefined)
228
+ body.revisionNote = args.revisionNote;
213
229
  // Origin link (safe-pr-review-flow): a launch driven by the in-app
214
230
  // agent carries its conversation id (from the loopback header, via
215
231
  // ctx) so the PR decision card is posted back into that conversation.
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
11
11
  const os_1 = __importDefault(require("os"));
12
12
  const db_1 = require("./db");
13
13
  const queue_manager_1 = require("./queue-manager");
14
+ const agent_defaults_1 = require("./agent-defaults");
14
15
  const chat_manager_1 = require("./chat-manager");
15
16
  const setup_manager_1 = require("./setup-manager");
16
17
  const proposal_manager_1 = require("./proposal-manager");
@@ -35,6 +36,7 @@ const project_env_1 = require("./project-env");
35
36
  const workspace_manager_1 = require("./workspace-manager");
36
37
  const ticket_store_1 = require("./ticket-store");
37
38
  const jira_sync_manager_1 = require("./jira/jira-sync-manager");
39
+ const stuck_run_detector_1 = require("./stuck-run-detector");
38
40
  const loop_run_manager_1 = require("./loop-run-manager");
39
41
  const loop_executors_1 = require("./loop-executors");
40
42
  const rail_isolated_launch_1 = require("./rail-isolated-launch");
@@ -291,6 +293,10 @@ class ProjectRegistry {
291
293
  ctx.jiraSyncManager.stop();
292
294
  }
293
295
  catch { /* ignore */ }
296
+ try {
297
+ ctx.stuckRunDetector.stop();
298
+ }
299
+ catch { /* ignore */ }
294
300
  // Kill any terminal sessions belonging to this project
295
301
  try {
296
302
  (0, terminal_manager_1.getTerminalManager)().killAllForProject(id);
@@ -437,6 +443,10 @@ class ProjectRegistry {
437
443
  ctx.jiraSyncManager.stop();
438
444
  }
439
445
  catch { /* ignore */ }
446
+ try {
447
+ ctx.stuckRunDetector.stop();
448
+ }
449
+ catch { /* ignore */ }
440
450
  // Release chokidar watchers + abort in-flight generations so a restart
441
451
  // does not leak handles/children — mirror removeProject()'s per-project teardown.
442
452
  try {
@@ -509,6 +519,10 @@ class ProjectRegistry {
509
519
  projectId: project.id,
510
520
  projectSlug: project.slug,
511
521
  desktopPort: this._desktopPort,
522
+ // Global Specrails Agents defaults — resolved at spawn time from
523
+ // desktop.sqlite so a Settings change applies to the next job without
524
+ // restarting (mirrors the pipelineTelemetryEnabled read-at-spawn rule).
525
+ agentDefaults: (provider) => (0, agent_defaults_1.resolveAgentDefaults)(this._desktopDb, provider),
512
526
  onJobAdmission: (projectDb, job) => {
513
527
  const ticketIds = (0, ticket_store_1.extractTicketIdsFromCommand)(job.command);
514
528
  (0, rails_store_1.claimTicketOutcomeOwners)(projectDb, ticketIds, job.id);
@@ -899,6 +913,22 @@ class ProjectRegistry {
899
913
  // See resolveLoopBaseEnv.
900
914
  const loopRunManager = new loop_run_manager_1.LoopRunManager(db, boundBroadcast, (0, loop_executors_1.createLoopExecutors)({
901
915
  env: () => (0, workspace_resolution_1.resolveLoopBaseEnv)({ slug: project.slug, path: project.path }, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
916
+ // Global Specrails Agents defaults, loop path: per-agent model overrides
917
+ // ride SPECRAILS_PROFILE_PATH on each ai-step spawn. Resolved per step
918
+ // (no restart); inert (null) unless the global layer carries per-agent
919
+ // models for the provider — byte-identical legacy behaviour otherwise.
920
+ profilePathFor: (0, agent_defaults_1.createLoopProfilePathResolver)({
921
+ desktopDb: this._desktopDb,
922
+ profileRoot: () => (0, workspace_resolution_1.resolveProjectExecution)({ slug: project.slug, path: project.path }).cwd,
923
+ // Lazy require: several suites mock './queue-manager' with only the
924
+ // class export — a top-level named import would throw on module eval.
925
+ // The closure only runs at loop ai-step spawn time, never in tests
926
+ // that stub the queue.
927
+ supportsProfiles: (root) => {
928
+ const { projectSupportsProfiles } = require('./queue-manager');
929
+ return projectSupportsProfiles(root);
930
+ },
931
+ }),
902
932
  }));
903
933
  const getTicketSpec = (ticketId) => {
904
934
  try {
@@ -1081,7 +1111,9 @@ class ProjectRegistry {
1081
1111
  console.error(`[loops] terminal effects failed for ${runId}:`, err);
1082
1112
  }
1083
1113
  };
1084
- const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb };
1114
+ const stuckRunDetector = new stuck_run_detector_1.StuckRunDetector(project.id, { db }, boundBroadcast);
1115
+ stuckRunDetector.start();
1116
+ const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, stuckRunDetector, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb };
1085
1117
  this._contexts.set(project.id, ctx);
1086
1118
  const loopRecoveryOk = this._recoverOrphanLoopRuns(project, db, railLoopRuns, onLoopRunFinished, orphanLoopRuns);
1087
1119
  if (loopRecoveryOk) {
@@ -4,8 +4,11 @@ exports.registerLoopRunRoutes = registerLoopRunRoutes;
4
4
  const feature_flags_1 = require("./feature-flags");
5
5
  const loops_store_1 = require("./loops-store");
6
6
  const loop_runs_store_1 = require("./loop-runs-store");
7
+ const run_duration_stats_1 = require("./run-duration-stats");
7
8
  const loop_constants_1 = require("./loop-constants");
8
9
  const providers_1 = require("./providers");
10
+ const runtime_1 = require("./providers/runtime");
11
+ const agent_defaults_1 = require("./agent-defaults");
9
12
  const provider_selection_1 = require("./provider-selection");
10
13
  const spec_models_1 = require("./spec-models");
11
14
  const workspace_resolution_1 = require("./workspace-resolution");
@@ -40,6 +43,22 @@ function registerLoopRunRoutes(deps) {
40
43
  },
41
44
  });
42
45
  });
46
+ // Measured duration band for "runs like this have taken X–Y". Returns
47
+ // `range: null` below the sample floor — the caller MUST render nothing
48
+ // rather than a guess (honest-metrics contract; see run-duration-stats).
49
+ router.get('/:projectId/run-duration-range', (req, res) => {
50
+ const c = ctx(req);
51
+ const loopId = typeof req.query.loopId === 'string' ? req.query.loopId : null;
52
+ const command = typeof req.query.command === 'string' ? req.query.command : null;
53
+ if (!loopId && !command) {
54
+ res.status(400).json({ error: 'loopId or command is required' });
55
+ return;
56
+ }
57
+ const range = loopId
58
+ ? (0, run_duration_stats_1.getLoopDurationRange)(c.db, c.project.id, loopId)
59
+ : (0, run_duration_stats_1.getJobCommandDurationRange)(c.db, (0, run_duration_stats_1.jobCommandShape)(command));
60
+ res.json({ range, minSamples: run_duration_stats_1.MIN_DURATION_SAMPLES });
61
+ });
43
62
  router.post('/:projectId/loop-runs', (req, res) => {
44
63
  if (!(0, feature_flags_1.isLoopsEnabled)()) {
45
64
  res.status(404).json({ error: 'Not Found' });
@@ -89,7 +108,9 @@ function registerLoopRunRoutes(deps) {
89
108
  return;
90
109
  }
91
110
  // Optional explicit model — validated against the chosen provider's catalog
92
- // (mirrors Add Spec). Omitted ⇒ the provider's default.
111
+ // (mirrors Add Spec). Omitted ⇒ the global Specrails Agents default when
112
+ // set (read at launch time — no restart), else the provider's default.
113
+ const globalAgentDefaults = (0, agent_defaults_1.resolveAgentDefaults)(c.desktopDb, provider);
93
114
  let model;
94
115
  if (requestedModel !== undefined && requestedModel !== null) {
95
116
  if (!(0, spec_models_1.isValidModelForProvider)(requestedModel, provider)) {
@@ -99,7 +120,7 @@ function registerLoopRunRoutes(deps) {
99
120
  model = requestedModel;
100
121
  }
101
122
  else {
102
- model = adapter.defaultModel();
123
+ model = globalAgentDefaults?.pipelineModel ?? adapter.defaultModel();
103
124
  }
104
125
  let effort;
105
126
  if (reasoning_effort !== undefined && reasoning_effort !== null) {
@@ -114,6 +135,10 @@ function registerLoopRunRoutes(deps) {
114
135
  }
115
136
  effort = reasoning_effort;
116
137
  }
138
+ else if (globalAgentDefaults?.pipelineEffort
139
+ && (0, runtime_1.isReasoningEffortValidForModel)(adapter, model, globalAgentDefaults.pipelineEffort)) {
140
+ effort = globalAgentDefaults.pipelineEffort;
141
+ }
117
142
  const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: c.project.slug, path: c.project.path });
118
143
  const runId = (0, ids_1.newId)();
119
144
  c.loopRunManager
@@ -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,