specrails-desktop 2.46.0 → 2.48.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/README.md +2 -0
- package/client/dist/assets/{ActivityFeedPage-CsHQPpvl.js → ActivityFeedPage-CGq6QhP5.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-C4GZ-kdf.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-_IPmL0ZS.js +2 -0
- package/client/dist/assets/AgentModeCodePane-B5WG5DOP.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-rnD-EPMd.js → AgentModeJobsPane-w1N_HIXh.js} +1 -1
- package/client/dist/assets/AgentsPage-CmwnnT46.js +87 -0
- package/client/dist/assets/AnalyticsPage-BUMsKtsJ.js +1 -0
- package/client/dist/assets/CodePage-PjgywJPS.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-BXY1pH4y.js +1 -0
- package/client/dist/assets/DocsDialog-ccQETHKB.js +11 -0
- package/client/dist/assets/DocsPage-DzEgplB3.js +11 -0
- package/client/dist/assets/{ExportDropdown-pWZGYCv3.js → ExportDropdown-B8bBGopr.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-CEwGkJ8F.js +19 -0
- package/client/dist/assets/JobDetailModal-BYXmuTDe.js +1 -0
- package/client/dist/assets/JobDetailPage-CWp6MwJj.js +1 -0
- package/client/dist/assets/JobsPage-D1O1uyG8.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-CtmOE4-b.js → LoopBuilderPage-DQE-7Vby.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-BYeLjQbr.js → LoopPreviewModal-Bk7zIXD5.js} +1 -1
- package/client/dist/assets/LoopsPage-CqG7mx56.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-UpnqccPx.js +1 -0
- package/client/dist/assets/PluginsPage-CjgVvmpW.js +2 -0
- package/client/dist/assets/{ProjectSettingsDialog-DsCRIc4v.js → ProjectSettingsDialog-D6vBCoay.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-BQm_3TO8.js → RepositoryDeliveries-C36ipKp_.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-DnAFtjTz.js → RepositoryScopeSelector-0Gz_fR1E.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-Dr9RYs7q.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-sCMHYFJo.js +1 -0
- package/client/dist/assets/TicketDetailModalContext-C5GgJlEt.js +14 -0
- package/client/dist/assets/{Trans-BwLgX6n7.js → Trans-9pHTy2fH.js} +1 -1
- package/client/dist/assets/{addspec-D2YIQ0GU.js → addspec-BOvgmcgk.js} +1 -1
- package/client/dist/assets/{addspec-B_TPJt-R.js → addspec-Clmyfn_R.js} +1 -1
- package/client/dist/assets/{addspec-A1lDQa4M.js → addspec-CsNaBpHp.js} +1 -1
- package/client/dist/assets/{addspec-CQBjzt24.js → addspec-D2tvWmmV.js} +1 -1
- package/client/dist/assets/{addspec-CLYLvaVP.js → addspec-D3nFwvD0.js} +1 -1
- package/client/dist/assets/{addspec-BPpZ_olp.js → addspec-DVLEhK3w.js} +1 -1
- package/client/dist/assets/{addspec-iFdQ6D99.js → addspec-DXbK-tb0.js} +1 -1
- package/client/dist/assets/{addspec-BHZd0N5F.js → addspec-OHG8DFEn.js} +1 -1
- package/client/dist/assets/agentRuntime-BUimiX__.js +1 -0
- package/client/dist/assets/agentRuntime-BcN97sAB.js +1 -0
- package/client/dist/assets/agentRuntime-Ckyg-ahT.js +1 -0
- package/client/dist/assets/agentRuntime-DJCwyX4t.js +1 -0
- package/client/dist/assets/agentRuntime-DVzPJOl0.js +1 -0
- package/client/dist/assets/agentRuntime-V9AZTG3c.js +1 -0
- package/client/dist/assets/agentRuntime-bAAjw1vw.js +1 -0
- package/client/dist/assets/agentRuntime-v4qKAYH-.js +1 -0
- package/client/dist/assets/{agents-45GbINfh.js → agents-CA5eT1d0.js} +1 -1
- package/client/dist/assets/{agents-DMKFStHL.js → agents-CAgy-5uS.js} +1 -1
- package/client/dist/assets/{agents-IUOgqT5t.js → agents-CNm6Qzlv.js} +1 -1
- package/client/dist/assets/{agents-ms7TwG16.js → agents-CTwbx-fb.js} +1 -1
- package/client/dist/assets/{agents-DaYcxAr1.js → agents-DEGV1Vao.js} +1 -1
- package/client/dist/assets/{agents-BHp0_FQt.js → agents-HsDF1aQU.js} +1 -1
- package/client/dist/assets/{agents-Bvyi3y6R.js → agents-IQjdA-z9.js} +1 -1
- package/client/dist/assets/{agents-rzPnqoBR.js → agents-RkGoteXh.js} +1 -1
- package/client/dist/assets/analytics-BNgZ1Nj7.js +1 -0
- package/client/dist/assets/analytics-BlypLLR6.js +1 -0
- package/client/dist/assets/analytics-CoEzWPx4.js +1 -0
- package/client/dist/assets/analytics-DFh5lSOl.js +1 -0
- package/client/dist/assets/analytics-DZp-DS2U.js +1 -0
- package/client/dist/assets/analytics-DjLlE7jS.js +1 -0
- package/client/dist/assets/analytics-jzCkOec3.js +1 -0
- package/client/dist/assets/analytics-nZIjjtCc.js +1 -0
- package/client/dist/assets/{dashboard-BK_mez-z.js → dashboard-C29Qwa-L.js} +1 -1
- package/client/dist/assets/{dashboard-BwkjxI6H.js → dashboard-CSJzf5OT.js} +1 -1
- package/client/dist/assets/{dashboard-C3bDKf7s.js → dashboard-CTfEtkqX.js} +1 -1
- package/client/dist/assets/{dashboard-Bt0cCzeG.js → dashboard-Cr4D8oMn.js} +1 -1
- package/client/dist/assets/{dashboard-fRrP0Kv0.js → dashboard-Csf1yGSN.js} +1 -1
- package/client/dist/assets/{dashboard-CdvjGfog.js → dashboard-D40HP2ks.js} +1 -1
- package/client/dist/assets/dashboard-DBAP2_SQ.js +1 -0
- package/client/dist/assets/{dashboard-B14uYXkG.js → dashboard-DMklepmz.js} +1 -1
- package/client/dist/assets/file-text-CFT-xXKN.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-CeZv93iR.js → formatDistanceToNow-w_2U5pm0.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DxAkjvJb.js → getTimezoneOffsetInMilliseconds-BRymg1Kk.js} +1 -1
- package/client/dist/assets/index-D0ScFZvR.css +2 -0
- package/client/dist/assets/index-DORCrgbI.js +77 -0
- package/client/dist/assets/jira-api-BqHbSdxK.js +1 -0
- package/client/dist/assets/jobs-C6qmvUAi.js +1 -0
- package/client/dist/assets/jobs-C9j2PuwW.js +1 -0
- package/client/dist/assets/jobs-DLHdDkHc.js +1 -0
- package/client/dist/assets/jobs-DSp9DoD_.js +1 -0
- package/client/dist/assets/jobs-DTQO08nJ.js +1 -0
- package/client/dist/assets/jobs-DoFaEl9g.js +1 -0
- package/client/dist/assets/jobs-Dpw6AEOP.js +1 -0
- package/client/dist/assets/jobs-DrF8Do8s.js +1 -0
- package/client/dist/assets/{narration-BSldQfjd.js → narration--hUhjM5v.js} +1 -1
- package/client/dist/assets/{narration-DKLjXIFI.js → narration-CHI06czm.js} +1 -1
- package/client/dist/assets/{narration-4OSg0b5-.js → narration-DjKB3pQZ.js} +1 -1
- package/client/dist/assets/{narration-BPMOQPFe.js → narration-DtCYvRLM.js} +1 -1
- package/client/dist/assets/{narration-D-e9ssxA.js → narration-DvLQBW9U.js} +1 -1
- package/client/dist/assets/{narration-DnrKC3mF.js → narration-eSx3gR0W.js} +1 -1
- package/client/dist/assets/{narration-Dypjxeel.js → narration-n3hOA7LY.js} +1 -1
- package/client/dist/assets/{narration-CvQdWdR5.js → narration-thJOeT2-.js} +1 -1
- package/client/dist/assets/packet-BNW8ODQw.js +1 -0
- package/client/dist/assets/packet-CXYvfdjy.js +1 -0
- package/client/dist/assets/packet-CtIlB8YE.js +1 -0
- package/client/dist/assets/packet-DA1SJkMH.js +1 -0
- package/client/dist/assets/packet-DYcWfBip.js +1 -0
- package/client/dist/assets/packet-DseX9yKC.js +1 -0
- package/client/dist/assets/packet-_m5aOy_d.js +1 -0
- package/client/dist/assets/packet-qOemUAm3.js +1 -0
- package/client/dist/assets/{project-repositories-BvBj9XH9.js → project-repositories-BsCs1PPA.js} +1 -1
- package/client/dist/assets/provider-capabilities-Vi1ZGvAK.js +1 -0
- package/client/dist/assets/settings-BsehKpty.js +1 -0
- package/client/dist/assets/settings-CQWob3SB.js +1 -0
- package/client/dist/assets/settings-CgCl0hzy.js +1 -0
- package/client/dist/assets/settings-DLRLd22L.js +1 -0
- package/client/dist/assets/settings-Dn5prAFl.js +1 -0
- package/client/dist/assets/settings-HZSu-K4l.js +1 -0
- package/client/dist/assets/{settings-DEgcA0jd.js → settings-p4OVIyRh.js} +1 -1
- package/client/dist/assets/settings-rddaldUp.js +1 -0
- package/client/dist/assets/setup-B5FHuxvB.js +1 -0
- package/client/dist/assets/setup-BihzXadx.js +1 -0
- package/client/dist/assets/setup-BtC6Hg4K.js +1 -0
- package/client/dist/assets/setup-C9Fy9PKQ.js +1 -0
- package/client/dist/assets/setup-CROyqD5N.js +1 -0
- package/client/dist/assets/setup-DpqzaEEA.js +1 -0
- package/client/dist/assets/setup-Dwzt5nO1.js +1 -0
- package/client/dist/assets/setup-zLEDnG7N.js +1 -0
- package/client/dist/assets/{spending-DNM6NAoA.js → spending-hcgxHP4O.js} +1 -1
- package/client/dist/assets/{useDesktop-mxHVQOdx.js → useDesktop-CAisjKWC.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-DnxHj8jP.js → useSharedWebSocket-BHIfW4QT.js} +2 -2
- package/client/dist/assets/wrench-BfmNO9Vw.js +1 -0
- package/client/dist/index.html +27 -26
- package/docs/guide/de/integrations/7-local-engines.md +37 -0
- package/docs/guide/en/integrations/7-local-engines.md +37 -0
- package/docs/guide/es/integrations/7-local-engines.md +37 -0
- package/docs/guide/fr/integrations/7-local-engines.md +37 -0
- package/docs/guide/it/integrations/7-local-engines.md +37 -0
- package/docs/guide/ja/integrations/7-local-engines.md +37 -0
- package/docs/guide/pt/integrations/7-local-engines.md +37 -0
- package/docs/guide/zh/integrations/7-local-engines.md +37 -0
- package/docs/internals/adding-a-provider.md +12 -0
- package/docs/internals/local-agent-runner.md +153 -0
- package/docs/internals/review-packet.md +8 -0
- package/docs/local-providers.md +215 -0
- package/docs/plans/implementation-efficiency-artifact.json +18 -0
- package/docs/plans/implementation-efficiency-evaluation.json +1817 -0
- package/docs/plans/implementation-efficiency-evaluation.md +41 -0
- package/docs/plans/implementation-efficiency-progress.md +48 -0
- package/docs/plans/implementation-efficiency-release.md +41 -0
- package/docs/plans/implementation-efficiency.md +124 -0
- package/local-runner/dist/specrails-local-runner.js +15815 -0
- package/package.json +7 -5
- package/server/dist/agent-chat-manager.js +8 -1
- package/server/dist/agent-chat-router.js +5 -2
- package/server/dist/agent-fence-promotion.js +53 -0
- package/server/dist/agent-mcp-config.js +4 -1
- package/server/dist/agent-runtime-bridge.js +35 -26
- package/server/dist/agent-runtime-controls-router.js +20 -1
- package/server/dist/agent-runtime-controls.js +94 -12
- package/server/dist/agent-runtime-effective-config.js +53 -0
- package/server/dist/agent-runtime-events.js +46 -0
- package/server/dist/agent-runtime-history.js +47 -0
- package/server/dist/agent-runtime-loader.js +84 -5
- package/server/dist/agent-runtime-metrics.js +77 -1
- package/server/dist/agent-runtime-package.js +205 -0
- package/server/dist/agent-runtime-settings-router.js +96 -5
- package/server/dist/agent-runtime-settings.js +163 -7
- package/server/dist/agent-spec-framing.js +10 -4
- package/server/dist/binary-probe.js +12 -0
- package/server/dist/blueprint-router.js +3 -2
- package/server/dist/chat-manager.js +5 -1
- package/server/dist/desktop-router.js +87 -4
- package/server/dist/index.js +13 -0
- package/server/dist/interactive-job-session.js +3 -1
- package/server/dist/local-engine-detection.js +118 -0
- package/server/dist/loop-executors.js +4 -3
- package/server/dist/loop-role-engines.js +129 -0
- package/server/dist/loop-run-manager.js +14 -6
- package/server/dist/mcp/tools/app-settings.js +30 -4
- package/server/dist/mcp/tools/rails.js +2 -1
- package/server/dist/pr-body.js +11 -2
- package/server/dist/pricing.js +15 -0
- package/server/dist/provider-detection.js +63 -1
- package/server/dist/provider-selection.js +26 -1
- package/server/dist/providers/index.js +4 -1
- package/server/dist/providers/local-adapter-registry.js +56 -0
- package/server/dist/providers/local-adapter.js +426 -0
- package/server/dist/providers/registry.js +39 -4
- package/server/dist/rail-isolated-launch.js +4 -3
- package/server/dist/rails-router.js +90 -8
- package/server/dist/result-event.js +17 -0
- package/server/dist/review-packet.js +1 -0
- package/server/dist/schemas/agent-runtime.schema.json +28 -3
- package/server/dist/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
- package/server/dist/setup-manager.js +5 -1
- package/server/dist/spec-models.js +14 -2
- package/server/dist/worktree-node-modules.js +97 -65
- package/client/dist/assets/AgentBrowserCapture-BVcCK7x0.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-C5F3m29G.js +0 -2
- package/client/dist/assets/AgentModeCodePane-C5QuKsu0.js +0 -2
- package/client/dist/assets/AgentsPage-DiyJWZcU.js +0 -87
- package/client/dist/assets/AnalyticsPage-Dkmb0dnd.js +0 -1
- package/client/dist/assets/CodePage-C0dH99-t.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-DuTjr2n8.js +0 -1
- package/client/dist/assets/DocsDialog-BVZJZHVR.js +0 -11
- package/client/dist/assets/DocsPage-D6KHTv8V.js +0 -11
- package/client/dist/assets/InteractiveJobComposer-Ck3PQxjN.js +0 -19
- package/client/dist/assets/JobDetailModal-DkoMH3ie.js +0 -1
- package/client/dist/assets/JobDetailPage-CBVivcl2.js +0 -1
- package/client/dist/assets/JobsPage-CzxEeca9.js +0 -1
- package/client/dist/assets/LoopsPage-6iW7g25k.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-B6wVevPq.js +0 -1
- package/client/dist/assets/PluginsPage-Bf1z3WPF.js +0 -2
- package/client/dist/assets/ReviewPacketPage-CaXAwtR7.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-9aRFpJ_J.js +0 -1
- package/client/dist/assets/TicketDetailModalContext-B1X5glzV.js +0 -14
- package/client/dist/assets/agentRuntime-BUagD2I_.js +0 -1
- package/client/dist/assets/agentRuntime-BkL70WtX.js +0 -1
- package/client/dist/assets/agentRuntime-CWXVqgqE.js +0 -1
- package/client/dist/assets/agentRuntime-CvNBdUAe.js +0 -1
- package/client/dist/assets/agentRuntime-D4RmeuNY.js +0 -1
- package/client/dist/assets/agentRuntime-DTidBWKg.js +0 -1
- package/client/dist/assets/agentRuntime-DhKYfyaU.js +0 -1
- package/client/dist/assets/agentRuntime-LL8_GLx9.js +0 -1
- package/client/dist/assets/analytics-47pYygtV.js +0 -1
- package/client/dist/assets/analytics-B0vt1g5U.js +0 -1
- package/client/dist/assets/analytics-BMzCvrCU.js +0 -1
- package/client/dist/assets/analytics-BlFN3Zwx.js +0 -1
- package/client/dist/assets/analytics-CZYzL-nR.js +0 -1
- package/client/dist/assets/analytics-DAsb9MJ8.js +0 -1
- package/client/dist/assets/analytics-FXQlQ2Yt.js +0 -1
- package/client/dist/assets/analytics-RnFCNkah.js +0 -1
- package/client/dist/assets/dashboard-BfBir_6o.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/index-Cfh9aaep.css +0 -2
- package/client/dist/assets/index-DYoyDWim.js +0 -76
- package/client/dist/assets/jira-api-ChjVGJx_.js +0 -1
- package/client/dist/assets/jobs-5PQJbdOf.js +0 -1
- package/client/dist/assets/jobs-B2B08Oy9.js +0 -1
- package/client/dist/assets/jobs-BJZtBbwb.js +0 -1
- package/client/dist/assets/jobs-BlWEsQb7.js +0 -1
- package/client/dist/assets/jobs-ConfD8xD.js +0 -1
- package/client/dist/assets/jobs-DMPU64ms.js +0 -1
- package/client/dist/assets/jobs-DojASiTp.js +0 -1
- package/client/dist/assets/jobs-kVHkNpB-.js +0 -1
- package/client/dist/assets/packet-BQ2tXfJI.js +0 -1
- package/client/dist/assets/packet-BdpOJ1Cy.js +0 -1
- package/client/dist/assets/packet-C3Ievb9R.js +0 -1
- package/client/dist/assets/packet-C4zJIG0Y.js +0 -1
- package/client/dist/assets/packet-Cx-zRuWn.js +0 -1
- package/client/dist/assets/packet-DUTGX52M.js +0 -1
- package/client/dist/assets/packet-DhsKtUPB.js +0 -1
- package/client/dist/assets/packet-z3LJ8Zyy.js +0 -1
- package/client/dist/assets/provider-capabilities-CXipwrXJ.js +0 -1
- package/client/dist/assets/settings-B3rWEwOx.js +0 -1
- package/client/dist/assets/settings-BcVoiMyw.js +0 -1
- package/client/dist/assets/settings-BlYvaQ7w.js +0 -1
- package/client/dist/assets/settings-CD5uEBiL.js +0 -1
- package/client/dist/assets/settings-CyhS0DLJ.js +0 -1
- package/client/dist/assets/settings-DCMFUGDs.js +0 -1
- package/client/dist/assets/settings-DmbYKyMJ.js +0 -1
- package/client/dist/assets/setup-2xGgu4Sk.js +0 -1
- package/client/dist/assets/setup-Ba11l2JA.js +0 -1
- package/client/dist/assets/setup-BzW4kMok.js +0 -1
- package/client/dist/assets/setup-COzwYhkJ.js +0 -1
- package/client/dist/assets/setup-CRVULhtF.js +0 -1
- package/client/dist/assets/setup-DgvlxwpE.js +0 -1
- package/client/dist/assets/setup-Dw_wYeEY.js +0 -1
- package/client/dist/assets/setup-ZPtEeJPW.js +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Local (OpenAI-compatible) engine detection.
|
|
3
|
+
//
|
|
4
|
+
// A local connection has no binary to probe: reachability is decided by a
|
|
5
|
+
// bounded `GET <baseUrl>/models`. The result feeds the app-level provider
|
|
6
|
+
// detection singleton (provider-detection.ts) AND the local adapter's dynamic
|
|
7
|
+
// model catalog (modelCatalog() reads `getCachedModels(id)`).
|
|
8
|
+
//
|
|
9
|
+
// Mapping (design D4):
|
|
10
|
+
// 2xx + `data[]` → installed + executable, authenticated, models
|
|
11
|
+
// 401 / 403 → installed + executable, unauthenticated, []
|
|
12
|
+
// anything else / error → installed: false (unreachable)
|
|
13
|
+
//
|
|
14
|
+
// Spec: openspec/changes/local-ai-engines/specs/provider-auto-detection/spec.md
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.LOCAL_PROBE_TIMEOUT_MS = void 0;
|
|
17
|
+
exports.probeConnection = probeConnection;
|
|
18
|
+
exports.setCachedProbe = setCachedProbe;
|
|
19
|
+
exports.getCachedProbe = getCachedProbe;
|
|
20
|
+
exports.getCachedModels = getCachedModels;
|
|
21
|
+
exports.clearCachedProbe = clearCachedProbe;
|
|
22
|
+
exports._resetForTests = _resetForTests;
|
|
23
|
+
exports.LOCAL_PROBE_TIMEOUT_MS = 3000;
|
|
24
|
+
function modelsUrl(baseUrl) {
|
|
25
|
+
return `${baseUrl.replace(/\/+$/, '')}/models`;
|
|
26
|
+
}
|
|
27
|
+
function extractModelIds(body) {
|
|
28
|
+
if (!body || typeof body !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const data = body.data;
|
|
31
|
+
if (!Array.isArray(data))
|
|
32
|
+
return null;
|
|
33
|
+
const ids = [];
|
|
34
|
+
for (const entry of data) {
|
|
35
|
+
if (entry && typeof entry === 'object' && typeof entry.id === 'string') {
|
|
36
|
+
ids.push(entry.id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
/** Bounded, never-throwing reachability + models probe of one connection. */
|
|
42
|
+
async function probeConnection(conn, opts = {}) {
|
|
43
|
+
const timeoutMs = opts.timeoutMs ?? exports.LOCAL_PROBE_TIMEOUT_MS;
|
|
44
|
+
const doFetch = opts.fetch ?? fetch;
|
|
45
|
+
const env = opts.env ?? process.env;
|
|
46
|
+
const startedAt = Date.now();
|
|
47
|
+
const latency = () => Date.now() - startedAt;
|
|
48
|
+
const headers = { accept: 'application/json' };
|
|
49
|
+
let apiKeyEnvMissing = false;
|
|
50
|
+
if (conn.apiKeyEnv) {
|
|
51
|
+
const key = env[conn.apiKeyEnv];
|
|
52
|
+
if (key && key.length > 0)
|
|
53
|
+
headers.authorization = `Bearer ${key}`;
|
|
54
|
+
else
|
|
55
|
+
apiKeyEnvMissing = true;
|
|
56
|
+
}
|
|
57
|
+
const extra = apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {};
|
|
58
|
+
const unreachable = (error) => ({
|
|
59
|
+
reachable: false, installed: false, executable: false, authState: 'unknown',
|
|
60
|
+
models: [], latencyMs: latency(), error, ...extra,
|
|
61
|
+
});
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await doFetch(modelsUrl(conn.baseUrl), {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers,
|
|
67
|
+
redirect: 'manual',
|
|
68
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const timedOut = err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError');
|
|
74
|
+
return unreachable(timedOut ? `timed out after ${timeoutMs} ms` : message);
|
|
75
|
+
}
|
|
76
|
+
if (res.status === 401 || res.status === 403) {
|
|
77
|
+
return {
|
|
78
|
+
reachable: true, installed: true, executable: true, authState: 'unauthenticated',
|
|
79
|
+
models: [], latencyMs: latency(), error: `endpoint answered HTTP ${res.status} (not authorized)`, ...extra,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (res.status < 200 || res.status >= 300) {
|
|
83
|
+
return unreachable(`endpoint answered HTTP ${res.status}`);
|
|
84
|
+
}
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = await res.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return unreachable('endpoint did not return JSON');
|
|
91
|
+
}
|
|
92
|
+
const models = extractModelIds(body);
|
|
93
|
+
if (models === null)
|
|
94
|
+
return unreachable('endpoint response has no `data` array');
|
|
95
|
+
return {
|
|
96
|
+
reachable: true, installed: true, executable: true, authState: 'authenticated',
|
|
97
|
+
models, latencyMs: latency(), ...extra,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ─── Module cache (per connection id) ────────────────────────────────────────
|
|
101
|
+
const _cache = new Map();
|
|
102
|
+
function setCachedProbe(id, result) {
|
|
103
|
+
_cache.set(id, result);
|
|
104
|
+
}
|
|
105
|
+
function getCachedProbe(id) {
|
|
106
|
+
return _cache.get(id) ?? null;
|
|
107
|
+
}
|
|
108
|
+
/** Models discovered by the most recent SUCCESSFUL probe of `id` (else []). */
|
|
109
|
+
function getCachedModels(id) {
|
|
110
|
+
const cached = _cache.get(id);
|
|
111
|
+
return cached && cached.authState === 'authenticated' ? [...cached.models] : [];
|
|
112
|
+
}
|
|
113
|
+
function clearCachedProbe(id) {
|
|
114
|
+
_cache.delete(id);
|
|
115
|
+
}
|
|
116
|
+
function _resetForTests() {
|
|
117
|
+
_cache.clear();
|
|
118
|
+
}
|
|
@@ -19,6 +19,7 @@ const node_fs_1 = require("node:fs");
|
|
|
19
19
|
const node_path_1 = require("node:path");
|
|
20
20
|
const win_spawn_1 = require("./util/win-spawn");
|
|
21
21
|
const providers_1 = require("./providers");
|
|
22
|
+
const registry_1 = require("./providers/registry");
|
|
22
23
|
const workspace_manager_1 = require("./workspace-manager");
|
|
23
24
|
const claude_trust_1 = require("./claude-trust");
|
|
24
25
|
const spawn_lifecycle_1 = require("./spawn-lifecycle");
|
|
@@ -222,7 +223,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
222
223
|
return null;
|
|
223
224
|
}
|
|
224
225
|
},
|
|
225
|
-
async runAiStep({ coreRun, prompt, sessionId, provider, model, effort, profileName, cwd, repoDir, executionManifest, onLine, onRawLine, onSpawn, aiStepTimeoutMs, idleTimeoutMs }) {
|
|
226
|
+
async runAiStep({ runtimeProviderOverride, coreRun, prompt, sessionId, provider, model, effort, profileName, cwd, repoDir, executionManifest, onLine, onRawLine, onSpawn, aiStepTimeoutMs, idleTimeoutMs }) {
|
|
226
227
|
const adapter = (0, providers_1.getAdapter)(provider);
|
|
227
228
|
// First iteration spawns headless (rail-job); subsequent iterations resume
|
|
228
229
|
// the session so the agent keeps prior context across iterations.
|
|
@@ -246,7 +247,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
246
247
|
return (0, agent_runtime_bridge_1.runAgentRuntimeInvocation)({
|
|
247
248
|
contextPath: core.contextPath, cwd, env: { ...programmaticStepEnv(resolveEnv(), repoDir, executionManifest), SPECRAILS_EXECUTION_CONTEXT: core.contextPath }, configPath: runtimeConfigPath(cwd),
|
|
248
249
|
change: (0, agent_runtime_bridge_1.runtimeChangeName)(coreRun.runId), resume: admitted,
|
|
249
|
-
defaultProvider: provider,
|
|
250
|
+
defaultProvider: provider, providerOverride: runtimeProviderOverride,
|
|
250
251
|
onLine, onRawLine, onSpawn, timeoutMs: aiStepTimeoutMs,
|
|
251
252
|
});
|
|
252
253
|
}
|
|
@@ -262,7 +263,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
262
263
|
const effectivePrompt = (0, runtime_1.formatProviderCommand)(adapter, [core?.promptPrefix, prompt].filter(Boolean).join('\n\n'), cwd, sessionId ?? undefined);
|
|
263
264
|
const buildOpts = {
|
|
264
265
|
prompt: effectivePrompt,
|
|
265
|
-
...(adapter.id === 'claude' ? { systemPrompt: loop_constants_1.FOREGROUND_RULE } : {}),
|
|
266
|
+
...(adapter.id === 'claude' || (0, registry_1.isLocalAdapterId)(adapter.id) ? { systemPrompt: loop_constants_1.FOREGROUND_RULE } : {}),
|
|
266
267
|
model,
|
|
267
268
|
sessionId: sessionId ?? undefined,
|
|
268
269
|
reasoning_effort: effort,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LoopRoleEnginesError = exports.LOOP_ROLES = exports.ROLES_ENGINE = void 0;
|
|
7
|
+
exports.loopRoleEnginesPath = loopRoleEnginesPath;
|
|
8
|
+
exports.loadLoopRoleEngines = loadLoopRoleEngines;
|
|
9
|
+
exports.validateLoopRoleEngines = validateLoopRoleEngines;
|
|
10
|
+
exports.saveLoopRoleEngines = saveLoopRoleEngines;
|
|
11
|
+
exports.resolveLoopRoleEngine = resolveLoopRoleEngine;
|
|
12
|
+
// Per-project engines for the loop-side AI roles (hybrid-role-engines).
|
|
13
|
+
//
|
|
14
|
+
// The pipeline roles architect/developer/reviewer already live in the project's
|
|
15
|
+
// `agent-runtime.json` (Settings ▸ Specrails Agents ▸ runtime) and Core runs
|
|
16
|
+
// them on whatever provider each role names — cloud or local, mixed freely.
|
|
17
|
+
// What kept a hybrid setup from working was the rail: a selected engine became
|
|
18
|
+
// a `runtimeProviderOverride` that flattened all three roles, and the loop's
|
|
19
|
+
// own AI steps (verify/fix, the Loop Decider) always inherited that engine.
|
|
20
|
+
//
|
|
21
|
+
// This module owns the two LOOP roles and the `roles` engine sentinel:
|
|
22
|
+
// - `verifier` — every non-core ai-step of the factory loop (verify, fix,
|
|
23
|
+
// freestyle-less prose steps) and the provider a custom loop's ai-steps run on.
|
|
24
|
+
// - `decider` — the Loop Decider (a cheap/fast model is the natural pick).
|
|
25
|
+
// A rail whose engine is `roles` launches WITHOUT a provider override (Core
|
|
26
|
+
// keeps the per-role settings) and resolves the loop steps from this file.
|
|
27
|
+
//
|
|
28
|
+
// Storage: `<workspace>/.specrails/loop-role-engines.json`, next to
|
|
29
|
+
// `agent-runtime.json`; absent ⇒ every loop role inherits the project's primary.
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const providers_1 = require("./providers");
|
|
33
|
+
const spec_models_1 = require("./spec-models");
|
|
34
|
+
const workspace_resolution_1 = require("./workspace-resolution");
|
|
35
|
+
const provider_selection_1 = require("./provider-selection");
|
|
36
|
+
/** The rail engine value meaning "per-role engines, no flattening override". */
|
|
37
|
+
exports.ROLES_ENGINE = 'roles';
|
|
38
|
+
exports.LOOP_ROLES = ['verifier', 'decider'];
|
|
39
|
+
class LoopRoleEnginesError extends Error {
|
|
40
|
+
}
|
|
41
|
+
exports.LoopRoleEnginesError = LoopRoleEnginesError;
|
|
42
|
+
function loopRoleEnginesPath(project) {
|
|
43
|
+
return node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'loop-role-engines.json');
|
|
44
|
+
}
|
|
45
|
+
function loadLoopRoleEngines(project) {
|
|
46
|
+
const file = loopRoleEnginesPath(project);
|
|
47
|
+
if (!(0, node_fs_1.existsSync)(file))
|
|
48
|
+
return {};
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const role of exports.LOOP_ROLES) {
|
|
53
|
+
const entry = raw[role];
|
|
54
|
+
if (entry && typeof entry === 'object' && typeof entry.provider === 'string') {
|
|
55
|
+
const { provider, model, effort } = entry;
|
|
56
|
+
out[role] = { provider, ...(typeof model === 'string' && model ? { model } : {}), ...(typeof effort === 'string' && effort ? { effort } : {}) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Validates against the project's DETECTED providers; an unknown role or provider is rejected, a stale model is rejected. */
|
|
66
|
+
function validateLoopRoleEngines(value, project) {
|
|
67
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
68
|
+
throw new LoopRoleEnginesError('Loop role engines must be an object');
|
|
69
|
+
const raw = value;
|
|
70
|
+
for (const key of Object.keys(raw))
|
|
71
|
+
if (!exports.LOOP_ROLES.includes(key))
|
|
72
|
+
throw new LoopRoleEnginesError(`Unknown loop role "${key}"`);
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const role of exports.LOOP_ROLES) {
|
|
75
|
+
const entry = raw[role];
|
|
76
|
+
if (entry === undefined || entry === null)
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof entry !== 'object' || Array.isArray(entry))
|
|
79
|
+
throw new LoopRoleEnginesError(`Loop role "${role}" must be an object`);
|
|
80
|
+
const { provider, model, effort, ...rest } = entry;
|
|
81
|
+
if (Object.keys(rest).length)
|
|
82
|
+
throw new LoopRoleEnginesError(`Loop role "${role}" has unknown fields: ${Object.keys(rest).join(', ')}`);
|
|
83
|
+
if (typeof provider !== 'string' || !provider.trim())
|
|
84
|
+
throw new LoopRoleEnginesError(`Loop role "${role}" needs a provider`);
|
|
85
|
+
const check = (0, provider_selection_1.validateRequestedProvider)(project, provider);
|
|
86
|
+
if (!check.ok)
|
|
87
|
+
throw new LoopRoleEnginesError(`Loop role "${role}": ${check.error}`);
|
|
88
|
+
const engine = { provider: check.provider };
|
|
89
|
+
if (model !== undefined && model !== null && model !== '') {
|
|
90
|
+
if (typeof model !== 'string' || model.length > 256 || /[\r\n\0]/.test(model))
|
|
91
|
+
throw new LoopRoleEnginesError(`Loop role "${role}" has an invalid model`);
|
|
92
|
+
if (!(0, spec_models_1.isValidModelForProvider)(model, check.provider))
|
|
93
|
+
throw new LoopRoleEnginesError(`Loop role "${role}": model "${model}" is not valid for provider "${check.provider}"`);
|
|
94
|
+
engine.model = model;
|
|
95
|
+
}
|
|
96
|
+
if (effort !== undefined && effort !== null && effort !== '') {
|
|
97
|
+
if (typeof effort !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/.test(effort))
|
|
98
|
+
throw new LoopRoleEnginesError(`Loop role "${role}" has an invalid effort`);
|
|
99
|
+
engine.effort = effort;
|
|
100
|
+
}
|
|
101
|
+
out[role] = engine;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function saveLoopRoleEngines(project, value) {
|
|
106
|
+
const file = loopRoleEnginesPath(project);
|
|
107
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(file), { recursive: true });
|
|
108
|
+
(0, node_fs_1.writeFileSync)(file, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The engine a loop role runs on for THIS launch: the stored role engine when
|
|
112
|
+
* its provider is still detected, else the fallback (the rail's primary path).
|
|
113
|
+
* Model falls back to the adapter default; effort is dropped when the adapter
|
|
114
|
+
* cannot honour it (local engines without the opt-in) or the model rejects it.
|
|
115
|
+
*/
|
|
116
|
+
function resolveLoopRoleEngine(project, role, fallback, stored = loadLoopRoleEngines(project)) {
|
|
117
|
+
const entry = stored[role];
|
|
118
|
+
if (!entry)
|
|
119
|
+
return fallback;
|
|
120
|
+
const check = (0, provider_selection_1.validateRequestedProvider)(project, entry.provider);
|
|
121
|
+
if (!check.ok || !(0, providers_1.hasAdapter)(check.provider))
|
|
122
|
+
return fallback;
|
|
123
|
+
const adapter = (0, providers_1.getAdapter)(check.provider);
|
|
124
|
+
const model = entry.model && (0, spec_models_1.isValidModelForProvider)(entry.model, check.provider) ? entry.model : adapter.defaultModel();
|
|
125
|
+
const effortAllowed = !((0, providers_1.isLocalAdapterId)(check.provider) && !adapter.capabilities.supportsReasoningEffort);
|
|
126
|
+
const allowed = effortAllowed ? (0, providers_1.reasoningEffortsForModel)(adapter, model) : [];
|
|
127
|
+
const effort = entry.effort && allowed.includes(entry.effort) ? entry.effort : undefined;
|
|
128
|
+
return { provider: check.provider, model, ...(effort ? { effort } : {}) };
|
|
129
|
+
}
|
|
@@ -5,6 +5,8 @@ exports.truncate = truncate;
|
|
|
5
5
|
exports.resolveRunVars = resolveRunVars;
|
|
6
6
|
exports.extractChangeId = extractChangeId;
|
|
7
7
|
exports.recoverOrphanLoopStepAccounting = recoverOrphanLoopStepAccounting;
|
|
8
|
+
const agent_runtime_events_1 = require("./agent-runtime-events");
|
|
9
|
+
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
8
10
|
const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
|
|
9
11
|
const win_spawn_1 = require("./util/win-spawn");
|
|
10
12
|
const db_1 = require("./db");
|
|
@@ -561,7 +563,7 @@ class LoopRunManager {
|
|
|
561
563
|
async run(req) {
|
|
562
564
|
// Later fresh steps must verify the admitted spec, even if its caller or
|
|
563
565
|
// backlog changes while the implementation is running.
|
|
564
|
-
req = { ...req, graph: structuredClone(req.graph) };
|
|
566
|
+
req = { ...req, runtimeProviderOverride: (0, agent_runtime_settings_1.validateRuntimeProviderOverride)(req.runtimeProviderOverride), graph: structuredClone(req.graph) };
|
|
565
567
|
if (req.spec)
|
|
566
568
|
req = { ...req, spec: structuredClone(req.spec) };
|
|
567
569
|
if (req.executionManifest)
|
|
@@ -752,6 +754,12 @@ class LoopRunManager {
|
|
|
752
754
|
logLine(line);
|
|
753
755
|
return;
|
|
754
756
|
}
|
|
757
|
+
if (eventType === 'runtime-efficiency-event') {
|
|
758
|
+
const validated = (0, agent_runtime_events_1.runtimeEfficiencyEventLine)(line, runId);
|
|
759
|
+
if (!validated || (0, agent_runtime_events_1.isRecordedRuntimeEfficiencyEvent)(this.db, runId, validated))
|
|
760
|
+
return;
|
|
761
|
+
line = validated;
|
|
762
|
+
}
|
|
755
763
|
const s = takeSeq();
|
|
756
764
|
try {
|
|
757
765
|
(0, db_1.appendEvent)(this.db, runId, s, { event_type: eventType, source: 'stdout', payload: line });
|
|
@@ -1145,7 +1153,7 @@ class LoopRunManager {
|
|
|
1145
1153
|
nextEventSeq: takeSeq,
|
|
1146
1154
|
recoveryStepKey: aiRecoveryKey,
|
|
1147
1155
|
})
|
|
1148
|
-
: this.executors.runAiStep({ coreRun, prompt, sessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, profileName: req.profileName, cwd: req.cwd, repoDir: req.repoDir, executionManifest: req.executionManifest, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs: effectiveTimeoutMs, idleTimeoutMs });
|
|
1156
|
+
: this.executors.runAiStep({ runtimeProviderOverride: req.runtimeProviderOverride, coreRun, prompt, sessionId, provider: nodeProvider, model: nodeModel, effort: nodeEffort, profileName: req.profileName, cwd: req.cwd, repoDir: req.repoDir, executionManifest: req.executionManifest, onLine: logLine, onRawLine, onSpawn: (c) => this._activeChild.set(runId, c), aiStepTimeoutMs: effectiveTimeoutMs, idleTimeoutMs });
|
|
1149
1157
|
};
|
|
1150
1158
|
let res = await runAttempt(aiSessionId);
|
|
1151
1159
|
if (this._disposed)
|
|
@@ -1418,7 +1426,7 @@ class LoopRunManager {
|
|
|
1418
1426
|
iteration += 1;
|
|
1419
1427
|
const goal = (0, loop_constants_1.resolveConstants)((0, loop_graph_1.interpolateSpec)(String(node.data?.goal ?? 'The loop goal is met.'), req.spec), constMap);
|
|
1420
1428
|
// `iteration` was just incremented — it IS this pass's 1-based number.
|
|
1421
|
-
emitStep('decider', `🔍 ${nodeLabel || 'Loop Decider'} (iteration ${iteration})`, node.id, iteration);
|
|
1429
|
+
emitStep('decider', `🔍 ${nodeLabel || 'Loop Decider'} (iteration ${iteration}${req.deciderEngine ? `, ${req.deciderEngine.provider}/${req.deciderEngine.model}` : ''})`, node.id, iteration);
|
|
1422
1430
|
logLine(`Goal: ${goal}`);
|
|
1423
1431
|
// BUG-32: capture the real Decider start BEFORE the await (see AI step).
|
|
1424
1432
|
const deciderStart = new Date(this.now()).toISOString();
|
|
@@ -1428,9 +1436,9 @@ class LoopRunManager {
|
|
|
1428
1436
|
// Give the Decider the spec so it can verify completeness against the
|
|
1429
1437
|
// FULL scope instead of trusting a step's self-reported success.
|
|
1430
1438
|
userPrompt: [(0, multi_repo_execution_store_1.executionManifestPrompt)(req.executionManifest), (0, loop_decider_1.buildDeciderUserPrompt)({ goal, history, spec: req.spec })].filter(Boolean).join('\n\n'),
|
|
1431
|
-
provider: nodeProvider,
|
|
1432
|
-
model: nodeModel,
|
|
1433
|
-
effort: nodeEffort,
|
|
1439
|
+
provider: req.deciderEngine?.provider ?? nodeProvider,
|
|
1440
|
+
model: req.deciderEngine?.model ?? nodeModel,
|
|
1441
|
+
effort: req.deciderEngine ? req.deciderEngine.effort : nodeEffort,
|
|
1434
1442
|
cwd: req.cwd,
|
|
1435
1443
|
repoDir: req.repoDir, executionManifest: req.executionManifest,
|
|
1436
1444
|
onLine: logLine,
|
|
@@ -4,6 +4,7 @@ exports.appTools = appTools;
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const desktop_db_1 = require("../../desktop-db");
|
|
6
6
|
const mcp_tiers_1 = require("../mcp-tiers");
|
|
7
|
+
const types_1 = require("./types");
|
|
7
8
|
// App-level settings the MCP may read/write. NOTE: the MCP enable flag and the
|
|
8
9
|
// permission-tier toggles are intentionally NOT writable here — an external LLM
|
|
9
10
|
// must never be able to escalate its own permissions. Those are user-only,
|
|
@@ -20,12 +21,19 @@ function appTools() {
|
|
|
20
21
|
name: 'specrails_settings',
|
|
21
22
|
title: 'App settings',
|
|
22
23
|
description: 'Read or update app-level Specrails settings (theme, language, daily budget, cost-alert threshold, Code Explorer summary language + monthly summary budget). ' +
|
|
23
|
-
'Actions: get, set.
|
|
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
|
+
'The MCP enable flag and permission tiers are read-only here (user-controlled in the app). ' +
|
|
24
26
|
'dailyBudgetUsd here is APP-WIDE (all projects combined); per-project caps live in specrails_analytics(budget_set).',
|
|
25
27
|
hintTier: 'read',
|
|
26
|
-
tier: (args) => (args.action === 'set' ? 'write' : 'read'),
|
|
28
|
+
tier: (args) => (args.action === 'set' || args.action === 'runtime_providers.save' ? 'write' : 'read'),
|
|
27
29
|
inputSchema: {
|
|
28
|
-
action: zod_1.z.enum(['get', 'set']).describe('Operation'),
|
|
30
|
+
action: zod_1.z.enum(['get', 'set', 'runtime_providers.list', 'runtime_providers.test', 'runtime_providers.save']).describe('Operation'),
|
|
31
|
+
baseUrl: zod_1.z.string().optional().describe('runtime_providers.test: OpenAI-compatible base URL (e.g. http://127.0.0.1:11434/v1)'),
|
|
32
|
+
apiKeyEnv: zod_1.z.string().optional().describe('runtime_providers.test: env var NAME holding the API key (never the key itself)'),
|
|
33
|
+
providers: zod_1.z
|
|
34
|
+
.array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()))
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('runtime_providers.save: the FULL connections list ({ id, kind: "cli", cli } | { id, kind: "openai-compatible", baseUrl, apiKeyEnv?, label?, defaultModel?, rates?, supportsReasoningEffort? })'),
|
|
29
37
|
theme: zod_1.z.enum(WRITABLE_THEMES).optional(),
|
|
30
38
|
language: zod_1.z.enum(WRITABLE_LANGS).optional(),
|
|
31
39
|
dailyBudgetUsd: zod_1.z
|
|
@@ -45,8 +53,26 @@ function appTools() {
|
|
|
45
53
|
.optional()
|
|
46
54
|
.describe('Code Explorer monthly AI file-summary budget in USD (app-wide, default 5)'),
|
|
47
55
|
},
|
|
48
|
-
handler: (ctx, args) => {
|
|
56
|
+
handler: async (ctx, args) => {
|
|
49
57
|
const db = ctx.desktopDb;
|
|
58
|
+
// Provider connections (local AI engines) — mirror the REST routes over
|
|
59
|
+
// loopback so validation, adapter re-sync and broadcasts stay identical.
|
|
60
|
+
if (args.action === 'runtime_providers.list') {
|
|
61
|
+
return (0, types_1.apiCall)(ctx, 'GET', '/runtime-providers');
|
|
62
|
+
}
|
|
63
|
+
if (args.action === 'runtime_providers.test') {
|
|
64
|
+
if (typeof args.baseUrl !== 'string' || !args.baseUrl)
|
|
65
|
+
throw new Error('runtime_providers.test requires baseUrl.');
|
|
66
|
+
return (0, types_1.apiCall)(ctx, 'POST', '/runtime-providers/test', {
|
|
67
|
+
baseUrl: args.baseUrl,
|
|
68
|
+
...(typeof args.apiKeyEnv === 'string' && args.apiKeyEnv ? { apiKeyEnv: args.apiKeyEnv } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (args.action === 'runtime_providers.save') {
|
|
72
|
+
if (!Array.isArray(args.providers) || !args.providers.length)
|
|
73
|
+
throw new Error('runtime_providers.save requires a non-empty providers list.');
|
|
74
|
+
return (0, types_1.apiCall)(ctx, 'PUT', '/runtime-providers', { providers: args.providers });
|
|
75
|
+
}
|
|
50
76
|
if (args.action === 'get') {
|
|
51
77
|
const theme = normalizeStoredTheme(db);
|
|
52
78
|
return {
|
|
@@ -47,6 +47,7 @@ function railsTools() {
|
|
|
47
47
|
'stop (destructive — kills all active jobs and loop runs for the rail). ' +
|
|
48
48
|
'For on_review tickets with an already-open GitHub PR (including a published pr_ready delivery), launch automatically tries to continue that PR head branch; Jira-linked in_progress tickets can do the same when the PR match is explicit; fresh tickets still start from the project integration branch. ' +
|
|
49
49
|
'When launched from the in-app agent chat without an explicit aiEngine, the engine defaults to your conversation\'s provider. On that same provider, an omitted model inherits the conversation model; an explicit model wins (pass aiEngine to override; launch_all always uses each rail\'s stored engine). ' +
|
|
50
|
+
'For Implement and Batch Implement, the selected launch provider applies to ALL runtime roles (architect, developer, reviewer); an explicit launch model/effort also applies to each role. Resuming an existing execution keeps its original frozen role configuration; start a new launch to change providers. ' +
|
|
50
51
|
'User-facing naming: call the free-form autonomous mode "Freestyle"; use "freestyle" as the canonical API enum value for that same capability. ' +
|
|
51
52
|
'For small OpenSpec-governed work, recommend "SDD Quick (OpenSpec)" and launch with mode "loop" plus loopId "factory:sdd-quick-openspec"; keep Freestyle for ticket-local implementation-only work. ' +
|
|
52
53
|
'NAMING: railIndex is the 0-BASED internal identity; the dashboard shows rails 1-based ("Rail N" = railIndex N-1). When talking to the user, ALWAYS say "Rail <railIndex + 1>" (or the rail\'s custom name) — results include railLabel with the correct user-facing label.',
|
|
@@ -91,7 +92,7 @@ function railsTools() {
|
|
|
91
92
|
.string()
|
|
92
93
|
.nullable()
|
|
93
94
|
.optional()
|
|
94
|
-
.describe('AI engine/provider override; null = project primary (set_engine; also optional override for launch). Must be an installed provider. For launch from the in-app agent chat, omitting it defaults to the launching conversation\'s provider (overriding any stored rail engine) — pass a string or null explicitly to control it.'),
|
|
95
|
+
.describe('AI engine/provider override; null = project primary (set_engine; also optional override for launch). Must be an installed provider, or "roles" to run each role on its own configured engine (Settings ▸ Specrails Agents: architect/developer/reviewer from the runtime config, verifier/decider from the loop roles; requires the agent runtime). For launch from the in-app agent chat, omitting it defaults to the launching conversation\'s provider (overriding any stored rail engine) — pass a string or null explicitly to control it.'),
|
|
95
96
|
// set_name / create_rail
|
|
96
97
|
name: zod_1.z
|
|
97
98
|
.string()
|
package/server/dist/pr-body.js
CHANGED
|
@@ -188,7 +188,7 @@ function extractSpecNarrative(description) {
|
|
|
188
188
|
if (solutionSections.length > 0) {
|
|
189
189
|
for (const s of solutionSections) {
|
|
190
190
|
if (s.content.trim())
|
|
191
|
-
solutionParts.push(
|
|
191
|
+
solutionParts.push(sectionDigest(s));
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
else {
|
|
@@ -197,7 +197,7 @@ function extractSpecNarrative(description) {
|
|
|
197
197
|
const rest = headed.filter((s) => s !== problemSection && s.content.trim());
|
|
198
198
|
if (rest.length > 0) {
|
|
199
199
|
for (const s of rest)
|
|
200
|
-
solutionParts.push(
|
|
200
|
+
solutionParts.push(sectionDigest(s));
|
|
201
201
|
}
|
|
202
202
|
else if (!problemSection && preamble && problem === preamble) {
|
|
203
203
|
// Heading-less description: first paragraph = problem, remainder = solution.
|
|
@@ -219,6 +219,15 @@ function extractSpecNarrative(description) {
|
|
|
219
219
|
overflow: solutionFull,
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* A headed section as markdown that still renders as markdown: the bold
|
|
224
|
+
* label on its own line, then the content as a BLOCK. The old inline
|
|
225
|
+
* `**Title** — content` join put a numbered list's first item on the label
|
|
226
|
+
* line, so every renderer (GitHub PR body, the review packet) lost the list.
|
|
227
|
+
*/
|
|
228
|
+
function sectionDigest(s) {
|
|
229
|
+
return `**${s.title.trim()}**\n\n${s.content.trim()}`;
|
|
230
|
+
}
|
|
222
231
|
/** Split markdown into sections at `#`-headings and standalone `**Bold**` label lines. */
|
|
223
232
|
function splitSections(text) {
|
|
224
233
|
const lines = text.split('\n');
|
package/server/dist/pricing.js
CHANGED
|
@@ -23,6 +23,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
exports.PRICING = void 0;
|
|
24
24
|
exports.resolvePriceEntry = resolvePriceEntry;
|
|
25
25
|
exports.estimateCostUsd = estimateCostUsd;
|
|
26
|
+
exports.estimateLocalCostUsd = estimateLocalCostUsd;
|
|
26
27
|
exports.lastReviewedAt = lastReviewedAt;
|
|
27
28
|
exports.providerNeedsCostEstimation = providerNeedsCostEstimation;
|
|
28
29
|
const registry_1 = require("./providers/registry");
|
|
@@ -159,6 +160,20 @@ function estimateCostUsd(providerId, model, usage) {
|
|
|
159
160
|
: 0;
|
|
160
161
|
return inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
161
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Cost of a LOCAL (OpenAI-compatible) invocation from the connection's
|
|
165
|
+
* user-supplied per-1M rates. `null` when the connection has no rates or the
|
|
166
|
+
* usage is empty — never a rate-card guess for a local model.
|
|
167
|
+
*/
|
|
168
|
+
function estimateLocalCostUsd(rates, usage) {
|
|
169
|
+
if (!rates)
|
|
170
|
+
return null;
|
|
171
|
+
const tokensIn = usage.tokens_in ?? 0;
|
|
172
|
+
const tokensOut = usage.tokens_out ?? 0;
|
|
173
|
+
if (tokensIn <= 0 && tokensOut <= 0)
|
|
174
|
+
return null;
|
|
175
|
+
return tokensIn / 1_000_000 * rates.inputPer1M + tokensOut / 1_000_000 * rates.outputPer1M;
|
|
176
|
+
}
|
|
162
177
|
/**
|
|
163
178
|
* Returns the oldest `lastReviewedAt` date across the table. Surfaced by an
|
|
164
179
|
* optional diagnostic endpoint so the maintainer can spot staleness quickly.
|
|
@@ -48,6 +48,7 @@ exports.isGeminiBetaDisabled = isGeminiBetaDisabled;
|
|
|
48
48
|
exports.getDetectionSnapshot = getDetectionSnapshot;
|
|
49
49
|
exports.getDetectedIdsSync = getDetectedIdsSync;
|
|
50
50
|
exports.getSnapshotSync = getSnapshotSync;
|
|
51
|
+
exports.refreshLocalDetection = refreshLocalDetection;
|
|
51
52
|
exports.refreshDetection = refreshDetection;
|
|
52
53
|
exports._resetDetectionForTests = _resetDetectionForTests;
|
|
53
54
|
const fs = __importStar(require("fs"));
|
|
@@ -55,6 +56,9 @@ const os = __importStar(require("os"));
|
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const providers_1 = require("./providers");
|
|
57
58
|
const core_compat_1 = require("./core-compat");
|
|
59
|
+
const local_adapter_1 = require("./providers/local-adapter");
|
|
60
|
+
const local_adapter_registry_1 = require("./providers/local-adapter-registry");
|
|
61
|
+
const local_engine_detection_1 = require("./local-engine-detection");
|
|
58
62
|
const CACHE_TTL_MS = 60_000;
|
|
59
63
|
/** Auth probes are offline file checks today, but keep the contract bounded. */
|
|
60
64
|
exports.AUTH_PROBE_TIMEOUT_MS = 1500;
|
|
@@ -135,7 +139,34 @@ function usabilityError(adapter, d) {
|
|
|
135
139
|
}
|
|
136
140
|
return undefined;
|
|
137
141
|
}
|
|
142
|
+
/** Local (OpenAI-compatible) adapter: the bounded HTTP models probe, cached for
|
|
143
|
+
* the adapter's dynamic `modelCatalog()`. */
|
|
144
|
+
async function probeLocalAdapter(adapter) {
|
|
145
|
+
let probe;
|
|
146
|
+
try {
|
|
147
|
+
probe = await (0, local_engine_detection_1.probeConnection)({ baseUrl: adapter.localConnection.baseUrl, apiKeyEnv: adapter.localConnection.apiKeyEnv });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
probe = { reachable: false, installed: false, executable: false, authState: 'unknown', models: [], latencyMs: 0, error: err instanceof Error ? err.message : String(err) };
|
|
151
|
+
}
|
|
152
|
+
(0, local_engine_detection_1.setCachedProbe)(adapter.id, probe);
|
|
153
|
+
return {
|
|
154
|
+
id: adapter.id,
|
|
155
|
+
displayName: adapter.displayName,
|
|
156
|
+
installed: probe.installed,
|
|
157
|
+
executable: probe.executable,
|
|
158
|
+
authState: probe.authState,
|
|
159
|
+
usable: probe.reachable,
|
|
160
|
+
error: probe.reachable ? undefined : (probe.error ?? `${adapter.displayName} endpoint is unreachable.`),
|
|
161
|
+
kind: 'local',
|
|
162
|
+
models: probe.models,
|
|
163
|
+
latencyMs: probe.latencyMs,
|
|
164
|
+
...(probe.apiKeyEnvMissing ? { apiKeyEnvMissing: true } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
138
167
|
async function probeAdapter(adapter, home) {
|
|
168
|
+
if ((0, local_adapter_1.isLocalAdapter)(adapter))
|
|
169
|
+
return probeLocalAdapter(adapter);
|
|
139
170
|
const vetoed = isVetoed(adapter.id);
|
|
140
171
|
let detection;
|
|
141
172
|
try {
|
|
@@ -179,7 +210,10 @@ let _snapshot = null;
|
|
|
179
210
|
let _inflight = null;
|
|
180
211
|
async function runDetection() {
|
|
181
212
|
const home = os.homedir();
|
|
182
|
-
|
|
213
|
+
// Local adapters are probed in the SAME cycle as CLIs; under the kill
|
|
214
|
+
// switch they are skipped entirely (they should not be registered anyway).
|
|
215
|
+
const localEnabled = (0, local_adapter_registry_1.isLocalEnginesEnabled)();
|
|
216
|
+
const adapters = (0, providers_1.listAdapters)().filter((a) => localEnabled || !(0, local_adapter_1.isLocalAdapter)(a));
|
|
183
217
|
const results = await Promise.all(adapters.map((a) => probeAdapter(a, home)));
|
|
184
218
|
const providers = {};
|
|
185
219
|
for (const r of results)
|
|
@@ -221,6 +255,34 @@ function getSnapshotSync() {
|
|
|
221
255
|
* Refresh and report whether the usable set changed vs the previous snapshot.
|
|
222
256
|
* Callers use the flag to decide whether to broadcast `providers.detected_changed`.
|
|
223
257
|
*/
|
|
258
|
+
/**
|
|
259
|
+
* Re-probe ONLY the registered local adapters (after a connections save) and
|
|
260
|
+
* merge into the current snapshot without re-spawning CLI probes. Returns
|
|
261
|
+
* whether the usable set changed. Falls back to a full refresh when no
|
|
262
|
+
* snapshot exists yet.
|
|
263
|
+
*/
|
|
264
|
+
async function refreshLocalDetection() {
|
|
265
|
+
if (!_snapshot)
|
|
266
|
+
return refreshDetection();
|
|
267
|
+
const before = _snapshot.detected.join(',');
|
|
268
|
+
const localEnabled = (0, local_adapter_registry_1.isLocalEnginesEnabled)();
|
|
269
|
+
const locals = (0, providers_1.listAdapters)().filter(local_adapter_1.isLocalAdapter);
|
|
270
|
+
const providers = {};
|
|
271
|
+
for (const [id, row] of Object.entries(_snapshot.providers)) {
|
|
272
|
+
if (row.kind !== 'local')
|
|
273
|
+
providers[id] = row;
|
|
274
|
+
}
|
|
275
|
+
if (localEnabled) {
|
|
276
|
+
const results = await Promise.all(locals.map((a) => probeLocalAdapter(a)));
|
|
277
|
+
for (const r of results)
|
|
278
|
+
providers[r.id] = r;
|
|
279
|
+
}
|
|
280
|
+
const ordered = (0, providers_1.listAdapters)().map((a) => a.id);
|
|
281
|
+
const detected = ordered.filter((id) => providers[id]?.usable);
|
|
282
|
+
const snapshot = { providers, detected, at: _snapshot.at };
|
|
283
|
+
_snapshot = snapshot;
|
|
284
|
+
return { snapshot, changed: before !== detected.join(',') };
|
|
285
|
+
}
|
|
224
286
|
async function refreshDetection() {
|
|
225
287
|
const before = _snapshot?.detected.join(',') ?? null;
|
|
226
288
|
const snapshot = await getDetectionSnapshot({ refresh: true });
|
|
@@ -21,8 +21,10 @@ exports.installedProviders = installedProviders;
|
|
|
21
21
|
exports.isProviderEnabled = isProviderEnabled;
|
|
22
22
|
exports.isMultiProvider = isMultiProvider;
|
|
23
23
|
exports.derivePrimaryProvider = derivePrimaryProvider;
|
|
24
|
+
exports.defaultMachineProvider = defaultMachineProvider;
|
|
24
25
|
exports.resolveProvider = resolveProvider;
|
|
25
26
|
exports.validateRequestedProvider = validateRequestedProvider;
|
|
27
|
+
const registry_1 = require("./providers/registry");
|
|
26
28
|
/** Fixed preference order for primary derivation when the stored primary is gone. */
|
|
27
29
|
exports.PROVIDER_PREFERENCE_ORDER = ['claude', 'codex', 'gemini', 'kimi'];
|
|
28
30
|
let _detectedSupplier = null;
|
|
@@ -72,14 +74,37 @@ function isMultiProvider(project) {
|
|
|
72
74
|
function derivePrimaryProvider(project) {
|
|
73
75
|
const available = installedProviders(project);
|
|
74
76
|
const stored = project.provider;
|
|
75
|
-
|
|
77
|
+
// A stored primary that is a LOCAL engine never wins over a detected CLI:
|
|
78
|
+
// local ids are opt-in per invocation, the machine's primary stays a CLI
|
|
79
|
+
// whenever one is detected (a local-only machine may still pick the local id).
|
|
80
|
+
const cliAvailable = available.filter((p) => !(0, registry_1.isLocalAdapterId)(p));
|
|
81
|
+
if (stored && available.includes(stored) && (cliAvailable.length === 0 || !(0, registry_1.isLocalAdapterId)(stored)))
|
|
76
82
|
return stored;
|
|
77
83
|
for (const p of exports.PROVIDER_PREFERENCE_ORDER) {
|
|
78
84
|
if (available.includes(p))
|
|
79
85
|
return p;
|
|
80
86
|
}
|
|
87
|
+
if (cliAvailable.length > 0)
|
|
88
|
+
return cliAvailable[0];
|
|
89
|
+
// Local-only machine: the first detected local engine is the primary.
|
|
90
|
+
if (available.some((p) => (0, registry_1.isLocalAdapterId)(p)))
|
|
91
|
+
return available[0];
|
|
81
92
|
return stored ?? available[0];
|
|
82
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The provider an APP-LEVEL surface (Agent Chat missions, the Project Builder)
|
|
96
|
+
* starts a fresh conversation on when the caller names none: the machine's
|
|
97
|
+
* primary per `derivePrimaryProvider` over the detected set — a local-only
|
|
98
|
+
* machine therefore lands on its local engine instead of a `claude` that is
|
|
99
|
+
* not installed. Falls back to `claude` while no detection snapshot exists
|
|
100
|
+
* (startup, unit tests), byte-identical to the old hardcoded default.
|
|
101
|
+
*/
|
|
102
|
+
function defaultMachineProvider() {
|
|
103
|
+
const detected = detectedProviders();
|
|
104
|
+
if (!detected)
|
|
105
|
+
return 'claude';
|
|
106
|
+
return derivePrimaryProvider({ provider: 'claude', providers: detected });
|
|
107
|
+
}
|
|
83
108
|
/**
|
|
84
109
|
* Resolve the effective provider for a per-invocation request.
|
|
85
110
|
* Returns the requested provider when it is available; otherwise falls back to
|