specrails-desktop 2.34.0 → 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-WeiofeL6.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-BkCXBQWk.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 +60 -3
- package/server/dist/queue-manager.js +47 -4
- package/server/dist/rail-isolated-launch.js +119 -5
- package/server/dist/rail-pr-store.js +40 -3
- package/server/dist/rail-worktree-release.js +17 -1
- 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/server/dist/worktree-node-modules.js +138 -16
- package/client/dist/assets/ActivityFeedPage-DIDxNsDj.js +0 -1
- package/client/dist/assets/AgentBrowserCapture-7pJ2G5VY.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BSSyTnTM.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DzW6gvDw.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-9wJff15f.js +0 -1
- package/client/dist/assets/AgentsPage-CIA67psb.js +0 -87
- package/client/dist/assets/AnalyticsPage-C0LwYELz.js +0 -1
- package/client/dist/assets/CodePage-RtKvHmAO.js +0 -2
- package/client/dist/assets/DesktopAnalyticsPage-BU8Y_Ts1.js +0 -1
- package/client/dist/assets/DocsDialog-DIpoaGlQ.js +0 -11
- package/client/dist/assets/DocsPage-ME47KZpK.js +0 -11
- package/client/dist/assets/IntegrationsPage-Bs00_tl4.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-BGjIe9R6.js +0 -19
- package/client/dist/assets/JobDetailModal-1UZkqH4I.js +0 -1
- package/client/dist/assets/JobDetailPage-CN65sAf1.js +0 -1
- package/client/dist/assets/JobsPage-f-4dqys7.js +0 -1
- package/client/dist/assets/LoopPreviewModal-DDTnTyH0.js +0 -1
- package/client/dist/assets/LoopsPage-BVNchIex.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +0 -1
- package/client/dist/assets/PluginsPage-CdFzGEfQ.js +0 -2
- package/client/dist/assets/TemplatePreviewModal-RDi0khFG.js +0 -1
- package/client/dist/assets/TicketDetailModal-MhJzrqbi.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-BmsFWRly.js +0 -1
- package/client/dist/assets/dist-js-J5W_N5ng.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-C_sdsSZt.js +0 -63
- package/client/dist/assets/index-DCN5QbDm.css +0 -2
- 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
|
@@ -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 |
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DRIFT_REVISION_COUNT = exports.DRIFT_COST_SHARE = exports.MAX_VERSION_CHAIN = exports.HUMAN_REVIEW_BAND = void 0;
|
|
4
|
+
exports.selectVariant = selectVariant;
|
|
5
|
+
exports.computeDriftNudges = computeDriftNudges;
|
|
6
|
+
exports.composeReviewPacket = composeReviewPacket;
|
|
7
|
+
exports.packetHasUnsourcedNumericClaim = packetHasUnsourcedNumericClaim;
|
|
8
|
+
const delivery_evidence_1 = require("./delivery-evidence");
|
|
9
|
+
const rail_pr_store_1 = require("./rail-pr-store");
|
|
10
|
+
const pr_body_1 = require("./pr-body");
|
|
11
|
+
const ai_invocations_1 = require("./ai-invocations");
|
|
12
|
+
const TEST_FILE_RE = /(\.test\.|\.spec\.|(^|\/)__tests__\/)/;
|
|
13
|
+
function churnForRuns(db, runIds) {
|
|
14
|
+
const ids = [...new Set(runIds.filter((id) => typeof id === 'string' && id.length > 0))];
|
|
15
|
+
if (ids.length === 0)
|
|
16
|
+
return [];
|
|
17
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
18
|
+
try {
|
|
19
|
+
return db.prepare(`
|
|
20
|
+
SELECT job_id, file_path, added_lines, removed_lines
|
|
21
|
+
FROM file_story_contributions
|
|
22
|
+
WHERE job_id IN (${placeholders})
|
|
23
|
+
`).all(...ids);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Variant selection reads ONLY durable outcomes. `no-changes` must win over
|
|
31
|
+
* `success` (a run that changed nothing has nothing to show), and a mixed batch
|
|
32
|
+
* is `partial` rather than a success that quietly hides a failed ticket.
|
|
33
|
+
*/
|
|
34
|
+
function selectVariant(row, units) {
|
|
35
|
+
if (row.decision === 'no_changes' || row.delivery_outcome === 'no_changes' || row.status_code === 'no_changes') {
|
|
36
|
+
return 'no-changes';
|
|
37
|
+
}
|
|
38
|
+
if (row.implementation_outcome === 'failed' || row.decision === 'implementation_failed')
|
|
39
|
+
return 'failed';
|
|
40
|
+
const succeeded = units.filter((unit) => unit.implementationOutcome === 'succeeded' || (unit.implementationOutcome == null && unit.succeeded)).length;
|
|
41
|
+
const failed = units.filter((unit) => unit.implementationOutcome === 'failed' || (unit.implementationOutcome == null && !unit.succeeded)).length;
|
|
42
|
+
if (row.implementation_outcome === 'partially_succeeded' || row.delivery_outcome === 'partial' || (succeeded > 0 && failed > 0)) {
|
|
43
|
+
return 'partial';
|
|
44
|
+
}
|
|
45
|
+
return 'success';
|
|
46
|
+
}
|
|
47
|
+
function headlineFor(variant, succeeded, total) {
|
|
48
|
+
switch (variant) {
|
|
49
|
+
case 'no-changes': return 'headline.noChanges';
|
|
50
|
+
case 'failed': return succeeded > 0 ? 'headline.failedPartial' : 'headline.failed';
|
|
51
|
+
case 'partial': return 'headline.partial';
|
|
52
|
+
default: return total > 1 ? 'headline.successMany' : 'headline.success';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Reviewer band documented by sr-reviewer's schema: 50-69 ⇒ recommend a human. */
|
|
56
|
+
exports.HUMAN_REVIEW_BAND = { min: 50, max: 69 };
|
|
57
|
+
function buildProof(units, evidence, churn, confidence) {
|
|
58
|
+
const proof = [];
|
|
59
|
+
// ── Tier 1: what Specrails measured itself ────────────────────────────────
|
|
60
|
+
const testFiles = [...new Set(churn.filter((row) => TEST_FILE_RE.test(row.file_path)).map((row) => row.file_path))];
|
|
61
|
+
const totalFiles = new Set(churn.map((row) => row.file_path)).size;
|
|
62
|
+
const added = churn.reduce((sum, row) => sum + (row.added_lines || 0), 0);
|
|
63
|
+
const removed = churn.reduce((sum, row) => sum + (row.removed_lines || 0), 0);
|
|
64
|
+
if (totalFiles > 0) {
|
|
65
|
+
proof.push({
|
|
66
|
+
tier: 'app-verified',
|
|
67
|
+
code: 'proof.filesChanged',
|
|
68
|
+
values: { files: totalFiles, added, removed },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
proof.push(testFiles.length > 0
|
|
72
|
+
? { tier: 'app-verified', code: 'proof.testFilesChanged', values: { count: testFiles.length } }
|
|
73
|
+
// The honest negative: silence here would read as "tests fine".
|
|
74
|
+
: { tier: 'app-verified', code: 'proof.noTestFilesChanged' });
|
|
75
|
+
const verifiedBranches = units.filter((unit) => unit.finalSha && unit.finalSha !== unit.initialSha).length;
|
|
76
|
+
if (verifiedBranches > 0) {
|
|
77
|
+
proof.push({ tier: 'app-verified', code: 'proof.commitsRecorded', values: { branches: verifiedBranches } });
|
|
78
|
+
}
|
|
79
|
+
// ── Tier 2: what the agent itself reported ────────────────────────────────
|
|
80
|
+
const sentinels = (evidence ?? []).filter((unit) => unit.sentinel !== 'absent');
|
|
81
|
+
if (sentinels.length === 0) {
|
|
82
|
+
proof.push({ tier: 'ai-reported', code: 'proof.noVerificationReported' });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const failed = sentinels.filter((unit) => unit.sentinel === 'fail');
|
|
86
|
+
if (failed.length > 0) {
|
|
87
|
+
// Failure leads: a packet must never bury a reported failure under a pass.
|
|
88
|
+
proof.push({
|
|
89
|
+
tier: 'ai-reported',
|
|
90
|
+
code: 'proof.verificationFailed',
|
|
91
|
+
values: { count: failed.length },
|
|
92
|
+
...(failed[0].sentinelDetail ? { rawExcerpt: failed[0].sentinelDetail } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const passed = sentinels.filter((unit) => unit.sentinel === 'pass');
|
|
96
|
+
if (passed.length > 0) {
|
|
97
|
+
proof.push({ tier: 'ai-reported', code: 'proof.verificationPassed', values: { count: passed.length } });
|
|
98
|
+
}
|
|
99
|
+
const tail = sentinels.map((unit) => unit.verifyTail).find((value) => Boolean(value));
|
|
100
|
+
if (tail)
|
|
101
|
+
proof.push({ tier: 'ai-reported', code: 'proof.verifyOutput', rawExcerpt: tail });
|
|
102
|
+
}
|
|
103
|
+
// ── Tier 3: the reviewer grading its own pipeline ─────────────────────────
|
|
104
|
+
if (confidence) {
|
|
105
|
+
if (confidence.overall !== null) {
|
|
106
|
+
proof.push({
|
|
107
|
+
tier: 'reviewer-score',
|
|
108
|
+
code: 'proof.reviewerScore',
|
|
109
|
+
values: { overall: confidence.overall },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const [aspect, score] of Object.entries(confidence.aspects)) {
|
|
113
|
+
proof.push({ tier: 'reviewer-score', code: 'proof.reviewerAspect', values: { aspect, score } });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
proof.push({ tier: 'reviewer-score', code: 'proof.reviewerScoreUnavailable' });
|
|
118
|
+
}
|
|
119
|
+
return proof;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* "What to watch out for" is capped and derived from REAL signals only —
|
|
123
|
+
* reviewer flags, the human-review band, and a code-changed-without-tests
|
|
124
|
+
* observation. Confabulated caution would train the user to ignore the section.
|
|
125
|
+
*/
|
|
126
|
+
function buildWatchOut(confidence, churn, variant) {
|
|
127
|
+
const out = [];
|
|
128
|
+
if (confidence?.overall !== null && confidence?.overall !== undefined) {
|
|
129
|
+
if (confidence.overall >= exports.HUMAN_REVIEW_BAND.min && confidence.overall <= exports.HUMAN_REVIEW_BAND.max) {
|
|
130
|
+
out.push({ tier: 'reviewer-score', code: 'watch.humanReviewRecommended', values: { overall: confidence.overall } });
|
|
131
|
+
}
|
|
132
|
+
else if (confidence.overall < exports.HUMAN_REVIEW_BAND.min) {
|
|
133
|
+
out.push({ tier: 'reviewer-score', code: 'watch.lowConfidence', values: { overall: confidence.overall } });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const flag of (confidence?.flags ?? []).slice(0, 3)) {
|
|
137
|
+
out.push({ tier: 'reviewer-score', code: 'watch.reviewerFlag', rawExcerpt: flag });
|
|
138
|
+
}
|
|
139
|
+
const codeFiles = churn.filter((row) => !TEST_FILE_RE.test(row.file_path));
|
|
140
|
+
const testFiles = churn.filter((row) => TEST_FILE_RE.test(row.file_path));
|
|
141
|
+
if (out.length < 3 && variant !== 'no-changes' && codeFiles.length > 0 && testFiles.length === 0) {
|
|
142
|
+
out.push({ tier: 'app-verified', code: 'watch.noTestsForChangedCode', values: { files: codeFiles.length } });
|
|
143
|
+
}
|
|
144
|
+
return out.slice(0, 3);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Walk the supersession chain back to the original build. Bounded so a corrupted
|
|
148
|
+
* lineage cannot spin, and tolerant of a missing ancestor (an old row may have
|
|
149
|
+
* been pruned) — the chain simply starts where the data does.
|
|
150
|
+
*/
|
|
151
|
+
exports.MAX_VERSION_CHAIN = 25;
|
|
152
|
+
function versionChain(db, row) {
|
|
153
|
+
const chain = [row];
|
|
154
|
+
const seen = new Set([row.id]);
|
|
155
|
+
let cursor = row;
|
|
156
|
+
while (chain.length < exports.MAX_VERSION_CHAIN && cursor.revision_of) {
|
|
157
|
+
const previous = (0, rail_pr_store_1.getPrDelivery)(db, cursor.revision_of);
|
|
158
|
+
if (!previous || seen.has(previous.id))
|
|
159
|
+
break;
|
|
160
|
+
seen.add(previous.id);
|
|
161
|
+
chain.push(previous);
|
|
162
|
+
cursor = previous;
|
|
163
|
+
}
|
|
164
|
+
return chain.reverse(); // oldest first: v1 … vN
|
|
165
|
+
}
|
|
166
|
+
/** Fraction of the original build's cost above which revisions are worth flagging. */
|
|
167
|
+
exports.DRIFT_COST_SHARE = 0.5;
|
|
168
|
+
/** Revision count backstop — advisory, never a block. */
|
|
169
|
+
exports.DRIFT_REVISION_COUNT = 3;
|
|
170
|
+
/**
|
|
171
|
+
* Reshape-the-spec nudges. Every trigger is a real measurement (cumulative cost
|
|
172
|
+
* vs the original build, churn landing outside the original file set, revision
|
|
173
|
+
* depth) so the banner can show its own numbers instead of asserting a vibe.
|
|
174
|
+
* A hard count alone was rejected: it treats a 40-cent typo fix and a nine-dollar
|
|
175
|
+
* architectural thrash identically.
|
|
176
|
+
*/
|
|
177
|
+
function computeDriftNudges(input) {
|
|
178
|
+
const nudges = [];
|
|
179
|
+
const [original, ...revisions] = input.versions;
|
|
180
|
+
if (revisions.length === 0)
|
|
181
|
+
return nudges;
|
|
182
|
+
const originalCost = original?.costUsd ?? null;
|
|
183
|
+
const revisionCost = revisions.reduce((sum, version) => sum + (version.costUsd ?? 0), 0);
|
|
184
|
+
if (originalCost !== null && originalCost > 0 && revisionCost > originalCost * exports.DRIFT_COST_SHARE) {
|
|
185
|
+
nudges.push({
|
|
186
|
+
code: 'drift.costShare',
|
|
187
|
+
values: {
|
|
188
|
+
revisions: revisions.length,
|
|
189
|
+
revisionCost: revisionCost.toFixed(2),
|
|
190
|
+
originalCost: originalCost.toFixed(2),
|
|
191
|
+
share: Math.round((revisionCost / originalCost) * 100),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (input.originalFileSet.size > 0 && input.currentFiles.size > 0) {
|
|
196
|
+
const outside = [...input.currentFiles].filter((file) => !input.originalFileSet.has(file));
|
|
197
|
+
if (outside.length * 2 > input.currentFiles.size) {
|
|
198
|
+
nudges.push({ code: 'drift.outOfScopeChurn', values: { files: outside.length, total: input.currentFiles.size } });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (revisions.length >= exports.DRIFT_REVISION_COUNT) {
|
|
202
|
+
nudges.push({ code: 'drift.revisionCount', values: { revisions: revisions.length } });
|
|
203
|
+
}
|
|
204
|
+
return nudges;
|
|
205
|
+
}
|
|
206
|
+
function composeReviewPacket({ db, row }) {
|
|
207
|
+
const ticketIds = safeParse(row.ticket_ids, []);
|
|
208
|
+
const units = safeParse(row.branches, []);
|
|
209
|
+
const runIds = safeParse(row.run_ids, []);
|
|
210
|
+
const snapshot = (0, rail_pr_store_1.readSpecSnapshot)(row.spec_snapshot);
|
|
211
|
+
const evidence = (0, delivery_evidence_1.readSettleEvidence)(row.settle_evidence);
|
|
212
|
+
const churn = churnForRuns(db, runIds);
|
|
213
|
+
const variant = selectVariant(row, units);
|
|
214
|
+
const confidence = (evidence?.units ?? [])
|
|
215
|
+
.map((unit) => unit.confidence)
|
|
216
|
+
.find((score) => score !== null) ?? null;
|
|
217
|
+
const sections = ticketIds.map((ticketId) => {
|
|
218
|
+
const snap = snapshot?.find((entry) => entry.ticketId === ticketId) ?? null;
|
|
219
|
+
const narrative = (0, pr_body_1.extractSpecNarrative)(snap?.description ?? null);
|
|
220
|
+
const unitRows = units.filter((unit) => unit.ticketId === ticketId);
|
|
221
|
+
const unitRunIds = [...new Set(unitRows.map((unit) => unit.runId).filter((id) => Boolean(id)))];
|
|
222
|
+
// A run that covered several tickets (a batch collapsed into one checkout)
|
|
223
|
+
// cannot have its files split per ticket: provenance keys them to the run's
|
|
224
|
+
// primary ticket. Report null rather than repeat delivery totals per section.
|
|
225
|
+
const sharedRun = unitRunIds.some((runId) => new Set(units.filter((unit) => unit.runId === runId).map((unit) => unit.ticketId)).size > 1);
|
|
226
|
+
const unitChurn = !sharedRun && unitRunIds.length > 0
|
|
227
|
+
? churn.filter((row) => row.job_id != null && unitRunIds.includes(row.job_id))
|
|
228
|
+
: null;
|
|
229
|
+
return {
|
|
230
|
+
ticketId,
|
|
231
|
+
title: snap?.title ?? null,
|
|
232
|
+
problem: narrative.problem,
|
|
233
|
+
solution: narrative.solution,
|
|
234
|
+
labels: snap?.labels ?? [],
|
|
235
|
+
implementationOutcome: unitRows[0]?.implementationOutcome ?? null,
|
|
236
|
+
deliveryOutcome: unitRows[0]?.deliveryOutcome ?? null,
|
|
237
|
+
changed: unitRows[0]?.changed ?? null,
|
|
238
|
+
churn: unitChurn === null ? null : {
|
|
239
|
+
filesTouched: new Set(unitChurn.map((c) => c.file_path)).size,
|
|
240
|
+
addedLines: unitChurn.reduce((sum, c) => sum + (c.added_lines || 0), 0),
|
|
241
|
+
removedLines: unitChurn.reduce((sum, c) => sum + (c.removed_lines || 0), 0),
|
|
242
|
+
testFilesTouched: [...new Set(unitChurn.filter((c) => TEST_FILE_RE.test(c.file_path)).map((c) => c.file_path))],
|
|
243
|
+
},
|
|
244
|
+
runIds: unitRunIds,
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
const succeededCount = units.length > 0
|
|
248
|
+
? units.filter((unit) => unit.implementationOutcome === 'succeeded' || (unit.implementationOutcome == null && unit.succeeded)).length
|
|
249
|
+
: row.implementation_outcome === 'failed' ? 0 : ticketIds.length;
|
|
250
|
+
const failedCount = units.length > 0
|
|
251
|
+
? units.filter((unit) => unit.implementationOutcome === 'failed' || (unit.implementationOutcome == null && !unit.succeeded)).length
|
|
252
|
+
: row.implementation_outcome === 'failed' ? ticketIds.length : 0;
|
|
253
|
+
const costSum = (0, ai_invocations_1.sumInvocationCostForRuns)(db, runIds);
|
|
254
|
+
const chain = versionChain(db, row);
|
|
255
|
+
const versions = chain.map((entry, index) => {
|
|
256
|
+
const entryRunIds = safeParse(entry.run_ids, []);
|
|
257
|
+
const entryCost = (0, ai_invocations_1.sumInvocationCostForRuns)(db, entryRunIds);
|
|
258
|
+
return {
|
|
259
|
+
prDeliveryId: entry.id,
|
|
260
|
+
version: index + 1,
|
|
261
|
+
revisionNote: entry.revision_note,
|
|
262
|
+
decision: entry.decision,
|
|
263
|
+
costUsd: entryCost?.totalUsd ?? null,
|
|
264
|
+
costEstimated: entryCost?.estimated ?? false,
|
|
265
|
+
current: entry.id === row.id,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
const chainCosts = versions.map((version) => version.costUsd).filter((value) => value !== null);
|
|
269
|
+
// Churn comparison uses the ORIGINAL generation's files as the scope baseline:
|
|
270
|
+
// a revision touching mostly new files is the honest signal that the spec no
|
|
271
|
+
// longer describes the work.
|
|
272
|
+
const originalFiles = chain.length > 1
|
|
273
|
+
? new Set(churnForRuns(db, safeParse(chain[0].run_ids, [])).map((entry) => entry.file_path))
|
|
274
|
+
: new Set();
|
|
275
|
+
const driftNudges = computeDriftNudges({
|
|
276
|
+
versions,
|
|
277
|
+
originalFileSet: originalFiles,
|
|
278
|
+
currentFiles: new Set(churn.map((entry) => entry.file_path)),
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
schemaVersion: 1,
|
|
282
|
+
prDeliveryId: row.id,
|
|
283
|
+
railIndex: row.rail_index,
|
|
284
|
+
variant,
|
|
285
|
+
decision: row.decision,
|
|
286
|
+
statusCode: row.status_code,
|
|
287
|
+
headlineCode: headlineFor(variant, succeededCount, ticketIds.length),
|
|
288
|
+
ticketIds,
|
|
289
|
+
baseBranch: row.base_branch,
|
|
290
|
+
loopName: row.loop_name,
|
|
291
|
+
prUrl: row.pr_url,
|
|
292
|
+
prNumber: row.pr_number,
|
|
293
|
+
succeededCount,
|
|
294
|
+
failedCount,
|
|
295
|
+
totalCount: units.length > 0 ? new Set(units.map((u) => u.ticketId)).size : ticketIds.length,
|
|
296
|
+
sections,
|
|
297
|
+
proof: buildProof(units, evidence?.units ?? null, churn, confidence),
|
|
298
|
+
watchOut: buildWatchOut(confidence, churn, variant),
|
|
299
|
+
confidence,
|
|
300
|
+
cost: { totalUsd: costSum?.totalUsd ?? null, estimated: costSum?.estimated ?? false },
|
|
301
|
+
evidenceUnavailable: evidence === null || evidence.harvest === 'failed',
|
|
302
|
+
runIds,
|
|
303
|
+
supersedesDeliveryId: row.supersedes_delivery_id,
|
|
304
|
+
revisionNote: row.revision_note,
|
|
305
|
+
versions,
|
|
306
|
+
chainCostUsd: chainCosts.length > 0 ? chainCosts.reduce((sum, value) => sum + value, 0) : null,
|
|
307
|
+
chainCostEstimated: versions.some((version) => version.costEstimated),
|
|
308
|
+
driftNudges,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Assert-by-construction guard used in tests and by the route: no packet field
|
|
312
|
+
* may carry a numeric verification claim without a structured source. */
|
|
313
|
+
function packetHasUnsourcedNumericClaim(packet) {
|
|
314
|
+
return packet.proof.some((item) => item.tier === 'ai-reported'
|
|
315
|
+
&& Object.keys(item.values ?? {}).some((key) => key === 'tests' || key === 'passed' || key === 'failedTests'));
|
|
316
|
+
}
|
|
317
|
+
function safeParse(raw, fallback) {
|
|
318
|
+
if (!raw)
|
|
319
|
+
return fallback;
|
|
320
|
+
try {
|
|
321
|
+
const parsed = JSON.parse(raw);
|
|
322
|
+
return parsed ?? fallback;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return fallback;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SEED_EVIDENCE_CAP = exports.SEED_DIFF_CAP = exports.SEED_SPEC_BODY_CAP = void 0;
|
|
4
|
+
exports.buildRevisionSeed = buildRevisionSeed;
|
|
5
|
+
/** Bounds keep the seed a briefing, not a transcript. */
|
|
6
|
+
exports.SEED_SPEC_BODY_CAP = 4000;
|
|
7
|
+
exports.SEED_DIFF_CAP = 4000;
|
|
8
|
+
exports.SEED_EVIDENCE_CAP = 1200;
|
|
9
|
+
function clamp(value, cap) {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
return trimmed.length <= cap ? trimmed : `${trimmed.slice(0, cap)}\n…(truncated)`;
|
|
12
|
+
}
|
|
13
|
+
/** One-line digest of what the previous run's verification actually reported. */
|
|
14
|
+
function evidenceLine(evidence) {
|
|
15
|
+
if (!evidence || evidence.units.length === 0)
|
|
16
|
+
return 'No verification evidence was captured for the previous run.';
|
|
17
|
+
const failed = evidence.units.filter((unit) => unit.sentinel === 'fail');
|
|
18
|
+
const passed = evidence.units.filter((unit) => unit.sentinel === 'pass');
|
|
19
|
+
const parts = [];
|
|
20
|
+
if (failed.length > 0) {
|
|
21
|
+
// Failure first: a revision briefing must not bury it behind a pass.
|
|
22
|
+
const detail = failed.map((unit) => unit.sentinelDetail).find(Boolean);
|
|
23
|
+
parts.push(`The previous run REPORTED FAILED verification${detail ? ` (${clamp(detail, 200)})` : ''}.`);
|
|
24
|
+
}
|
|
25
|
+
if (passed.length > 0)
|
|
26
|
+
parts.push('The previous run reported its verification passed.');
|
|
27
|
+
if (parts.length === 0)
|
|
28
|
+
parts.push('The previous run did not report a verification verdict.');
|
|
29
|
+
const score = evidence.units.map((unit) => unit.confidence?.overall).find((value) => typeof value === 'number');
|
|
30
|
+
if (typeof score === 'number')
|
|
31
|
+
parts.push(`Its reviewer scored the work ${score}/100.`);
|
|
32
|
+
return parts.join(' ');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The briefing a revision run is seeded with. Deterministic (same input ⇒ same
|
|
36
|
+
* text) so it is cache-friendly and testable, and honest: it never claims the
|
|
37
|
+
* previous run verified anything the evidence does not say.
|
|
38
|
+
*/
|
|
39
|
+
function buildRevisionSeed(input) {
|
|
40
|
+
const specs = (input.specSnapshot ?? []).length > 0
|
|
41
|
+
? (input.specSnapshot ?? []).map((entry) => [
|
|
42
|
+
`### #${entry.ticketId} ${entry.title ?? '(untitled)'}`,
|
|
43
|
+
entry.description ? clamp(entry.description, exports.SEED_SPEC_BODY_CAP) : '(no description recorded)',
|
|
44
|
+
].join('\n')).join('\n\n')
|
|
45
|
+
: input.ticketIds.map((id) => `### #${id}\n(spec text was not captured at launch)`).join('\n\n');
|
|
46
|
+
return [
|
|
47
|
+
'## Revision briefing',
|
|
48
|
+
'',
|
|
49
|
+
`This is revision ${input.revisionNumber} of work that has ALREADY been delivered for the spec(s) below.`,
|
|
50
|
+
'The code exists on the branch listed here. Do NOT start over, re-plan, or re-implement it.',
|
|
51
|
+
'',
|
|
52
|
+
'## What the user asked to change',
|
|
53
|
+
'',
|
|
54
|
+
clamp(input.note, exports.SEED_SPEC_BODY_CAP),
|
|
55
|
+
'',
|
|
56
|
+
'## The original request (frozen at launch)',
|
|
57
|
+
'',
|
|
58
|
+
specs,
|
|
59
|
+
'',
|
|
60
|
+
'## Where the existing work lives',
|
|
61
|
+
'',
|
|
62
|
+
input.branches.length > 0
|
|
63
|
+
? `Branch(es): ${input.branches.join(', ')} (based on ${input.baseBranch}).`
|
|
64
|
+
: `Base branch: ${input.baseBranch}.`,
|
|
65
|
+
...(input.branchDiffSummary
|
|
66
|
+
? ['', 'Already changed on that branch:', '', clamp(input.branchDiffSummary, exports.SEED_DIFF_CAP)]
|
|
67
|
+
: []),
|
|
68
|
+
'',
|
|
69
|
+
'## What the previous run reported',
|
|
70
|
+
'',
|
|
71
|
+
clamp(evidenceLine(input.evidence), exports.SEED_EVIDENCE_CAP),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|