specrails-desktop 2.60.0 → 2.62.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 (190) hide show
  1. package/client/dist/assets/ActivityFeedPage-C_pqc281.js +1 -0
  2. package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-vz6rbDtu.js} +1 -1
  3. package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +2 -0
  5. package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-99fBBLri.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CTwm_6T8.js} +3 -3
  7. package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-YD8MLoPa.js} +1 -1
  8. package/client/dist/assets/CodePage-CLJn2F2e.js +3 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-MWGQVbwp.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-XUCYKoq0.js} +1 -1
  11. package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-dOLC8bXA.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-BvKQgwM9.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +19 -0
  14. package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-CkiubrkZ.js} +1 -1
  15. package/client/dist/assets/JobDetailPage-CNe4Wn7l.js +1 -0
  16. package/client/dist/assets/JobsPage-DSts_L_d.js +1 -0
  17. package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +1 -0
  18. package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-Dv3dxHr_.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-DN2lbSnL.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-DHgIreEW.js} +2 -2
  21. package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +1 -0
  22. package/client/dist/assets/PluginsPage-D1EJ9oja.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +1 -0
  24. package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-CW4eKG3x.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-DrtPqvc_.js} +1 -1
  26. package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +4 -0
  27. package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-DcFlhUxy.js} +1 -1
  28. package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +16 -0
  29. package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-CAY2Rd2f.js} +1 -1
  30. package/client/dist/assets/agent-5bSixVzz.js +1 -0
  31. package/client/dist/assets/agent-BR4EHfhq.js +1 -0
  32. package/client/dist/assets/agent-C0IKkJP8.js +1 -0
  33. package/client/dist/assets/agent-C7li9ayC.js +1 -0
  34. package/client/dist/assets/agent-D0dRmEen.js +1 -0
  35. package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
  36. package/client/dist/assets/agent-Dip4h2YE.js +1 -0
  37. package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
  38. package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
  39. package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
  40. package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
  41. package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
  42. package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
  43. package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
  44. package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
  45. package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
  46. package/client/dist/assets/ban-E4EaZe3a.js +1 -0
  47. package/client/dist/assets/chevron-up-CwGUHd6D.js +1 -0
  48. package/client/dist/assets/dist-Bq10nS2m.js +1 -0
  49. package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DlQY1Lm_.js} +1 -1
  50. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-COUeq65G.js} +1 -1
  51. package/client/dist/assets/index-BacrO_ub.css +2 -0
  52. package/client/dist/assets/index-CONHvEJw.js +83 -0
  53. package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-C94VNHNY.js} +1 -1
  54. package/client/dist/assets/layers-eDMjG41I.js +1 -0
  55. package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
  56. package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
  57. package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
  58. package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
  59. package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
  60. package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
  61. package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
  62. package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
  63. package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
  64. package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
  65. package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
  66. package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
  67. package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
  68. package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
  69. package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
  70. package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
  71. package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-Cc3oNv8X.js} +1 -1
  72. package/client/dist/assets/save-B6wGWwBR.js +1 -0
  73. package/client/dist/assets/{settings-Dn-pXukr.js → settings-BKJZ2KEM.js} +1 -1
  74. package/client/dist/assets/{settings-BBj_kZaD.js → settings-BV3jocIx.js} +1 -1
  75. package/client/dist/assets/{settings-COlH_P33.js → settings-BjM2Dw8_.js} +1 -1
  76. package/client/dist/assets/settings-CKu0LTib.js +1 -0
  77. package/client/dist/assets/{settings-D77mC_Lb.js → settings-DA77M48m.js} +1 -1
  78. package/client/dist/assets/{settings-ByVE7jdZ.js → settings-LfFnl1UH.js} +1 -1
  79. package/client/dist/assets/{settings-DZKy7MqN.js → settings-P1IQvhnL.js} +1 -1
  80. package/client/dist/assets/{settings-B5Hy_Kol.js → settings-eIMJjyK_.js} +1 -1
  81. package/client/dist/assets/{spending-Oov7p52A.js → spending-DemeFfrH.js} +1 -1
  82. package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-9Lwoel_8.js} +1 -1
  83. package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-DL0xYoZ2.js} +1 -1
  84. package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-BEHPeU1Y.js} +2 -2
  85. package/client/dist/assets/zap-CHo3aCD1.js +1 -0
  86. package/client/dist/index.html +26 -22
  87. package/docs/guide/de/integrations/6-agent-chat.md +11 -0
  88. package/docs/guide/en/integrations/6-agent-chat.md +11 -0
  89. package/docs/guide/es/integrations/6-agent-chat.md +11 -0
  90. package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
  91. package/docs/guide/it/integrations/6-agent-chat.md +11 -0
  92. package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
  93. package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
  94. package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
  95. package/docs/internals/README.md +1 -0
  96. package/docs/internals/agent-sessions.md +157 -0
  97. package/docs/internals/api-reference.md +33 -4
  98. package/docs/internals/configuration.md +36 -0
  99. package/docs/internals/core-runtime-updates.md +10 -3
  100. package/docs/internals/operations-runbook.md +11 -0
  101. package/docs/internals/programmatic-agent-runtime.md +2 -0
  102. package/docs/internals/source-map.md +40 -1
  103. package/mcp-bridge/dist/specrails-mcp.js +82 -15
  104. package/package.json +1 -1
  105. package/server/dist/agent-mcp-config.js +41 -0
  106. package/server/dist/core-compat.js +2 -0
  107. package/server/dist/desktop-db.js +101 -4
  108. package/server/dist/desktop-router.js +31 -2
  109. package/server/dist/index.js +58 -0
  110. package/server/dist/mcp/tools/app-settings.js +11 -0
  111. package/server/dist/mcp/tools/mission.js +45 -2
  112. package/server/dist/mcp/tools/specs.js +2 -2
  113. package/server/dist/modules/accounting/runtime/result-event.js +9 -1
  114. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
  115. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
  116. package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
  117. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
  118. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
  119. package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
  120. package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
  121. package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
  122. package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
  123. package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
  124. package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
  125. package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
  126. package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
  127. package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
  128. package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
  129. package/server/dist/modules/agent-sessions/index.js +31 -0
  130. package/server/dist/modules/agent-sessions/ports.js +2 -0
  131. package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
  132. package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
  133. package/server/dist/modules/agents/runtime/agent-store.js +3 -1
  134. package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
  135. package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
  136. package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
  137. package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
  138. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +41 -11
  139. package/server/dist/modules/loops/runtime/loop-run-manager.js +4 -0
  140. package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
  141. package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
  142. package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
  143. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
  144. package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
  145. package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
  146. package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
  147. package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
  148. package/server/dist/modules/project-settings/adapters/http.js +14 -2
  149. package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
  150. package/server/dist/modules/project-settings/domain.js +24 -0
  151. package/server/dist/modules/project-settings/index.js +2 -1
  152. package/server/dist/modules/specs/runtime/spec-models.js +1 -1
  153. package/server/dist/plugins/codex-spawn.js +15 -7
  154. package/server/dist/project-registry.js +32 -0
  155. package/server/dist/project-router-settings.js +7 -1
  156. package/server/dist/providers/claude-adapter.js +6 -1
  157. package/server/dist/schemas/agent-runtime.schema.json +9 -7
  158. package/client/dist/assets/ActivityFeedPage-3J7fR8yB.js +0 -1
  159. package/client/dist/assets/AgentModeAnalyticsPane-Dps30tk9.js +0 -2
  160. package/client/dist/assets/AgentModeCodePane-CvLimey8.js +0 -2
  161. package/client/dist/assets/CodePage-D981zRHy.js +0 -3
  162. package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
  163. package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +0 -1
  164. package/client/dist/assets/JobsPage-BSrl8P7u.js +0 -1
  165. package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
  166. package/client/dist/assets/MinimizedChatsContext-BD2bsRBe.js +0 -1
  167. package/client/dist/assets/PluginsPage-B8f49Mvn.js +0 -2
  168. package/client/dist/assets/ProjectSettingsDialog-D7q-O6h3.js +0 -1
  169. package/client/dist/assets/ReviewPacketPage-DpZSzF-Q.js +0 -4
  170. package/client/dist/assets/TicketDetailModalContext-BHmtj1As.js +0 -16
  171. package/client/dist/assets/agent--8QAfoXL.js +0 -1
  172. package/client/dist/assets/agent-3HxXkIWX.js +0 -1
  173. package/client/dist/assets/agent-CpFCz32T.js +0 -1
  174. package/client/dist/assets/agent-DVnw6ybO.js +0 -1
  175. package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
  176. package/client/dist/assets/agent-hv57-zyG.js +0 -1
  177. package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
  178. package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
  179. package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
  180. package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
  181. package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
  182. package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
  183. package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
  184. package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
  185. package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
  186. package/client/dist/assets/index-Bfunvi6w.css +0 -2
  187. package/client/dist/assets/index-g8S62tpS.js +0 -83
  188. package/client/dist/assets/save-OsUqVn99.js +0 -1
  189. package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
  190. package/client/dist/assets/zap-CUOlvtT1.js +0 -1
@@ -19,6 +19,8 @@ const desktop_router_1 = require("./desktop-router");
19
19
  const provider_detection_1 = require("./provider-detection");
20
20
  const provider_selection_1 = require("./provider-selection");
21
21
  const desktop_db_1 = require("./desktop-db");
22
+ const sqlite_1 = require("./modules/project-settings/adapters/sqlite");
23
+ const agent_sessions_1 = require("./modules/agent-sessions");
22
24
  const legacy_migration_1 = require("./legacy-migration");
23
25
  const framework_reseed_1 = require("./framework-reseed");
24
26
  const project_router_1 = require("./project-router");
@@ -31,6 +33,13 @@ const feature_flags_1 = require("./feature-flags");
31
33
  const mobile_1 = require("./mobile");
32
34
  const mcp_1 = require("./mcp");
33
35
  const agent_chat_manager_1 = require("./modules/missions/runtime/agent-chat-manager");
36
+ const mission_core_sessions_1 = require("./modules/missions/runtime/mission-core-sessions");
37
+ const session_host_registry_1 = require("./modules/agent-sessions/runtime/session-host-registry");
38
+ const http_2 = require("./modules/agent-sessions/adapters/http");
39
+ const host_process_1 = require("./modules/agent-sessions/adapters/host-process");
40
+ const core_sessions_availability_1 = require("./modules/agent-sessions/runtime/core-sessions-availability");
41
+ const registry_1 = require("./providers/registry");
42
+ const agent_capability_1 = require("./mcp/agent-capability");
34
43
  const agent_chat_router_1 = require("./modules/missions/runtime/agent-chat-router");
35
44
  const blueprint_chat_manager_1 = require("./modules/builder/runtime/blueprint-chat-manager");
36
45
  const blueprint_router_1 = require("./modules/builder/runtime/blueprint-router");
@@ -282,6 +291,7 @@ let _registry = null;
282
291
  let _mobileGateway = null;
283
292
  let _mcpManager = null;
284
293
  let _agentChatManager = null;
294
+ let _sessionHosts = null;
285
295
  let _blueprintChatManager = null;
286
296
  let _headroomManager = null;
287
297
  server.on('upgrade', (request, socket, head) => {
@@ -654,10 +664,56 @@ function applyPtyWsRateLimiting(ws) {
654
664
  });
655
665
  const agentChatManager = new agent_chat_manager_1.AgentChatManager(broadcast, registry.desktopDb, port, registry);
656
666
  _agentChatManager = agentChatManager;
667
+ // Core agent sessions (core-agent-sessions-host): one supervised Core host per
668
+ // project scope; missions use it when SPECRAILS_CORE_SESSIONS allows and the
669
+ // selected Core advertises `sessions`. Otherwise missions keep legacy runners.
670
+ const sessionHosts = new session_host_registry_1.SessionHostRegistry({
671
+ launcher: new host_process_1.CoreHostLauncher({ host: { name: 'specrails-desktop', version: process.env.npm_package_version ?? 'unknown' } }),
672
+ clock: { now: () => Date.now(), after: (ms, callback) => { const timer = setTimeout(callback, ms); timer.unref?.(); return { cancel: () => clearTimeout(timer) }; } },
673
+ onStatus: (scope, status, detail, code) => {
674
+ console.log(`[agent-sessions] ${scope}: ${status}${detail ? ` (${detail})` : ''}`);
675
+ // App-level event (no projectId): open missions show a notice when their scope degrades.
676
+ broadcast({ type: 'agent_sessions_host', scope, status, detail: detail ?? null, code: code ?? null, timestamp: new Date().toISOString() });
677
+ },
678
+ });
679
+ _sessionHosts = sessionHosts;
680
+ const projectAllowsSubagents = (projectId) => {
681
+ // A pinned project that is not loaded cannot vouch for sub-agents.
682
+ const context = registry.getContext(projectId);
683
+ return context ? (0, sqlite_1.getProjectSettings)(context.db).allowSubagents : false;
684
+ };
685
+ const missionCoreSessions = new mission_core_sessions_1.MissionCoreSessions({
686
+ db: registry.desktopDb,
687
+ registry: sessionHosts,
688
+ port,
689
+ broadcast: (message) => broadcast(message),
690
+ adapterFor: registry_1.getAdapter,
691
+ availability: () => (0, core_sessions_availability_1.coreSessionsAvailability)(),
692
+ projectKey: (projectId) => registry.getProjectRow(projectId)?.slug ?? null,
693
+ revokeCapability: agent_capability_1.revokeAgentCapability,
694
+ // "Allow sub-agents": the project setting decides; missions without a project use the app setting.
695
+ subagentPolicy: (conversation) => (0, agent_sessions_1.resolveSubagentPolicy)({
696
+ surface: 'mission',
697
+ projectAllows: conversation.pinned_project_id ? projectAllowsSubagents(conversation.pinned_project_id) : null,
698
+ globalAllows: (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb),
699
+ }),
700
+ // "Run sub-agents with": the project's choice, or the app-wide one without a project.
701
+ subagentRuntime: (conversation) => {
702
+ if (!conversation.pinned_project_id)
703
+ return (0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb);
704
+ const context = registry.getContext(conversation.pinned_project_id);
705
+ return context ? (0, sqlite_1.getProjectSettings)(context.db).subagentRuntime : null;
706
+ },
707
+ });
708
+ agentChatManager.setCoreSessions(missionCoreSessions);
709
+ registry.onSettingsChanged((scope) => { void missionCoreSessions.refreshSubagentPolicy(scope); });
710
+ registry.onProjectRemoved(({ slug }) => { void sessionHosts.stop(slug); });
657
711
  // Publish the instance to the process-wide registry so the rails layer can
658
712
  // post PR-decision cards (safe-pr-review-flow). Left null when agent chat is
659
713
  // disabled — the rails callers are null-safe and simply skip the card.
660
714
  (0, agent_chat_registry_1.setAgentChatManager)((0, agent_chat_router_1.isAgentChatEnabled)() ? agentChatManager : null);
715
+ // Before the agent router: `/api/agent/session-hosts` is not a conversation route.
716
+ app.use('/api/agent/session-hosts', (0, http_2.createSessionHostsRouter)(sessionHosts));
661
717
  app.use('/api/agent', (0, agent_chat_router_1.createAgentChatRouter)({ manager: agentChatManager, desktopDb: registry.desktopDb }));
662
718
  // Project Builder day-0 chat + orchestrated commit (add-project-builder).
663
719
  // App-level like agent chat — a Builder conversation runs before any project
@@ -790,6 +846,8 @@ async function shutdown() {
790
846
  Promise.resolve().then(() => _mobileGateway?.stop()),
791
847
  Promise.resolve().then(() => _mcpManager?.stop()),
792
848
  Promise.resolve().then(() => _agentChatManager?.shutdown()),
849
+ // Core session hosts record running work as interrupted and release their journals.
850
+ Promise.resolve().then(() => _sessionHosts?.stopAll()),
793
851
  Promise.resolve().then(() => _blueprintChatManager?.shutdown()),
794
852
  ]);
795
853
  // The shell can exit before its application descendants. Keep the event
@@ -23,6 +23,7 @@ function appTools() {
23
23
  description: 'Read or update app-level Specrails settings (theme, language, daily budget, cost-alert threshold, Code Explorer summary language + monthly summary budget). ' +
24
24
  'Actions: get, set, runtime_providers.list (connections + live status), runtime_providers.test (probe an OpenAI-compatible baseUrl without saving), runtime_providers.save (replace the connections list — local AI engines re-register live). ' +
25
25
  'The MCP enable flag and permission tiers are read-only here (user-controlled in the app). ' +
26
+ '"Allow sub-agents" and "Run sub-agents with" are also read-only here (an agent must not grant itself sub-agents): get reports allowSubagentsWithoutProject and subagentRuntimeWithoutProject, and with projectId the project settings (null runtime = same as the mission agent). ' +
26
27
  'dailyBudgetUsd here is APP-WIDE (all projects combined); per-project caps live in specrails_analytics(budget_set).',
27
28
  hintTier: 'read',
28
29
  tier: (args) => (args.action === 'set' || args.action === 'runtime_providers.save' ? 'write' : 'read'),
@@ -43,6 +44,7 @@ function appTools() {
43
44
  .optional()
44
45
  .describe('APP-WIDE daily budget cap USD across all projects; null clears it. Per-project caps live in specrails_analytics(budget_set).'),
45
46
  costAlertThresholdUsd: zod_1.z.number().min(0).nullable().optional(),
47
+ projectId: zod_1.z.string().optional().describe('get: also report this project\'s "Allow sub-agents" setting'),
46
48
  summaryLanguage: zod_1.z
47
49
  .enum(['en', 'es'])
48
50
  .optional()
@@ -83,6 +85,11 @@ function appTools() {
83
85
  // Code Explorer settings (same keys + defaults as GET /api/code-explorer-settings)
84
86
  summaryLanguage: (0, desktop_db_1.getDesktopSetting)(db, 'summary_language') === 'es' ? 'es' : 'en',
85
87
  summaryMonthlyBudgetUsd: nonNegNum((0, desktop_db_1.getDesktopSetting)(db, 'summary_monthly_budget_usd')) ?? 5.0,
88
+ allowSubagentsWithoutProject: (0, desktop_db_1.getGlobalAllowSubagents)(db),
89
+ subagentRuntimeWithoutProject: (0, desktop_db_1.getGlobalSubagentRuntime)(db),
90
+ ...(typeof args.projectId === 'string' && args.projectId
91
+ ? await projectSubagentSettings(ctx, args.projectId)
92
+ : {}),
86
93
  mcp: {
87
94
  enabled: (0, mcp_tiers_1.isMcpEnabled)(db),
88
95
  tierWrite: (0, mcp_tiers_1.isTierEnabled)(db, 'write'),
@@ -161,3 +168,7 @@ function writeNumOrClear(db, key, value) {
161
168
  else
162
169
  (0, desktop_db_1.setDesktopSetting)(db, key, String(value));
163
170
  }
171
+ async function projectSubagentSettings(ctx, projectId) {
172
+ const settings = (await (0, types_1.apiCall)(ctx, 'GET', `/projects/${encodeURIComponent(projectId)}/settings`));
173
+ return { projectAllowSubagents: settings.allowSubagents === true, projectSubagentRuntime: settings.subagentRuntime ?? null };
174
+ }
@@ -2,18 +2,30 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.missionTools = missionTools;
4
4
  const zod_1 = require("zod");
5
+ const agent_chat_registry_1 = require("../../modules/missions/runtime/agent-chat-registry");
5
6
  function missionTools() {
6
7
  return [{
7
8
  name: 'specrails_mission',
8
9
  title: 'Mission user updates',
9
- description: 'First-party mission only: acknowledge authenticated user updates received during the current running turn. For MCP mission_user_updates, read every block, replan, and acknowledge_updates with the exact latest revision in a separate call before invoking other tools. For the initial Mission input ID or native user messages carrying queueId, acknowledge_inputs with those inputIds only after reading them; this records a read receipt and does not release the MCP revision gate. Does not execute, retry or undo any action and cannot change permissions, provider or project pin.',
10
+ description: 'First-party mission only: acknowledge authenticated user updates received during the current running turn. For MCP mission_user_updates, read every block, replan, and acknowledge_updates with the exact latest revision in a separate call before invoking other tools. For the initial Mission input ID or native user messages carrying queueId, acknowledge_inputs with those inputIds only after reading them; this records a read receipt and does not release the MCP revision gate. Does not execute, retry or undo any action and cannot change permissions, provider or project pin. ' +
11
+ 'When the mission says Specrails launches its sub-agents: subagent_start (description, instructions, optional contextTurns) delegates a task to a sub-agent on the configured provider; subagent_wait returns finished results (results you never wait for arrive later as a message); subagent_list shows them; subagent_stop stops them. These actions are refused in missions whose agent launches its own sub-agents.',
12
+ // Delegation is gated by the project's "Allow sub-agents" / "Run sub-agents with" settings, like native sub-agents.
10
13
  tier: 'read',
11
14
  inputSchema: {
12
- action: zod_1.z.enum(['acknowledge_updates', 'acknowledge_inputs']),
15
+ action: zod_1.z.enum(['acknowledge_updates', 'acknowledge_inputs', 'subagent_start', 'subagent_wait', 'subagent_list', 'subagent_stop']),
16
+ description: zod_1.z.string().min(1).max(200).optional().describe('subagent_start: a short name for the task, shown to the user'),
17
+ instructions: zod_1.z.string().min(1).max(64 * 1024).optional().describe('subagent_start: the complete, self-contained task for the sub-agent'),
18
+ agentType: zod_1.z.string().min(1).max(100).optional().describe('subagent_start: optional label for the kind of work (e.g. "review")'),
19
+ contextTurns: zod_1.z.number().int().min(0).max(10).optional().describe('subagent_start: how many recent turns of this conversation to hand over'),
20
+ subagentIds: zod_1.z.array(zod_1.z.string().min(1).max(200)).max(50).optional().describe('subagent_wait / subagent_stop: limit to these sub-agents (default: all of this mission)'),
21
+ timeoutSeconds: zod_1.z.number().int().min(0).max(300).optional().describe('subagent_wait: how long to wait (default 60, max 300); call again to keep waiting'),
13
22
  revision: zod_1.z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Required for acknowledge_updates: exact latest revision delivered in mission_user_updates. Never guess; an older acknowledgement cannot release newer messages.'),
14
23
  inputIds: zod_1.z.array(zod_1.z.string().min(1).max(200)).min(1).max(50).optional().describe('Required for acknowledge_inputs: exact initial Mission input ID or queueId values from native user messages you have read in this invocation.'),
15
24
  },
16
25
  handler: (ctx, args) => {
26
+ // Delegation actions are async; the acknowledge actions stay synchronous (callers rely on sync throws).
27
+ if (typeof args.action === 'string' && args.action.startsWith('subagent_'))
28
+ return delegationAction(ctx, args);
17
29
  if (args.action === 'acknowledge_inputs') {
18
30
  if (!ctx.firstPartyAgent || !ctx.acknowledgeAgentInputsRead)
19
31
  throw new Error('This action requires an active first-party mission turn.');
@@ -27,3 +39,34 @@ function missionTools() {
27
39
  },
28
40
  }];
29
41
  }
42
+ async function delegationAction(ctx, args) {
43
+ if (!ctx.firstPartyAgent || !ctx.originConversationId)
44
+ throw new Error('This action requires an active first-party mission turn.');
45
+ const manager = (0, agent_chat_registry_1.getAgentChatManager)();
46
+ if (!manager)
47
+ throw new Error('Missions are not available.');
48
+ const conversationId = ctx.originConversationId;
49
+ const ids = Array.isArray(args.subagentIds) ? args.subagentIds : undefined;
50
+ switch (args.action) {
51
+ case 'subagent_start': {
52
+ if (typeof args.description !== 'string' || typeof args.instructions !== 'string')
53
+ throw new Error('subagent_start needs a description and instructions.');
54
+ return manager.delegateSubagent(conversationId, {
55
+ description: args.description, prompt: args.instructions,
56
+ ...(typeof args.agentType === 'string' ? { agentType: args.agentType } : {}),
57
+ ...(typeof args.contextTurns === 'number' ? { contextTurns: args.contextTurns } : {}),
58
+ });
59
+ }
60
+ case 'subagent_wait':
61
+ return manager.waitSubagents(conversationId, ids, (typeof args.timeoutSeconds === 'number' ? args.timeoutSeconds : 60) * 1000);
62
+ case 'subagent_stop':
63
+ return { stopped: await manager.stopSubagents(conversationId, ids) };
64
+ case 'subagent_list':
65
+ return {
66
+ subagents: (manager.sessionState(conversationId)?.subagents ?? [])
67
+ .filter((node) => node.delegated)
68
+ .map((node) => ({ subagentId: node.subagentId, description: node.description, phase: node.phase, provider: node.delegated.driver, model: node.delegated.model, result: node.resultSummary })),
69
+ };
70
+ }
71
+ throw new Error(`Unknown action: ${String(args.action)}`);
72
+ }
@@ -57,7 +57,7 @@ function specsTools() {
57
57
  'assignee, prerequisites, metadata. THE canonical way to persist a spec refined in conversation — do NOT route ' +
58
58
  'a finished spec through create/generate, which re-generate the content with AI and lossily rewrite it. ' +
59
59
  'Agent-authored inserts (use b) are enriched with a Contract Layer BY DEFAULT via one short background AI pass ' +
60
- '— pass contractRefine: false to skip it, e.g. when the user declined it), ' +
60
+ '— never ask the user about it; pass contractRefine: false only when they asked for no contract layer unprompted), ' +
61
61
  'generate (same as create but takes a pre-formed idea string; both accept contextScope/attachments/createLocal), ' +
62
62
  'ai_edit (ai-spawn, AI-edit a ticket title+description), ' +
63
63
  'cancel_ai_edit (abort an in-flight ai-edit), contract_refine (ai-spawn, append/re-fire a Contract Layer — Claude or Codex; ' +
@@ -172,7 +172,7 @@ function specsTools() {
172
172
  contractRefine: zod_1.z
173
173
  .boolean()
174
174
  .optional()
175
- .describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude or Codex). DEFAULTS TO TRUE — pass false to opt out (e.g. the user asked for no contract layer)'),
175
+ .describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude or Codex). DEFAULTS TO TRUE and is never asked about — pass false only when the user asked for no contract layer unprompted'),
176
176
  attachmentIds: zod_1.z.array(zod_1.z.string()).optional().describe('create/generate/ai_edit: attachment ids (create/generate require pendingSpecId)'),
177
177
  // ── ai_edit ──────────────────────────────────────────────────────
178
178
  instructions: zod_1.z.string().optional().describe('ai_edit: natural-language editing instructions (required)'),
@@ -21,6 +21,7 @@
21
21
  // Spec: openspec/changes/add-multi-provider-support/specs/project-spending/spec.md
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.finaliseInvocationResult = finaliseInvocationResult;
24
+ exports.finaliseNormalisedResult = finaliseNormalisedResult;
24
25
  exports.normaliseResultEvent = normaliseResultEvent;
25
26
  const pricing_1 = require("./pricing");
26
27
  const local_adapter_1 = require("../../../providers/local-adapter");
@@ -30,7 +31,14 @@ const local_adapter_1 = require("../../../providers/local-adapter");
30
31
  * pricing-table fallback when the adapter does not report `total_cost_usd`.
31
32
  */
32
33
  function finaliseInvocationResult(adapter, events, opts = {}) {
33
- const result = adapter.extractResult(events);
34
+ return finaliseNormalisedResult(adapter, adapter.extractResult(events), opts);
35
+ }
36
+ /**
37
+ * Same finalisation for a result that is already normalised (e.g. a turn
38
+ * reported by Core's session runtime): model/duration stamping and the
39
+ * native-cost-absent estimation rules, with identical null semantics.
40
+ */
41
+ function finaliseNormalisedResult(adapter, result, opts = {}) {
34
42
  const cloned = { ...result };
35
43
  // Stamp the model from the caller when the adapter did not derive one. The
36
44
  // adapter contract permits leaving `model` undefined; the manager knows the
@@ -166,6 +166,18 @@ async function runAgentRuntimeInvocation(options) {
166
166
  config.providers = (0, agent_runtime_settings_1.stripDesktopConnectionFields)(config.providers, (0, agent_runtime_settings_1.coreConnectionFieldGates)(runtime.api?.capabilities));
167
167
  if (runtime.api?.capabilities?.configurableGuardrails !== 1)
168
168
  delete config.guardrails;
169
+ // Setup commands are a `setupCommands` capability: an older Core rejects the unknown key, so strip it and say so once in the run log.
170
+ if (config.setup !== undefined && !(0, agent_runtime_settings_1.coreSupportsSetupCommands)(runtime.api?.capabilities)) {
171
+ if (config.setup.length) {
172
+ try {
173
+ options.onLine?.(`[runtime] setup commands ignored: the installed Core does not advertise setupCommands (${config.setup.length} configured)\n`);
174
+ }
175
+ catch { /* Advisory log observer. */ }
176
+ }
177
+ delete config.setup;
178
+ }
179
+ else if (config.setup)
180
+ config.setup = scopedHostChecks(config.setup, admittedContext.repositories);
169
181
  if (definitionEngine && (runtime.api?.capabilities?.engineV2 !== 1 || runtime.api?.capabilities?.workflowDefinitions !== 1))
170
182
  throw new Error('engine_unsupported: Update Core to run workflow definitions');
171
183
  const compilationConfig = structuredClone(config);
@@ -373,9 +373,34 @@ class AgentRuntimeControls {
373
373
  this.statusCache.delete(runId);
374
374
  }
375
375
  }
376
+ /** The error a run logged when it failed before any Core step ran; null otherwise. */
377
+ launchFailure(runId) {
378
+ const run = this.ctx.db.prepare('SELECT final_outcome, iteration_count, core_revision FROM loop_runs WHERE id = ?').get(runId);
379
+ if (!run || run.final_outcome !== 'failed' || run.iteration_count || run.core_revision != null)
380
+ return null;
381
+ const rows = this.ctx.db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'log' ORDER BY seq DESC LIMIT 20").all(runId);
382
+ for (const row of rows) {
383
+ try {
384
+ const line = JSON.parse(row.payload).line;
385
+ if (typeof line === 'string' && /^error:/i.test(line.trim()))
386
+ return line.trim().replace(/^error:\s*/i, '').slice(0, 2000);
387
+ }
388
+ catch { /* not a log line */ }
389
+ }
390
+ return null;
391
+ }
376
392
  /** Read-only recovery assessment. canResume is admission, not a repair claim. */
377
393
  async diagnose(runId) {
378
394
  const summary = await this.summary(runId);
395
+ // Rejected before Core started (e.g. an unsupported model/effort): no runtime
396
+ // scope was ever written, so the launch error is the whole diagnosis.
397
+ const launchError = summary.status === 'unavailable' ? this.launchFailure(runId) : null;
398
+ if (launchError)
399
+ return {
400
+ runId, summary: { ...summary, error: launchError }, recommendation: 'fix_launch_precondition',
401
+ reason: 'The run was rejected before Core started, so there is no runtime state or evidence. Fix the reported precondition (or change the launch settings) before launching again; Resume is not possible.',
402
+ launchError, repeatFailureCount: null, recentFailures: [],
403
+ };
379
404
  if (summary.historical || summary.status === 'unavailable')
380
405
  return {
381
406
  runId, summary, recommendation: 'inspect_saved_evidence',
@@ -76,6 +76,8 @@ function resolveEffectiveRuntimeConfig(input, options) {
76
76
  }
77
77
  fillDefaultRoleModels(config);
78
78
  config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
79
+ if (config.setup)
80
+ config.setup = config.setup.filter(check => options.repositoryIds.includes(check.repositoryId));
79
81
  return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
80
82
  }
81
83
  /**
@@ -21,6 +21,10 @@ function withDefaultRoleModels(body) {
21
21
  return { ...body, agents: filled.agents, ...(filled.roles ? { roles: filled.roles } : {}), ...(filled.fixer ? { fixer: filled.fixer } : {}) };
22
22
  }
23
23
  const loop_role_engines_1 = require("../../loops/runtime/loop-role-engines");
24
+ /** The settings API always answers `setup` as an array; the file keeps it absent until the user saves one. */
25
+ function withSetupList(config) {
26
+ return config ? { ...config, setup: config.setup ?? [] } : config;
27
+ }
24
28
  function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
25
29
  /** Guardrail catalog of the loaded Core (configurable-guardrails): ids + phase; the UI owns labels. `supported:false` on older cores. */
26
30
  router.get('/:projectId/agent-runtime/guardrails', async (_req, res) => {
@@ -70,7 +74,7 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
70
74
  openRolesAvailable = capabilities?.openRoles === 1;
71
75
  }
72
76
  catch { /* Ordinary settings remain readable without capability support. */ }
73
- res.json({ configured, config, runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null, efficiencyAvailable, openRolesAvailable,
77
+ res.json({ configured, config: withSetupList(config), runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null, efficiencyAvailable, openRolesAvailable,
74
78
  workflowRoleDefaults: openRolesAvailable && config ? (0, agent_runtime_effective_config_1.workflowRoleDefaults)(config) : {} });
75
79
  }
76
80
  catch (err) {
@@ -147,7 +151,7 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
147
151
  }
148
152
  }
149
153
  const saved = (0, agent_runtime_settings_1.saveAgentRuntimeConfig)(ctx(req).project, config);
150
- res.json({ configured: true, config: saved, runtimeAvailable, efficiencyAvailable, openRolesAvailable });
154
+ res.json({ configured: true, config: withSetupList(saved), runtimeAvailable, efficiencyAvailable, openRolesAvailable });
151
155
  }
152
156
  catch (err) {
153
157
  const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
@@ -25,6 +25,7 @@ exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
25
25
  exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
26
26
  exports.validateRuntimeProviderOverride = validateRuntimeProviderOverride;
27
27
  exports.forCoreRuntime = forCoreRuntime;
28
+ exports.coreSupportsSetupCommands = coreSupportsSetupCommands;
28
29
  const node_fs_1 = __importDefault(require("node:fs"));
29
30
  const node_path_1 = __importDefault(require("node:path"));
30
31
  const node_os_1 = __importDefault(require("node:os"));
@@ -136,17 +137,18 @@ function validateLoopRuntimeSettings(input, providers = loadRuntimeProviders())
136
137
  const config = validateAgentRuntimeConfig({ ...input, enabled: true, providers: portableProviders, verification: [] });
137
138
  if (['architect', 'developer', 'reviewer', 'fixer'].some(role => !config.rolePrompts?.[role]?.trim()))
138
139
  throw new AgentRuntimeConfigError('Loop agents require an explicit definition for architect, developer, reviewer and fixer');
139
- const { enabled: _enabled, providers: _providers, verification: _checks, ...settings } = config;
140
+ const { enabled: _enabled, providers: _providers, verification: _checks, setup: _setup, ...settings } = config;
140
141
  return settings;
141
142
  }
142
143
  /** Read only repository checks from the old project file. Role migration is explicit. */
143
144
  function loadLoopRuntimeConfig(file, input, selection = { provider: 'claude' }) {
144
- let verification = [];
145
+ let verification = [], setup = undefined;
145
146
  try {
146
147
  const saved = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
147
148
  if (!saved || typeof saved !== 'object' || Array.isArray(saved))
148
149
  throw new AgentRuntimeConfigError('Saved runtime configuration must be an object');
149
150
  verification = saved.verification ?? [];
151
+ setup = saved.setup;
150
152
  }
151
153
  catch (error) {
152
154
  if (error.code !== 'ENOENT')
@@ -163,7 +165,7 @@ function loadLoopRuntimeConfig(file, input, selection = { provider: 'claude' })
163
165
  settings.fixer = inherit(settings.fixer);
164
166
  for (const [role, agent] of Object.entries(settings.roles ?? {}))
165
167
  settings.roles[role] = inherit(agent);
166
- return validateAgentRuntimeConfig({ ...settings, providers, enabled: true, verification });
168
+ return validateAgentRuntimeConfig({ ...settings, providers, enabled: true, verification, ...(setup !== undefined ? { setup } : {}) });
167
169
  }
168
170
  const CLI_PROVIDERS = ['claude', 'codex', 'gemini', 'kimi'];
169
171
  const ROLES = ['architect', 'developer', 'reviewer'];
@@ -260,10 +262,12 @@ function validateAgentRuntimeConfig(input) {
260
262
  if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
261
263
  throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
262
264
  }
263
- const keys = config.verification.flatMap(command => command.key ? [command.key] : []);
265
+ // `setup` entries share the verification entry shape and rules (Core validates both with one validator; keys are unique across the two lists).
266
+ const hostCommands = [...config.verification, ...(config.setup ?? [])];
267
+ const keys = hostCommands.flatMap(command => command.key ? [command.key] : []);
264
268
  if (new Set(keys).size !== keys.length)
265
269
  throw new AgentRuntimeConfigError('Verification check keys must be unique');
266
- for (const command of config.verification) {
270
+ for (const command of hostCommands) {
267
271
  for (const value of [command.label, ...(command.policy?.inputs ?? []), ...(command.policy?.toolchainInputs ?? []), ...(command.policy?.resources ?? [])])
268
272
  if (value !== undefined && (!value.trim() || value.includes('\0')))
269
273
  throw new AgentRuntimeConfigError('Invalid verification label or policy input');
@@ -482,7 +486,7 @@ function validateRuntimeProviderOverride(value) {
482
486
  throw new AgentRuntimeConfigError('Invalid runtime provider override effort');
483
487
  return structuredClone(raw);
484
488
  }
485
- /** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`. */
489
+ /** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`, and `setup` unless it advertises `setupCommands`. */
486
490
  function forCoreRuntime(config, capabilities) {
487
491
  let out = config;
488
492
  if (capabilities?.openRoles !== 1 && out.roles !== undefined) {
@@ -495,6 +499,10 @@ function forCoreRuntime(config, capabilities) {
495
499
  const { guardrails: _dropped, ...rest } = out;
496
500
  out = rest;
497
501
  }
502
+ if (!coreSupportsSetupCommands(capabilities) && out.setup !== undefined) {
503
+ const { setup: _dropped, ...rest } = out;
504
+ out = rest;
505
+ }
498
506
  // The per-role thinking switch is known only to cores advertising `roleThinkingControl`.
499
507
  if (capabilities?.roleThinkingControl !== 1 && ([...Object.values(out.agents ?? {}), ...Object.values(out.roles ?? {})].some(agent => agent?.thinking !== undefined) || out.fixer?.thinking !== undefined)) {
500
508
  const strip = (agent) => { const { thinking: _t, ...rest } = agent; return rest; };
@@ -502,3 +510,7 @@ function forCoreRuntime(config, capabilities) {
502
510
  }
503
511
  return out;
504
512
  }
513
+ /** Core advertises `setupCommands: 1` when its verify piece runs `RuntimeConfig.setup` before each verification plan. */
514
+ function coreSupportsSetupCommands(capabilities) {
515
+ return (capabilities?.setupCommands ?? 0) >= 1;
516
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CoreHostLauncher = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const agent_runtime_loader_1 = require("../../agent-runtime/runtime/agent-runtime-loader");
6
+ const core_node_runtime_1 = require("../../../core-node-runtime");
7
+ const win_spawn_1 = require("../../../util/win-spawn");
8
+ const errors_1 = require("../domain/errors");
9
+ const protocol_1 = require("../domain/protocol");
10
+ const rpc_client_1 = require("./rpc-client");
11
+ /**
12
+ * Launches `node <core>/cli.js host --stdio --scope <scope>` through the same
13
+ * Core and Node resolution as implementation runs (bundled Node under pkg,
14
+ * Windows-safe environment), then performs the protocol handshake.
15
+ */
16
+ class CoreHostLauncher {
17
+ options;
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ }
21
+ async launch(scope) {
22
+ const cli = (this.options.cli ?? agent_runtime_loader_1.findCoreAgentRuntimeCli)();
23
+ if (!cli)
24
+ throw new errors_1.SessionRequestError('The selected Core runtime does not include the agent runtime CLI', { code: 'driver_unavailable', retryable: false });
25
+ const env = (0, win_spawn_1.windowsSpawnEnv)(this.options.env ?? process.env);
26
+ const args = [cli, 'host', '--stdio', '--scope', scope];
27
+ const command = (this.options.node ?? core_node_runtime_1.resolveCoreNodeRuntime)();
28
+ const child = (this.options.spawnProcess ?? ((cmd, argv, opts) => (0, node_child_process_1.spawn)(cmd, argv, { env: opts.env, shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })))(command, args, { env });
29
+ if (!child.stdin || !child.stdout)
30
+ throw new errors_1.SessionRequestError('Session host has no stdio pipes', { code: 'internal', retryable: true });
31
+ let stderr = '';
32
+ child.stderr?.on('data', (chunk) => {
33
+ const text = chunk.toString('utf8');
34
+ stderr = (stderr + text).slice(-8_192);
35
+ this.options.onStderr?.(scope, text);
36
+ });
37
+ const exited = new Promise((resolve) => {
38
+ child.once('exit', (code) => resolve(code));
39
+ child.once('error', () => resolve(null));
40
+ });
41
+ const grace = this.options.shutdownGraceMs ?? 10_000;
42
+ const client = new rpc_client_1.StdioSessionHostClient({ stdin: child.stdin, stdout: child.stdout }, {
43
+ ...(this.options.requestTimeoutMs !== undefined ? { timeoutMs: this.options.requestTimeoutMs } : {}),
44
+ onClose: async () => {
45
+ // Ask for a graceful stop (interruptions recorded, lease released), then make sure the tree is gone.
46
+ await Promise.race([client.request('host.shutdown', { graceMs: grace }).catch(() => undefined), exited]);
47
+ const forced = setTimeout(() => { if (child.pid)
48
+ (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL'); }, grace);
49
+ await exited;
50
+ clearTimeout(forced);
51
+ },
52
+ });
53
+ void exited.then((code) => client.markClosed(`Session host exited (code ${code ?? 'none'})${stderr ? `: ${stderr.trim().split('\n').at(-1)}` : ''}`));
54
+ let initialize;
55
+ try {
56
+ initialize = await client.request('initialize', {
57
+ protocolVersions: [...protocol_1.SUPPORTED_PROTOCOL_VERSIONS],
58
+ host: this.options.host ?? { name: 'specrails-desktop', version: 'dev' },
59
+ scope,
60
+ });
61
+ if (initialize.capabilities?.sessions !== 1)
62
+ throw new errors_1.SessionRequestError('Core session host does not advertise sessions', { code: 'protocol_mismatch', retryable: false });
63
+ }
64
+ catch (error) {
65
+ if (child.pid)
66
+ (0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
67
+ client.markClosed('Session host failed to initialize');
68
+ throw error;
69
+ }
70
+ return Object.assign(client, { initialize, pid: child.pid });
71
+ }
72
+ }
73
+ exports.CoreHostLauncher = CoreHostLauncher;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSessionHostsRouter = createSessionHostsRouter;
4
+ const express_1 = require("express");
5
+ const SCOPE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6
+ /**
7
+ * Inbound adapter: Core session host status per scope and manual retry of a
8
+ * degraded scope (after the cause, e.g. another Desktop instance, is gone).
9
+ */
10
+ function createSessionHostsRouter(control) {
11
+ const router = (0, express_1.Router)();
12
+ router.get('/', (_req, res) => {
13
+ res.json({ hosts: control.hosts() });
14
+ });
15
+ router.post('/:scope/retry', async (req, res) => {
16
+ const scope = String(req.params.scope);
17
+ if (!SCOPE_RE.test(scope)) {
18
+ res.status(400).json({ error: 'Invalid scope' });
19
+ return;
20
+ }
21
+ try {
22
+ await control.retry(scope);
23
+ res.json({ host: control.hosts().find((host) => host.scope === scope) ?? { scope, status: 'absent', detail: null, code: null } });
24
+ }
25
+ catch (error) {
26
+ // The retry ran; the scope reports why it is still unavailable.
27
+ res.status(502).json({ error: error instanceof Error ? error.message : 'Retry failed', host: control.hosts().find((host) => host.scope === scope) ?? null });
28
+ }
29
+ });
30
+ return router;
31
+ }