specrails-desktop 2.61.0 → 2.63.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 (230) hide show
  1. package/cli/dist/specrails-desktop.js +39 -5
  2. package/client/dist/assets/ActivityFeedPage-Bn2MpG5M.js +1 -0
  3. package/client/dist/assets/{AgentBrowserCapture-CkNWDZT3.js → AgentBrowserCapture-CHlXWdXq.js} +1 -1
  4. package/client/dist/assets/AgentModeAnalyticsPane-yj-YCgrK.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-Xczcyghb.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-U1J6PaBo.js → AgentModeJobsPane-DtOW01ln.js} +1 -1
  7. package/client/dist/assets/AgentsPage-BMfJ4mzU.js +118 -0
  8. package/client/dist/assets/AnalyticsPage-CGn5cgtc.js +1 -0
  9. package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
  10. package/client/dist/assets/CodePage-C8NIginr.js +3 -0
  11. package/client/dist/assets/DesktopAnalyticsPage-C87r4y26.js +1 -0
  12. package/client/dist/assets/{DocsDialog-CSlMPemn.js → DocsDialog-Z50a_KPv.js} +1 -1
  13. package/client/dist/assets/{DocsPage-BKlYAvWq.js → DocsPage-DIArbEPl.js} +1 -1
  14. package/client/dist/assets/{ExportDropdown-CweP_1GP.js → ExportDropdown-CJdCqYd7.js} +1 -1
  15. package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
  16. package/client/dist/assets/InteractiveJobComposer-BxLh4me-.js +19 -0
  17. package/client/dist/assets/{JobDetailModal-DYyGq1Vp.js → JobDetailModal-C7dWoRu6.js} +1 -1
  18. package/client/dist/assets/JobDetailPage-Ilu294WS.js +1 -0
  19. package/client/dist/assets/JobsPage-fPq1hefb.js +1 -0
  20. package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +1 -0
  21. package/client/dist/assets/{LoopPreviewModal-DkLRfsYX.js → LoopPreviewModal-ChI3NQC7.js} +1 -1
  22. package/client/dist/assets/{LoopWindowSurface-CuACkXFV.js → LoopWindowSurface-D1WB9i-9.js} +1 -1
  23. package/client/dist/assets/LoopsPage-CCwc0Biu.js +2 -0
  24. package/client/dist/assets/MinimizedChatsContext-eH_KaAZ1.js +1 -0
  25. package/client/dist/assets/PluginsPage-DfDfEfbW.js +2 -0
  26. package/client/dist/assets/ProjectSettingsDialog-9NHnG8JQ.js +1 -0
  27. package/client/dist/assets/RepositoryDeliveries-DgzNnJmn.js +5 -0
  28. package/client/dist/assets/{RepositoryScopeSelector-CeCr6Jz-.js → RepositoryScopeSelector-BSgUqk_u.js} +1 -1
  29. package/client/dist/assets/ReviewPacketPage-D34bGpSA.js +4 -0
  30. package/client/dist/assets/{TemplatePreviewModal-B8yd1AGU.js → TemplatePreviewModal-CcfSxWQ7.js} +1 -1
  31. package/client/dist/assets/TicketDetailModalContext-BoafEcm4.js +16 -0
  32. package/client/dist/assets/Trans-CVSryAL1.js +1 -0
  33. package/client/dist/assets/agent-5bSixVzz.js +1 -0
  34. package/client/dist/assets/agent-BR4EHfhq.js +1 -0
  35. package/client/dist/assets/agent-C0IKkJP8.js +1 -0
  36. package/client/dist/assets/agent-C7li9ayC.js +1 -0
  37. package/client/dist/assets/agent-D0dRmEen.js +1 -0
  38. package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
  39. package/client/dist/assets/agent-Dip4h2YE.js +1 -0
  40. package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
  41. package/client/dist/assets/ban-wqfdzoXt.js +1 -0
  42. package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
  43. package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
  44. package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
  45. package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
  46. package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
  47. package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
  48. package/client/dist/assets/download-C9wP_42h.js +1 -0
  49. package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
  50. package/client/dist/assets/file-text-C0blZHLh.js +1 -0
  51. package/client/dist/assets/{formatDistanceToNow-DZ3VWsHq.js → formatDistanceToNow-NVM6rK3i.js} +1 -1
  52. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-9JiGlFrx.js → getTimezoneOffsetInMilliseconds-ZHg7WpdH.js} +1 -1
  53. package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
  54. package/client/dist/assets/index-Culan6y7.js +83 -0
  55. package/client/dist/assets/index-D1v33giW.css +2 -0
  56. package/client/dist/assets/jira-api-Bl8xEIpk.js +1 -0
  57. package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
  58. package/client/dist/assets/layers-BCLTgHXO.js +1 -0
  59. package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
  60. package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
  61. package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
  62. package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
  63. package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
  64. package/client/dist/assets/plus-fHDdjBk1.js +1 -0
  65. package/client/dist/assets/{project-repositories-CadLZOnr.js → project-repositories-CvJf1a6H.js} +1 -1
  66. package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
  67. package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
  68. package/client/dist/assets/save-LmPSfsvZ.js +1 -0
  69. package/client/dist/assets/search-BDVp3fkH.js +1 -0
  70. package/client/dist/assets/{settings-Dn-pXukr.js → settings-BO2N-oPf.js} +1 -1
  71. package/client/dist/assets/{settings-ByVE7jdZ.js → settings-BQChAs_w.js} +1 -1
  72. package/client/dist/assets/{settings-BBj_kZaD.js → settings-BzmYlcQB.js} +1 -1
  73. package/client/dist/assets/{settings-COlH_P33.js → settings-CGjXb6TX.js} +1 -1
  74. package/client/dist/assets/{settings-D77mC_Lb.js → settings-CMxEWY46.js} +1 -1
  75. package/client/dist/assets/{settings-DZKy7MqN.js → settings-C_dRSgI2.js} +1 -1
  76. package/client/dist/assets/{settings-B5Hy_Kol.js → settings-Hw7av-B1.js} +1 -1
  77. package/client/dist/assets/settings-nryN6wuS.js +1 -0
  78. package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
  79. package/client/dist/assets/spending-BfjSJjBd.js +1 -0
  80. package/client/dist/assets/square-D0JTbRSR.js +1 -0
  81. package/client/dist/assets/trash-D8zYUmTg.js +1 -0
  82. package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
  83. package/client/dist/assets/upload-CCHvUkRs.js +1 -0
  84. package/client/dist/assets/{useDesktop-DKNbOF0u.js → useDesktop-C5JVBeur.js} +1 -1
  85. package/client/dist/assets/{useRuntimeRuns-D3YMw9qx.js → useRuntimeRuns-DohBmeg8.js} +1 -1
  86. package/client/dist/assets/useSharedWebSocket-CNOvmgTr.js +2 -0
  87. package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
  88. package/client/dist/assets/x-Bu7X_eR2.js +1 -0
  89. package/client/dist/assets/zap-bugHyWzQ.js +1 -0
  90. package/client/dist/index.html +44 -41
  91. package/docs/ci-cd.md +8 -1
  92. package/docs/customizing.md +1 -0
  93. package/docs/getting-started.md +1 -1
  94. package/docs/guide/de/integrations/6-agent-chat.md +11 -0
  95. package/docs/guide/de/settings/5-project-environment.md +27 -0
  96. package/docs/guide/en/integrations/6-agent-chat.md +11 -0
  97. package/docs/guide/en/settings/5-project-environment.md +27 -0
  98. package/docs/guide/es/integrations/6-agent-chat.md +11 -0
  99. package/docs/guide/es/settings/5-project-environment.md +27 -0
  100. package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
  101. package/docs/guide/fr/settings/5-project-environment.md +27 -0
  102. package/docs/guide/it/integrations/6-agent-chat.md +11 -0
  103. package/docs/guide/it/settings/5-project-environment.md +27 -0
  104. package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
  105. package/docs/guide/ja/settings/5-project-environment.md +27 -0
  106. package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
  107. package/docs/guide/pt/settings/5-project-environment.md +27 -0
  108. package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
  109. package/docs/guide/zh/settings/5-project-environment.md +27 -0
  110. package/docs/internals/README.md +1 -0
  111. package/docs/internals/agent-sessions.md +157 -0
  112. package/docs/internals/api-reference.md +35 -4
  113. package/docs/internals/configuration.md +80 -5
  114. package/docs/internals/core-runtime-updates.md +10 -3
  115. package/docs/internals/operations-runbook.md +11 -0
  116. package/docs/internals/programmatic-agent-runtime.md +1 -1
  117. package/docs/internals/safe-pr-review-flow.md +15 -0
  118. package/docs/internals/source-map.md +42 -1
  119. package/docs/mcp.md +1 -1
  120. package/docs/platforms/macos.md +2 -0
  121. package/docs/platforms/windows-parity.md +4 -4
  122. package/docs/platforms/windows.md +9 -9
  123. package/local-runner/dist/specrails-local-runner.js +1 -1
  124. package/mcp-bridge/dist/specrails-mcp.js +82 -15
  125. package/package.json +8 -9
  126. package/server/dist/agent-mcp-config.js +41 -0
  127. package/server/dist/core-compat.js +2 -0
  128. package/server/dist/desktop-db.js +101 -4
  129. package/server/dist/desktop-router.js +31 -2
  130. package/server/dist/index.js +58 -0
  131. package/server/dist/mcp/tools/app-settings.js +11 -0
  132. package/server/dist/mcp/tools/env.js +38 -3
  133. package/server/dist/mcp/tools/mission.js +45 -2
  134. package/server/dist/mcp/tools/specs.js +2 -2
  135. package/server/dist/modules/accounting/runtime/result-event.js +9 -1
  136. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
  137. package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
  138. package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
  139. package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
  140. package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
  141. package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
  142. package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
  143. package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
  144. package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
  145. package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
  146. package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
  147. package/server/dist/modules/agent-sessions/index.js +31 -0
  148. package/server/dist/modules/agent-sessions/ports.js +2 -0
  149. package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
  150. package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
  151. package/server/dist/modules/agents/runtime/agent-store.js +3 -1
  152. package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
  153. package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
  154. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +15 -4
  155. package/server/dist/modules/loops/runtime/loop-run-manager.js +11 -0
  156. package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
  157. package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
  158. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
  159. package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
  160. package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
  161. package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
  162. package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
  163. package/server/dist/modules/project-settings/adapters/http.js +38 -2
  164. package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
  165. package/server/dist/modules/project-settings/domain.js +24 -0
  166. package/server/dist/modules/project-settings/index.js +2 -1
  167. package/server/dist/modules/specs/runtime/spec-models.js +1 -1
  168. package/server/dist/path-resolver.js +135 -20
  169. package/server/dist/plugins/codex-spawn.js +15 -7
  170. package/server/dist/project-env.js +265 -17
  171. package/server/dist/project-registry.js +46 -0
  172. package/server/dist/project-router-settings.js +24 -1
  173. package/server/dist/providers/claude-adapter.js +6 -1
  174. package/server/dist/worktree-node-modules.js +95 -1
  175. package/client/dist/assets/ActivityFeedPage-Wi5XNgoG.js +0 -1
  176. package/client/dist/assets/AgentModeAnalyticsPane-CXz2j-Bt.js +0 -2
  177. package/client/dist/assets/AgentModeCodePane-DnsBPtpS.js +0 -2
  178. package/client/dist/assets/AgentsPage-CXrqu5vo.js +0 -118
  179. package/client/dist/assets/AnalyticsPage-Cvtt-LOO.js +0 -1
  180. package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
  181. package/client/dist/assets/CodePage-D35o-MFO.js +0 -3
  182. package/client/dist/assets/DesktopAnalyticsPage-CsDFy5rp.js +0 -1
  183. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
  184. package/client/dist/assets/InteractiveJobComposer-CFKvbQeJ.js +0 -19
  185. package/client/dist/assets/JobDetailPage-C6gpdYvj.js +0 -1
  186. package/client/dist/assets/JobsPage-CgS0pnKd.js +0 -1
  187. package/client/dist/assets/LoopBuilderPage-jUp20DSZ.js +0 -1
  188. package/client/dist/assets/LoopsPage-ClSrWzDd.js +0 -2
  189. package/client/dist/assets/MinimizedChatsContext-CroAs6Kh.js +0 -1
  190. package/client/dist/assets/PluginsPage-nCKQOwRL.js +0 -2
  191. package/client/dist/assets/ProjectSettingsDialog-BVDYwllU.js +0 -1
  192. package/client/dist/assets/RepositoryDeliveries-DJCm1pZr.js +0 -5
  193. package/client/dist/assets/ReviewPacketPage-kTErrEjx.js +0 -4
  194. package/client/dist/assets/TicketDetailModalContext-YBJX3RmP.js +0 -16
  195. package/client/dist/assets/Trans-D5eOjfvG.js +0 -1
  196. package/client/dist/assets/agent--8QAfoXL.js +0 -1
  197. package/client/dist/assets/agent-3HxXkIWX.js +0 -1
  198. package/client/dist/assets/agent-CpFCz32T.js +0 -1
  199. package/client/dist/assets/agent-DVnw6ybO.js +0 -1
  200. package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
  201. package/client/dist/assets/agent-hv57-zyG.js +0 -1
  202. package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
  203. package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
  204. package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
  205. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
  206. package/client/dist/assets/download-CFHyyHcw.js +0 -1
  207. package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
  208. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  209. package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
  210. package/client/dist/assets/index-Bfunvi6w.css +0 -2
  211. package/client/dist/assets/index-lTGJJTuk.js +0 -83
  212. package/client/dist/assets/jira-api-Bp0zXwjg.js +0 -1
  213. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
  214. package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
  215. package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
  216. package/client/dist/assets/plus-CsnjfkmX.js +0 -1
  217. package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
  218. package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
  219. package/client/dist/assets/save-OsUqVn99.js +0 -1
  220. package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
  221. package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
  222. package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
  223. package/client/dist/assets/spending-DQDNtM8q.js +0 -1
  224. package/client/dist/assets/square-Ci-PFcFe.js +0 -1
  225. package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
  226. package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
  227. package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
  228. package/client/dist/assets/useSharedWebSocket-DaWuiO0S.js +0 -2
  229. package/client/dist/assets/x-CBMI9ejn.js +0 -1
  230. package/client/dist/assets/zap-CUOlvtT1.js +0 -1
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissionSessionProjector = void 0;
4
+ const desktop_db_1 = require("../../../desktop-db");
5
+ const result_event_1 = require("../../accounting/runtime/result-event");
6
+ const agent_store_1 = require("../../agents/runtime/agent-store");
7
+ const agent_session_store_1 = require("./agent-session-store");
8
+ /**
9
+ * Missions' ProjectionSink: persists what happens in a mission's Core session
10
+ * OUTSIDE user turns (which the mission manager settles itself): turns Core or
11
+ * policy started after sub-agents finished, the sub-agent tree and its output,
12
+ * and the resident phase. Every apply writes its rows and the cursor in one
13
+ * transaction; WebSocket messages are sent only after the commit.
14
+ */
15
+ class MissionSessionProjector {
16
+ conversationId;
17
+ deps;
18
+ background = new Map();
19
+ constructor(conversationId, deps) {
20
+ this.conversationId = conversationId;
21
+ this.deps = deps;
22
+ }
23
+ cursor(_sessionId) {
24
+ return (0, agent_session_store_1.getSessionCursor)(this.deps.db, this.conversationId)?.lastSeq ?? 0;
25
+ }
26
+ apply(_sessionId, seq, ops) {
27
+ const conversationId = this.conversationId;
28
+ const outbox = [];
29
+ const timestamp = new Date().toISOString();
30
+ let processEnded = false;
31
+ this.deps.db.transaction(() => {
32
+ for (const op of ops) {
33
+ switch (op.kind) {
34
+ case 'resident.phase': {
35
+ const before = (0, agent_session_store_1.getSessionCursor)(this.deps.db, conversationId);
36
+ (0, agent_session_store_1.setResidentState)(this.deps.db, conversationId, op.phase, op.processAlive, op.liveSubagents);
37
+ if (before?.processAlive && !op.processAlive)
38
+ processEnded = true;
39
+ outbox.push({ type: 'agent_resident_state', conversationId, phase: op.phase, liveSubagents: op.liveSubagents, processAlive: op.processAlive, timestamp });
40
+ break;
41
+ }
42
+ case 'turn.opened':
43
+ if (op.origin === 'user')
44
+ break;
45
+ this.background.set(op.turnId, { origin: op.origin, triggeredBy: op.triggeredBy, startedAt: op.at });
46
+ outbox.push({ type: 'agent_turn_started', conversationId, turnId: op.turnId, origin: op.origin, triggeredBy: op.triggeredBy, timestamp });
47
+ break;
48
+ case 'turn.delta':
49
+ if (op.channel === 'text' && this.background.has(op.turnId))
50
+ outbox.push({ type: 'agent_stream', conversationId, turnId: op.turnId, delta: op.delta, timestamp });
51
+ break;
52
+ case 'turn.tool':
53
+ if (!this.background.has(op.turnId))
54
+ break;
55
+ outbox.push(op.tool.phase === 'started'
56
+ ? { type: 'agent_tool', conversationId, turnId: op.turnId, tool: op.tool.name, toolId: op.tool.toolUseId, timestamp }
57
+ : { type: 'agent_tool_result', conversationId, turnId: op.turnId, toolId: op.tool.toolUseId, output: (op.tool.output ?? '').slice(0, 4000), ...(op.tool.isError ? { isError: true } : {}), timestamp });
58
+ break;
59
+ case 'turn.closed': {
60
+ const turn = this.background.get(op.turnId);
61
+ if (!turn)
62
+ break;
63
+ this.background.delete(op.turnId);
64
+ const conversation = (0, agent_store_1.getAgentConversation)(this.deps.db, conversationId);
65
+ if (!conversation)
66
+ break;
67
+ // A journal replay (projection rebuild) must not record a turn twice.
68
+ if ((0, agent_session_store_1.isCoreTurnRecorded)(this.deps.db, conversationId, op.turnId)) {
69
+ outbox.push({ type: 'agent_turn_done', conversationId, turnId: op.turnId, origin: turn.origin, status: op.status, triggeredBy: turn.triggeredBy, timestamp });
70
+ break;
71
+ }
72
+ const message = op.text.trim()
73
+ ? (0, agent_store_1.addAgentMessage)(this.deps.db, { conversationId, role: 'assistant', content: op.text, turnOrigin: turn.origin, coreTurnId: op.turnId })
74
+ : null;
75
+ this.recordInvocation(conversation, { id: (0, agent_session_store_1.coreTurnInvocationId)(conversation.id, op.turnId), providerId: conversation.provider, model: null, usage: op.usage, status: op.status, origin: turn.origin, startedAt: turn.startedAt, finishedAt: op.at });
76
+ outbox.push({ type: 'agent_turn_done', conversationId, turnId: op.turnId, origin: turn.origin, status: op.status, triggeredBy: turn.triggeredBy, fullText: op.text, ...(message ? { messageId: message.id } : {}), timestamp });
77
+ if (conversation.pinned_project_id)
78
+ outbox.push({ type: 'spending.invalidated', projectId: conversation.pinned_project_id });
79
+ break;
80
+ }
81
+ case 'subagent.upsert':
82
+ (0, agent_session_store_1.upsertSubagent)(this.deps.db, conversationId, op.subagent);
83
+ outbox.push({ type: 'agent_subagent', conversationId, subagent: op.subagent, timestamp });
84
+ break;
85
+ case 'subagent.output':
86
+ if (!this.subagentKnown(op.subagentId))
87
+ break;
88
+ (0, agent_session_store_1.appendSubagentEvent)(this.deps.db, conversationId, op.subagentId, op.seq, op.channel, op.delta, op.tool);
89
+ outbox.push({ type: 'agent_subagent_event', conversationId, subagentId: op.subagentId, seq: op.seq, channel: op.channel, ...(op.delta !== null ? { delta: op.delta } : {}), ...(op.tool ? { tool: op.tool } : {}), timestamp });
90
+ break;
91
+ case 'subagent.billed': {
92
+ // A delegated sub-agent's own spend: recorded once (deterministic id), on top of the parent's.
93
+ const conversation = (0, agent_store_1.getAgentConversation)(this.deps.db, conversationId);
94
+ if (!conversation)
95
+ break;
96
+ const invocationId = `core-subagent:${conversationId}:${op.subagentId}:${op.seq}`;
97
+ if (this.deps.db.prepare('SELECT 1 FROM agent_invocations WHERE id = ?').get(invocationId))
98
+ break;
99
+ const startedAt = this.subagentStartedAt(op.subagentId) ?? op.at;
100
+ this.recordInvocation(conversation, { id: invocationId, providerId: op.driver, model: op.model, usage: op.usage, status: 'completed', origin: 'subagent', startedAt, finishedAt: op.at });
101
+ if (conversation.pinned_project_id)
102
+ outbox.push({ type: 'spending.invalidated', projectId: conversation.pinned_project_id });
103
+ break;
104
+ }
105
+ case 'session.updated':
106
+ outbox.push({ type: 'agent_session_updated', conversationId, outcome: op.outcome, changes: op.changes, timestamp });
107
+ break;
108
+ case 'notice':
109
+ outbox.push({ type: 'agent_session_notice', conversationId, level: op.level, code: op.code, message: op.message, timestamp });
110
+ break;
111
+ case 'provider.ref':
112
+ case 'input.state':
113
+ case 'session.closed':
114
+ // User inputs, provider refs and closure are settled by the mission manager.
115
+ break;
116
+ }
117
+ }
118
+ (0, agent_session_store_1.advanceSessionCursor)(this.deps.db, conversationId, seq);
119
+ })();
120
+ for (const message of outbox)
121
+ this.deps.broadcast(message);
122
+ if (processEnded)
123
+ this.deps.onProcessEnded?.(conversationId);
124
+ }
125
+ subagentKnown(subagentId) {
126
+ return !!this.deps.db.prepare('SELECT 1 FROM agent_subagents WHERE conversation_id = ? AND subagent_id = ?').get(this.conversationId, subagentId);
127
+ }
128
+ subagentStartedAt(subagentId) {
129
+ const row = this.deps.db.prepare('SELECT started_at FROM agent_subagents WHERE conversation_id = ? AND subagent_id = ?').get(this.conversationId, subagentId);
130
+ return row?.started_at ?? null;
131
+ }
132
+ recordInvocation(conversation, invocation) {
133
+ let adapter;
134
+ try {
135
+ adapter = this.deps.adapterFor(invocation.providerId);
136
+ }
137
+ catch {
138
+ return;
139
+ }
140
+ const usage = invocation.usage;
141
+ const normalised = {
142
+ ...(usage.inputTokens !== null ? { tokens_in: usage.inputTokens } : {}),
143
+ ...(usage.outputTokens !== null ? { tokens_out: usage.outputTokens } : {}),
144
+ ...(usage.cacheReadTokens !== null ? { tokens_cache_read: usage.cacheReadTokens } : {}),
145
+ ...(usage.cacheWriteTokens !== null ? { tokens_cache_create: usage.cacheWriteTokens } : {}),
146
+ ...(usage.costUsd !== null && !usage.costEstimated ? { total_cost_usd: usage.costUsd } : {}),
147
+ ...(usage.model ?? invocation.model ? { model: usage.model ?? invocation.model } : {}),
148
+ };
149
+ const fallbackModel = invocation.model ?? (invocation.providerId === conversation.provider ? conversation.model : null);
150
+ const { result, estimated } = (0, result_event_1.finaliseNormalisedResult)(adapter, normalised, {
151
+ ...(fallbackModel ? { fallbackModel } : {}),
152
+ durationMs: Math.max(0, Date.parse(invocation.finishedAt) - Date.parse(invocation.startedAt)),
153
+ });
154
+ (0, desktop_db_1.recordAgentInvocation)(this.deps.db, {
155
+ id: invocation.id,
156
+ conversation_id: conversation.id,
157
+ project_id: conversation.pinned_project_id ?? null,
158
+ provider: adapter.id,
159
+ status: invocation.status === 'completed' ? 'success' : invocation.status === 'stopped' || invocation.status === 'interrupted' ? 'aborted' : 'failed',
160
+ started_at: invocation.startedAt,
161
+ finished_at: invocation.finishedAt,
162
+ total_cost_usd_estimated: estimated,
163
+ origin: invocation.origin,
164
+ ...result,
165
+ });
166
+ }
167
+ }
168
+ exports.MissionSessionProjector = MissionSessionProjector;
@@ -1,16 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerProjectSettingsHttp = registerProjectSettingsHttp;
4
+ exports.registerEnvPassthroughStatusHttp = registerEnvPassthroughStatusHttp;
4
5
  const __1 = require("..");
5
6
  /** Inbound adapter: translate HTTP into use cases and domain errors into HTTP. */
6
- function registerProjectSettingsHttp(router, serviceForRequest) {
7
+ function registerProjectSettingsHttp(router, serviceForRequest,
8
+ /** Observer for committed changes (e.g. open agent sessions re-reading policy). */
9
+ onUpdated) {
7
10
  router.get('/:projectId/settings', (req, res) => {
8
11
  res.json(serviceForRequest(req).getSettings());
9
12
  });
10
13
  router.patch('/:projectId/settings', (req, res) => {
11
14
  try {
12
- const settings = serviceForRequest(req).updateSettings(req.body);
15
+ const service = serviceForRequest(req);
16
+ const previous = onUpdated ? service.getSettings() : null;
17
+ const settings = service.updateSettings(req.body);
13
18
  res.json({ ok: true, settings });
19
+ if (onUpdated && previous) {
20
+ try {
21
+ onUpdated(req, settings, previous);
22
+ }
23
+ catch (error) {
24
+ console.error('[project-router] settings observer error:', error);
25
+ }
26
+ }
14
27
  }
15
28
  catch (error) {
16
29
  if (error instanceof __1.SettingsValidationError) {
@@ -22,3 +35,26 @@ function registerProjectSettingsHttp(router, serviceForRequest) {
22
35
  }
23
36
  });
24
37
  }
38
+ /** Inbound adapter for `GET /:projectId/env-passthrough/status` and
39
+ * `POST /:projectId/env-passthrough/recheck`. Responses carry names and
40
+ * states only; values never reach this adapter. */
41
+ function registerEnvPassthroughStatusHttp(router, port) {
42
+ router.get('/:projectId/env-passthrough/status', async (req, res) => {
43
+ try {
44
+ res.json(await port.read(req));
45
+ }
46
+ catch (error) {
47
+ console.error('[project-router] env status error:', error);
48
+ res.status(500).json({ error: 'Failed to read environment status' });
49
+ }
50
+ });
51
+ router.post('/:projectId/env-passthrough/recheck', async (req, res) => {
52
+ try {
53
+ res.json(await port.recheck(req));
54
+ }
55
+ catch (error) {
56
+ console.error('[project-router] env recheck error:', error);
57
+ res.status(500).json({ error: 'Failed to recheck environment' });
58
+ }
59
+ });
60
+ }
@@ -18,13 +18,25 @@ function parseWorktreeEnvPassthrough(raw) {
18
18
  return [];
19
19
  }
20
20
  }
21
+ /** A stored value that no longer parses (hand-edited, older shape) reads as "same as the mission agent". */
22
+ function readSubagentRuntime(raw) {
23
+ if (!raw)
24
+ return null;
25
+ try {
26
+ return (0, domain_1.parseSubagentRuntimeSetting)(JSON.parse(raw));
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
21
32
  function getProjectSettings(db) {
22
- // Read the settings as one snapshot instead of preparing six separate queries.
33
+ // Read the settings as one snapshot instead of preparing separate queries.
23
34
  const rows = db.prepare(`
24
35
  SELECT key, value FROM queue_state WHERE key IN (
25
36
  'config.pipeline_telemetry_enabled', 'config.orchestrator_model',
26
37
  'config.pre_prompt', 'config.freestyle_pre_prompt',
27
- 'config.integration_branch', 'config.worktree_env_passthrough'
38
+ 'config.integration_branch', 'config.worktree_env_passthrough',
39
+ 'config.allow_subagents', 'config.subagent_runtime'
28
40
  )
29
41
  `).all();
30
42
  const settings = new Map(rows.map(row => [row.key, row]));
@@ -42,6 +54,8 @@ function getProjectSettings(db) {
42
54
  freestylePrePrompt: freestylePrePromptRow?.value ?? '',
43
55
  integrationBranch: integrationBranchRow?.value ?? '',
44
56
  worktreeEnvPassthrough: parseWorktreeEnvPassthrough(worktreeEnvPassthroughRow?.value),
57
+ allowSubagents: settings.get('config.allow_subagents')?.value === 'true',
58
+ subagentRuntime: readSubagentRuntime(settings.get('config.subagent_runtime')?.value),
45
59
  };
46
60
  }
47
61
  /** Resolve the effective Freestyle pre-prompt: the per-project override when
@@ -81,6 +95,20 @@ function updateProjectSettings(db, patch) {
81
95
  db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.integration_branch', ?)`).run(patch.integrationBranch.trim());
82
96
  }
83
97
  }
98
+ if (patch.allowSubagents !== undefined) {
99
+ if (patch.allowSubagents) {
100
+ db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.allow_subagents', 'true')`).run();
101
+ }
102
+ else {
103
+ db.prepare(`DELETE FROM queue_state WHERE key = 'config.allow_subagents'`).run();
104
+ }
105
+ }
106
+ if (patch.subagentRuntime !== undefined) {
107
+ if (patch.subagentRuntime === null)
108
+ db.prepare(`DELETE FROM queue_state WHERE key = 'config.subagent_runtime'`).run();
109
+ else
110
+ db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.subagent_runtime', ?)`).run(JSON.stringify(patch.subagentRuntime));
111
+ }
84
112
  if (patch.worktreeEnvPassthrough !== undefined) {
85
113
  const names = (0, domain_1.normalizeWorktreeEnvPassthrough)(patch.worktreeEnvPassthrough);
86
114
  if (names.length === 0) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SettingsValidationError = exports.WORKTREE_ENV_NAME_RE = exports.DEFAULT_FREESTYLE_PRE_PROMPT = void 0;
4
+ exports.parseSubagentRuntimeSetting = parseSubagentRuntimeSetting;
4
5
  exports.normalizeWorktreeEnvPassthrough = normalizeWorktreeEnvPassthrough;
5
6
  exports.parseProjectSettingsPatch = parseProjectSettingsPatch;
6
7
  const git_branch_name_1 = require("../../shared/git-branch-name");
@@ -18,6 +19,22 @@ exports.DEFAULT_FREESTYLE_PRE_PROMPT = [
18
19
  'Never ask for confirmation; there is no human to answer. Choose the recommended option and proceed.',
19
20
  'Run every command in the FOREGROUND and wait for it to finish — never background a command (no run_in_background, no trailing `&`): your reply ends the step and anything still running is killed with its output lost.',
20
21
  ].join('\n');
22
+ const PROVIDER_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
23
+ /** Strict shape check shared by project and app settings; null clears the choice. */
24
+ function parseSubagentRuntimeSetting(value) {
25
+ if (value === null)
26
+ return null;
27
+ const input = value;
28
+ if (!input || typeof input !== 'object' || Array.isArray(input))
29
+ throw new SettingsValidationError('subagentRuntime must be an object or null');
30
+ if (typeof input.provider !== 'string' || !PROVIDER_ID_RE.test(input.provider))
31
+ throw new SettingsValidationError('subagentRuntime.provider must be a provider id');
32
+ if (typeof input.model !== 'string' || !input.model.trim() || input.model.length > 200)
33
+ throw new SettingsValidationError('subagentRuntime.model must be a model name');
34
+ if (input.effort !== undefined && input.effort !== null && (typeof input.effort !== 'string' || !input.effort.trim() || input.effort.length > 50))
35
+ throw new SettingsValidationError('subagentRuntime.effort must be an effort level or null');
36
+ return { provider: input.provider, model: input.model.trim(), effort: typeof input.effort === 'string' ? input.effort.trim() : null };
37
+ }
21
38
  exports.WORKTREE_ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
39
  const WORKTREE_ENV_MAX_NAMES = 64;
23
40
  const WORKTREE_ENV_MAX_NAME_LENGTH = 128;
@@ -82,6 +99,13 @@ function parseProjectSettingsPatch(input) {
82
99
  throw new SettingsValidationError(error instanceof Error ? error.message : 'invalid worktreeEnvPassthrough');
83
100
  }
84
101
  }
102
+ if (values.subagentRuntime !== undefined)
103
+ patch.subagentRuntime = parseSubagentRuntimeSetting(values.subagentRuntime);
104
+ if (values.allowSubagents !== undefined) {
105
+ if (typeof values.allowSubagents !== 'boolean')
106
+ throw new SettingsValidationError('allowSubagents must be a boolean');
107
+ patch.allowSubagents = values.allowSubagents;
108
+ }
85
109
  if (values.integrationBranch !== undefined) {
86
110
  const branch = values.integrationBranch;
87
111
  if (typeof branch !== 'string')
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SettingsValidationError = exports.normalizeWorktreeEnvPassthrough = exports.WORKTREE_ENV_NAME_RE = exports.DEFAULT_FREESTYLE_PRE_PROMPT = exports.createProjectSettingsService = void 0;
3
+ exports.SettingsValidationError = exports.parseSubagentRuntimeSetting = exports.normalizeWorktreeEnvPassthrough = exports.WORKTREE_ENV_NAME_RE = exports.DEFAULT_FREESTYLE_PRE_PROMPT = exports.createProjectSettingsService = void 0;
4
4
  // Public application API. Infrastructure adapters are wired only at composition roots.
5
5
  var application_1 = require("./application");
6
6
  Object.defineProperty(exports, "createProjectSettingsService", { enumerable: true, get: function () { return application_1.createProjectSettingsService; } });
@@ -8,4 +8,5 @@ var domain_1 = require("./domain");
8
8
  Object.defineProperty(exports, "DEFAULT_FREESTYLE_PRE_PROMPT", { enumerable: true, get: function () { return domain_1.DEFAULT_FREESTYLE_PRE_PROMPT; } });
9
9
  Object.defineProperty(exports, "WORKTREE_ENV_NAME_RE", { enumerable: true, get: function () { return domain_1.WORKTREE_ENV_NAME_RE; } });
10
10
  Object.defineProperty(exports, "normalizeWorktreeEnvPassthrough", { enumerable: true, get: function () { return domain_1.normalizeWorktreeEnvPassthrough; } });
11
+ Object.defineProperty(exports, "parseSubagentRuntimeSetting", { enumerable: true, get: function () { return domain_1.parseSubagentRuntimeSetting; } });
11
12
  Object.defineProperty(exports, "SettingsValidationError", { enumerable: true, get: function () { return domain_1.SettingsValidationError; } });
@@ -9,7 +9,7 @@ exports.CLAUDE_MODELS = [
9
9
  { value: 'sonnet', label: 'Sonnet 5.5' },
10
10
  { value: 'fable', label: 'Fable 5.1' },
11
11
  { value: 'opus', label: 'Claude Opus' },
12
- { value: 'haiku', label: 'Claude Haiku' },
12
+ { value: 'haiku', label: 'Claude Haiku 5.5' },
13
13
  ];
14
14
  exports.CODEX_MODELS = [
15
15
  { value: 'gpt-6.1-sol', label: 'GPT-6.1 Sol' },
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AUTH_ENV_VARS = void 0;
6
+ exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = exports.AUTH_ENV_VARS = void 0;
7
7
  exports.ensureWindowsBaseEnv = ensureWindowsBaseEnv;
8
8
  exports.resolveBundledRuntimePath = resolveBundledRuntimePath;
9
9
  exports.resolveBundledNodeExe = resolveBundledNodeExe;
@@ -12,6 +12,10 @@ exports.parseLoginShellOutput = parseLoginShellOutput;
12
12
  exports.augmentPathFromLoginShell = augmentPathFromLoginShell;
13
13
  exports.parseLoginShellEnv = parseLoginShellEnv;
14
14
  exports.augmentEnvFromLoginShell = augmentEnvFromLoginShell;
15
+ exports.resolveLoginShellProbeTimeoutMs = resolveLoginShellProbeTimeoutMs;
16
+ exports.hasCompleteLoginShellEnvBlock = hasCompleteLoginShellEnvBlock;
17
+ exports.readEnvFromLoginShell = readEnvFromLoginShell;
18
+ exports.readEnvFromLoginShellSyncDetailed = readEnvFromLoginShellSyncDetailed;
15
19
  exports.readEnvFromLoginShellSync = readEnvFromLoginShellSync;
16
20
  exports.augmentEnvFromLoginShellSync = augmentEnvFromLoginShellSync;
17
21
  exports.augmentAuthEnvFromLoginShell = augmentAuthEnvFromLoginShell;
@@ -580,17 +584,130 @@ async function augmentEnvFromLoginShell(names, opts = {}) {
580
584
  });
581
585
  });
582
586
  }
583
- /** Read missing configured names without mutating the caller's or global env.
584
- * Callers own any cache so project credentials cannot leak across scopes. */
585
- function readEnvFromLoginShellSync(names, opts = {}) {
587
+ /** Default budget for the asynchronous project-env probe. Real interactive
588
+ * profiles (oh-my-zsh, nvm, powerlevel10k, conda) routinely exceed 1.5 s. */
589
+ exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = 10_000;
590
+ const LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS = 120_000;
591
+ const LOGIN_SHELL_OUTPUT_CAP = 1024 * 1024;
592
+ /** `SPECRAILS_LOGIN_SHELL_TIMEOUT_MS` overrides the asynchronous probe budget.
593
+ * Invalid or non-positive values fall back to the 10 s default; values are
594
+ * capped at 2 minutes so a typo cannot park a probe forever. */
595
+ function resolveLoginShellProbeTimeoutMs(env = process.env) {
596
+ const raw = env.SPECRAILS_LOGIN_SHELL_TIMEOUT_MS?.trim();
597
+ if (!raw || !/^\d+$/.test(raw))
598
+ return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
599
+ const value = Number(raw);
600
+ if (!Number.isFinite(value) || value <= 0)
601
+ return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
602
+ return Math.min(value, LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS);
603
+ }
604
+ /** True when the probe output contains the complete env sentinel block. The
605
+ * block is the proof that the printf ran, independent of the shell's status. */
606
+ function hasCompleteLoginShellEnvBlock(stdout) {
607
+ const begin = stdout.indexOf(ENV_BEGIN);
608
+ return begin !== -1 && stdout.indexOf(ENV_END, begin + ENV_BEGIN.length) !== -1;
609
+ }
610
+ function loginShellProbeSkipped(names = []) {
611
+ return { values: {}, status: 'skipped', exitCode: null, shell: null, names };
612
+ }
613
+ function pickRecovered(stdout, wanted) {
614
+ const recovered = parseLoginShellEnv(stdout);
615
+ const out = {};
616
+ for (const key of wanted) {
617
+ const val = recovered[key];
618
+ if (val)
619
+ out[key] = val;
620
+ }
621
+ return out;
622
+ }
623
+ function settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut) {
624
+ if (hasCompleteLoginShellEnvBlock(stdout)) {
625
+ return { values: pickRecovered(stdout, wanted), status: 'ok', exitCode, shell, names: wanted };
626
+ }
627
+ return { values: {}, status: timedOut ? 'timeout' : 'failed', exitCode, shell, names: wanted };
628
+ }
629
+ function wantedLoginShellNames(names, env) {
586
630
  if (process.platform === 'win32')
587
- return {};
631
+ return null;
588
632
  if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true')
589
- return {};
633
+ return null;
634
+ return normalizeLoginShellEnvNames(names).filter((name) => !env[name]);
635
+ }
636
+ /** Asynchronously read missing configured names from the login shell without
637
+ * blocking the event loop or mutating any environment. Values from a complete
638
+ * sentinel block are accepted regardless of the shell's exit status. Output is
639
+ * capped at 1 MiB; the child is killed at the timeout. Callers own any cache. */
640
+ function readEnvFromLoginShell(names, opts = {}) {
590
641
  const env = opts.env ?? process.env;
591
- const wanted = normalizeLoginShellEnvNames(names).filter((name) => !env[name]);
642
+ const wanted = wantedLoginShellNames(names, env);
643
+ if (wanted === null)
644
+ return Promise.resolve(loginShellProbeSkipped());
592
645
  if (wanted.length === 0)
593
- return {};
646
+ return Promise.resolve(loginShellProbeSkipped(wanted));
647
+ const spawnFn = opts.spawnFn ?? child_process_1.spawn;
648
+ const timeoutMs = opts.timeoutMs ?? resolveLoginShellProbeTimeoutMs(env);
649
+ const shell = resolveLoginShell(env);
650
+ const command = buildLoginShellEnvCommand(wanted);
651
+ return new Promise((resolve) => {
652
+ let child;
653
+ try {
654
+ child = spawnFn(shell, ['-l', '-i', '-c', command], { env, stdio: ['ignore', 'pipe', 'pipe'] });
655
+ }
656
+ catch {
657
+ resolve({ values: {}, status: 'failed', exitCode: null, shell, names: wanted });
658
+ return;
659
+ }
660
+ let stdout = '';
661
+ let timedOut = false;
662
+ let settled = false;
663
+ const kill = () => { try {
664
+ child.kill('SIGKILL');
665
+ }
666
+ catch { /* ignore */ } };
667
+ const timer = setTimeout(() => {
668
+ timedOut = true;
669
+ kill();
670
+ // Settle now: a background job started by the profile can keep the pipes
671
+ // (and so 'close') open long after the shell itself was killed. A complete
672
+ // block printed before a hung exit hook is still a success.
673
+ finish(null);
674
+ }, timeoutMs);
675
+ const finish = (exitCode) => {
676
+ if (settled)
677
+ return;
678
+ settled = true;
679
+ clearTimeout(timer);
680
+ resolve(settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut));
681
+ };
682
+ child.stdout?.on('data', (chunk) => {
683
+ if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP)
684
+ return;
685
+ stdout += chunk.toString('utf-8');
686
+ if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP) {
687
+ stdout = stdout.slice(0, LOGIN_SHELL_OUTPUT_CAP);
688
+ if (!hasCompleteLoginShellEnvBlock(stdout))
689
+ kill();
690
+ }
691
+ });
692
+ child.stderr?.on('data', () => { });
693
+ child.on('error', () => finish(null));
694
+ child.on('close', (code) => finish(typeof code === 'number' ? code : null));
695
+ });
696
+ }
697
+ function isSpawnSyncTimeout(res) {
698
+ const error = res.error;
699
+ return Boolean(error && (error.code === 'ETIMEDOUT' || /ETIMEDOUT/.test(error.message)));
700
+ }
701
+ /** Synchronous, detailed variant used only as the cold fallback at spawn time.
702
+ * Keeps the short 1.5 s budget so a cold spawn never freezes the server for
703
+ * long, and accepts a complete sentinel block regardless of exit status. */
704
+ function readEnvFromLoginShellSyncDetailed(names, opts = {}) {
705
+ const env = opts.env ?? process.env;
706
+ const wanted = wantedLoginShellNames(names, env);
707
+ if (wanted === null)
708
+ return loginShellProbeSkipped();
709
+ if (wanted.length === 0)
710
+ return loginShellProbeSkipped(wanted);
594
711
  const spawnSyncFn = opts.spawnSyncFn ?? child_process_1.spawnSync;
595
712
  const timeoutMs = opts.timeoutMs ?? LOGIN_SHELL_TIMEOUT_MS;
596
713
  const shell = resolveLoginShell(env);
@@ -602,23 +719,21 @@ function readEnvFromLoginShellSync(names, opts = {}) {
602
719
  encoding: 'utf8',
603
720
  stdio: ['ignore', 'pipe', 'pipe'],
604
721
  timeout: timeoutMs,
605
- maxBuffer: 1024 * 1024,
722
+ // An interactive shell can ignore SIGTERM; the cold fallback must stay bounded.
723
+ killSignal: 'SIGKILL',
724
+ maxBuffer: LOGIN_SHELL_OUTPUT_CAP,
606
725
  });
607
726
  }
608
727
  catch {
609
- return {};
728
+ return { values: {}, status: 'failed', exitCode: null, shell, names: wanted };
610
729
  }
611
- if (res.error || res.status !== 0)
612
- return {};
613
730
  const stdout = typeof res.stdout === 'string' ? res.stdout : res.stdout?.toString('utf8') ?? '';
614
- const recovered = parseLoginShellEnv(stdout);
615
- const out = {};
616
- for (const key of wanted) {
617
- const val = recovered[key];
618
- if (val)
619
- out[key] = val;
620
- }
621
- return out;
731
+ return settleLoginShellProbe(stdout, wanted, shell, typeof res.status === 'number' ? res.status : null, isSpawnSyncTimeout(res));
732
+ }
733
+ /** Read missing configured names without mutating the caller's or global env.
734
+ * Callers own any cache so project credentials cannot leak across scopes. */
735
+ function readEnvFromLoginShellSync(names, opts = {}) {
736
+ return readEnvFromLoginShellSyncDetailed(names, opts).values;
622
737
  }
623
738
  /** Compatibility backfill for callers that intentionally augment process.env.
624
739
  * Failed probes remain retryable; successful names are already present. */
@@ -3,18 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveCodexPluginMcpServers = resolveCodexPluginMcpServers;
6
7
  exports.buildCodexPluginArgs = buildCodexPluginArgs;
7
8
  const path_1 = __importDefault(require("path"));
8
9
  const manager_1 = require("./manager");
9
- /** Materialize app-installed plugins in the actual Codex invocation. The
10
+ /** MCP servers of app-installed plugins active for this Codex invocation. The
10
11
  * legacy per-project CODEX_HOME is an install record, not the provider's
11
12
  * runtime home: switching HOME would discard the user's login and settings. */
12
- function buildCodexPluginArgs(options) {
13
+ function resolveCodexPluginMcpServers(options) {
13
14
  if (options.providerId !== 'codex')
14
15
  return [];
15
16
  const manager = (0, manager_1.getPluginManager)();
16
17
  const state = manager.getProjectState(options.stateRoot);
17
- const args = [];
18
+ const servers = [];
18
19
  for (const [name, installed] of Object.entries(state.plugins ?? {})) {
19
20
  if (!installed || typeof installed !== 'object')
20
21
  continue;
@@ -30,7 +31,6 @@ function buildCodexPluginArgs(options) {
30
31
  // owned keys or overwrite Specrails' authenticated bridge.
31
32
  if (keys.length !== 1 || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(keys[0]) || keys[0] === 'specrails')
32
33
  continue;
33
- const server = keys[0];
34
34
  const serverArgs = [...entry.args];
35
35
  // Serena's '.' must mean this invocation's source/worktree, even when
36
36
  // the CLI starts in an app-managed conversation or artifact directory.
@@ -39,9 +39,17 @@ function buildCodexPluginArgs(options) {
39
39
  if (project >= 0)
40
40
  serverArgs[project + 1] = path_1.default.resolve(options.repositoryPath);
41
41
  }
42
- const prefix = `mcp_servers.${server}`;
43
- args.push('-c', `${prefix}.command=${JSON.stringify(entry.command)}`, '-c', `${prefix}.args=${JSON.stringify(serverArgs)}`, '-c', `${prefix}.enabled=true`);
44
- for (const [key, value] of Object.entries(entry.env ?? {})) {
42
+ servers.push({ name: keys[0], command: entry.command, args: serverArgs, ...(entry.env && Object.keys(entry.env).length ? { env: { ...entry.env } } : {}) });
43
+ }
44
+ return servers;
45
+ }
46
+ /** Materialize app-installed plugins in a spawned Codex CLI invocation. */
47
+ function buildCodexPluginArgs(options) {
48
+ const args = [];
49
+ for (const server of resolveCodexPluginMcpServers(options)) {
50
+ const prefix = `mcp_servers.${server.name}`;
51
+ args.push('-c', `${prefix}.command=${JSON.stringify(server.command)}`, '-c', `${prefix}.args=${JSON.stringify(server.args)}`, '-c', `${prefix}.enabled=true`);
52
+ for (const [key, value] of Object.entries(server.env ?? {})) {
45
53
  args.push('-c', `${prefix}.env.${JSON.stringify(key)}=${JSON.stringify(value)}`);
46
54
  }
47
55
  }