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.
- package/client/dist/assets/ActivityFeedPage-C_pqc281.js +1 -0
- package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-vz6rbDtu.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +2 -0
- package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-99fBBLri.js} +1 -1
- package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CTwm_6T8.js} +3 -3
- package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-YD8MLoPa.js} +1 -1
- package/client/dist/assets/CodePage-CLJn2F2e.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-MWGQVbwp.js} +1 -1
- package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-XUCYKoq0.js} +1 -1
- package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-dOLC8bXA.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-BvKQgwM9.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +19 -0
- package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-CkiubrkZ.js} +1 -1
- package/client/dist/assets/JobDetailPage-CNe4Wn7l.js +1 -0
- package/client/dist/assets/JobsPage-DSts_L_d.js +1 -0
- package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-Dv3dxHr_.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-DN2lbSnL.js} +1 -1
- package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-DHgIreEW.js} +2 -2
- package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +1 -0
- package/client/dist/assets/PluginsPage-D1EJ9oja.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-CW4eKG3x.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-DrtPqvc_.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-DcFlhUxy.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +16 -0
- package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-CAY2Rd2f.js} +1 -1
- package/client/dist/assets/agent-5bSixVzz.js +1 -0
- package/client/dist/assets/agent-BR4EHfhq.js +1 -0
- package/client/dist/assets/agent-C0IKkJP8.js +1 -0
- package/client/dist/assets/agent-C7li9ayC.js +1 -0
- package/client/dist/assets/agent-D0dRmEen.js +1 -0
- package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
- package/client/dist/assets/agent-Dip4h2YE.js +1 -0
- package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
- package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
- package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
- package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
- package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
- package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
- package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
- package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
- package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
- package/client/dist/assets/ban-E4EaZe3a.js +1 -0
- package/client/dist/assets/chevron-up-CwGUHd6D.js +1 -0
- package/client/dist/assets/dist-Bq10nS2m.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DlQY1Lm_.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-COUeq65G.js} +1 -1
- package/client/dist/assets/index-BacrO_ub.css +2 -0
- package/client/dist/assets/index-CONHvEJw.js +83 -0
- package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-C94VNHNY.js} +1 -1
- package/client/dist/assets/layers-eDMjG41I.js +1 -0
- package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
- package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
- package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
- package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
- package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
- package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
- package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
- package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
- package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
- package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
- package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
- package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
- package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
- package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
- package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
- package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
- package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-Cc3oNv8X.js} +1 -1
- package/client/dist/assets/save-B6wGWwBR.js +1 -0
- package/client/dist/assets/{settings-Dn-pXukr.js → settings-BKJZ2KEM.js} +1 -1
- package/client/dist/assets/{settings-BBj_kZaD.js → settings-BV3jocIx.js} +1 -1
- package/client/dist/assets/{settings-COlH_P33.js → settings-BjM2Dw8_.js} +1 -1
- package/client/dist/assets/settings-CKu0LTib.js +1 -0
- package/client/dist/assets/{settings-D77mC_Lb.js → settings-DA77M48m.js} +1 -1
- package/client/dist/assets/{settings-ByVE7jdZ.js → settings-LfFnl1UH.js} +1 -1
- package/client/dist/assets/{settings-DZKy7MqN.js → settings-P1IQvhnL.js} +1 -1
- package/client/dist/assets/{settings-B5Hy_Kol.js → settings-eIMJjyK_.js} +1 -1
- package/client/dist/assets/{spending-Oov7p52A.js → spending-DemeFfrH.js} +1 -1
- package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-9Lwoel_8.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-DL0xYoZ2.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-BEHPeU1Y.js} +2 -2
- package/client/dist/assets/zap-CHo3aCD1.js +1 -0
- package/client/dist/index.html +26 -22
- package/docs/guide/de/integrations/6-agent-chat.md +11 -0
- package/docs/guide/en/integrations/6-agent-chat.md +11 -0
- package/docs/guide/es/integrations/6-agent-chat.md +11 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
- package/docs/guide/it/integrations/6-agent-chat.md +11 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/agent-sessions.md +157 -0
- package/docs/internals/api-reference.md +33 -4
- package/docs/internals/configuration.md +36 -0
- package/docs/internals/core-runtime-updates.md +10 -3
- package/docs/internals/operations-runbook.md +11 -0
- package/docs/internals/programmatic-agent-runtime.md +2 -0
- package/docs/internals/source-map.md +40 -1
- package/mcp-bridge/dist/specrails-mcp.js +82 -15
- package/package.json +1 -1
- package/server/dist/agent-mcp-config.js +41 -0
- package/server/dist/core-compat.js +2 -0
- package/server/dist/desktop-db.js +101 -4
- package/server/dist/desktop-router.js +31 -2
- package/server/dist/index.js +58 -0
- package/server/dist/mcp/tools/app-settings.js +11 -0
- package/server/dist/mcp/tools/mission.js +45 -2
- package/server/dist/mcp/tools/specs.js +2 -2
- package/server/dist/modules/accounting/runtime/result-event.js +9 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
- package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
- package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
- package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
- package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
- package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
- package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
- package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
- package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
- package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
- package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
- package/server/dist/modules/agent-sessions/index.js +31 -0
- package/server/dist/modules/agent-sessions/ports.js +2 -0
- package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
- package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
- package/server/dist/modules/agents/runtime/agent-store.js +3 -1
- package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
- package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
- package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
- package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +41 -11
- package/server/dist/modules/loops/runtime/loop-run-manager.js +4 -0
- package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
- package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
- package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
- package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
- package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
- package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
- package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
- package/server/dist/modules/project-settings/adapters/http.js +14 -2
- package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
- package/server/dist/modules/project-settings/domain.js +24 -0
- package/server/dist/modules/project-settings/index.js +2 -1
- package/server/dist/modules/specs/runtime/spec-models.js +1 -1
- package/server/dist/plugins/codex-spawn.js +15 -7
- package/server/dist/project-registry.js +32 -0
- package/server/dist/project-router-settings.js +7 -1
- package/server/dist/providers/claude-adapter.js +6 -1
- package/server/dist/schemas/agent-runtime.schema.json +9 -7
- package/client/dist/assets/ActivityFeedPage-3J7fR8yB.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-Dps30tk9.js +0 -2
- package/client/dist/assets/AgentModeCodePane-CvLimey8.js +0 -2
- package/client/dist/assets/CodePage-D981zRHy.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
- package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +0 -1
- package/client/dist/assets/JobsPage-BSrl8P7u.js +0 -1
- package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-BD2bsRBe.js +0 -1
- package/client/dist/assets/PluginsPage-B8f49Mvn.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-D7q-O6h3.js +0 -1
- package/client/dist/assets/ReviewPacketPage-DpZSzF-Q.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-BHmtj1As.js +0 -16
- package/client/dist/assets/agent--8QAfoXL.js +0 -1
- package/client/dist/assets/agent-3HxXkIWX.js +0 -1
- package/client/dist/assets/agent-CpFCz32T.js +0 -1
- package/client/dist/assets/agent-DVnw6ybO.js +0 -1
- package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
- package/client/dist/assets/agent-hv57-zyG.js +0 -1
- package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
- package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
- package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
- package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
- package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
- package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
- package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
- package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
- package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
- package/client/dist/assets/index-Bfunvi6w.css +0 -2
- package/client/dist/assets/index-g8S62tpS.js +0 -83
- package/client/dist/assets/save-OsUqVn99.js +0 -1
- package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
- package/client/dist/assets/zap-CUOlvtT1.js +0 -1
package/server/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|