specrails-desktop 2.47.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-DSCpSOGV.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-Dw3DNFkh.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-BwuvqS2Y.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-Byvq8Oqx.js → LoopBuilderPage-DQE-7Vby.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-BMC9Mgj1.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-nspqxH-M.js → ProjectSettingsDialog-D6vBCoay.js} +1 -1
- package/client/dist/assets/{RepositoryDeliveries-Dq9NJOWi.js → RepositoryDeliveries-C36ipKp_.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-C3jKG2z0.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-C0KyPo2O.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-DyI4rbHa.js → agents-CA5eT1d0.js} +1 -1
- package/client/dist/assets/{agents-_5mJMkAE.js → agents-CAgy-5uS.js} +1 -1
- package/client/dist/assets/{agents-BIT1UdTy.js → agents-CNm6Qzlv.js} +1 -1
- package/client/dist/assets/{agents-CST-lpYX.js → agents-CTwbx-fb.js} +1 -1
- package/client/dist/assets/{agents-C7fKfvm8.js → agents-DEGV1Vao.js} +1 -1
- package/client/dist/assets/{agents-DsgGMGwU.js → agents-HsDF1aQU.js} +1 -1
- package/client/dist/assets/{agents-Cm2JYrAa.js → agents-IQjdA-z9.js} +1 -1
- package/client/dist/assets/{agents-H6tVQUNP.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-D44phX1J.js → formatDistanceToNow-w_2U5pm0.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EAh9LFSx.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-UWi_xIZ-.js → narration--hUhjM5v.js} +1 -1
- package/client/dist/assets/{narration-DiPyj06T.js → narration-CHI06czm.js} +1 -1
- package/client/dist/assets/{narration-DrBAf8uv.js → narration-DjKB3pQZ.js} +1 -1
- package/client/dist/assets/{narration-CqNvgRi9.js → narration-DtCYvRLM.js} +1 -1
- package/client/dist/assets/{narration-B1uGfofH.js → narration-DvLQBW9U.js} +1 -1
- package/client/dist/assets/{narration-BKcL4Srl.js → narration-eSx3gR0W.js} +1 -1
- package/client/dist/assets/{narration-OyujFouL.js → narration-n3hOA7LY.js} +1 -1
- package/client/dist/assets/{narration-BtjKyHVM.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-GRYdjlKB.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-CN9PHM65.js → spending-hcgxHP4O.js} +1 -1
- package/client/dist/assets/{useDesktop-iius_s5d.js → useDesktop-CAisjKWC.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-D7hkZ3Ka.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/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 +7 -2
- package/server/dist/agent-runtime-controls-router.js +12 -1
- package/server/dist/agent-runtime-controls.js +37 -2
- package/server/dist/agent-runtime-effective-config.js +28 -7
- package/server/dist/agent-runtime-metrics.js +1 -1
- package/server/dist/agent-runtime-settings-router.js +76 -6
- package/server/dist/agent-runtime-settings.js +137 -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 +2 -1
- package/server/dist/loop-role-engines.js +129 -0
- package/server/dist/loop-run-manager.js +4 -4
- package/server/dist/mcp/tools/app-settings.js +30 -4
- package/server/dist/mcp/tools/rails.js +1 -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 +3 -3
- package/server/dist/rails-router.js +66 -10
- package/server/dist/result-event.js +17 -0
- package/server/dist/review-packet.js +1 -0
- package/server/dist/schemas/agent-runtime.schema.json +10 -2
- 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-mc1c5pgS.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane--UkamBTm.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DJCswsvU.js +0 -2
- package/client/dist/assets/AgentsPage-ChElPUL9.js +0 -87
- package/client/dist/assets/AnalyticsPage-BGtkFHBp.js +0 -1
- package/client/dist/assets/CodePage-BBnw3VGn.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-B85tNScc.js +0 -1
- package/client/dist/assets/DocsDialog-D4cXrOMl.js +0 -11
- package/client/dist/assets/DocsPage-BCAbC6qy.js +0 -11
- package/client/dist/assets/InteractiveJobComposer-Coq-Zhk0.js +0 -19
- package/client/dist/assets/JobDetailModal--Mjam0Nc.js +0 -1
- package/client/dist/assets/JobDetailPage-B6ShWSBh.js +0 -1
- package/client/dist/assets/JobsPage-DHU_ep_B.js +0 -1
- package/client/dist/assets/LoopsPage-CFGl-Nw8.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-DHjHBYqa.js +0 -1
- package/client/dist/assets/PluginsPage-BHygRnW6.js +0 -2
- package/client/dist/assets/ReviewPacketPage-C9mqhi65.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-BmwoAjOr.js +0 -1
- package/client/dist/assets/TicketDetailModalContext-lYmoONrd.js +0 -14
- package/client/dist/assets/agentRuntime-62GWdc26.js +0 -1
- package/client/dist/assets/agentRuntime-BN1mKAuY.js +0 -1
- package/client/dist/assets/agentRuntime-BNBCzQLM.js +0 -1
- package/client/dist/assets/agentRuntime-BQE_K1GG.js +0 -1
- package/client/dist/assets/agentRuntime-CCU3HCtS.js +0 -1
- package/client/dist/assets/agentRuntime-DT72Bpdv.js +0 -1
- package/client/dist/assets/agentRuntime-ZfMPeeiP.js +0 -1
- package/client/dist/assets/agentRuntime-g3W8Ry4d.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-DgMBighL.js +0 -76
- package/client/dist/assets/jira-api-BQR6G0YY.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
|
@@ -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");
|
|
@@ -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
|
+
}
|
|
@@ -1426,7 +1426,7 @@ class LoopRunManager {
|
|
|
1426
1426
|
iteration += 1;
|
|
1427
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);
|
|
1428
1428
|
// `iteration` was just incremented — it IS this pass's 1-based number.
|
|
1429
|
-
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);
|
|
1430
1430
|
logLine(`Goal: ${goal}`);
|
|
1431
1431
|
// BUG-32: capture the real Decider start BEFORE the await (see AI step).
|
|
1432
1432
|
const deciderStart = new Date(this.now()).toISOString();
|
|
@@ -1436,9 +1436,9 @@ class LoopRunManager {
|
|
|
1436
1436
|
// Give the Decider the spec so it can verify completeness against the
|
|
1437
1437
|
// FULL scope instead of trusting a step's self-reported success.
|
|
1438
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'),
|
|
1439
|
-
provider: nodeProvider,
|
|
1440
|
-
model: nodeModel,
|
|
1441
|
-
effort: nodeEffort,
|
|
1439
|
+
provider: req.deciderEngine?.provider ?? nodeProvider,
|
|
1440
|
+
model: req.deciderEngine?.model ?? nodeModel,
|
|
1441
|
+
effort: req.deciderEngine ? req.deciderEngine.effort : nodeEffort,
|
|
1442
1442
|
cwd: req.cwd,
|
|
1443
1443
|
repoDir: req.repoDir, executionManifest: req.executionManifest,
|
|
1444
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 {
|
|
@@ -92,7 +92,7 @@ function railsTools() {
|
|
|
92
92
|
.string()
|
|
93
93
|
.nullable()
|
|
94
94
|
.optional()
|
|
95
|
-
.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.'),
|
|
96
96
|
// set_name / create_rail
|
|
97
97
|
name: zod_1.z
|
|
98
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
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Spec: openspec/specs/multi-provider-architecture/spec.md
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.UnknownProviderError = exports.CUSTOM_MODEL_ALIAS_MAX_LENGTH = exports.isSafeCustomModelAlias = exports.isModelAvailableForAdapter = exports.isReasoningEffortValidForModel = exports.defaultReasoningEffortForModel = exports.reasoningEffortsForModel = exports.requireToolPolicy = exports.pureOutputToolPolicy = exports.supportsToolPolicy = exports.parseStreamEvents = exports.buildProviderRepoAccessArgs = exports.formatProviderCommand = exports.buildProviderEnv = exports.kimiAdapter = exports.geminiAdapter = exports.codexAdapter = exports.claudeAdapter = exports.listAdapters = exports.hasAdapter = exports.getAdapter = void 0;
|
|
10
|
+
exports.UnknownProviderError = exports.CUSTOM_MODEL_ALIAS_MAX_LENGTH = exports.isSafeCustomModelAlias = exports.isModelAvailableForAdapter = exports.isReasoningEffortValidForModel = exports.defaultReasoningEffortForModel = exports.reasoningEffortsForModel = exports.requireToolPolicy = exports.pureOutputToolPolicy = exports.supportsToolPolicy = exports.parseStreamEvents = exports.buildProviderRepoAccessArgs = exports.formatProviderCommand = exports.buildProviderEnv = exports.kimiAdapter = exports.geminiAdapter = exports.codexAdapter = exports.claudeAdapter = exports.adapterKind = exports.isLocalAdapterId = exports.unregisterAdapter = exports.listAdapters = exports.hasAdapter = exports.getAdapter = void 0;
|
|
11
11
|
const registry_1 = require("./registry");
|
|
12
12
|
const claude_adapter_1 = require("./claude-adapter");
|
|
13
13
|
Object.defineProperty(exports, "claudeAdapter", { enumerable: true, get: function () { return claude_adapter_1.claudeAdapter; } });
|
|
@@ -25,6 +25,9 @@ var registry_2 = require("./registry");
|
|
|
25
25
|
Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return registry_2.getAdapter; } });
|
|
26
26
|
Object.defineProperty(exports, "hasAdapter", { enumerable: true, get: function () { return registry_2.hasAdapter; } });
|
|
27
27
|
Object.defineProperty(exports, "listAdapters", { enumerable: true, get: function () { return registry_2.listAdapters; } });
|
|
28
|
+
Object.defineProperty(exports, "unregisterAdapter", { enumerable: true, get: function () { return registry_2.unregisterAdapter; } });
|
|
29
|
+
Object.defineProperty(exports, "isLocalAdapterId", { enumerable: true, get: function () { return registry_2.isLocalAdapterId; } });
|
|
30
|
+
Object.defineProperty(exports, "adapterKind", { enumerable: true, get: function () { return registry_2.adapterKind; } });
|
|
28
31
|
var runtime_1 = require("./runtime");
|
|
29
32
|
Object.defineProperty(exports, "buildProviderEnv", { enumerable: true, get: function () { return runtime_1.buildProviderEnv; } });
|
|
30
33
|
Object.defineProperty(exports, "formatProviderCommand", { enumerable: true, get: function () { return runtime_1.formatProviderCommand; } });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Dynamic registration of local (OpenAI-compatible) adapters — one per
|
|
3
|
+
// connection in `~/.specrails/runtime-providers.json`. Called at boot (after
|
|
4
|
+
// the connections load) and after every successful `PUT /api/runtime-providers`.
|
|
5
|
+
//
|
|
6
|
+
// Kill switch: SPECRAILS_LOCAL_ENGINES=0|false|off ⇒ no registration, and any
|
|
7
|
+
// previously registered local adapter is removed (byte-identical legacy).
|
|
8
|
+
//
|
|
9
|
+
// Spec: openspec/changes/local-ai-engines/specs/local-ai-engines/spec.md
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.isLocalEnginesEnabled = isLocalEnginesEnabled;
|
|
12
|
+
exports.listLocalAdapters = listLocalAdapters;
|
|
13
|
+
exports.listLocalConnections = listLocalConnections;
|
|
14
|
+
exports.syncLocalAdapters = syncLocalAdapters;
|
|
15
|
+
const registry_1 = require("./registry");
|
|
16
|
+
const local_adapter_1 = require("./local-adapter");
|
|
17
|
+
function isLocalEnginesEnabled() {
|
|
18
|
+
const raw = (process.env.SPECRAILS_LOCAL_ENGINES ?? '').trim().toLowerCase();
|
|
19
|
+
return !(raw === '0' || raw === 'false' || raw === 'off');
|
|
20
|
+
}
|
|
21
|
+
/** Every currently registered local adapter, in registry order. */
|
|
22
|
+
function listLocalAdapters() {
|
|
23
|
+
return (0, registry_1.listAdapters)().filter(local_adapter_1.isLocalAdapter);
|
|
24
|
+
}
|
|
25
|
+
/** The connections behind the registered local adapters. */
|
|
26
|
+
function listLocalConnections() {
|
|
27
|
+
return listLocalAdapters().map((a) => a.localConnection);
|
|
28
|
+
}
|
|
29
|
+
function syncLocalAdapters(connections) {
|
|
30
|
+
const result = { registered: [], unregistered: [], skipped: [] };
|
|
31
|
+
const wanted = new Map();
|
|
32
|
+
if (isLocalEnginesEnabled()) {
|
|
33
|
+
for (const conn of connections) {
|
|
34
|
+
if (conn.kind !== 'openai-compatible' || typeof conn.baseUrl !== 'string')
|
|
35
|
+
continue;
|
|
36
|
+
if ((0, registry_1.hasAdapter)(conn.id) && (0, registry_1.adapterKind)(conn.id) !== 'local') {
|
|
37
|
+
result.skipped.push(conn.id);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
wanted.set(conn.id, conn);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const existing of listLocalAdapters()) {
|
|
44
|
+
if (!wanted.has(existing.id)) {
|
|
45
|
+
if ((0, registry_1.unregisterAdapter)(existing.id))
|
|
46
|
+
result.unregistered.push(existing.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [id, conn] of wanted) {
|
|
50
|
+
// Re-register on every sync so a changed baseUrl/rates/label takes effect;
|
|
51
|
+
// in-flight jobs keep the adapter instance they resolved earlier.
|
|
52
|
+
(0, registry_1.register)((0, local_adapter_1.createLocalAdapter)({ ...conn }), 'local');
|
|
53
|
+
result.registered.push(id);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|