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
@@ -0,0 +1,359 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AgentDefaultsValidationError = exports.GLOBAL_DEFAULTS_PROFILE_NAME = exports.AGENT_DEFAULTS_SETTING_KEY = void 0;
37
+ exports.readAgentDefaultsSettings = readAgentDefaultsSettings;
38
+ exports.applyAgentDefaultsPatch = applyAgentDefaultsPatch;
39
+ exports.resolveAgentDefaults = resolveAgentDefaults;
40
+ exports.mergeProfileWithAgentDefaults = mergeProfileWithAgentDefaults;
41
+ exports.synthesizeProfileFromDefaults = synthesizeProfileFromDefaults;
42
+ exports.ensureGlobalProfileSnapshot = ensureGlobalProfileSnapshot;
43
+ exports.buildAgentDefaultsCatalog = buildAgentDefaultsCatalog;
44
+ exports.createLoopProfilePathResolver = createLoopProfilePathResolver;
45
+ /**
46
+ * Global Specrails Agents defaults — app-level (per-machine) per-provider
47
+ * customization of the specrails-core pipeline agents: the pipeline
48
+ * (orchestrator) model + reasoning effort, and per-agent model overrides for
49
+ * providers whose runtime honours execution profiles (claude, kimi).
50
+ *
51
+ * Storage is one JSON blob under `desktop_settings['specrails_agent_defaults']`
52
+ * (no migration — plain k/v). Every consumer reads AT SPAWN/LAUNCH TIME, so a
53
+ * settings change applies to the next run without restarting the app.
54
+ *
55
+ * Layering contract (least → most specific):
56
+ * built-in adapter default < GLOBAL agent defaults < per-project setting
57
+ * (orchestrator model, resolved model) < per-project profile < explicit
58
+ * per-launch selection.
59
+ * The global layer therefore only ever fills gaps — it never overrides an
60
+ * explicit user choice made closer to the run.
61
+ */
62
+ const fs = __importStar(require("fs"));
63
+ const os = __importStar(require("os"));
64
+ const path = __importStar(require("path"));
65
+ const crypto_1 = require("crypto");
66
+ const desktop_db_1 = require("./desktop-db");
67
+ const providers_1 = require("./providers");
68
+ const runtime_1 = require("./providers/runtime");
69
+ exports.AGENT_DEFAULTS_SETTING_KEY = 'specrails_agent_defaults';
70
+ /** Synthetic profile name recorded when a run's profile came purely from the
71
+ * global defaults layer (no project profile existed). */
72
+ exports.GLOBAL_DEFAULTS_PROFILE_NAME = 'global-defaults';
73
+ /** Agent ids the v1 settings surface may override — the baseline pipeline trio
74
+ * every adapter guarantees (`adapter.baselineAgents()`). */
75
+ const AGENT_ID_PATTERN = /^(sr|custom)-[a-z0-9][a-z0-9-]*$/;
76
+ class AgentDefaultsValidationError extends Error {
77
+ code;
78
+ constructor(code, message) {
79
+ super(message);
80
+ this.code = code;
81
+ this.name = 'AgentDefaultsValidationError';
82
+ }
83
+ }
84
+ exports.AgentDefaultsValidationError = AgentDefaultsValidationError;
85
+ const EMPTY_SETTINGS = { version: 1, providers: {} };
86
+ function isRecord(value) {
87
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
88
+ }
89
+ /** Structural sanitize for values read from disk — never throws, drops junk. */
90
+ function sanitizeStored(raw) {
91
+ if (!isRecord(raw) || !isRecord(raw.providers))
92
+ return { ...EMPTY_SETTINGS, providers: {} };
93
+ const providers = {};
94
+ for (const [id, value] of Object.entries(raw.providers)) {
95
+ if (!isRecord(value))
96
+ continue;
97
+ const entry = { custom: value.custom === true };
98
+ if (typeof value.pipelineModel === 'string' && value.pipelineModel)
99
+ entry.pipelineModel = value.pipelineModel;
100
+ if (typeof value.pipelineEffort === 'string' && value.pipelineEffort)
101
+ entry.pipelineEffort = value.pipelineEffort;
102
+ if (isRecord(value.agentModels)) {
103
+ const agents = {};
104
+ for (const [agentId, model] of Object.entries(value.agentModels)) {
105
+ if (AGENT_ID_PATTERN.test(agentId) && typeof model === 'string' && model)
106
+ agents[agentId] = model;
107
+ }
108
+ if (Object.keys(agents).length > 0)
109
+ entry.agentModels = agents;
110
+ }
111
+ providers[id] = entry;
112
+ }
113
+ return { version: 1, providers };
114
+ }
115
+ /** Read + sanitize the stored settings. Never throws. */
116
+ function readAgentDefaultsSettings(db) {
117
+ try {
118
+ const raw = (0, desktop_db_1.getDesktopSetting)(db, exports.AGENT_DEFAULTS_SETTING_KEY);
119
+ if (!raw)
120
+ return { ...EMPTY_SETTINGS, providers: {} };
121
+ return sanitizeStored(JSON.parse(raw));
122
+ }
123
+ catch {
124
+ return { ...EMPTY_SETTINGS, providers: {} };
125
+ }
126
+ }
127
+ /**
128
+ * Validate ONE provider's config from an untrusted PATCH body.
129
+ * Throws `AgentDefaultsValidationError` with an API-stable `code`.
130
+ */
131
+ function validateProviderPatch(providerId, value) {
132
+ let adapter;
133
+ try {
134
+ adapter = (0, providers_1.getAdapter)(providerId);
135
+ }
136
+ catch {
137
+ throw new AgentDefaultsValidationError('unknown_provider', `Unknown provider "${providerId}"`);
138
+ }
139
+ if (!isRecord(value)) {
140
+ throw new AgentDefaultsValidationError('invalid_provider_config', `Config for "${providerId}" must be an object`);
141
+ }
142
+ const entry = { custom: value.custom === true };
143
+ if (value.pipelineModel !== undefined && value.pipelineModel !== null && value.pipelineModel !== '') {
144
+ if (!(0, runtime_1.isModelAvailableForAdapter)(adapter, value.pipelineModel)) {
145
+ throw new AgentDefaultsValidationError('invalid_model', `Model is not valid for provider "${providerId}"`);
146
+ }
147
+ entry.pipelineModel = value.pipelineModel;
148
+ }
149
+ if (value.pipelineEffort !== undefined && value.pipelineEffort !== null && value.pipelineEffort !== '') {
150
+ const effortModel = entry.pipelineModel ?? adapter.defaultModel();
151
+ if (!(0, runtime_1.isReasoningEffortValidForModel)(adapter, effortModel, value.pipelineEffort)) {
152
+ throw new AgentDefaultsValidationError('invalid_effort', `Effort is not valid for provider "${providerId}" and model "${effortModel}"`);
153
+ }
154
+ entry.pipelineEffort = value.pipelineEffort;
155
+ }
156
+ if (value.agentModels !== undefined && value.agentModels !== null) {
157
+ if (!isRecord(value.agentModels)) {
158
+ throw new AgentDefaultsValidationError('invalid_agent_models', 'agentModels must be an object');
159
+ }
160
+ const agents = {};
161
+ for (const [agentId, model] of Object.entries(value.agentModels)) {
162
+ if (model === undefined || model === null || model === '')
163
+ continue;
164
+ if (adapter.capabilities.profiles !== true) {
165
+ throw new AgentDefaultsValidationError('per_agent_not_supported', `Provider "${providerId}" does not support per-agent model overrides`);
166
+ }
167
+ if (!adapter.baselineAgents().includes(agentId)) {
168
+ throw new AgentDefaultsValidationError('unknown_agent', `Unknown agent "${agentId}" for provider "${providerId}"`);
169
+ }
170
+ if (!(0, runtime_1.isModelAvailableForAdapter)(adapter, model)) {
171
+ throw new AgentDefaultsValidationError('invalid_agent_model', `Model for "${agentId}" is not valid for provider "${providerId}"`);
172
+ }
173
+ agents[agentId] = model;
174
+ }
175
+ if (Object.keys(agents).length > 0)
176
+ entry.agentModels = agents;
177
+ }
178
+ return entry;
179
+ }
180
+ /**
181
+ * Apply a PATCH body: every provider key present REPLACES that provider's
182
+ * stored config wholesale (deterministic; no deep-merge ambiguity). Returns
183
+ * the canonical stored settings. Throws `AgentDefaultsValidationError`.
184
+ */
185
+ function applyAgentDefaultsPatch(db, body) {
186
+ if (!isRecord(body) || !isRecord(body.providers)) {
187
+ throw new AgentDefaultsValidationError('invalid_body', "body must include a 'providers' object");
188
+ }
189
+ const next = readAgentDefaultsSettings(db);
190
+ for (const [providerId, value] of Object.entries(body.providers)) {
191
+ next.providers[providerId] = validateProviderPatch(providerId, value);
192
+ }
193
+ (0, desktop_db_1.setDesktopSetting)(db, exports.AGENT_DEFAULTS_SETTING_KEY, JSON.stringify(next));
194
+ return next;
195
+ }
196
+ /**
197
+ * Resolve the effective global defaults for one provider — validated against
198
+ * the adapter's live catalog so a stale stored model degrades to "no override"
199
+ * (fail-open to built-in defaults) instead of poisoning a spawn.
200
+ * Returns null when the layer is inactive or nothing survives validation.
201
+ */
202
+ function resolveAgentDefaults(db, providerId) {
203
+ let adapter;
204
+ try {
205
+ adapter = (0, providers_1.getAdapter)(providerId);
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ const entry = readAgentDefaultsSettings(db).providers[providerId];
211
+ if (!entry?.custom)
212
+ return null;
213
+ const pipelineModel = entry.pipelineModel && (0, runtime_1.isModelAvailableForAdapter)(adapter, entry.pipelineModel) ? entry.pipelineModel : null;
214
+ const pipelineEffort = entry.pipelineEffort && adapter.capabilities.supportsReasoningEffort === true ? entry.pipelineEffort : null;
215
+ const agentModels = {};
216
+ if (adapter.capabilities.profiles === true && entry.agentModels) {
217
+ for (const [agentId, model] of Object.entries(entry.agentModels)) {
218
+ if (adapter.baselineAgents().includes(agentId)
219
+ && (0, runtime_1.isModelAvailableForAdapter)(adapter, model)) {
220
+ agentModels[agentId] = model;
221
+ }
222
+ }
223
+ }
224
+ if (!pipelineModel && !pipelineEffort && Object.keys(agentModels).length === 0)
225
+ return null;
226
+ return { pipelineModel, pipelineEffort, agentModels };
227
+ }
228
+ /**
229
+ * Fill a resolved profile's gaps from the global layer: only agents WITHOUT an
230
+ * explicit model get one. The profile's own choices (including orchestrator)
231
+ * always win. Returns the original object untouched when nothing changed.
232
+ */
233
+ function mergeProfileWithAgentDefaults(profile, defaults) {
234
+ let changed = false;
235
+ const agents = profile.agents.map((agent) => {
236
+ if (!agent.model && defaults.agentModels[agent.id]) {
237
+ changed = true;
238
+ return { ...agent, model: defaults.agentModels[agent.id] };
239
+ }
240
+ return agent;
241
+ });
242
+ if (!changed)
243
+ return { changed: false, profile };
244
+ return { changed: true, profile: { ...profile, agents } };
245
+ }
246
+ /** Baseline-trio profile synthesized purely from the global layer (used when
247
+ * the project has no profile of its own). */
248
+ function synthesizeProfileFromDefaults(adapter, defaults) {
249
+ return {
250
+ schemaVersion: 1,
251
+ name: exports.GLOBAL_DEFAULTS_PROFILE_NAME,
252
+ description: 'Synthesized from the app-level Specrails Agents defaults',
253
+ provider: adapter.id,
254
+ orchestrator: { model: defaults.pipelineModel ?? adapter.defaultModel() },
255
+ agents: adapter.baselineAgents().map((id) => ({
256
+ id,
257
+ ...(defaults.agentModels[id] ? { model: defaults.agentModels[id] } : {}),
258
+ })),
259
+ routing: [{ default: true, agent: 'sr-developer' }],
260
+ };
261
+ }
262
+ /**
263
+ * Content-addressed snapshot for the LOOP path (no per-job dir exists when the
264
+ * engine spawns ai-steps). Identical content reuses the same immutable file, so
265
+ * concurrent runs never race and a settings change lands in a NEW file — an
266
+ * in-flight run keeps the profile it resolved. Never throws to the caller.
267
+ */
268
+ function ensureGlobalProfileSnapshot(providerId, profile, baseDir = path.join(os.homedir(), '.specrails', 'agent-defaults')) {
269
+ const body = JSON.stringify(profile, null, 2) + '\n';
270
+ const hash = (0, crypto_1.createHash)('sha256').update(body).digest('hex').slice(0, 12);
271
+ const file = path.join(baseDir, `${providerId}-${hash}.json`);
272
+ if (fs.existsSync(file))
273
+ return file;
274
+ fs.mkdirSync(baseDir, { recursive: true });
275
+ const tmp = path.join(baseDir, `.${providerId}-${hash}.${process.pid}.tmp`);
276
+ fs.writeFileSync(tmp, body, { encoding: 'utf8', mode: 0o444 });
277
+ try {
278
+ fs.renameSync(tmp, file);
279
+ }
280
+ catch {
281
+ // A concurrent writer won the rename — the content is identical by hash.
282
+ try {
283
+ fs.unlinkSync(tmp);
284
+ }
285
+ catch { /* best-effort */ }
286
+ }
287
+ return file;
288
+ }
289
+ /** Catalog payload for the settings UI — every REGISTERED provider (detection
290
+ * state is a client concern; undetected providers render non-clickable). */
291
+ function buildAgentDefaultsCatalog() {
292
+ return (0, providers_1.listAdapters)().map((adapter) => {
293
+ const models = adapter.modelCatalog().map((m) => ({ ...m }));
294
+ const effortsByModel = {};
295
+ for (const m of models) {
296
+ effortsByModel[m.value] = [...(0, runtime_1.reasoningEffortsForModel)(adapter, m.value)];
297
+ }
298
+ return {
299
+ id: adapter.id,
300
+ displayName: adapter.displayName,
301
+ models,
302
+ defaultModel: adapter.defaultModel(),
303
+ baselineAgents: [...adapter.baselineAgents()],
304
+ perAgentModels: adapter.capabilities.profiles === true,
305
+ supportsEffort: adapter.capabilities.supportsReasoningEffort === true,
306
+ customModelAliases: adapter.capabilities.customModelAliases === true,
307
+ effortsByModel,
308
+ };
309
+ });
310
+ }
311
+ /**
312
+ * Loop-path profile resolver factory. Returns `(provider) => snapshotPath|null`
313
+ * for injection as `SPECRAILS_PROFILE_PATH` on loop ai-step spawns.
314
+ *
315
+ * Deliberately inert unless the global layer carries per-agent models for the
316
+ * provider — when inactive (or when the project profile already pins every
317
+ * agent) it returns null and the run behaves byte-identically to today (core
318
+ * resolves `.specrails/profiles/project-default.json` on its own).
319
+ */
320
+ function createLoopProfilePathResolver(io) {
321
+ return (provider) => {
322
+ try {
323
+ const defaults = resolveAgentDefaults(io.desktopDb, provider);
324
+ if (!defaults || Object.keys(defaults.agentModels).length === 0)
325
+ return null;
326
+ const adapter = (0, providers_1.getAdapter)(provider);
327
+ if (adapter.capabilities.profiles !== true || adapter.capabilities.profileEnvSupport !== true)
328
+ return null;
329
+ const root = io.profileRoot();
330
+ if (!io.supportsProfiles(root))
331
+ return null;
332
+ let base = null;
333
+ try {
334
+ const resolve = io.resolveProfile
335
+ ?? ((r, e, p) => require('./profile-manager').resolveProfile(r, e, p));
336
+ base = resolve(root, undefined, provider);
337
+ }
338
+ catch {
339
+ base = null;
340
+ }
341
+ let effective = null;
342
+ if (base) {
343
+ const merged = mergeProfileWithAgentDefaults(base.profile, defaults);
344
+ // Nothing to add ⇒ stay out of the way (core's own file fallback rules).
345
+ effective = merged.changed ? merged.profile : null;
346
+ }
347
+ else {
348
+ effective = synthesizeProfileFromDefaults(adapter, defaults);
349
+ }
350
+ if (!effective)
351
+ return null;
352
+ return ensureGlobalProfileSnapshot(provider, effective, io.snapshotDir);
353
+ }
354
+ catch (err) {
355
+ console.warn(`[agent-defaults] loop profile resolution failed for ${provider}: ${err.message}`);
356
+ return null;
357
+ }
358
+ };
359
+ }
@@ -10,6 +10,7 @@ exports.mergeSpecrailsIntoWorkspaceMcp = mergeSpecrailsIntoWorkspaceMcp;
10
10
  exports.buildAgentMcpArgs = buildAgentMcpArgs;
11
11
  exports.removeAgentCapabilityFile = removeAgentCapabilityFile;
12
12
  exports.prepareAgentMcp = prepareAgentMcp;
13
+ exports.syncExternalServersIntoJsonFile = syncExternalServersIntoJsonFile;
13
14
  const fs_1 = __importDefault(require("fs"));
14
15
  const path_1 = __importDefault(require("path"));
15
16
  const os_1 = __importDefault(require("os"));
@@ -18,6 +19,7 @@ const path_resolver_1 = require("./path-resolver");
18
19
  const win_spawn_1 = require("./util/win-spawn");
19
20
  const agent_tier_1 = require("./agent-tier");
20
21
  const providers_1 = require("./providers");
22
+ const external_mcp_1 = require("./external-mcp");
21
23
  const AGENT_CONVERSATION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
22
24
  // ─── Agent MCP wiring (design D8 / D1) ────────────────────────────────────────
23
25
  //
@@ -141,7 +143,13 @@ function buildAgentMcpArgs(opts) {
141
143
  return [];
142
144
  const dir = agentDir(opts.conversationId);
143
145
  const file = path_1.default.join(dir, 'mcp.json');
144
- fs_1.default.writeFileSync(file, JSON.stringify({ mcpServers: { specrails: entry } }, null, 2), { mode: 0o600 });
146
+ const mcpServers = { specrails: entry };
147
+ for (const server of opts.external ?? []) {
148
+ if (server.name === 'specrails')
149
+ continue;
150
+ mcpServers[server.name] = server.config;
151
+ }
152
+ fs_1.default.writeFileSync(file, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
145
153
  try {
146
154
  fs_1.default.chmodSync(file, 0o600);
147
155
  }
@@ -228,6 +236,27 @@ function codexMcpOverrides(entry) {
228
236
  }
229
237
  return args;
230
238
  }
239
+ /** External-server variant of the codex `-c` overrides. Callers pre-filter with
240
+ * `isCodexInjectable` (TOML-safe name + string command). */
241
+ function codexExternalOverrides(server) {
242
+ const c = (kv) => ['-c', kv];
243
+ const command = String(server.config.command);
244
+ const argsList = Array.isArray(server.config.args)
245
+ ? server.config.args.filter((a) => typeof a === 'string')
246
+ : [];
247
+ const args = [
248
+ ...c(`mcp_servers.${server.name}.command=${JSON.stringify(command)}`),
249
+ ...c(`mcp_servers.${server.name}.args=[${argsList.map((a) => JSON.stringify(a)).join(', ')}]`),
250
+ ];
251
+ const env = server.config.env;
252
+ if (env && typeof env === 'object' && !Array.isArray(env)) {
253
+ for (const [k, v] of Object.entries(env)) {
254
+ if (typeof v === 'string')
255
+ args.push(...c(`mcp_servers.${server.name}.env.${k}=${JSON.stringify(v)}`));
256
+ }
257
+ }
258
+ return args;
259
+ }
231
260
  /**
232
261
  * Prepares the specrails MCP for the given provider and returns the spawn
233
262
  * extraArgs + env to merge. Returns empty wiring when the bridge is unavailable
@@ -235,6 +264,7 @@ function codexMcpOverrides(entry) {
235
264
  */
236
265
  function prepareAgentMcp(opts) {
237
266
  const adapter = (0, providers_1.getAdapter)(opts.adapterId);
267
+ const external = (opts.external ?? []).filter((s) => s.name !== 'specrails');
238
268
  if (adapter.id === 'claude') {
239
269
  return { extraArgs: buildAgentMcpArgs(opts), env: {} };
240
270
  }
@@ -243,12 +273,14 @@ function prepareAgentMcp(opts) {
243
273
  return { extraArgs: [], env: {} };
244
274
  if (adapter.mcpRegistration === 'cli-add') {
245
275
  // Inline -c overrides contain only the capability FILE PATH, never its secret.
246
- return { extraArgs: codexMcpOverrides(entry), env: {} };
276
+ const externalArgs = external.filter(external_mcp_1.isCodexInjectable).flatMap(codexExternalOverrides);
277
+ return { extraArgs: [...codexMcpOverrides(entry), ...externalArgs], env: {} };
247
278
  }
248
279
  // Project-json providers write only their native project config in the
249
280
  // conversation-isolated cwd.
250
281
  const file = adapter.projectMcpPath?.(opts.cwd) ?? path_1.default.join(opts.cwd, '.mcp.json');
251
282
  mergeServerIntoJsonFile(file, entry);
283
+ syncExternalServersIntoJsonFile(file, external);
252
284
  if (adapter.id === 'gemini') {
253
285
  // gemini-cli has NEVER read .mcp.json (a Claude convention) — its only MCP
254
286
  // surface is `mcpServers` in settings.json (user or <cwd>/.gemini project
@@ -286,3 +318,63 @@ function mergeServerIntoJsonFile(file, entry) {
286
318
  }
287
319
  catch { /* best-effort */ }
288
320
  }
321
+ /**
322
+ * Sync the user's EXTERNAL servers into a persistent settings file (gemini
323
+ * `.gemini/settings.json`, kimi `.kimi-code/mcp.json`) with owned-key hygiene:
324
+ * a sidecar (`external-owned.json`, same dir) records every key the app wrote,
325
+ * and each spawn removes the previously-owned set before merging the currently
326
+ * enabled one — so disabling an entry removes it on the next spawn while keys
327
+ * the app never wrote (and `specrails`, managed separately) are untouched.
328
+ */
329
+ function syncExternalServersIntoJsonFile(file, external) {
330
+ const dir = path_1.default.dirname(file);
331
+ const sidecar = path_1.default.join(dir, 'external-owned.json');
332
+ let owned = [];
333
+ try {
334
+ const parsed = JSON.parse(fs_1.default.readFileSync(sidecar, 'utf-8'));
335
+ if (parsed && Array.isArray(parsed.owned)) {
336
+ owned = parsed.owned.filter((n) => typeof n === 'string');
337
+ }
338
+ }
339
+ catch {
340
+ /* first spawn / no sidecar yet */
341
+ }
342
+ // Nothing owned before and nothing to inject now → leave the file alone.
343
+ if (owned.length === 0 && external.length === 0)
344
+ return;
345
+ let current = {};
346
+ try {
347
+ if (fs_1.default.existsSync(file)) {
348
+ const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
349
+ if (parsed && typeof parsed === 'object')
350
+ current = parsed;
351
+ }
352
+ }
353
+ catch {
354
+ current = {};
355
+ }
356
+ const mcpServers = { ...(current.mcpServers ?? {}) };
357
+ for (const name of owned) {
358
+ if (name !== 'specrails')
359
+ delete mcpServers[name];
360
+ }
361
+ const nextOwned = [];
362
+ for (const server of external) {
363
+ if (server.name === 'specrails')
364
+ continue;
365
+ mcpServers[server.name] = server.config;
366
+ nextOwned.push(server.name);
367
+ }
368
+ const next = { ...current, mcpServers };
369
+ fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
370
+ fs_1.default.writeFileSync(file, JSON.stringify(next, null, 2), { mode: 0o600 });
371
+ try {
372
+ fs_1.default.chmodSync(file, 0o600);
373
+ }
374
+ catch { /* best-effort */ }
375
+ fs_1.default.writeFileSync(sidecar, JSON.stringify({ owned: nextOwned }, null, 2), { mode: 0o600 });
376
+ try {
377
+ fs_1.default.chmodSync(sidecar, 0o600);
378
+ }
379
+ catch { /* best-effort */ }
380
+ }
@@ -69,6 +69,10 @@ coding pipelines over them.
69
69
  \`mcp_specrails_<canonical name>\` (e.g. \`mcp_specrails_specrails_specs\`).
70
70
  Use whatever form your tool list shows; they are identical tools. They are
71
71
  MCP tools, NOT shell commands or files — never hunt for them on disk.
72
+ - Additional USER-CONFIGURED tools (external MCP servers) may also appear in
73
+ your tool list. Use them for their own domains when helpful, but ALL
74
+ Specrails app operations (specs, rails, jobs, projects, …) MUST still go
75
+ through the \`specrails_*\` tools — never through a lookalike external tool.
72
76
  - Target a project with \`specrails_select_project\` (or the \`projectId\`
73
77
  argument). If none is pinned ("Home") and the request is project-specific, ASK
74
78
  whether to create a project or search across all — do not guess.
@@ -428,11 +432,19 @@ summary later.
428
432
  relaunch strategy even when \`main\` did not have that spec before the PR. If
429
433
  asked why a strategy was chosen, verify against active PR contents before
430
434
  answering.
431
- - A published PR card (\`decision:'pr_ready'\`) is STILL an open PR continuation
432
- target, not a blocker. If the user asks for modifications on that PR, assign
433
- the same spec(s) to a rail and launch again; do NOT tell them they must publish,
434
- discard, or merge first. \`pr_decision_pending\` should only block unresolved
435
- deliveries with no continuable PR head yet.
435
+ - **"Change something about work already delivered" = a REVISION launch.** When
436
+ the user asks for a modification to a delivery that is awaiting their decision
437
+ (any non-terminal card: \`on_review\`, \`pr_draft\`, \`pr_ready\`, \`no_changes\`,
438
+ \`implementation_failed\`), call \`specrails_rails(launch)\` with
439
+ \`revisionOfDeliveryId\` = that card's \`prDeliveryId\` and \`revisionNote\` = what
440
+ they asked for, in their own words. Do NOT tell them to publish, discard or
441
+ merge first, and do NOT relaunch the ordinary implement/batch mode: a revision
442
+ runs a dedicated Architect-less loop that builds on the existing branch instead
443
+ of re-planning from scratch. The rail must still carry exactly that delivery's
444
+ specs; a mismatch returns \`invalid_revision_target\`, which means re-check the
445
+ rail's spec assignment rather than retrying blindly.
446
+ - \`pr_decision_pending\` therefore only blocks a launch that is neither a
447
+ revision nor a continuation of an open PR head.
436
448
  - Launch proposal shape: tickets (ids + titles), rail number, mode, engine and
437
449
  model/profile, plus "runs for minutes and costs money". If the API mode is
438
450
  \`freestyle\`, write "Freestyle" to the user. Wait for yes.
@@ -495,13 +495,50 @@ class PlaywrightPageHandle {
495
495
  // target=_blank, OAuth login windows). The popup lives in the SAME browser
496
496
  // context — cookies and the window.opener/postMessage relationship are
497
497
  // intact, which is exactly what an OAuth redirect/popup flow needs.
498
+ //
499
+ // HARDENING (Okta-class IdPs): a popup opened from a cross-origin IFRAME
500
+ // (out-of-process sign-in widgets) is attributed to the iframe's target,
501
+ // and the page-level 'popup' event can miss it — the login window then
502
+ // "never appears". So we ALSO watch the shared context for new pages whose
503
+ // opener chain resolves to this page and adopt those. `seen` dedupes the
504
+ // two doors; pages from other sessions have a different opener and are
505
+ // ignored.
498
506
  try {
499
- this.page.on('popup', (p) => {
507
+ const seen = new WeakSet();
508
+ const adopt = (p) => {
509
+ if (seen.has(p))
510
+ return;
511
+ seen.add(p);
500
512
  try {
501
513
  cb(new PlaywrightPageHandle(p));
502
514
  }
503
515
  catch { /* never break the event loop */ }
504
- });
516
+ };
517
+ this.page.on('popup', adopt);
518
+ const ctx = this.page.context?.();
519
+ if (ctx?.on) {
520
+ const onContextPage = (p) => {
521
+ void (async () => {
522
+ try {
523
+ if (seen.has(p) || p === this.page)
524
+ return;
525
+ const opener = await p.opener?.();
526
+ if (opener === this.page)
527
+ adopt(p);
528
+ }
529
+ catch { /* ignore */ }
530
+ })();
531
+ };
532
+ ctx.on('page', onContextPage);
533
+ // The shared context outlives this page — detach on page close so a
534
+ // long-lived app never accumulates dead listeners.
535
+ this.page.on('close', () => {
536
+ try {
537
+ ctx.off?.('page', onContextPage);
538
+ }
539
+ catch { /* ignore */ }
540
+ });
541
+ }
505
542
  }
506
543
  catch { /* ignore */ }
507
544
  }
package/server/dist/db.js CHANGED
@@ -1464,6 +1464,43 @@ const MIGRATIONS = [
1464
1464
  ON agent_tests(provider, agent_name)
1465
1465
  `);
1466
1466
  },
1467
+ // Migration 56: nontech-review-experience Wave 1 — two additive nullable
1468
+ // JSON columns on rail_pr_deliveries. `spec_snapshot` freezes each covered
1469
+ // ticket's title/description/labels at LAUNCH so the review packet renders
1470
+ // what the user actually asked (the live store can be edited mid-run).
1471
+ // `settle_evidence` persists the deterministic settle-time harvest (the
1472
+ // VERIFICATION sentinel, the verify step's output tail, the reviewer's
1473
+ // confidence-score.json) captured BEFORE worktree release — the packet's
1474
+ // proof sections survive worktree teardown. NULL on legacy rows = honest
1475
+ // "not captured". Additive + idempotent (guarded by PRAGMA table_info).
1476
+ (db) => {
1477
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1478
+ .map((r) => r.name);
1479
+ if (!cols.includes('spec_snapshot')) {
1480
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_snapshot TEXT`);
1481
+ }
1482
+ if (!cols.includes('settle_evidence')) {
1483
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN settle_evidence TEXT`);
1484
+ }
1485
+ },
1486
+ // Migration 57: nontech-review-experience Wave 3 — revision metadata on the
1487
+ // NEW delivery generation a revision creates. `revision_note` is the user's
1488
+ // one-sentence instruction (so packet v2 can show what was asked to change)
1489
+ // and `revision_of` points at the generation being revised. Deliberately NOT
1490
+ // a new `decision` value: an unknown decision makes older clients drop the
1491
+ // whole PR card, and the shipped generation/supersession model already
1492
+ // expresses "a newer attempt replaced the previous one". Additive +
1493
+ // idempotent; NULL on every pre-existing row and on ordinary launches.
1494
+ (db) => {
1495
+ const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
1496
+ .map((r) => r.name);
1497
+ if (!cols.includes('revision_note')) {
1498
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_note TEXT`);
1499
+ }
1500
+ if (!cols.includes('revision_of')) {
1501
+ db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_of TEXT`);
1502
+ }
1503
+ },
1467
1504
  ];
1468
1505
  function applyMigrations(db) {
1469
1506
  // Ensure the migrations table exists (migration 1 creates it, but we need
@@ -2019,6 +2056,7 @@ function getProjectSettings(db) {
2019
2056
  return {
2020
2057
  pipelineTelemetryEnabled: telemetryRow?.value === 'true',
2021
2058
  orchestratorModel: modelRow?.value ?? 'sonnet',
2059
+ orchestratorModelExplicit: modelRow?.value !== undefined,
2022
2060
  prePrompt: prePromptRow?.value ?? '',
2023
2061
  freestylePrePrompt: freestylePrePromptRow?.value ?? '',
2024
2062
  integrationBranch: integrationBranchRow?.value ?? '',