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
|
@@ -5,6 +5,8 @@ exports.isBrowserCaptureEnabled = isBrowserCaptureEnabled;
|
|
|
5
5
|
exports.isJiraEnabled = isJiraEnabled;
|
|
6
6
|
exports.isInteractiveJobsEnabled = isInteractiveJobsEnabled;
|
|
7
7
|
exports.isLoopsEnabled = isLoopsEnabled;
|
|
8
|
+
exports.isReviewPacketEnabled = isReviewPacketEnabled;
|
|
9
|
+
exports.areDeliveryRevisionsEnabled = areDeliveryRevisionsEnabled;
|
|
8
10
|
function isCodeExplorerEnabled() {
|
|
9
11
|
return process.env.SPECRAILS_CODE_EXPLORER !== 'false';
|
|
10
12
|
}
|
|
@@ -54,3 +56,24 @@ function isInteractiveJobsEnabled() {
|
|
|
54
56
|
function isLoopsEnabled() {
|
|
55
57
|
return process.env.SPECRAILS_LOOPS_SECTION !== 'false';
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Review packet (nontech-review-experience): the plain-language what-changed +
|
|
61
|
+
* proof surface a non-technical user decides on, replacing raw git vocabulary
|
|
62
|
+
* at the review moment. Server-side default ON; set
|
|
63
|
+
* SPECRAILS_REVIEW_PACKET="false" to 404 the packet route (emergency
|
|
64
|
+
* rollback — the existing decision strip keeps working untouched). The client
|
|
65
|
+
* gates separately on VITE_FEATURE_REVIEW_PACKET.
|
|
66
|
+
*/
|
|
67
|
+
function isReviewPacketEnabled() {
|
|
68
|
+
return process.env.SPECRAILS_REVIEW_PACKET !== 'false';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Delivery revisions (nontech-review-experience Wave 3): the ability to launch a
|
|
72
|
+
* change against a delivery that is still awaiting the user's decision. Default
|
|
73
|
+
* ON; set SPECRAILS_DELIVERY_REVISIONS="false" to restore the byte-identical
|
|
74
|
+
* legacy guard — every launch against an undecided delivery 409s again and the
|
|
75
|
+
* packet's "Ask for changes" cannot be satisfied.
|
|
76
|
+
*/
|
|
77
|
+
function areDeliveryRevisionsEnabled() {
|
|
78
|
+
return process.env.SPECRAILS_DELIVERY_REVISIONS !== 'false';
|
|
79
|
+
}
|
|
@@ -78,6 +78,39 @@ exports.LOOP_COMMANDS = [
|
|
|
78
78
|
requiredCapability: 'freestyle',
|
|
79
79
|
ticketScope: 'per-ticket',
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'revise',
|
|
83
|
+
label: 'revise',
|
|
84
|
+
description: 'Revision step (nontech-review-experience): apply the ONE change the user asked for on top of work already delivered, then have the reviewer re-grade it. No re-planning, no re-implementing.',
|
|
85
|
+
ticketScope: 'all',
|
|
86
|
+
// Deliberately NOT the implement pipeline: the plan and the code already
|
|
87
|
+
// exist, so an Architect pass would re-derive both and pay the full cost of
|
|
88
|
+
// a first run for a one-sentence tweak. The reviewer pass is orchestrated
|
|
89
|
+
// from HERE rather than added to core, because the sr-* agents are already
|
|
90
|
+
// present in the run's worktree via the overlay — that keeps the loop a
|
|
91
|
+
// pure desktop concern with zero specrails-core coupling, and it is what
|
|
92
|
+
// keeps the review packet's reviewer tier populated for revisions.
|
|
93
|
+
template: [
|
|
94
|
+
'A previous run already delivered work for this spec, and the user has now asked for ONE specific change to it. Apply exactly that change.',
|
|
95
|
+
'',
|
|
96
|
+
'What the user asked to change:',
|
|
97
|
+
'{{const:REVISION_REQUEST}}',
|
|
98
|
+
'',
|
|
99
|
+
'If the section above is EMPTY, no change was actually requested: do not',
|
|
100
|
+
'guess, do not re-implement anything, and do not touch the branch. Report',
|
|
101
|
+
'that no revision instruction was provided and stop.',
|
|
102
|
+
'',
|
|
103
|
+
'Rules:',
|
|
104
|
+
'- Start from the work that is ALREADY on this branch. Read it first (git diff against the base branch) so you extend it instead of redoing it.',
|
|
105
|
+
'- Make the SMALLEST change that satisfies the request. Do not refactor, re-plan, or re-implement anything the user did not ask about.',
|
|
106
|
+
'- If the request is ambiguous, choose the most conservative reading and say which one you chose.',
|
|
107
|
+
'- If the request cannot be done without breaking something the spec requires, stop and report that instead of forcing it.',
|
|
108
|
+
'',
|
|
109
|
+
'Then re-grade the result: run the `sr-reviewer` agent over the resulting diff so its review report and `confidence-score.json` reflect THIS revision, not the previous run.',
|
|
110
|
+
'',
|
|
111
|
+
'{{const:GUARDRAILS}}',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
},
|
|
81
114
|
{
|
|
82
115
|
name: 'fix',
|
|
83
116
|
label: 'fix',
|
|
@@ -121,6 +121,16 @@ function aiStepExtraArgs(adapter, cwd, repoDir) {
|
|
|
121
121
|
}
|
|
122
122
|
function createLoopExecutors(opts = {}) {
|
|
123
123
|
const resolveEnv = () => typeof opts.env === 'function' ? opts.env() : opts.env ?? process.env;
|
|
124
|
+
// Deciders deliberately excluded: they run no pipeline agents.
|
|
125
|
+
const withProfileEnv = (env, provider) => {
|
|
126
|
+
try {
|
|
127
|
+
const profilePath = opts.profilePathFor?.(provider) ?? null;
|
|
128
|
+
return profilePath ? { ...env, SPECRAILS_PROFILE_PATH: profilePath } : env;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return env;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
124
134
|
return {
|
|
125
135
|
// Cheap working-tree fingerprint for the engine's non-convergence guard:
|
|
126
136
|
// HEAD sha + the porcelain dirty set, hashed. Two identical fingerprints
|
|
@@ -150,7 +160,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
150
160
|
// for the pipeline's I/O exactly like QueueManager: SPECRAILS_REPO_DIR +
|
|
151
161
|
// claude `--add-dir <repoDir>` (see the shared helpers above). Best-effort
|
|
152
162
|
// agent self-heal on Windows.
|
|
153
|
-
const stepEnv = aiStepEnv(resolveEnv(), repoDir);
|
|
163
|
+
const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
|
|
154
164
|
const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
|
|
155
165
|
const effectivePrompt = (0, runtime_1.formatProviderCommand)(adapter, prompt, cwd, sessionId ?? undefined);
|
|
156
166
|
const buildOpts = {
|
|
@@ -294,7 +304,7 @@ function createLoopExecutors(opts = {}) {
|
|
|
294
304
|
const adapter = (0, providers_1.getAdapter)(provider);
|
|
295
305
|
if (!adapter.capabilities.persistentStdin)
|
|
296
306
|
return null;
|
|
297
|
-
const stepEnv = aiStepEnv(resolveEnv(), repoDir);
|
|
307
|
+
const stepEnv = withProfileEnv(aiStepEnv(resolveEnv(), repoDir), provider);
|
|
298
308
|
const extraArgs = aiStepExtraArgs(adapter, cwd, repoDir);
|
|
299
309
|
if (repoDir) {
|
|
300
310
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FACTORY_LOOPS = void 0;
|
|
3
|
+
exports.FACTORY_LOOPS = exports.FACTORY_REVISION_LOOP_ID = void 0;
|
|
4
4
|
exports.getFactoryLoop = getFactoryLoop;
|
|
5
5
|
exports.isFactoryLoopId = isFactoryLoopId;
|
|
6
6
|
exports.factoryLoopMode = factoryLoopMode;
|
|
@@ -23,6 +23,8 @@ const SDD_QUICK_OPENSPEC_FACTORY = {
|
|
|
23
23
|
mode: 'loop',
|
|
24
24
|
graph: (0, loop_templates_1.opsxLifecycleGraph)(),
|
|
25
25
|
};
|
|
26
|
+
/** The Architect-less revision loop every "ask for a change" launch runs. */
|
|
27
|
+
exports.FACTORY_REVISION_LOOP_ID = 'factory:revision';
|
|
26
28
|
exports.FACTORY_LOOPS = [
|
|
27
29
|
{
|
|
28
30
|
id: 'factory:implement',
|
|
@@ -49,6 +51,19 @@ exports.FACTORY_LOOPS = [
|
|
|
49
51
|
requiredCapability: 'freestyle',
|
|
50
52
|
graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:freestyle}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
|
|
51
53
|
},
|
|
54
|
+
{
|
|
55
|
+
id: exports.FACTORY_REVISION_LOOP_ID,
|
|
56
|
+
name: 'Revision',
|
|
57
|
+
// Listed so the platform's behaviour is discoverable, but `launchable:false`:
|
|
58
|
+
// the app runs this itself when a user asks for a change to a delivery. Its
|
|
59
|
+
// prompt consumes `{{const:REVISION_REQUEST}}`, and an unresolved constant
|
|
60
|
+
// renders as an EMPTY string, so a hand-launched run would have a blank
|
|
61
|
+
// central instruction. The launch route refuses it; the prompt guards too.
|
|
62
|
+
launchable: false,
|
|
63
|
+
description: "Apply the ONE change the user asked for on top of work already delivered, re-grade it with the reviewer, then verify + refine until green. No re-planning. Runs automatically when you ask for changes on a review — it is not started by hand.",
|
|
64
|
+
mode: 'loop',
|
|
65
|
+
graph: (0, loop_templates_1.fixLoopGraph)(['{{cmd:revise}}'], GREEN_GOAL, FACTORY_MAX_ITERATIONS, FACTORY_LOOP_TIMEOUT_MIN, FACTORY_AI_STEP_TIMEOUT_MIN),
|
|
66
|
+
},
|
|
52
67
|
SDD_QUICK_OPENSPEC_FACTORY,
|
|
53
68
|
];
|
|
54
69
|
const FACTORY_ALIASES = new Map([
|
|
@@ -65,6 +65,8 @@ function registerLoopsRoutes(router, deps) {
|
|
|
65
65
|
description: f.description,
|
|
66
66
|
mode: f.mode,
|
|
67
67
|
requiredCapability: f.requiredCapability ?? null,
|
|
68
|
+
// Absent means launchable (every pre-existing factory loop is).
|
|
69
|
+
launchable: f.launchable !== false,
|
|
68
70
|
graph: f.graph,
|
|
69
71
|
})),
|
|
70
72
|
});
|
|
@@ -122,6 +122,14 @@ function railsTools() {
|
|
|
122
122
|
.min(1)
|
|
123
123
|
.optional()
|
|
124
124
|
.describe('Deliver INTO an existing open PR instead of creating a new one (launch). When the user names an existing PR ("extend PR #151"), pass its number here — the rail then works on that PR\'s head branch and settle pushes to it; NEVER launch without it in that case (a plain launch would create a duplicate PR). The PR must be open and same-repo (fork PRs are rejected with target_pr_fork).'),
|
|
125
|
+
revisionOfDeliveryId: zod_1.z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Revise a delivery that is ALREADY awaiting the user\'s decision (launch). This is the only way to launch against an undecided delivery: pass its prDeliveryId (from rails.prDeliveries) together with revisionNote. Use it whenever the user asks for a change to work you already delivered — never publish/discard/merge first. The rail must still carry exactly that delivery\'s specs.'),
|
|
129
|
+
revisionNote: zod_1.z
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('What to change, in the user\'s own words (required with revisionOfDeliveryId). It is injected into the revision run and shown on the updated review packet as "what you asked to change".'),
|
|
125
133
|
},
|
|
126
134
|
async handler(ctx, args) {
|
|
127
135
|
const base = `${(0, types_1.projectPath)(ctx, args.projectId)}/rails`;
|
|
@@ -210,6 +218,14 @@ function railsTools() {
|
|
|
210
218
|
body.reasoning_effort = args.reasoning_effort;
|
|
211
219
|
if (args.targetPrNumber !== undefined)
|
|
212
220
|
body.targetPrNumber = args.targetPrNumber;
|
|
221
|
+
// Revision of a delivery already awaiting the user's decision: the
|
|
222
|
+
// ONE launch allowed against an undecided delivery. The route
|
|
223
|
+
// re-validates the exemption (must be the rail's active delivery and
|
|
224
|
+
// cover its full spec set), so a wrong id fails closed there.
|
|
225
|
+
if (args.revisionOfDeliveryId !== undefined)
|
|
226
|
+
body.revisionOfDeliveryId = args.revisionOfDeliveryId;
|
|
227
|
+
if (args.revisionNote !== undefined)
|
|
228
|
+
body.revisionNote = args.revisionNote;
|
|
213
229
|
// Origin link (safe-pr-review-flow): a launch driven by the in-app
|
|
214
230
|
// agent carries its conversation id (from the loopback header, via
|
|
215
231
|
// ctx) so the PR decision card is posted back into that conversation.
|
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const os_1 = __importDefault(require("os"));
|
|
12
12
|
const db_1 = require("./db");
|
|
13
13
|
const queue_manager_1 = require("./queue-manager");
|
|
14
|
+
const agent_defaults_1 = require("./agent-defaults");
|
|
14
15
|
const chat_manager_1 = require("./chat-manager");
|
|
15
16
|
const setup_manager_1 = require("./setup-manager");
|
|
16
17
|
const proposal_manager_1 = require("./proposal-manager");
|
|
@@ -35,6 +36,7 @@ const project_env_1 = require("./project-env");
|
|
|
35
36
|
const workspace_manager_1 = require("./workspace-manager");
|
|
36
37
|
const ticket_store_1 = require("./ticket-store");
|
|
37
38
|
const jira_sync_manager_1 = require("./jira/jira-sync-manager");
|
|
39
|
+
const stuck_run_detector_1 = require("./stuck-run-detector");
|
|
38
40
|
const loop_run_manager_1 = require("./loop-run-manager");
|
|
39
41
|
const loop_executors_1 = require("./loop-executors");
|
|
40
42
|
const rail_isolated_launch_1 = require("./rail-isolated-launch");
|
|
@@ -291,6 +293,10 @@ class ProjectRegistry {
|
|
|
291
293
|
ctx.jiraSyncManager.stop();
|
|
292
294
|
}
|
|
293
295
|
catch { /* ignore */ }
|
|
296
|
+
try {
|
|
297
|
+
ctx.stuckRunDetector.stop();
|
|
298
|
+
}
|
|
299
|
+
catch { /* ignore */ }
|
|
294
300
|
// Kill any terminal sessions belonging to this project
|
|
295
301
|
try {
|
|
296
302
|
(0, terminal_manager_1.getTerminalManager)().killAllForProject(id);
|
|
@@ -437,6 +443,10 @@ class ProjectRegistry {
|
|
|
437
443
|
ctx.jiraSyncManager.stop();
|
|
438
444
|
}
|
|
439
445
|
catch { /* ignore */ }
|
|
446
|
+
try {
|
|
447
|
+
ctx.stuckRunDetector.stop();
|
|
448
|
+
}
|
|
449
|
+
catch { /* ignore */ }
|
|
440
450
|
// Release chokidar watchers + abort in-flight generations so a restart
|
|
441
451
|
// does not leak handles/children — mirror removeProject()'s per-project teardown.
|
|
442
452
|
try {
|
|
@@ -509,6 +519,10 @@ class ProjectRegistry {
|
|
|
509
519
|
projectId: project.id,
|
|
510
520
|
projectSlug: project.slug,
|
|
511
521
|
desktopPort: this._desktopPort,
|
|
522
|
+
// Global Specrails Agents defaults — resolved at spawn time from
|
|
523
|
+
// desktop.sqlite so a Settings change applies to the next job without
|
|
524
|
+
// restarting (mirrors the pipelineTelemetryEnabled read-at-spawn rule).
|
|
525
|
+
agentDefaults: (provider) => (0, agent_defaults_1.resolveAgentDefaults)(this._desktopDb, provider),
|
|
512
526
|
onJobAdmission: (projectDb, job) => {
|
|
513
527
|
const ticketIds = (0, ticket_store_1.extractTicketIdsFromCommand)(job.command);
|
|
514
528
|
(0, rails_store_1.claimTicketOutcomeOwners)(projectDb, ticketIds, job.id);
|
|
@@ -899,6 +913,22 @@ class ProjectRegistry {
|
|
|
899
913
|
// See resolveLoopBaseEnv.
|
|
900
914
|
const loopRunManager = new loop_run_manager_1.LoopRunManager(db, boundBroadcast, (0, loop_executors_1.createLoopExecutors)({
|
|
901
915
|
env: () => (0, workspace_resolution_1.resolveLoopBaseEnv)({ slug: project.slug, path: project.path }, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
|
|
916
|
+
// Global Specrails Agents defaults, loop path: per-agent model overrides
|
|
917
|
+
// ride SPECRAILS_PROFILE_PATH on each ai-step spawn. Resolved per step
|
|
918
|
+
// (no restart); inert (null) unless the global layer carries per-agent
|
|
919
|
+
// models for the provider — byte-identical legacy behaviour otherwise.
|
|
920
|
+
profilePathFor: (0, agent_defaults_1.createLoopProfilePathResolver)({
|
|
921
|
+
desktopDb: this._desktopDb,
|
|
922
|
+
profileRoot: () => (0, workspace_resolution_1.resolveProjectExecution)({ slug: project.slug, path: project.path }).cwd,
|
|
923
|
+
// Lazy require: several suites mock './queue-manager' with only the
|
|
924
|
+
// class export — a top-level named import would throw on module eval.
|
|
925
|
+
// The closure only runs at loop ai-step spawn time, never in tests
|
|
926
|
+
// that stub the queue.
|
|
927
|
+
supportsProfiles: (root) => {
|
|
928
|
+
const { projectSupportsProfiles } = require('./queue-manager');
|
|
929
|
+
return projectSupportsProfiles(root);
|
|
930
|
+
},
|
|
931
|
+
}),
|
|
902
932
|
}));
|
|
903
933
|
const getTicketSpec = (ticketId) => {
|
|
904
934
|
try {
|
|
@@ -1081,7 +1111,9 @@ class ProjectRegistry {
|
|
|
1081
1111
|
console.error(`[loops] terminal effects failed for ${runId}:`, err);
|
|
1082
1112
|
}
|
|
1083
1113
|
};
|
|
1084
|
-
const
|
|
1114
|
+
const stuckRunDetector = new stuck_run_detector_1.StuckRunDetector(project.id, { db }, boundBroadcast);
|
|
1115
|
+
stuckRunDetector.start();
|
|
1116
|
+
const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, stuckRunDetector, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb };
|
|
1085
1117
|
this._contexts.set(project.id, ctx);
|
|
1086
1118
|
const loopRecoveryOk = this._recoverOrphanLoopRuns(project, db, railLoopRuns, onLoopRunFinished, orphanLoopRuns);
|
|
1087
1119
|
if (loopRecoveryOk) {
|
|
@@ -4,8 +4,11 @@ exports.registerLoopRunRoutes = registerLoopRunRoutes;
|
|
|
4
4
|
const feature_flags_1 = require("./feature-flags");
|
|
5
5
|
const loops_store_1 = require("./loops-store");
|
|
6
6
|
const loop_runs_store_1 = require("./loop-runs-store");
|
|
7
|
+
const run_duration_stats_1 = require("./run-duration-stats");
|
|
7
8
|
const loop_constants_1 = require("./loop-constants");
|
|
8
9
|
const providers_1 = require("./providers");
|
|
10
|
+
const runtime_1 = require("./providers/runtime");
|
|
11
|
+
const agent_defaults_1 = require("./agent-defaults");
|
|
9
12
|
const provider_selection_1 = require("./provider-selection");
|
|
10
13
|
const spec_models_1 = require("./spec-models");
|
|
11
14
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
@@ -40,6 +43,22 @@ function registerLoopRunRoutes(deps) {
|
|
|
40
43
|
},
|
|
41
44
|
});
|
|
42
45
|
});
|
|
46
|
+
// Measured duration band for "runs like this have taken X–Y". Returns
|
|
47
|
+
// `range: null` below the sample floor — the caller MUST render nothing
|
|
48
|
+
// rather than a guess (honest-metrics contract; see run-duration-stats).
|
|
49
|
+
router.get('/:projectId/run-duration-range', (req, res) => {
|
|
50
|
+
const c = ctx(req);
|
|
51
|
+
const loopId = typeof req.query.loopId === 'string' ? req.query.loopId : null;
|
|
52
|
+
const command = typeof req.query.command === 'string' ? req.query.command : null;
|
|
53
|
+
if (!loopId && !command) {
|
|
54
|
+
res.status(400).json({ error: 'loopId or command is required' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const range = loopId
|
|
58
|
+
? (0, run_duration_stats_1.getLoopDurationRange)(c.db, c.project.id, loopId)
|
|
59
|
+
: (0, run_duration_stats_1.getJobCommandDurationRange)(c.db, (0, run_duration_stats_1.jobCommandShape)(command));
|
|
60
|
+
res.json({ range, minSamples: run_duration_stats_1.MIN_DURATION_SAMPLES });
|
|
61
|
+
});
|
|
43
62
|
router.post('/:projectId/loop-runs', (req, res) => {
|
|
44
63
|
if (!(0, feature_flags_1.isLoopsEnabled)()) {
|
|
45
64
|
res.status(404).json({ error: 'Not Found' });
|
|
@@ -89,7 +108,9 @@ function registerLoopRunRoutes(deps) {
|
|
|
89
108
|
return;
|
|
90
109
|
}
|
|
91
110
|
// Optional explicit model — validated against the chosen provider's catalog
|
|
92
|
-
// (mirrors Add Spec). Omitted ⇒ the
|
|
111
|
+
// (mirrors Add Spec). Omitted ⇒ the global Specrails Agents default when
|
|
112
|
+
// set (read at launch time — no restart), else the provider's default.
|
|
113
|
+
const globalAgentDefaults = (0, agent_defaults_1.resolveAgentDefaults)(c.desktopDb, provider);
|
|
93
114
|
let model;
|
|
94
115
|
if (requestedModel !== undefined && requestedModel !== null) {
|
|
95
116
|
if (!(0, spec_models_1.isValidModelForProvider)(requestedModel, provider)) {
|
|
@@ -99,7 +120,7 @@ function registerLoopRunRoutes(deps) {
|
|
|
99
120
|
model = requestedModel;
|
|
100
121
|
}
|
|
101
122
|
else {
|
|
102
|
-
model = adapter.defaultModel();
|
|
123
|
+
model = globalAgentDefaults?.pipelineModel ?? adapter.defaultModel();
|
|
103
124
|
}
|
|
104
125
|
let effort;
|
|
105
126
|
if (reasoning_effort !== undefined && reasoning_effort !== null) {
|
|
@@ -114,6 +135,10 @@ function registerLoopRunRoutes(deps) {
|
|
|
114
135
|
}
|
|
115
136
|
effort = reasoning_effort;
|
|
116
137
|
}
|
|
138
|
+
else if (globalAgentDefaults?.pipelineEffort
|
|
139
|
+
&& (0, runtime_1.isReasoningEffortValidForModel)(adapter, model, globalAgentDefaults.pipelineEffort)) {
|
|
140
|
+
effort = globalAgentDefaults.pipelineEffort;
|
|
141
|
+
}
|
|
117
142
|
const exec = (0, workspace_resolution_1.resolveProjectExecution)({ slug: c.project.slug, path: c.project.path });
|
|
118
143
|
const runId = (0, ids_1.newId)();
|
|
119
144
|
c.loopRunManager
|
|
@@ -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,
|