specrails-desktop 2.34.1 → 2.35.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-LCBJ5WiD.js +1 -0
- package/client/dist/assets/AgentBrowserCapture-B-xQpyZr.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-BI1Vcf3T.js +2 -0
- package/client/dist/assets/AgentModeCodePane-BTRwCl9h.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-VzeBJLqP.js +1 -0
- package/client/dist/assets/AgentsPage-CJj1K24O.js +87 -0
- package/client/dist/assets/AnalyticsPage-CQpV5_fX.js +1 -0
- package/client/dist/assets/CodePage-9-tFh0jB.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-BcZ0_O_C.js +1 -0
- package/client/dist/assets/DocsDialog-BPAzDICW.js +11 -0
- package/client/dist/assets/DocsPage-Dqo4Hn6G.js +11 -0
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-D36fZWai.js +19 -0
- package/client/dist/assets/JobDetailModal-BxsktbkQ.js +1 -0
- package/client/dist/assets/JobDetailPage-WWyCy6Sp.js +1 -0
- package/client/dist/assets/JobsPage-CEbM7uxU.js +1 -0
- package/client/dist/assets/{LoopBuilderPage-B1GJJ0QZ.js → LoopBuilderPage-CYsSN8io.js} +2 -2
- package/client/dist/assets/LoopPreviewModal-BJhgX5wY.js +1 -0
- package/client/dist/assets/LoopsPage-DB4OqMot.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-CcjBHMHd.js +1 -0
- package/client/dist/assets/PluginsPage-CR8K4nM_.js +2 -0
- package/client/dist/assets/{ProjectSettingsDialog-D6Vxjpv5.js → ProjectSettingsDialog-C45-qhF4.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-Cz_QQeC3.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-wFBd2vQP.js +1 -0
- package/client/dist/assets/TicketDetailModal-BvZoyDAL.js +14 -0
- package/client/dist/assets/{Trans-CTqw6rr9.js → Trans-DXZ-BLXb.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-C0ImhVKp.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-EjGOpn4b.js → getTimezoneOffsetInMilliseconds-owhDdTXs.js} +1 -1
- package/client/dist/assets/git-refresh-wTW0rO0Z.js +1 -0
- package/client/dist/assets/i18n-DwgvwTnx.js +2 -0
- package/client/dist/assets/index-DhjjFyU5.js +66 -0
- package/client/dist/assets/index-xBFsOEiJ.css +2 -0
- package/client/dist/assets/jira-api-CcZMcEN5.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/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-BQX3P7Cq.js} +1 -1
- package/client/dist/assets/useDesktop-BzX769yH.js +1 -0
- package/client/dist/index.html +29 -31
- package/docs/internals/embedded-browser-native-webview-evaluation.md +161 -0
- package/docs/internals/review-packet.md +212 -0
- package/package.json +1 -1
- package/server/dist/accept-ladder.js +61 -0
- package/server/dist/agent-chat-manager.js +22 -1
- package/server/dist/agent-defaults.js +359 -0
- package/server/dist/agent-operator-prompt.js +13 -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 +23 -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/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
|
@@ -292,14 +292,51 @@ function parseClaudeStreamLine(line) {
|
|
|
292
292
|
const tool = blocks.find((b) => b.type === 'tool_use');
|
|
293
293
|
if (tool?.name) {
|
|
294
294
|
const input = JSON.stringify(parsed.message?.content?.[0]?.input ?? {});
|
|
295
|
-
return withAssistantUsage({
|
|
295
|
+
return withAssistantUsage({
|
|
296
|
+
kind: 'tool-use',
|
|
297
|
+
name: tool.name,
|
|
298
|
+
inputPreview: input.slice(0, 200),
|
|
299
|
+
...(typeof tool.id === 'string'
|
|
300
|
+
? { toolUseId: tool.id }
|
|
301
|
+
: {}),
|
|
302
|
+
}, msg);
|
|
296
303
|
}
|
|
297
304
|
return withAssistantUsage({ kind: 'other', type, raw: parsed }, msg);
|
|
298
305
|
}
|
|
299
306
|
if (type === 'tool_use') {
|
|
300
307
|
const name = parsed.name ?? '<unnamed>';
|
|
301
308
|
const input = JSON.stringify(parsed.input ?? {});
|
|
302
|
-
return {
|
|
309
|
+
return {
|
|
310
|
+
kind: 'tool-use',
|
|
311
|
+
name,
|
|
312
|
+
inputPreview: input.slice(0, 200),
|
|
313
|
+
...(typeof parsed.id === 'string' ? { toolUseId: parsed.id } : {}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// `user`-role frames carry tool RESULTS back to the model. Surface a bounded
|
|
317
|
+
// text projection so activity surfaces (agent chat's execution log) can show
|
|
318
|
+
// what a tool returned; frames without a tool_result block stay 'other'.
|
|
319
|
+
if (type === 'user') {
|
|
320
|
+
const msg = parsed.message;
|
|
321
|
+
const blocks = Array.isArray(msg?.content) ? msg.content : [];
|
|
322
|
+
const resultBlock = blocks.find((b) => b.type === 'tool_result');
|
|
323
|
+
if (resultBlock) {
|
|
324
|
+
const content = resultBlock.content;
|
|
325
|
+
const text = typeof content === 'string'
|
|
326
|
+
? content
|
|
327
|
+
: Array.isArray(content)
|
|
328
|
+
? content
|
|
329
|
+
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
330
|
+
.map((c) => c.text)
|
|
331
|
+
.join('\n')
|
|
332
|
+
: '';
|
|
333
|
+
return {
|
|
334
|
+
kind: 'tool-result',
|
|
335
|
+
outputPreview: text.slice(0, 600),
|
|
336
|
+
...(typeof resultBlock.tool_use_id === 'string' ? { toolUseId: resultBlock.tool_use_id } : {}),
|
|
337
|
+
...(resultBlock.is_error === true ? { isError: true } : {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
303
340
|
}
|
|
304
341
|
return { kind: 'other', type, raw: parsed };
|
|
305
342
|
}
|
|
@@ -26,6 +26,7 @@ const result_event_1 = require("./result-event");
|
|
|
26
26
|
const crypto_1 = require("crypto");
|
|
27
27
|
const providers_1 = require("./providers");
|
|
28
28
|
const runtime_1 = require("./providers/runtime");
|
|
29
|
+
const agent_defaults_1 = require("./agent-defaults");
|
|
29
30
|
const codex_otel_bridge_1 = require("./codex-otel-bridge");
|
|
30
31
|
const db_1 = require("./db");
|
|
31
32
|
const interactive_job_session_1 = require("./interactive-job-session");
|
|
@@ -250,6 +251,7 @@ class QueueManager {
|
|
|
250
251
|
* For codex it controls the catalog model used at spawn time and as the
|
|
251
252
|
* fallback model name stamped onto the ai_invocations row. */
|
|
252
253
|
_resolvedModel;
|
|
254
|
+
_agentDefaults;
|
|
253
255
|
_onJobFinished;
|
|
254
256
|
/** Project-owned durable effects that are part of every queue admission
|
|
255
257
|
* (for example, recovery/rail ownership). Invoked after queued_jobs has
|
|
@@ -356,6 +358,7 @@ class QueueManager {
|
|
|
356
358
|
this._getDesktopDailyBudget = options?.getDesktopDailyBudget ?? null;
|
|
357
359
|
this._adapter = (0, providers_1.getAdapter)(options?.provider ?? 'claude');
|
|
358
360
|
this._resolvedModel = options?.resolvedModel ?? null;
|
|
361
|
+
this._agentDefaults = options?.agentDefaults ?? null;
|
|
359
362
|
this._onJobFinished = options?.onJobFinished ?? null;
|
|
360
363
|
this._onJobAdmission = options?.onJobAdmission ?? null;
|
|
361
364
|
this._onBudgetExceeded = options?.onBudgetExceeded ?? null;
|
|
@@ -2272,9 +2275,22 @@ class QueueManager {
|
|
|
2272
2275
|
// Per-job model override (consumed once) takes precedence — used by the
|
|
2273
2276
|
// freestyle model picker so the user can choose haiku/sonnet/opus per launch.
|
|
2274
2277
|
const modelOverride = this._jobModelSelection.get(jobId);
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
+
// Global Specrails Agents defaults (app Settings ▸ Specrails Agents) —
|
|
2279
|
+
// read AT SPAWN TIME (no restart) and layered BELOW every project-level
|
|
2280
|
+
// choice, ABOVE the built-in adapter default.
|
|
2281
|
+
let globalAgentDefaults = null;
|
|
2282
|
+
try {
|
|
2283
|
+
globalAgentDefaults = this._agentDefaults?.(adapter.id) ?? null;
|
|
2284
|
+
}
|
|
2285
|
+
catch {
|
|
2286
|
+
globalAgentDefaults = null;
|
|
2287
|
+
}
|
|
2288
|
+
const claudeProjectSettings = adapter.id === 'claude' && this._db ? (0, db_1.getProjectSettings)(this._db) : null;
|
|
2289
|
+
const fallbackRailModel = claudeProjectSettings
|
|
2290
|
+
? (claudeProjectSettings.orchestratorModelExplicit
|
|
2291
|
+
? claudeProjectSettings.orchestratorModel
|
|
2292
|
+
: globalAgentDefaults?.pipelineModel ?? claudeProjectSettings.orchestratorModel)
|
|
2293
|
+
: (this._resolvedModel ?? globalAgentDefaults?.pipelineModel ?? adapter.defaultModel());
|
|
2278
2294
|
// Relocate-artifacts: when relocated, claude is spawned from the workspace
|
|
2279
2295
|
// so add `--add-dir <repoDir>` so its tools can still reach repo files by
|
|
2280
2296
|
// absolute path. (gemini/codex get env-only tweaks at spawn time below.)
|
|
@@ -2301,7 +2317,24 @@ class QueueManager {
|
|
|
2301
2317
|
if (selection !== null && coreSupports) {
|
|
2302
2318
|
// selection is string (explicit) or undefined (default resolution)
|
|
2303
2319
|
const { resolveProfile, snapshotForJob, persistJobProfile, } = require('./profile-manager');
|
|
2304
|
-
|
|
2320
|
+
let resolved = resolveProfile(execution.cwd, selection ?? undefined, adapter.id);
|
|
2321
|
+
// Global agent defaults: fill per-agent model gaps in the resolved
|
|
2322
|
+
// profile, or synthesize a baseline profile when the project has
|
|
2323
|
+
// none. Explicit profile choices always win; inert when the global
|
|
2324
|
+
// layer is off (byte-identical legacy behaviour).
|
|
2325
|
+
if (globalAgentDefaults && adapter.capabilities.profiles === true) {
|
|
2326
|
+
if (resolved) {
|
|
2327
|
+
const merged = (0, agent_defaults_1.mergeProfileWithAgentDefaults)(resolved.profile, globalAgentDefaults);
|
|
2328
|
+
if (merged.changed)
|
|
2329
|
+
resolved = { name: resolved.name, profile: merged.profile };
|
|
2330
|
+
}
|
|
2331
|
+
else if (Object.keys(globalAgentDefaults.agentModels).length > 0) {
|
|
2332
|
+
resolved = {
|
|
2333
|
+
name: agent_defaults_1.GLOBAL_DEFAULTS_PROFILE_NAME,
|
|
2334
|
+
profile: (0, agent_defaults_1.synthesizeProfileFromDefaults)(adapter, globalAgentDefaults),
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2305
2338
|
if (resolved) {
|
|
2306
2339
|
profileSnapshotPath = snapshotForJob(this._projectSlug, jobId, resolved);
|
|
2307
2340
|
profileName = resolved.name;
|
|
@@ -2322,10 +2355,19 @@ class QueueManager {
|
|
|
2322
2355
|
// explicit per-job override > resolved profile orchestrator > project/default.
|
|
2323
2356
|
// resolveProfile validates the profile model against this adapter's catalog.
|
|
2324
2357
|
const railModel = modelOverride ?? profileOrchestratorModel ?? fallbackRailModel;
|
|
2358
|
+
// Pipeline-level reasoning effort from the global layer (rails never had a
|
|
2359
|
+
// per-job effort knob, so this is purely additive). Validated against the
|
|
2360
|
+
// FINAL model — claude/codex sub-agents inherit the process-level effort,
|
|
2361
|
+
// kimi rides its env knob via buildProviderEnv, gemini has no effort.
|
|
2362
|
+
const globalRailEffort = globalAgentDefaults?.pipelineEffort
|
|
2363
|
+
&& (0, runtime_1.isReasoningEffortValidForModel)(adapter, railModel, globalAgentDefaults.pipelineEffort)
|
|
2364
|
+
? globalAgentDefaults.pipelineEffort
|
|
2365
|
+
: undefined;
|
|
2325
2366
|
const railSpawnOptions = {
|
|
2326
2367
|
prompt: railPrompt,
|
|
2327
2368
|
systemPrompt: systemAppend || undefined,
|
|
2328
2369
|
model: railModel,
|
|
2370
|
+
...(globalRailEffort ? { reasoning_effort: globalRailEffort } : {}),
|
|
2329
2371
|
extraArgs: railExtraArgs.length > 0 ? railExtraArgs : undefined,
|
|
2330
2372
|
};
|
|
2331
2373
|
const args = adapter.buildArgs('rail-job', railSpawnOptions);
|
|
@@ -2544,6 +2586,7 @@ class QueueManager {
|
|
|
2544
2586
|
// spawn and APPEND on top of the CLI default instead of replacing it.
|
|
2545
2587
|
systemPrompt: isFreestyle ? (systemAppend || undefined) : undefined,
|
|
2546
2588
|
model: railModel,
|
|
2589
|
+
...(globalRailEffort ? { reasoning_effort: globalRailEffort } : {}),
|
|
2547
2590
|
extraArgs: !isFreestyle && systemAppend
|
|
2548
2591
|
? ['--append-system-prompt', systemAppend, ...(railExtraArgs ?? [])]
|
|
2549
2592
|
: (railExtraArgs.length > 0 ? railExtraArgs : undefined),
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.PrContinuationIsolationError = void 0;
|
|
37
|
+
exports.buildSpecSnapshot = buildSpecSnapshot;
|
|
37
38
|
exports.launchIsolatedRail = launchIsolatedRail;
|
|
38
39
|
exports.reconcileRailWorktrees = reconcileRailWorktrees;
|
|
39
40
|
/**
|
|
@@ -70,6 +71,7 @@ const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
|
70
71
|
const pr_naming_1 = require("./pr-naming");
|
|
71
72
|
const jira_db_1 = require("./jira/jira-db");
|
|
72
73
|
const db_1 = require("./db");
|
|
74
|
+
const delivery_evidence_1 = require("./delivery-evidence");
|
|
73
75
|
const integration_branch_1 = require("./integration-branch");
|
|
74
76
|
const repo_lock_1 = require("./repo-lock");
|
|
75
77
|
const rail_isolation_1 = require("./rail-isolation");
|
|
@@ -79,6 +81,7 @@ const rail_merge_orchestrator_1 = require("./rail-merge-orchestrator");
|
|
|
79
81
|
const loop_executors_1 = require("./loop-executors");
|
|
80
82
|
const worktree_overlay_1 = require("./worktree-overlay");
|
|
81
83
|
const worktree_node_modules_1 = require("./worktree-node-modules");
|
|
84
|
+
const revision_seed_1 = require("./revision-seed");
|
|
82
85
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
83
86
|
const feature_flags_1 = require("./feature-flags");
|
|
84
87
|
const file_provenance_1 = require("./file-provenance");
|
|
@@ -125,6 +128,66 @@ function unitNamingInput(ctx, ticketId) {
|
|
|
125
128
|
}
|
|
126
129
|
return { ticketId, title: spec?.title ?? null, labels: spec?.labels ?? null, jiraKey };
|
|
127
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Assemble the revision briefing from the generation being revised. Reads only
|
|
133
|
+
* durable rows, tolerates every one of them being absent (a legacy row carries
|
|
134
|
+
* no snapshot or evidence), and never throws — a thinner briefing is far better
|
|
135
|
+
* than a failed revision launch.
|
|
136
|
+
*/
|
|
137
|
+
function buildRevisionSeedForLaunch(db, revision, ticketIds) {
|
|
138
|
+
try {
|
|
139
|
+
const previous = (0, rail_pr_store_1.getPrDelivery)(db, revision.ofDeliveryId);
|
|
140
|
+
const branches = previous
|
|
141
|
+
? [...new Set(JSON.parse(previous.branches || '[]').map((b) => b.branch).filter(Boolean))]
|
|
142
|
+
: [];
|
|
143
|
+
// Revision depth walks the supersession chain, bounded so a corrupted
|
|
144
|
+
// lineage can never spin here.
|
|
145
|
+
let depth = 1;
|
|
146
|
+
let cursor = previous;
|
|
147
|
+
for (let hops = 0; hops < 20 && cursor?.revision_of; hops++) {
|
|
148
|
+
depth += 1;
|
|
149
|
+
cursor = (0, rail_pr_store_1.getPrDelivery)(db, cursor.revision_of) ?? undefined;
|
|
150
|
+
}
|
|
151
|
+
return (0, revision_seed_1.buildRevisionSeed)({
|
|
152
|
+
note: revision.note,
|
|
153
|
+
specSnapshot: previous ? (0, rail_pr_store_1.readSpecSnapshot)(previous.spec_snapshot) : null,
|
|
154
|
+
ticketIds: [...ticketIds],
|
|
155
|
+
branches,
|
|
156
|
+
baseBranch: previous?.base_branch ?? 'the integration branch',
|
|
157
|
+
branchDiffSummary: null,
|
|
158
|
+
evidence: previous ? (0, delivery_evidence_1.readSettleEvidence)(previous.settle_evidence) : null,
|
|
159
|
+
revisionNumber: depth,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Absolute floor: the user's instruction still reaches the run.
|
|
164
|
+
return revision.note;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Launch-time freeze of every covered ticket for the delivery row's
|
|
169
|
+
* spec_snapshot column (migration 56). The review packet's "what you asked"
|
|
170
|
+
* section renders this snapshot, never the live store — a spec edited mid-run
|
|
171
|
+
* must not rewrite what the delivery was asked to do. Failure-tolerant: an
|
|
172
|
+
* unreadable ticket degrades to a bare-id entry.
|
|
173
|
+
*/
|
|
174
|
+
function buildSpecSnapshot(ctx, ticketIds) {
|
|
175
|
+
return ticketIds.map((ticketId) => {
|
|
176
|
+
let spec;
|
|
177
|
+
try {
|
|
178
|
+
spec = ctx.getTicketSpec(ticketId);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
/* tolerated — snapshot stays bare */
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
ticketId,
|
|
185
|
+
title: spec?.title ?? null,
|
|
186
|
+
description: spec?.description ?? null,
|
|
187
|
+
labels: Array.isArray(spec?.labels) ? spec.labels : [],
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
128
191
|
/**
|
|
129
192
|
* Commit messages are consumed by GitHub↔Jira development panels. Branch names
|
|
130
193
|
* already carry the Jira key for new work; commits must too, especially when
|
|
@@ -323,7 +386,18 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
323
386
|
const baseRepo = ctx.project.path;
|
|
324
387
|
const slug = ctx.project.slug;
|
|
325
388
|
const worktreesRoot = path.join((0, artifact_registry_1.resolveHome)(), '.specrails', 'projects', slug, 'worktrees');
|
|
326
|
-
|
|
389
|
+
// A revision run is seeded with a full briefing (the instruction PLUS the
|
|
390
|
+
// frozen spec, the branch that carries the work and what the previous run
|
|
391
|
+
// actually reported) rather than the bare sentence — the run starts a FRESH
|
|
392
|
+
// provider session by contract, so durable context is the only thing it has.
|
|
393
|
+
// Per-RUN data, so it is layered over the project's constant map for this
|
|
394
|
+
// launch only and never persisted as a constant.
|
|
395
|
+
const constants = input.revision
|
|
396
|
+
? {
|
|
397
|
+
...(0, loop_constants_1.loadConstantMap)(ctx.desktopDb),
|
|
398
|
+
REVISION_REQUEST: buildRevisionSeedForLaunch(ctx.db, input.revision, ticketIds),
|
|
399
|
+
}
|
|
400
|
+
: (0, loop_constants_1.loadConstantMap)(ctx.desktopDb);
|
|
327
401
|
// Capture the PR-delivery mode ONCE at launch entry so a mid-flight env flip
|
|
328
402
|
// can never split one launch across the two delivery paths.
|
|
329
403
|
const prMode = (0, rail_isolation_1.isRailPrDeliveryEnabled)();
|
|
@@ -582,9 +656,16 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
582
656
|
supersedesDeliveryId: launchContinuation?.source === 'rail-pr-delivery'
|
|
583
657
|
? launchContinuation.deliveryId
|
|
584
658
|
: null,
|
|
659
|
+
specSnapshot: buildSpecSnapshot(ctx, ticketIds),
|
|
660
|
+
...(input.revision ? { revisionNote: input.revision.note, revisionOf: input.revision.ofDeliveryId } : {}),
|
|
585
661
|
}, input.requiredPrContinuation
|
|
586
662
|
? { id: input.requiredPrContinuation.deliveryId, decision: input.requiredPrContinuation.decision }
|
|
587
|
-
|
|
663
|
+
// A revision replaces the generation it revises, atomically, so the rail
|
|
664
|
+
// never shows two active deliveries and a failed revision restores the
|
|
665
|
+
// predecessor through the shipped rollback path.
|
|
666
|
+
: input.revision
|
|
667
|
+
? { id: input.revision.ofDeliveryId, decision: input.revision.decision }
|
|
668
|
+
: null);
|
|
588
669
|
prDeliveryId = generation.delivery.id;
|
|
589
670
|
supersededDelivery = generation.superseded;
|
|
590
671
|
input.onPrDeliveryCreated?.(prDeliveryId);
|
|
@@ -1362,7 +1443,25 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
1362
1443
|
const expectedHeadByBranch = (0, rail_worktree_release_1.durableBranchHeads)(settledBranchRecords);
|
|
1363
1444
|
const overlayEvidenceByBranch = (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(settledBranchRecords);
|
|
1364
1445
|
const settlementIgnoredByBranch = (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(settledBranchRecords);
|
|
1446
|
+
// Review-packet evidence harvest (deterministic, no model calls) — MUST
|
|
1447
|
+
// run while the reviewable worktrees are still mounted, i.e. before the
|
|
1448
|
+
// releaseRailWorktrees calls below. harvestDeliveryEvidence never throws
|
|
1449
|
+
// by contract; the belt-and-braces catch keeps settle unconditional.
|
|
1450
|
+
const harvest = io.harvestEvidence ?? delivery_evidence_1.harvestDeliveryEvidence;
|
|
1451
|
+
let settleEvidence;
|
|
1452
|
+
try {
|
|
1453
|
+
settleEvidence = harvest({ readEvents: (runId) => (0, db_1.getJobEvents)(ctx.db, runId) }, results.map((result) => ({
|
|
1454
|
+
ticketId: result.run.ticketId,
|
|
1455
|
+
runId: result.run.runId,
|
|
1456
|
+
worktreePath: result.run.handle.worktreePath,
|
|
1457
|
+
})));
|
|
1458
|
+
}
|
|
1459
|
+
catch (e) {
|
|
1460
|
+
console.warn(`[rail-isolated] evidence harvest failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1461
|
+
settleEvidence = undefined;
|
|
1462
|
+
}
|
|
1365
1463
|
const patch = {
|
|
1464
|
+
settleEvidence,
|
|
1366
1465
|
branches: settledBranchRecords,
|
|
1367
1466
|
worktreeIds,
|
|
1368
1467
|
implementationOutcome,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PrDeliveryGenerationConflict = exports.ACTIVE_PR_DECISIONS = exports.TERMINAL_PR_DECISIONS = void 0;
|
|
3
|
+
exports.PrDeliveryGenerationConflict = exports.ACTIVE_PR_DECISIONS = exports.TERMINAL_PR_DECISIONS = exports.REVISION_NOTE_CAP = void 0;
|
|
4
|
+
exports.boundSpecSnapshot = boundSpecSnapshot;
|
|
5
|
+
exports.boundRevisionNote = boundRevisionNote;
|
|
6
|
+
exports.readSpecSnapshot = readSpecSnapshot;
|
|
4
7
|
exports.isTerminalPrDecision = isTerminalPrDecision;
|
|
5
8
|
exports.createPrDelivery = createPrDelivery;
|
|
6
9
|
exports.createPrDeliveryGeneration = createPrDeliveryGeneration;
|
|
@@ -26,6 +29,34 @@ exports.toPrDeliverySnapshot = toPrDeliverySnapshot;
|
|
|
26
29
|
exports.toRailPrStateMessage = toRailPrStateMessage;
|
|
27
30
|
exports.toPrDecisionCardEnvelope = toPrDecisionCardEnvelope;
|
|
28
31
|
const ids_1 = require("./ids");
|
|
32
|
+
/** Defensive per-ticket cap; contract-layer specs run long but bounded. */
|
|
33
|
+
const SPEC_SNAPSHOT_DESCRIPTION_CAP = 32_768;
|
|
34
|
+
function boundSpecSnapshot(entries) {
|
|
35
|
+
return entries.map((entry) => ({
|
|
36
|
+
ticketId: entry.ticketId,
|
|
37
|
+
title: entry.title,
|
|
38
|
+
description: entry.description == null ? null : entry.description.slice(0, SPEC_SNAPSHOT_DESCRIPTION_CAP),
|
|
39
|
+
labels: entry.labels.slice(0, 32),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/** A revision instruction is one sentence of intent, not a document. Bound it so
|
|
43
|
+
* a pasted essay cannot bloat every snapshot that carries the row. */
|
|
44
|
+
exports.REVISION_NOTE_CAP = 2000;
|
|
45
|
+
function boundRevisionNote(note) {
|
|
46
|
+
return note.replace(/[\r\n\t\0]+/g, ' ').replace(/\s{2,}/g, ' ').trim().slice(0, exports.REVISION_NOTE_CAP);
|
|
47
|
+
}
|
|
48
|
+
/** Parse a persisted spec_snapshot column value (null-tolerant). */
|
|
49
|
+
function readSpecSnapshot(raw) {
|
|
50
|
+
if (!raw)
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
29
60
|
/** States after which the delivery is closed (no further decision possible). */
|
|
30
61
|
exports.TERMINAL_PR_DECISIONS = new Set(['completed', 'merged', 'discarded', 'superseded']);
|
|
31
62
|
/** States in which the delivery still awaits (or is executing) a user decision. */
|
|
@@ -53,9 +84,12 @@ function createPrDelivery(db, input) {
|
|
|
53
84
|
id, rail_index, loop_id, rail_key, ticket_ids, base_branch,
|
|
54
85
|
loop_name, origin_surface, origin_conversation_id,
|
|
55
86
|
implementation_outcome, delivery_outcome, status_code,
|
|
56
|
-
is_continuation, supersedes_delivery_id
|
|
87
|
+
is_continuation, supersedes_delivery_id, spec_snapshot,
|
|
88
|
+
revision_note, revision_of
|
|
57
89
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', 'pending',
|
|
58
|
-
'implementation_running', ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null
|
|
90
|
+
'implementation_running', ?, ?, ?, ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null, input.specSnapshot && input.specSnapshot.length > 0
|
|
91
|
+
? JSON.stringify(boundSpecSnapshot(input.specSnapshot))
|
|
92
|
+
: null, input.revisionNote ? boundRevisionNote(input.revisionNote) : null, input.revisionOf ?? null);
|
|
59
93
|
return getPrDelivery(db, id);
|
|
60
94
|
}
|
|
61
95
|
class PrDeliveryGenerationConflict extends Error {
|
|
@@ -345,6 +379,7 @@ const PATCH_COLUMNS = {
|
|
|
345
379
|
operationStartedAtMs: 'operation_started_at_ms',
|
|
346
380
|
cleanupWarnings: 'cleanup_warnings',
|
|
347
381
|
safetyArchives: 'safety_archives',
|
|
382
|
+
settleEvidence: 'settle_evidence',
|
|
348
383
|
};
|
|
349
384
|
function patchValue(key, patch) {
|
|
350
385
|
const raw = patch[key];
|
|
@@ -358,6 +393,8 @@ function patchValue(key, patch) {
|
|
|
358
393
|
return raw == null ? null : boundPrDiagnostic(String(raw));
|
|
359
394
|
if (key === 'isContinuation')
|
|
360
395
|
return raw ? 1 : 0;
|
|
396
|
+
if (key === 'settleEvidence')
|
|
397
|
+
return raw == null ? null : JSON.stringify(raw);
|
|
361
398
|
return raw;
|
|
362
399
|
}
|
|
363
400
|
/** Bounded, single-line diagnostics are safe to persist/broadcast as secondary
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prDeliveryRevisionAllowed = prDeliveryRevisionAllowed;
|
|
3
4
|
exports.createRailsRouter = createRailsRouter;
|
|
4
5
|
const express_1 = require("express");
|
|
5
6
|
const rails_store_1 = require("./rails-store");
|
|
@@ -11,6 +12,8 @@ const file_story_1 = require("./file-story");
|
|
|
11
12
|
const loops_store_1 = require("./loops-store");
|
|
12
13
|
const loop_runs_store_1 = require("./loop-runs-store");
|
|
13
14
|
const providers_1 = require("./providers");
|
|
15
|
+
const runtime_1 = require("./providers/runtime");
|
|
16
|
+
const agent_defaults_1 = require("./agent-defaults");
|
|
14
17
|
const spec_models_1 = require("./spec-models");
|
|
15
18
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
16
19
|
const loop_factory_1 = require("./loop-factory");
|
|
@@ -20,6 +23,8 @@ const loop_graph_1 = require("./loop-graph");
|
|
|
20
23
|
const rail_isolation_1 = require("./rail-isolation");
|
|
21
24
|
const rail_pr_store_1 = require("./rail-pr-store");
|
|
22
25
|
const loop_effect_1 = require("./loop-effect");
|
|
26
|
+
const review_packet_1 = require("./review-packet");
|
|
27
|
+
const accept_ladder_1 = require("./accept-ladder");
|
|
23
28
|
const rail_pr_decision_1 = require("./rail-pr-decision");
|
|
24
29
|
const active_pr_continuation_1 = require("./active-pr-continuation");
|
|
25
30
|
const rail_isolated_launch_1 = require("./rail-isolated-launch");
|
|
@@ -41,6 +46,22 @@ function prDeliveryContinuesTickets(delivery, ticketIds) {
|
|
|
41
46
|
const requested = new Set(ticketIds);
|
|
42
47
|
return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
|
|
43
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a launch may revise an undecided delivery. Deliberately strict:
|
|
51
|
+
* the named id must BE the rail's active generation, that generation must still
|
|
52
|
+
* be awaiting a decision (a terminal one is history, not revisable), and the
|
|
53
|
+
* rail must still carry exactly the delivery's ticket set — a subset would
|
|
54
|
+
* revise work the user never asked about while leaving the rest undecided.
|
|
55
|
+
*/
|
|
56
|
+
function prDeliveryRevisionAllowed(delivery, revisionOfDeliveryId, ticketIds) {
|
|
57
|
+
if (delivery.id !== revisionOfDeliveryId)
|
|
58
|
+
return false;
|
|
59
|
+
if ((0, rail_pr_store_1.isTerminalPrDecision)(delivery.decision))
|
|
60
|
+
return false;
|
|
61
|
+
const covered = new Set(delivery.ticketIds);
|
|
62
|
+
const requested = new Set(ticketIds);
|
|
63
|
+
return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
|
|
64
|
+
}
|
|
44
65
|
function prDeliveryCheckoutBlock(delivery) {
|
|
45
66
|
if (delivery.deliveryOutcome === 'blocked') {
|
|
46
67
|
return 'delivery is blocked and has no safely checkoutable result';
|
|
@@ -381,7 +402,20 @@ function createRailsRouter() {
|
|
|
381
402
|
return;
|
|
382
403
|
}
|
|
383
404
|
let { mode = 'implement' } = req.body ?? {};
|
|
384
|
-
const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber } = req.body ?? {};
|
|
405
|
+
const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber, revisionOfDeliveryId, revisionNote } = req.body ?? {};
|
|
406
|
+
// Revision launch (nontech-review-experience Wave 3): the user asked for a
|
|
407
|
+
// change to a delivery that is already awaiting their decision. Shape is
|
|
408
|
+
// validated here; the narrow guard exemption is enforced below.
|
|
409
|
+
if (revisionOfDeliveryId !== undefined && revisionOfDeliveryId !== null) {
|
|
410
|
+
if (typeof revisionOfDeliveryId !== 'string' || !/^[A-Za-z0-9-]{1,64}$/.test(revisionOfDeliveryId)) {
|
|
411
|
+
res.status(400).json({ error: 'invalid_revision_target', detail: 'revisionOfDeliveryId must be a delivery id' });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (typeof revisionNote !== 'string' || revisionNote.trim().length === 0) {
|
|
415
|
+
res.status(400).json({ error: 'revision_note_required', detail: 'revisionNote must describe the change to make' });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
385
419
|
// Explicit target PR (deliver-rail-into-existing-pr): the user names an
|
|
386
420
|
// existing open PR as the delivery destination. Shape-validated here;
|
|
387
421
|
// resolved and verified inside the isolated launch, fail-closed.
|
|
@@ -414,6 +448,18 @@ function createRailsRouter() {
|
|
|
414
448
|
res.status(404).json({ error: 'Factory loop not found' });
|
|
415
449
|
return;
|
|
416
450
|
}
|
|
451
|
+
// The revision loop is meaningless without a revision to apply: its prompt
|
|
452
|
+
// consumes {{const:REVISION_REQUEST}}, and an unresolved constant renders
|
|
453
|
+
// as an EMPTY string (loop-constants), so naming it directly would spawn a
|
|
454
|
+
// run whose central instruction is silently blank. It is reachable by id
|
|
455
|
+
// (fork/preview resolve it), so the launch door has to refuse it.
|
|
456
|
+
if (loopId === loop_factory_1.FACTORY_REVISION_LOOP_ID && !revisionOfDeliveryId) {
|
|
457
|
+
res.status(400).json({
|
|
458
|
+
error: 'revision_loop_requires_revision',
|
|
459
|
+
detail: 'factory:revision only runs as a revision — pass revisionOfDeliveryId and revisionNote instead of naming the loop',
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
417
463
|
mode = fmode;
|
|
418
464
|
}
|
|
419
465
|
if (!VALID_MODES.has(mode)) {
|
|
@@ -429,6 +475,17 @@ function createRailsRouter() {
|
|
|
429
475
|
if ((0, feature_flags_1.isLoopsEnabled)() && (typeof loopId !== 'string' || !loopId) && mode !== 'loop') {
|
|
430
476
|
loopId = (0, loop_factory_1.factoryLoopForMode)(mode)?.id;
|
|
431
477
|
}
|
|
478
|
+
// A revision must run the Architect-LESS revision loop, whatever the rail's
|
|
479
|
+
// stored mode is. Without this every one-sentence tweak would re-run the
|
|
480
|
+
// full implement pipeline — the exact cost the feature exists to avoid.
|
|
481
|
+
// An explicit loopId is respected (a caller naming a loop means it).
|
|
482
|
+
if (revisionOfDeliveryId && (0, feature_flags_1.isLoopsEnabled)() && (typeof loopId !== 'string' || !loopId || (0, loop_factory_1.isFactoryLoopId)(loopId))) {
|
|
483
|
+
const revisionLoop = (0, loop_factory_1.getFactoryLoop)(loop_factory_1.FACTORY_REVISION_LOOP_ID);
|
|
484
|
+
if (revisionLoop) {
|
|
485
|
+
loopId = revisionLoop.id;
|
|
486
|
+
mode = revisionLoop.mode;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
432
489
|
if (mode === 'loop' && !(0, feature_flags_1.isLoopsEnabled)()) {
|
|
433
490
|
res.status(403).json({ error: 'Loops are disabled on this server' });
|
|
434
491
|
return;
|
|
@@ -599,7 +656,14 @@ function createRailsRouter() {
|
|
|
599
656
|
res.status(400).json({ error: `model is not valid for provider "${loopProvider}"`, allowed: (0, spec_models_1.getModelsForProvider)(loopProvider) });
|
|
600
657
|
return;
|
|
601
658
|
}
|
|
602
|
-
|
|
659
|
+
// Global Specrails Agents defaults (Settings ▸ Specrails Agents): fill
|
|
660
|
+
// model/effort ONLY when the launch body carries no explicit choice —
|
|
661
|
+
// dashboard factory launches send neither. Read at launch time, so a
|
|
662
|
+
// settings change applies to the next launch without restart.
|
|
663
|
+
const globalAgentDefaults = (0, agent_defaults_1.resolveAgentDefaults)(c.desktopDb, loopProvider);
|
|
664
|
+
const loopModel = typeof model === 'string' && model
|
|
665
|
+
? model
|
|
666
|
+
: (globalAgentDefaults?.pipelineModel ?? loopAdapter.defaultModel());
|
|
603
667
|
let effort;
|
|
604
668
|
if (reasoning_effort !== undefined && reasoning_effort !== null) {
|
|
605
669
|
const allowed = (0, providers_1.reasoningEffortsForModel)(loopAdapter, loopModel);
|
|
@@ -613,6 +677,10 @@ function createRailsRouter() {
|
|
|
613
677
|
}
|
|
614
678
|
effort = reasoning_effort;
|
|
615
679
|
}
|
|
680
|
+
else if (globalAgentDefaults?.pipelineEffort
|
|
681
|
+
&& (0, runtime_1.isReasoningEffortValidForModel)(loopAdapter, loopModel, globalAgentDefaults.pipelineEffort)) {
|
|
682
|
+
effort = globalAgentDefaults.pipelineEffort;
|
|
683
|
+
}
|
|
616
684
|
// The loop's command(s) declare ticket scope + required capabilities.
|
|
617
685
|
const promptsText = loopGraph.nodes
|
|
618
686
|
.filter((n) => n.type === 'ai-step')
|
|
@@ -634,6 +702,7 @@ function createRailsRouter() {
|
|
|
634
702
|
// cards) is diagnosable from the UI, not just the server log.
|
|
635
703
|
let isolationUnavailableDetail;
|
|
636
704
|
let continuablePrDelivery = null;
|
|
705
|
+
let revisionRequest = null;
|
|
637
706
|
// Read-only vs mutating is DERIVED from the loop's nodes (see loop-effect),
|
|
638
707
|
// not a user flag — a content-read-only loop (no ai-step/shell) never writes,
|
|
639
708
|
// so it is not isolated; anything that can write is.
|
|
@@ -644,10 +713,32 @@ function createRailsRouter() {
|
|
|
644
713
|
if ((0, rail_isolation_1.isRailPrDeliveryEnabled)()) {
|
|
645
714
|
const pending = (0, rail_pr_store_1.getActivePrDeliveryByRail)(c.db, railIndex);
|
|
646
715
|
const pendingSnapshot = pending ? (0, rail_pr_store_1.toPrDeliverySnapshot)(pending) : null;
|
|
647
|
-
|
|
716
|
+
// A revision is the ONE launch allowed to proceed against an
|
|
717
|
+
// undecided delivery, and only on the exact terms below: the caller
|
|
718
|
+
// names that delivery AND covers its full ticket set. Anything else
|
|
719
|
+
// would silently append work to branches the user has not judged.
|
|
720
|
+
const revisionsEnabled = (0, feature_flags_1.areDeliveryRevisionsEnabled)();
|
|
721
|
+
const revisionOfPending = Boolean(revisionsEnabled && pendingSnapshot && revisionOfDeliveryId
|
|
722
|
+
&& prDeliveryRevisionAllowed(pendingSnapshot, revisionOfDeliveryId, rail.ticketIds));
|
|
723
|
+
if (revisionsEnabled && pendingSnapshot && revisionOfDeliveryId && !revisionOfPending) {
|
|
724
|
+
res.status(409).json({
|
|
725
|
+
error: 'invalid_revision_target',
|
|
726
|
+
prDeliveryId: pendingSnapshot.id,
|
|
727
|
+
detail: 'a revision must target the rail\'s active delivery and cover all of its specs',
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (pendingSnapshot && !revisionOfPending && !prDeliveryContinuesTickets(pendingSnapshot, rail.ticketIds)) {
|
|
648
732
|
res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
|
|
649
733
|
return;
|
|
650
734
|
}
|
|
735
|
+
if (revisionOfPending && pendingSnapshot) {
|
|
736
|
+
revisionRequest = {
|
|
737
|
+
ofDeliveryId: pendingSnapshot.id,
|
|
738
|
+
decision: pendingSnapshot.decision,
|
|
739
|
+
note: revisionNote,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
651
742
|
// Explicit target vs an undecided continuable delivery: the slot's
|
|
652
743
|
// active generation owns its PR. A DIFFERENT explicit target would
|
|
653
744
|
// append to the undecided branches → 409; the SAME PR is redundant
|
|
@@ -720,6 +811,9 @@ function createRailsRouter() {
|
|
|
720
811
|
...(typeof targetPrNumber === 'number' && !continuablePrDelivery
|
|
721
812
|
? { explicitPrTarget: { prNumber: targetPrNumber } }
|
|
722
813
|
: {}),
|
|
814
|
+
// Revision of an undecided delivery: the guard above proved the
|
|
815
|
+
// exemption, so pass the contract through for supersession.
|
|
816
|
+
...(revisionRequest ? { revision: revisionRequest } : {}),
|
|
723
817
|
});
|
|
724
818
|
res.status(202).json({ loopRunIds: ids, railIndex, mode, isolated: true });
|
|
725
819
|
return;
|
|
@@ -999,6 +1093,33 @@ function createRailsRouter() {
|
|
|
999
1093
|
}
|
|
1000
1094
|
res.json({ ok: true, jobIds: targetJobIds, loopRunIds: targetLoopRunIds, canceled: canceledCount });
|
|
1001
1095
|
});
|
|
1096
|
+
// GET /rails/pr-deliveries/:id/packet — the plain-language review packet for a
|
|
1097
|
+
// settled delivery: composed server-side from durable rows only (no model
|
|
1098
|
+
// calls, no live-store reads) plus the pre-resolved Accept ladder, so the
|
|
1099
|
+
// client never has to ask the user to choose between git actions. Read-only.
|
|
1100
|
+
router.get('/pr-deliveries/:prDeliveryId/packet', async (req, res) => {
|
|
1101
|
+
if (!(0, feature_flags_1.isReviewPacketEnabled)()) {
|
|
1102
|
+
res.status(404).json({ error: 'Not Found' });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const c = ctx(req);
|
|
1106
|
+
const row = (0, rail_pr_store_1.getPrDelivery)(c.db, req.params.prDeliveryId);
|
|
1107
|
+
if (!row) {
|
|
1108
|
+
res.status(404).json({ error: 'Delivery not found' });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
const packet = (0, review_packet_1.composeReviewPacket)({ db: c.db, row });
|
|
1113
|
+
// Capability probes are read-only and offline; a failure degrades to the
|
|
1114
|
+
// confirm-gated local path rather than blocking the packet.
|
|
1115
|
+
const acceptCapability = await (0, accept_ladder_1.resolveAcceptCapability)({ repoDir: c.project.path, exec: pr_publisher_1.defaultExec });
|
|
1116
|
+
res.json({ packet, acceptCapability, snapshot: (0, rail_pr_store_1.toPrDeliverySnapshot)(row) });
|
|
1117
|
+
}
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
console.error('[rails-router] packet composition failed:', err);
|
|
1120
|
+
res.status(500).json({ error: 'packet composition failed', detail: err.message });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1002
1123
|
// POST /rails/pr-decision — the ONE decision action (safe-pr-review-flow) both
|
|
1003
1124
|
// surfaces (dashboard rail row + agent-chat card) call on a rail_pr_deliveries
|
|
1004
1125
|
// row: create-pr | publish | discard/dismiss | poll/reopen | merge-local |
|