specrails-desktop 2.34.1 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +1 -0
- package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +2 -0
- package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +1 -0
- package/client/dist/assets/AgentsPage-C8J_42sm.js +87 -0
- package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +1 -0
- package/client/dist/assets/CodePage-BZ2MwdCu.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +1 -0
- package/client/dist/assets/DocsDialog-C76VdBJC.js +11 -0
- package/client/dist/assets/DocsPage-ziMlj6B6.js +11 -0
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +19 -0
- package/client/dist/assets/JobDetailModal-NAJmuYIT.js +1 -0
- package/client/dist/assets/JobDetailPage-fE0ZycUX.js +1 -0
- package/client/dist/assets/JobsPage-z5M_HVJG.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-B1GJJ0QZ.js → LoopBuilderPage-j1FQS47c.js} +2 -2
- package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +1 -0
- package/client/dist/assets/LoopsPage-Ll7zORZb.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +1 -0
- package/client/dist/assets/PluginsPage-DN4kGvJt.js +2 -0
- package/client/dist/assets/{ProjectSettingsDialog-D6Vxjpv5.js → ProjectSettingsDialog-OgsgxjzF.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +1 -0
- package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +14 -0
- package/client/dist/assets/{Trans-CTqw6rr9.js → Trans-Bsz9_gyq.js} +1 -1
- package/client/dist/assets/agent-BQgKv_2F.js +1 -0
- package/client/dist/assets/agent-C1NRDJqL.js +1 -0
- package/client/dist/assets/agent-CLNleiy6.js +1 -0
- package/client/dist/assets/agent-CmceArkQ.js +1 -0
- package/client/dist/assets/agent-D3UxQ3SR.js +1 -0
- package/client/dist/assets/agent-DB1JeE64.js +1 -0
- package/client/dist/assets/agent-DXkHcQ-D.js +1 -0
- package/client/dist/assets/agent-DjJJJ2M8.js +1 -0
- package/client/dist/assets/ban-E4EaZe3a.js +1 -0
- package/client/dist/assets/bot-DzuuuC5R.js +1 -0
- package/client/dist/assets/{browser-DxP-alBK.js → browser-BgqHDMXP.js} +1 -1
- package/client/dist/assets/{browser-CyR1Kp3m.js → browser-CsKJtrpv.js} +1 -1
- package/client/dist/assets/browser-Cu58EpZU.js +1 -0
- package/client/dist/assets/{browser-rRCwi7Y7.js → browser-CvXOJm-j.js} +1 -1
- package/client/dist/assets/{browser-CvgDySd9.js → browser-DOrFh1c7.js} +1 -1
- package/client/dist/assets/{browser-DwZljcRh.js → browser-DQv_Jo7x.js} +1 -1
- package/client/dist/assets/{browser-C25DkQiO.js → browser-LPgxvJu2.js} +1 -1
- package/client/dist/assets/{browser-CiJ7-L32.js → browser-eL-3MIEI.js} +1 -1
- package/client/dist/assets/button-CJqFnUOL.js +1 -0
- package/client/dist/assets/{chunk-KS7C4IRE-Cx5sqopX.js → chunk-KS7C4IRE-DEKu087k.js} +1 -1
- package/client/dist/assets/{commands-DyUZQ1FI.js → commands-6tYQih5m.js} +1 -1
- package/client/dist/assets/{commands-B9RZO4qS.js → commands-BK2QinXN.js} +1 -1
- package/client/dist/assets/{commands-Bbxu8kLU.js → commands-C2Nca76h.js} +1 -1
- package/client/dist/assets/{commands-CDU3qv68.js → commands-CKdLihNX.js} +1 -1
- package/client/dist/assets/{commands-DTQBF_q5.js → commands-CYIBTAlc.js} +1 -1
- package/client/dist/assets/{commands-hOtKuOXc.js → commands-CjUtYJIM.js} +1 -1
- package/client/dist/assets/{commands-BEwbAjtD.js → commands-Cq_u_8y9.js} +1 -1
- package/client/dist/assets/{commands-CzezgtQj.js → commands-D_ZHYQug.js} +1 -1
- package/client/dist/assets/core-B3mKBIh5.js +1 -0
- package/client/dist/assets/dashboard-B9GhC5ku.js +1 -0
- package/client/dist/assets/dashboard-C8WOEiPu.js +1 -0
- package/client/dist/assets/dashboard-CBnv6zOI.js +1 -0
- package/client/dist/assets/dashboard-CaP0fU87.js +1 -0
- package/client/dist/assets/dashboard-D4ZZ4x84.js +1 -0
- package/client/dist/assets/dashboard-DmuTQBX8.js +1 -0
- package/client/dist/assets/dashboard-DynLYj3d.js +1 -0
- package/client/dist/assets/dashboard-zj4gGFJ1.js +1 -0
- package/client/dist/assets/dialog-CdWCb02j.js +45 -0
- package/client/dist/assets/dist-js-CWeUdf5Z.js +1 -0
- package/client/dist/assets/dist-js-Rlh4FLH5.js +1 -0
- package/client/dist/assets/event-Bt7f3wXe.js +1 -0
- package/client/dist/assets/file-text-BcLCfcWZ.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-BOuXgebD.js → formatDistanceToNow-DQCvMgUc.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EjGOpn4b.js → getTimezoneOffsetInMilliseconds-C16cxSe1.js} +1 -1
- package/client/dist/assets/git-refresh-wTW0rO0Z.js +1 -0
- package/client/dist/assets/i18n-hnX2DPmo.js +2 -0
- package/client/dist/assets/index-DIhMEAVw.js +66 -0
- package/client/dist/assets/index-DwygcU7s.css +2 -0
- package/client/dist/assets/jira-api-B1-MUehg.js +1 -0
- package/client/dist/assets/loops-B7YpGizO.js +1 -0
- package/client/dist/assets/{loops-CMMQOw2P.js → loops-BomD2J-h.js} +1 -1
- package/client/dist/assets/{loops-B2_97D26.js → loops-BrQHSQw3.js} +1 -1
- package/client/dist/assets/{loops-BwxCndZk.js → loops-BvBwvTPA.js} +1 -1
- package/client/dist/assets/{loops-BISOkuCu.js → loops-CODk6Slz.js} +1 -1
- package/client/dist/assets/{loops-CD19kEzq.js → loops-DC6k-Ym-.js} +1 -1
- package/client/dist/assets/{loops-aEb4RxN2.js → loops-DSEg8-uY.js} +1 -1
- package/client/dist/assets/{loops-Bo6w_peZ.js → loops-DhCoXD_v.js} +1 -1
- package/client/dist/assets/{mcp-DsTwmjST.js → mcp-B8X8wBLu.js} +1 -1
- package/client/dist/assets/{mcp-SMkn8boG.js → mcp-C2AojZZN.js} +1 -1
- package/client/dist/assets/mcp-D8BT78_L.js +1 -0
- package/client/dist/assets/{mcp-D4ZjtIx3.js → mcp-Df5RWBNk.js} +1 -1
- package/client/dist/assets/{mcp-DIfBUv2D.js → mcp-Dj7BgMQW.js} +1 -1
- package/client/dist/assets/{mcp-D2pc5GGx.js → mcp-Q7iT7rcz.js} +1 -1
- package/client/dist/assets/{mcp-fSfiVYAc.js → mcp-YlvNvJbW.js} +1 -1
- package/client/dist/assets/{mcp-Czd_iyp0.js → mcp-mKFpTSRb.js} +1 -1
- package/client/dist/assets/narration-4Z2VUhZw.js +1 -0
- package/client/dist/assets/narration-BGuiEDj3.js +1 -0
- package/client/dist/assets/narration-BuSoqRmv.js +1 -0
- package/client/dist/assets/narration-CB9t29fy.js +1 -0
- package/client/dist/assets/narration-CU3x8FHD.js +1 -0
- package/client/dist/assets/narration-CWeXhfP5.js +1 -0
- package/client/dist/assets/narration-DRaDnhT5.js +1 -0
- package/client/dist/assets/narration-DtEbQgCw.js +1 -0
- package/client/dist/assets/packet-B1IxaL5f.js +1 -0
- package/client/dist/assets/packet-Bh5kkbiB.js +1 -0
- package/client/dist/assets/packet-By8fqYxW.js +1 -0
- package/client/dist/assets/packet-CZdvA44E.js +1 -0
- package/client/dist/assets/packet-CrjHRQCP.js +1 -0
- package/client/dist/assets/packet-Dwi2pTtx.js +1 -0
- package/client/dist/assets/packet-_-ZIa0IR.js +1 -0
- package/client/dist/assets/packet-n0W6E1Hn.js +1 -0
- package/client/dist/assets/save-B6wGWwBR.js +1 -0
- package/client/dist/assets/settings-Bd0x4TBa.js +1 -0
- package/client/dist/assets/settings-C1-7cXzC.js +1 -0
- package/client/dist/assets/settings-CAFaT38b.js +1 -0
- package/client/dist/assets/settings-Ct3v1nW2.js +1 -0
- package/client/dist/assets/settings-DHh7_IF5.js +1 -0
- package/client/dist/assets/settings-DQXAmdcv.js +1 -0
- package/client/dist/assets/settings-guzlhpup.js +1 -0
- package/client/dist/assets/settings-sZfTPMEH.js +1 -0
- package/client/dist/assets/{spending-BpYPsevQ.js → spending-EC9xqio-.js} +1 -1
- package/client/dist/assets/useDesktop-BQVt8lFd.js +1 -0
- package/client/dist/index.html +30 -32
- package/docs/internals/embedded-browser-native-webview-evaluation.md +161 -0
- package/docs/internals/review-packet.md +212 -0
- package/docs/mcp.md +16 -0
- package/package.json +1 -1
- package/server/dist/accept-ladder.js +61 -0
- package/server/dist/agent-chat-manager.js +24 -1
- package/server/dist/agent-defaults.js +359 -0
- package/server/dist/agent-mcp-config.js +94 -2
- package/server/dist/agent-operator-prompt.js +17 -5
- package/server/dist/browser-playwright.js +39 -2
- package/server/dist/db.js +38 -0
- package/server/dist/delivery-evidence.js +242 -0
- package/server/dist/desktop-router.js +45 -0
- package/server/dist/external-mcp.js +370 -0
- package/server/dist/feature-flags.js +23 -0
- package/server/dist/loop-command-catalog.js +33 -0
- package/server/dist/loop-executors.js +12 -2
- package/server/dist/loop-factory.js +16 -1
- package/server/dist/loops-router.js +2 -0
- package/server/dist/mcp/tools/rails.js +16 -0
- package/server/dist/project-registry.js +33 -1
- package/server/dist/project-router-loop-runs.js +27 -2
- package/server/dist/providers/claude-adapter.js +39 -2
- package/server/dist/queue-manager.js +47 -4
- package/server/dist/rail-isolated-launch.js +101 -2
- package/server/dist/rail-pr-store.js +40 -3
- package/server/dist/rails-router.js +124 -3
- package/server/dist/review-packet.js +327 -0
- package/server/dist/revision-seed.js +73 -0
- package/server/dist/run-duration-stats.js +71 -0
- package/server/dist/stuck-run-detector.js +153 -0
- package/server/dist/telemetry-receiver.js +22 -0
- package/client/dist/assets/ActivityFeedPage-B4yaoZsD.js +0 -1
- package/client/dist/assets/AgentBrowserCapture-Bhykbzog.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-prvv23b7.js +0 -2
- package/client/dist/assets/AgentModeCodePane-BmxySwPP.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-BvW6tTmU.js +0 -1
- package/client/dist/assets/AgentsPage-DinMMQPm.js +0 -87
- package/client/dist/assets/AnalyticsPage-d5GmoZY2.js +0 -1
- package/client/dist/assets/CodePage-Cgbj4lpK.js +0 -2
- package/client/dist/assets/DesktopAnalyticsPage-DbbZi6fw.js +0 -1
- package/client/dist/assets/DocsDialog-B87oF3aC.js +0 -11
- package/client/dist/assets/DocsPage-CqJ-yRkh.js +0 -11
- package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-nPGDKuCk.js +0 -19
- package/client/dist/assets/JobDetailModal-duouGMzi.js +0 -1
- package/client/dist/assets/JobDetailPage-Cw3Hu579.js +0 -1
- package/client/dist/assets/JobsPage-DYyV-qWT.js +0 -1
- package/client/dist/assets/LoopPreviewModal-Md6KF7rU.js +0 -1
- package/client/dist/assets/LoopsPage-BKNiwsy1.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +0 -1
- package/client/dist/assets/PluginsPage-r682uE6n.js +0 -2
- package/client/dist/assets/TemplatePreviewModal-DcgWnn23.js +0 -1
- package/client/dist/assets/TicketDetailModal-BPaqyvrs.js +0 -14
- package/client/dist/assets/agent-Bd-q3Wqp.js +0 -1
- package/client/dist/assets/agent-BlvdW4Hv.js +0 -1
- package/client/dist/assets/agent-C-KQhxeM.js +0 -1
- package/client/dist/assets/agent-Ca15w5eu.js +0 -1
- package/client/dist/assets/agent-D6fKaoMd.js +0 -1
- package/client/dist/assets/agent-HFMYMTbB.js +0 -1
- package/client/dist/assets/agent-QIEbkBjg.js +0 -1
- package/client/dist/assets/agent-mTCsAbCl.js +0 -1
- package/client/dist/assets/api-B2M7CtDw.js +0 -1
- package/client/dist/assets/arrow-left-CpIR4j7G.js +0 -1
- package/client/dist/assets/arrow-up-right-9DoMn7fD.js +0 -1
- package/client/dist/assets/browser-CXUcnTMI.js +0 -1
- package/client/dist/assets/button-BUIIBQPT.js +0 -1
- package/client/dist/assets/chevron-right-CViMxucY.js +0 -1
- package/client/dist/assets/dashboard-BHRqsGCV.js +0 -1
- package/client/dist/assets/dashboard-Bfr90vOl.js +0 -1
- package/client/dist/assets/dashboard-CTsM8x3m.js +0 -1
- package/client/dist/assets/dashboard-CqC6wX0h.js +0 -1
- package/client/dist/assets/dashboard-CuZtAszc.js +0 -1
- package/client/dist/assets/dashboard-DAcqIc-n.js +0 -1
- package/client/dist/assets/dashboard-DekiV-86.js +0 -1
- package/client/dist/assets/dashboard-DoDczc5S.js +0 -1
- package/client/dist/assets/dialog-CzIekCw0.js +0 -45
- package/client/dist/assets/dist-Cez5CRkX.js +0 -1
- package/client/dist/assets/dist-js-B1s_CM_v.js +0 -1
- package/client/dist/assets/dist-js-BNETKUa_.js +0 -1
- package/client/dist/assets/external-link-BcU5EWZp.js +0 -1
- package/client/dist/assets/file-text-CFT-xXKN.js +0 -1
- package/client/dist/assets/i18n-nJQvD5zJ.js +0 -2
- package/client/dist/assets/index-DCN5QbDm.css +0 -2
- package/client/dist/assets/index-DdC_faQv.js +0 -63
- package/client/dist/assets/jira-api-Bhixwn-d.js +0 -1
- package/client/dist/assets/loops-CnxqL8IL.js +0 -1
- package/client/dist/assets/mcp-uDkXYu4L.js +0 -1
- package/client/dist/assets/save-28sIlCdr.js +0 -1
- package/client/dist/assets/settings-BveOJoaI.js +0 -1
- package/client/dist/assets/settings-Cj1C6F5k.js +0 -1
- package/client/dist/assets/settings-CuVwOpe0.js +0 -1
- package/client/dist/assets/settings-D5X4DfRc.js +0 -1
- package/client/dist/assets/settings-DXwxe-BW.js +0 -1
- package/client/dist/assets/settings-Di81Ba_V.js +0 -1
- package/client/dist/assets/settings-jbLjCYC_.js +0 -1
- package/client/dist/assets/settings-wIZNPFnn.js +0 -1
- package/client/dist/assets/useBrowserCaptureSession-BBXBN0Na.js +0 -1
- package/client/dist/assets/useDesktop-B6Du2N83.js +0 -1
- package/client/dist/assets/useSharedWebSocket-DK96ctb7.js +0 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AgentDefaultsValidationError = exports.GLOBAL_DEFAULTS_PROFILE_NAME = exports.AGENT_DEFAULTS_SETTING_KEY = void 0;
|
|
37
|
+
exports.readAgentDefaultsSettings = readAgentDefaultsSettings;
|
|
38
|
+
exports.applyAgentDefaultsPatch = applyAgentDefaultsPatch;
|
|
39
|
+
exports.resolveAgentDefaults = resolveAgentDefaults;
|
|
40
|
+
exports.mergeProfileWithAgentDefaults = mergeProfileWithAgentDefaults;
|
|
41
|
+
exports.synthesizeProfileFromDefaults = synthesizeProfileFromDefaults;
|
|
42
|
+
exports.ensureGlobalProfileSnapshot = ensureGlobalProfileSnapshot;
|
|
43
|
+
exports.buildAgentDefaultsCatalog = buildAgentDefaultsCatalog;
|
|
44
|
+
exports.createLoopProfilePathResolver = createLoopProfilePathResolver;
|
|
45
|
+
/**
|
|
46
|
+
* Global Specrails Agents defaults — app-level (per-machine) per-provider
|
|
47
|
+
* customization of the specrails-core pipeline agents: the pipeline
|
|
48
|
+
* (orchestrator) model + reasoning effort, and per-agent model overrides for
|
|
49
|
+
* providers whose runtime honours execution profiles (claude, kimi).
|
|
50
|
+
*
|
|
51
|
+
* Storage is one JSON blob under `desktop_settings['specrails_agent_defaults']`
|
|
52
|
+
* (no migration — plain k/v). Every consumer reads AT SPAWN/LAUNCH TIME, so a
|
|
53
|
+
* settings change applies to the next run without restarting the app.
|
|
54
|
+
*
|
|
55
|
+
* Layering contract (least → most specific):
|
|
56
|
+
* built-in adapter default < GLOBAL agent defaults < per-project setting
|
|
57
|
+
* (orchestrator model, resolved model) < per-project profile < explicit
|
|
58
|
+
* per-launch selection.
|
|
59
|
+
* The global layer therefore only ever fills gaps — it never overrides an
|
|
60
|
+
* explicit user choice made closer to the run.
|
|
61
|
+
*/
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const os = __importStar(require("os"));
|
|
64
|
+
const path = __importStar(require("path"));
|
|
65
|
+
const crypto_1 = require("crypto");
|
|
66
|
+
const desktop_db_1 = require("./desktop-db");
|
|
67
|
+
const providers_1 = require("./providers");
|
|
68
|
+
const runtime_1 = require("./providers/runtime");
|
|
69
|
+
exports.AGENT_DEFAULTS_SETTING_KEY = 'specrails_agent_defaults';
|
|
70
|
+
/** Synthetic profile name recorded when a run's profile came purely from the
|
|
71
|
+
* global defaults layer (no project profile existed). */
|
|
72
|
+
exports.GLOBAL_DEFAULTS_PROFILE_NAME = 'global-defaults';
|
|
73
|
+
/** Agent ids the v1 settings surface may override — the baseline pipeline trio
|
|
74
|
+
* every adapter guarantees (`adapter.baselineAgents()`). */
|
|
75
|
+
const AGENT_ID_PATTERN = /^(sr|custom)-[a-z0-9][a-z0-9-]*$/;
|
|
76
|
+
class AgentDefaultsValidationError extends Error {
|
|
77
|
+
code;
|
|
78
|
+
constructor(code, message) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.name = 'AgentDefaultsValidationError';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.AgentDefaultsValidationError = AgentDefaultsValidationError;
|
|
85
|
+
const EMPTY_SETTINGS = { version: 1, providers: {} };
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
/** Structural sanitize for values read from disk — never throws, drops junk. */
|
|
90
|
+
function sanitizeStored(raw) {
|
|
91
|
+
if (!isRecord(raw) || !isRecord(raw.providers))
|
|
92
|
+
return { ...EMPTY_SETTINGS, providers: {} };
|
|
93
|
+
const providers = {};
|
|
94
|
+
for (const [id, value] of Object.entries(raw.providers)) {
|
|
95
|
+
if (!isRecord(value))
|
|
96
|
+
continue;
|
|
97
|
+
const entry = { custom: value.custom === true };
|
|
98
|
+
if (typeof value.pipelineModel === 'string' && value.pipelineModel)
|
|
99
|
+
entry.pipelineModel = value.pipelineModel;
|
|
100
|
+
if (typeof value.pipelineEffort === 'string' && value.pipelineEffort)
|
|
101
|
+
entry.pipelineEffort = value.pipelineEffort;
|
|
102
|
+
if (isRecord(value.agentModels)) {
|
|
103
|
+
const agents = {};
|
|
104
|
+
for (const [agentId, model] of Object.entries(value.agentModels)) {
|
|
105
|
+
if (AGENT_ID_PATTERN.test(agentId) && typeof model === 'string' && model)
|
|
106
|
+
agents[agentId] = model;
|
|
107
|
+
}
|
|
108
|
+
if (Object.keys(agents).length > 0)
|
|
109
|
+
entry.agentModels = agents;
|
|
110
|
+
}
|
|
111
|
+
providers[id] = entry;
|
|
112
|
+
}
|
|
113
|
+
return { version: 1, providers };
|
|
114
|
+
}
|
|
115
|
+
/** Read + sanitize the stored settings. Never throws. */
|
|
116
|
+
function readAgentDefaultsSettings(db) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = (0, desktop_db_1.getDesktopSetting)(db, exports.AGENT_DEFAULTS_SETTING_KEY);
|
|
119
|
+
if (!raw)
|
|
120
|
+
return { ...EMPTY_SETTINGS, providers: {} };
|
|
121
|
+
return sanitizeStored(JSON.parse(raw));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { ...EMPTY_SETTINGS, providers: {} };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validate ONE provider's config from an untrusted PATCH body.
|
|
129
|
+
* Throws `AgentDefaultsValidationError` with an API-stable `code`.
|
|
130
|
+
*/
|
|
131
|
+
function validateProviderPatch(providerId, value) {
|
|
132
|
+
let adapter;
|
|
133
|
+
try {
|
|
134
|
+
adapter = (0, providers_1.getAdapter)(providerId);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new AgentDefaultsValidationError('unknown_provider', `Unknown provider "${providerId}"`);
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
throw new AgentDefaultsValidationError('invalid_provider_config', `Config for "${providerId}" must be an object`);
|
|
141
|
+
}
|
|
142
|
+
const entry = { custom: value.custom === true };
|
|
143
|
+
if (value.pipelineModel !== undefined && value.pipelineModel !== null && value.pipelineModel !== '') {
|
|
144
|
+
if (!(0, runtime_1.isModelAvailableForAdapter)(adapter, value.pipelineModel)) {
|
|
145
|
+
throw new AgentDefaultsValidationError('invalid_model', `Model is not valid for provider "${providerId}"`);
|
|
146
|
+
}
|
|
147
|
+
entry.pipelineModel = value.pipelineModel;
|
|
148
|
+
}
|
|
149
|
+
if (value.pipelineEffort !== undefined && value.pipelineEffort !== null && value.pipelineEffort !== '') {
|
|
150
|
+
const effortModel = entry.pipelineModel ?? adapter.defaultModel();
|
|
151
|
+
if (!(0, runtime_1.isReasoningEffortValidForModel)(adapter, effortModel, value.pipelineEffort)) {
|
|
152
|
+
throw new AgentDefaultsValidationError('invalid_effort', `Effort is not valid for provider "${providerId}" and model "${effortModel}"`);
|
|
153
|
+
}
|
|
154
|
+
entry.pipelineEffort = value.pipelineEffort;
|
|
155
|
+
}
|
|
156
|
+
if (value.agentModels !== undefined && value.agentModels !== null) {
|
|
157
|
+
if (!isRecord(value.agentModels)) {
|
|
158
|
+
throw new AgentDefaultsValidationError('invalid_agent_models', 'agentModels must be an object');
|
|
159
|
+
}
|
|
160
|
+
const agents = {};
|
|
161
|
+
for (const [agentId, model] of Object.entries(value.agentModels)) {
|
|
162
|
+
if (model === undefined || model === null || model === '')
|
|
163
|
+
continue;
|
|
164
|
+
if (adapter.capabilities.profiles !== true) {
|
|
165
|
+
throw new AgentDefaultsValidationError('per_agent_not_supported', `Provider "${providerId}" does not support per-agent model overrides`);
|
|
166
|
+
}
|
|
167
|
+
if (!adapter.baselineAgents().includes(agentId)) {
|
|
168
|
+
throw new AgentDefaultsValidationError('unknown_agent', `Unknown agent "${agentId}" for provider "${providerId}"`);
|
|
169
|
+
}
|
|
170
|
+
if (!(0, runtime_1.isModelAvailableForAdapter)(adapter, model)) {
|
|
171
|
+
throw new AgentDefaultsValidationError('invalid_agent_model', `Model for "${agentId}" is not valid for provider "${providerId}"`);
|
|
172
|
+
}
|
|
173
|
+
agents[agentId] = model;
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(agents).length > 0)
|
|
176
|
+
entry.agentModels = agents;
|
|
177
|
+
}
|
|
178
|
+
return entry;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Apply a PATCH body: every provider key present REPLACES that provider's
|
|
182
|
+
* stored config wholesale (deterministic; no deep-merge ambiguity). Returns
|
|
183
|
+
* the canonical stored settings. Throws `AgentDefaultsValidationError`.
|
|
184
|
+
*/
|
|
185
|
+
function applyAgentDefaultsPatch(db, body) {
|
|
186
|
+
if (!isRecord(body) || !isRecord(body.providers)) {
|
|
187
|
+
throw new AgentDefaultsValidationError('invalid_body', "body must include a 'providers' object");
|
|
188
|
+
}
|
|
189
|
+
const next = readAgentDefaultsSettings(db);
|
|
190
|
+
for (const [providerId, value] of Object.entries(body.providers)) {
|
|
191
|
+
next.providers[providerId] = validateProviderPatch(providerId, value);
|
|
192
|
+
}
|
|
193
|
+
(0, desktop_db_1.setDesktopSetting)(db, exports.AGENT_DEFAULTS_SETTING_KEY, JSON.stringify(next));
|
|
194
|
+
return next;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Resolve the effective global defaults for one provider — validated against
|
|
198
|
+
* the adapter's live catalog so a stale stored model degrades to "no override"
|
|
199
|
+
* (fail-open to built-in defaults) instead of poisoning a spawn.
|
|
200
|
+
* Returns null when the layer is inactive or nothing survives validation.
|
|
201
|
+
*/
|
|
202
|
+
function resolveAgentDefaults(db, providerId) {
|
|
203
|
+
let adapter;
|
|
204
|
+
try {
|
|
205
|
+
adapter = (0, providers_1.getAdapter)(providerId);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const entry = readAgentDefaultsSettings(db).providers[providerId];
|
|
211
|
+
if (!entry?.custom)
|
|
212
|
+
return null;
|
|
213
|
+
const pipelineModel = entry.pipelineModel && (0, runtime_1.isModelAvailableForAdapter)(adapter, entry.pipelineModel) ? entry.pipelineModel : null;
|
|
214
|
+
const pipelineEffort = entry.pipelineEffort && adapter.capabilities.supportsReasoningEffort === true ? entry.pipelineEffort : null;
|
|
215
|
+
const agentModels = {};
|
|
216
|
+
if (adapter.capabilities.profiles === true && entry.agentModels) {
|
|
217
|
+
for (const [agentId, model] of Object.entries(entry.agentModels)) {
|
|
218
|
+
if (adapter.baselineAgents().includes(agentId)
|
|
219
|
+
&& (0, runtime_1.isModelAvailableForAdapter)(adapter, model)) {
|
|
220
|
+
agentModels[agentId] = model;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!pipelineModel && !pipelineEffort && Object.keys(agentModels).length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
return { pipelineModel, pipelineEffort, agentModels };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Fill a resolved profile's gaps from the global layer: only agents WITHOUT an
|
|
230
|
+
* explicit model get one. The profile's own choices (including orchestrator)
|
|
231
|
+
* always win. Returns the original object untouched when nothing changed.
|
|
232
|
+
*/
|
|
233
|
+
function mergeProfileWithAgentDefaults(profile, defaults) {
|
|
234
|
+
let changed = false;
|
|
235
|
+
const agents = profile.agents.map((agent) => {
|
|
236
|
+
if (!agent.model && defaults.agentModels[agent.id]) {
|
|
237
|
+
changed = true;
|
|
238
|
+
return { ...agent, model: defaults.agentModels[agent.id] };
|
|
239
|
+
}
|
|
240
|
+
return agent;
|
|
241
|
+
});
|
|
242
|
+
if (!changed)
|
|
243
|
+
return { changed: false, profile };
|
|
244
|
+
return { changed: true, profile: { ...profile, agents } };
|
|
245
|
+
}
|
|
246
|
+
/** Baseline-trio profile synthesized purely from the global layer (used when
|
|
247
|
+
* the project has no profile of its own). */
|
|
248
|
+
function synthesizeProfileFromDefaults(adapter, defaults) {
|
|
249
|
+
return {
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
name: exports.GLOBAL_DEFAULTS_PROFILE_NAME,
|
|
252
|
+
description: 'Synthesized from the app-level Specrails Agents defaults',
|
|
253
|
+
provider: adapter.id,
|
|
254
|
+
orchestrator: { model: defaults.pipelineModel ?? adapter.defaultModel() },
|
|
255
|
+
agents: adapter.baselineAgents().map((id) => ({
|
|
256
|
+
id,
|
|
257
|
+
...(defaults.agentModels[id] ? { model: defaults.agentModels[id] } : {}),
|
|
258
|
+
})),
|
|
259
|
+
routing: [{ default: true, agent: 'sr-developer' }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Content-addressed snapshot for the LOOP path (no per-job dir exists when the
|
|
264
|
+
* engine spawns ai-steps). Identical content reuses the same immutable file, so
|
|
265
|
+
* concurrent runs never race and a settings change lands in a NEW file — an
|
|
266
|
+
* in-flight run keeps the profile it resolved. Never throws to the caller.
|
|
267
|
+
*/
|
|
268
|
+
function ensureGlobalProfileSnapshot(providerId, profile, baseDir = path.join(os.homedir(), '.specrails', 'agent-defaults')) {
|
|
269
|
+
const body = JSON.stringify(profile, null, 2) + '\n';
|
|
270
|
+
const hash = (0, crypto_1.createHash)('sha256').update(body).digest('hex').slice(0, 12);
|
|
271
|
+
const file = path.join(baseDir, `${providerId}-${hash}.json`);
|
|
272
|
+
if (fs.existsSync(file))
|
|
273
|
+
return file;
|
|
274
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
275
|
+
const tmp = path.join(baseDir, `.${providerId}-${hash}.${process.pid}.tmp`);
|
|
276
|
+
fs.writeFileSync(tmp, body, { encoding: 'utf8', mode: 0o444 });
|
|
277
|
+
try {
|
|
278
|
+
fs.renameSync(tmp, file);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// A concurrent writer won the rename — the content is identical by hash.
|
|
282
|
+
try {
|
|
283
|
+
fs.unlinkSync(tmp);
|
|
284
|
+
}
|
|
285
|
+
catch { /* best-effort */ }
|
|
286
|
+
}
|
|
287
|
+
return file;
|
|
288
|
+
}
|
|
289
|
+
/** Catalog payload for the settings UI — every REGISTERED provider (detection
|
|
290
|
+
* state is a client concern; undetected providers render non-clickable). */
|
|
291
|
+
function buildAgentDefaultsCatalog() {
|
|
292
|
+
return (0, providers_1.listAdapters)().map((adapter) => {
|
|
293
|
+
const models = adapter.modelCatalog().map((m) => ({ ...m }));
|
|
294
|
+
const effortsByModel = {};
|
|
295
|
+
for (const m of models) {
|
|
296
|
+
effortsByModel[m.value] = [...(0, runtime_1.reasoningEffortsForModel)(adapter, m.value)];
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
id: adapter.id,
|
|
300
|
+
displayName: adapter.displayName,
|
|
301
|
+
models,
|
|
302
|
+
defaultModel: adapter.defaultModel(),
|
|
303
|
+
baselineAgents: [...adapter.baselineAgents()],
|
|
304
|
+
perAgentModels: adapter.capabilities.profiles === true,
|
|
305
|
+
supportsEffort: adapter.capabilities.supportsReasoningEffort === true,
|
|
306
|
+
customModelAliases: adapter.capabilities.customModelAliases === true,
|
|
307
|
+
effortsByModel,
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Loop-path profile resolver factory. Returns `(provider) => snapshotPath|null`
|
|
313
|
+
* for injection as `SPECRAILS_PROFILE_PATH` on loop ai-step spawns.
|
|
314
|
+
*
|
|
315
|
+
* Deliberately inert unless the global layer carries per-agent models for the
|
|
316
|
+
* provider — when inactive (or when the project profile already pins every
|
|
317
|
+
* agent) it returns null and the run behaves byte-identically to today (core
|
|
318
|
+
* resolves `.specrails/profiles/project-default.json` on its own).
|
|
319
|
+
*/
|
|
320
|
+
function createLoopProfilePathResolver(io) {
|
|
321
|
+
return (provider) => {
|
|
322
|
+
try {
|
|
323
|
+
const defaults = resolveAgentDefaults(io.desktopDb, provider);
|
|
324
|
+
if (!defaults || Object.keys(defaults.agentModels).length === 0)
|
|
325
|
+
return null;
|
|
326
|
+
const adapter = (0, providers_1.getAdapter)(provider);
|
|
327
|
+
if (adapter.capabilities.profiles !== true || adapter.capabilities.profileEnvSupport !== true)
|
|
328
|
+
return null;
|
|
329
|
+
const root = io.profileRoot();
|
|
330
|
+
if (!io.supportsProfiles(root))
|
|
331
|
+
return null;
|
|
332
|
+
let base = null;
|
|
333
|
+
try {
|
|
334
|
+
const resolve = io.resolveProfile
|
|
335
|
+
?? ((r, e, p) => require('./profile-manager').resolveProfile(r, e, p));
|
|
336
|
+
base = resolve(root, undefined, provider);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
base = null;
|
|
340
|
+
}
|
|
341
|
+
let effective = null;
|
|
342
|
+
if (base) {
|
|
343
|
+
const merged = mergeProfileWithAgentDefaults(base.profile, defaults);
|
|
344
|
+
// Nothing to add ⇒ stay out of the way (core's own file fallback rules).
|
|
345
|
+
effective = merged.changed ? merged.profile : null;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
effective = synthesizeProfileFromDefaults(adapter, defaults);
|
|
349
|
+
}
|
|
350
|
+
if (!effective)
|
|
351
|
+
return null;
|
|
352
|
+
return ensureGlobalProfileSnapshot(provider, effective, io.snapshotDir);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
console.warn(`[agent-defaults] loop profile resolution failed for ${provider}: ${err.message}`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.mergeSpecrailsIntoWorkspaceMcp = mergeSpecrailsIntoWorkspaceMcp;
|
|
|
10
10
|
exports.buildAgentMcpArgs = buildAgentMcpArgs;
|
|
11
11
|
exports.removeAgentCapabilityFile = removeAgentCapabilityFile;
|
|
12
12
|
exports.prepareAgentMcp = prepareAgentMcp;
|
|
13
|
+
exports.syncExternalServersIntoJsonFile = syncExternalServersIntoJsonFile;
|
|
13
14
|
const fs_1 = __importDefault(require("fs"));
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const os_1 = __importDefault(require("os"));
|
|
@@ -18,6 +19,7 @@ const path_resolver_1 = require("./path-resolver");
|
|
|
18
19
|
const win_spawn_1 = require("./util/win-spawn");
|
|
19
20
|
const agent_tier_1 = require("./agent-tier");
|
|
20
21
|
const providers_1 = require("./providers");
|
|
22
|
+
const external_mcp_1 = require("./external-mcp");
|
|
21
23
|
const AGENT_CONVERSATION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
22
24
|
// ─── Agent MCP wiring (design D8 / D1) ────────────────────────────────────────
|
|
23
25
|
//
|
|
@@ -141,7 +143,13 @@ function buildAgentMcpArgs(opts) {
|
|
|
141
143
|
return [];
|
|
142
144
|
const dir = agentDir(opts.conversationId);
|
|
143
145
|
const file = path_1.default.join(dir, 'mcp.json');
|
|
144
|
-
|
|
146
|
+
const mcpServers = { specrails: entry };
|
|
147
|
+
for (const server of opts.external ?? []) {
|
|
148
|
+
if (server.name === 'specrails')
|
|
149
|
+
continue;
|
|
150
|
+
mcpServers[server.name] = server.config;
|
|
151
|
+
}
|
|
152
|
+
fs_1.default.writeFileSync(file, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
|
|
145
153
|
try {
|
|
146
154
|
fs_1.default.chmodSync(file, 0o600);
|
|
147
155
|
}
|
|
@@ -228,6 +236,27 @@ function codexMcpOverrides(entry) {
|
|
|
228
236
|
}
|
|
229
237
|
return args;
|
|
230
238
|
}
|
|
239
|
+
/** External-server variant of the codex `-c` overrides. Callers pre-filter with
|
|
240
|
+
* `isCodexInjectable` (TOML-safe name + string command). */
|
|
241
|
+
function codexExternalOverrides(server) {
|
|
242
|
+
const c = (kv) => ['-c', kv];
|
|
243
|
+
const command = String(server.config.command);
|
|
244
|
+
const argsList = Array.isArray(server.config.args)
|
|
245
|
+
? server.config.args.filter((a) => typeof a === 'string')
|
|
246
|
+
: [];
|
|
247
|
+
const args = [
|
|
248
|
+
...c(`mcp_servers.${server.name}.command=${JSON.stringify(command)}`),
|
|
249
|
+
...c(`mcp_servers.${server.name}.args=[${argsList.map((a) => JSON.stringify(a)).join(', ')}]`),
|
|
250
|
+
];
|
|
251
|
+
const env = server.config.env;
|
|
252
|
+
if (env && typeof env === 'object' && !Array.isArray(env)) {
|
|
253
|
+
for (const [k, v] of Object.entries(env)) {
|
|
254
|
+
if (typeof v === 'string')
|
|
255
|
+
args.push(...c(`mcp_servers.${server.name}.env.${k}=${JSON.stringify(v)}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return args;
|
|
259
|
+
}
|
|
231
260
|
/**
|
|
232
261
|
* Prepares the specrails MCP for the given provider and returns the spawn
|
|
233
262
|
* extraArgs + env to merge. Returns empty wiring when the bridge is unavailable
|
|
@@ -235,6 +264,7 @@ function codexMcpOverrides(entry) {
|
|
|
235
264
|
*/
|
|
236
265
|
function prepareAgentMcp(opts) {
|
|
237
266
|
const adapter = (0, providers_1.getAdapter)(opts.adapterId);
|
|
267
|
+
const external = (opts.external ?? []).filter((s) => s.name !== 'specrails');
|
|
238
268
|
if (adapter.id === 'claude') {
|
|
239
269
|
return { extraArgs: buildAgentMcpArgs(opts), env: {} };
|
|
240
270
|
}
|
|
@@ -243,12 +273,14 @@ function prepareAgentMcp(opts) {
|
|
|
243
273
|
return { extraArgs: [], env: {} };
|
|
244
274
|
if (adapter.mcpRegistration === 'cli-add') {
|
|
245
275
|
// Inline -c overrides contain only the capability FILE PATH, never its secret.
|
|
246
|
-
|
|
276
|
+
const externalArgs = external.filter(external_mcp_1.isCodexInjectable).flatMap(codexExternalOverrides);
|
|
277
|
+
return { extraArgs: [...codexMcpOverrides(entry), ...externalArgs], env: {} };
|
|
247
278
|
}
|
|
248
279
|
// Project-json providers write only their native project config in the
|
|
249
280
|
// conversation-isolated cwd.
|
|
250
281
|
const file = adapter.projectMcpPath?.(opts.cwd) ?? path_1.default.join(opts.cwd, '.mcp.json');
|
|
251
282
|
mergeServerIntoJsonFile(file, entry);
|
|
283
|
+
syncExternalServersIntoJsonFile(file, external);
|
|
252
284
|
if (adapter.id === 'gemini') {
|
|
253
285
|
// gemini-cli has NEVER read .mcp.json (a Claude convention) — its only MCP
|
|
254
286
|
// surface is `mcpServers` in settings.json (user or <cwd>/.gemini project
|
|
@@ -286,3 +318,63 @@ function mergeServerIntoJsonFile(file, entry) {
|
|
|
286
318
|
}
|
|
287
319
|
catch { /* best-effort */ }
|
|
288
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Sync the user's EXTERNAL servers into a persistent settings file (gemini
|
|
323
|
+
* `.gemini/settings.json`, kimi `.kimi-code/mcp.json`) with owned-key hygiene:
|
|
324
|
+
* a sidecar (`external-owned.json`, same dir) records every key the app wrote,
|
|
325
|
+
* and each spawn removes the previously-owned set before merging the currently
|
|
326
|
+
* enabled one — so disabling an entry removes it on the next spawn while keys
|
|
327
|
+
* the app never wrote (and `specrails`, managed separately) are untouched.
|
|
328
|
+
*/
|
|
329
|
+
function syncExternalServersIntoJsonFile(file, external) {
|
|
330
|
+
const dir = path_1.default.dirname(file);
|
|
331
|
+
const sidecar = path_1.default.join(dir, 'external-owned.json');
|
|
332
|
+
let owned = [];
|
|
333
|
+
try {
|
|
334
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(sidecar, 'utf-8'));
|
|
335
|
+
if (parsed && Array.isArray(parsed.owned)) {
|
|
336
|
+
owned = parsed.owned.filter((n) => typeof n === 'string');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
/* first spawn / no sidecar yet */
|
|
341
|
+
}
|
|
342
|
+
// Nothing owned before and nothing to inject now → leave the file alone.
|
|
343
|
+
if (owned.length === 0 && external.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
let current = {};
|
|
346
|
+
try {
|
|
347
|
+
if (fs_1.default.existsSync(file)) {
|
|
348
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
349
|
+
if (parsed && typeof parsed === 'object')
|
|
350
|
+
current = parsed;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
current = {};
|
|
355
|
+
}
|
|
356
|
+
const mcpServers = { ...(current.mcpServers ?? {}) };
|
|
357
|
+
for (const name of owned) {
|
|
358
|
+
if (name !== 'specrails')
|
|
359
|
+
delete mcpServers[name];
|
|
360
|
+
}
|
|
361
|
+
const nextOwned = [];
|
|
362
|
+
for (const server of external) {
|
|
363
|
+
if (server.name === 'specrails')
|
|
364
|
+
continue;
|
|
365
|
+
mcpServers[server.name] = server.config;
|
|
366
|
+
nextOwned.push(server.name);
|
|
367
|
+
}
|
|
368
|
+
const next = { ...current, mcpServers };
|
|
369
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
370
|
+
fs_1.default.writeFileSync(file, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
371
|
+
try {
|
|
372
|
+
fs_1.default.chmodSync(file, 0o600);
|
|
373
|
+
}
|
|
374
|
+
catch { /* best-effort */ }
|
|
375
|
+
fs_1.default.writeFileSync(sidecar, JSON.stringify({ owned: nextOwned }, null, 2), { mode: 0o600 });
|
|
376
|
+
try {
|
|
377
|
+
fs_1.default.chmodSync(sidecar, 0o600);
|
|
378
|
+
}
|
|
379
|
+
catch { /* best-effort */ }
|
|
380
|
+
}
|
|
@@ -69,6 +69,10 @@ coding pipelines over them.
|
|
|
69
69
|
\`mcp_specrails_<canonical name>\` (e.g. \`mcp_specrails_specrails_specs\`).
|
|
70
70
|
Use whatever form your tool list shows; they are identical tools. They are
|
|
71
71
|
MCP tools, NOT shell commands or files — never hunt for them on disk.
|
|
72
|
+
- Additional USER-CONFIGURED tools (external MCP servers) may also appear in
|
|
73
|
+
your tool list. Use them for their own domains when helpful, but ALL
|
|
74
|
+
Specrails app operations (specs, rails, jobs, projects, …) MUST still go
|
|
75
|
+
through the \`specrails_*\` tools — never through a lookalike external tool.
|
|
72
76
|
- Target a project with \`specrails_select_project\` (or the \`projectId\`
|
|
73
77
|
argument). If none is pinned ("Home") and the request is project-specific, ASK
|
|
74
78
|
whether to create a project or search across all — do not guess.
|
|
@@ -428,11 +432,19 @@ summary later.
|
|
|
428
432
|
relaunch strategy even when \`main\` did not have that spec before the PR. If
|
|
429
433
|
asked why a strategy was chosen, verify against active PR contents before
|
|
430
434
|
answering.
|
|
431
|
-
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
435
|
+
- **"Change something about work already delivered" = a REVISION launch.** When
|
|
436
|
+
the user asks for a modification to a delivery that is awaiting their decision
|
|
437
|
+
(any non-terminal card: \`on_review\`, \`pr_draft\`, \`pr_ready\`, \`no_changes\`,
|
|
438
|
+
\`implementation_failed\`), call \`specrails_rails(launch)\` with
|
|
439
|
+
\`revisionOfDeliveryId\` = that card's \`prDeliveryId\` and \`revisionNote\` = what
|
|
440
|
+
they asked for, in their own words. Do NOT tell them to publish, discard or
|
|
441
|
+
merge first, and do NOT relaunch the ordinary implement/batch mode: a revision
|
|
442
|
+
runs a dedicated Architect-less loop that builds on the existing branch instead
|
|
443
|
+
of re-planning from scratch. The rail must still carry exactly that delivery's
|
|
444
|
+
specs; a mismatch returns \`invalid_revision_target\`, which means re-check the
|
|
445
|
+
rail's spec assignment rather than retrying blindly.
|
|
446
|
+
- \`pr_decision_pending\` therefore only blocks a launch that is neither a
|
|
447
|
+
revision nor a continuation of an open PR head.
|
|
436
448
|
- Launch proposal shape: tickets (ids + titles), rail number, mode, engine and
|
|
437
449
|
model/profile, plus "runs for minutes and costs money". If the API mode is
|
|
438
450
|
\`freestyle\`, write "Freestyle" to the user. Wait for yes.
|
|
@@ -495,13 +495,50 @@ class PlaywrightPageHandle {
|
|
|
495
495
|
// target=_blank, OAuth login windows). The popup lives in the SAME browser
|
|
496
496
|
// context — cookies and the window.opener/postMessage relationship are
|
|
497
497
|
// intact, which is exactly what an OAuth redirect/popup flow needs.
|
|
498
|
+
//
|
|
499
|
+
// HARDENING (Okta-class IdPs): a popup opened from a cross-origin IFRAME
|
|
500
|
+
// (out-of-process sign-in widgets) is attributed to the iframe's target,
|
|
501
|
+
// and the page-level 'popup' event can miss it — the login window then
|
|
502
|
+
// "never appears". So we ALSO watch the shared context for new pages whose
|
|
503
|
+
// opener chain resolves to this page and adopt those. `seen` dedupes the
|
|
504
|
+
// two doors; pages from other sessions have a different opener and are
|
|
505
|
+
// ignored.
|
|
498
506
|
try {
|
|
499
|
-
|
|
507
|
+
const seen = new WeakSet();
|
|
508
|
+
const adopt = (p) => {
|
|
509
|
+
if (seen.has(p))
|
|
510
|
+
return;
|
|
511
|
+
seen.add(p);
|
|
500
512
|
try {
|
|
501
513
|
cb(new PlaywrightPageHandle(p));
|
|
502
514
|
}
|
|
503
515
|
catch { /* never break the event loop */ }
|
|
504
|
-
}
|
|
516
|
+
};
|
|
517
|
+
this.page.on('popup', adopt);
|
|
518
|
+
const ctx = this.page.context?.();
|
|
519
|
+
if (ctx?.on) {
|
|
520
|
+
const onContextPage = (p) => {
|
|
521
|
+
void (async () => {
|
|
522
|
+
try {
|
|
523
|
+
if (seen.has(p) || p === this.page)
|
|
524
|
+
return;
|
|
525
|
+
const opener = await p.opener?.();
|
|
526
|
+
if (opener === this.page)
|
|
527
|
+
adopt(p);
|
|
528
|
+
}
|
|
529
|
+
catch { /* ignore */ }
|
|
530
|
+
})();
|
|
531
|
+
};
|
|
532
|
+
ctx.on('page', onContextPage);
|
|
533
|
+
// The shared context outlives this page — detach on page close so a
|
|
534
|
+
// long-lived app never accumulates dead listeners.
|
|
535
|
+
this.page.on('close', () => {
|
|
536
|
+
try {
|
|
537
|
+
ctx.off?.('page', onContextPage);
|
|
538
|
+
}
|
|
539
|
+
catch { /* ignore */ }
|
|
540
|
+
});
|
|
541
|
+
}
|
|
505
542
|
}
|
|
506
543
|
catch { /* ignore */ }
|
|
507
544
|
}
|
package/server/dist/db.js
CHANGED
|
@@ -1464,6 +1464,43 @@ const MIGRATIONS = [
|
|
|
1464
1464
|
ON agent_tests(provider, agent_name)
|
|
1465
1465
|
`);
|
|
1466
1466
|
},
|
|
1467
|
+
// Migration 56: nontech-review-experience Wave 1 — two additive nullable
|
|
1468
|
+
// JSON columns on rail_pr_deliveries. `spec_snapshot` freezes each covered
|
|
1469
|
+
// ticket's title/description/labels at LAUNCH so the review packet renders
|
|
1470
|
+
// what the user actually asked (the live store can be edited mid-run).
|
|
1471
|
+
// `settle_evidence` persists the deterministic settle-time harvest (the
|
|
1472
|
+
// VERIFICATION sentinel, the verify step's output tail, the reviewer's
|
|
1473
|
+
// confidence-score.json) captured BEFORE worktree release — the packet's
|
|
1474
|
+
// proof sections survive worktree teardown. NULL on legacy rows = honest
|
|
1475
|
+
// "not captured". Additive + idempotent (guarded by PRAGMA table_info).
|
|
1476
|
+
(db) => {
|
|
1477
|
+
const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
|
|
1478
|
+
.map((r) => r.name);
|
|
1479
|
+
if (!cols.includes('spec_snapshot')) {
|
|
1480
|
+
db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN spec_snapshot TEXT`);
|
|
1481
|
+
}
|
|
1482
|
+
if (!cols.includes('settle_evidence')) {
|
|
1483
|
+
db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN settle_evidence TEXT`);
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
// Migration 57: nontech-review-experience Wave 3 — revision metadata on the
|
|
1487
|
+
// NEW delivery generation a revision creates. `revision_note` is the user's
|
|
1488
|
+
// one-sentence instruction (so packet v2 can show what was asked to change)
|
|
1489
|
+
// and `revision_of` points at the generation being revised. Deliberately NOT
|
|
1490
|
+
// a new `decision` value: an unknown decision makes older clients drop the
|
|
1491
|
+
// whole PR card, and the shipped generation/supersession model already
|
|
1492
|
+
// expresses "a newer attempt replaced the previous one". Additive +
|
|
1493
|
+
// idempotent; NULL on every pre-existing row and on ordinary launches.
|
|
1494
|
+
(db) => {
|
|
1495
|
+
const cols = db.prepare(`PRAGMA table_info(rail_pr_deliveries)`).all()
|
|
1496
|
+
.map((r) => r.name);
|
|
1497
|
+
if (!cols.includes('revision_note')) {
|
|
1498
|
+
db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_note TEXT`);
|
|
1499
|
+
}
|
|
1500
|
+
if (!cols.includes('revision_of')) {
|
|
1501
|
+
db.exec(`ALTER TABLE rail_pr_deliveries ADD COLUMN revision_of TEXT`);
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1467
1504
|
];
|
|
1468
1505
|
function applyMigrations(db) {
|
|
1469
1506
|
// Ensure the migrations table exists (migration 1 creates it, but we need
|
|
@@ -2019,6 +2056,7 @@ function getProjectSettings(db) {
|
|
|
2019
2056
|
return {
|
|
2020
2057
|
pipelineTelemetryEnabled: telemetryRow?.value === 'true',
|
|
2021
2058
|
orchestratorModel: modelRow?.value ?? 'sonnet',
|
|
2059
|
+
orchestratorModelExplicit: modelRow?.value !== undefined,
|
|
2022
2060
|
prePrompt: prePromptRow?.value ?? '',
|
|
2023
2061
|
freestylePrePrompt: freestylePrePromptRow?.value ?? '',
|
|
2024
2062
|
integrationBranch: integrationBranchRow?.value ?? '',
|