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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CONFIDENCE_RAW_CAP = exports.VERIFY_TAIL_CAP = void 0;
|
|
7
|
+
exports.parseVerificationSentinel = parseVerificationSentinel;
|
|
8
|
+
exports.extractVerifyStepText = extractVerifyStepText;
|
|
9
|
+
exports.parseConfidenceScore = parseConfidenceScore;
|
|
10
|
+
exports.harvestDeliveryEvidence = harvestDeliveryEvidence;
|
|
11
|
+
exports.readSettleEvidence = readSettleEvidence;
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic settle-time evidence harvest for the review packet
|
|
14
|
+
* (nontech-review-experience Wave 1). Reads three sources with zero model
|
|
15
|
+
* calls: the `VERIFICATION: PASS/FAIL` sentinel from the run's persisted
|
|
16
|
+
* stream (the rail-merge-orchestrator regex precedent), a bounded tail of the
|
|
17
|
+
* verify step's output, and the reviewer's `confidence-score.json` written
|
|
18
|
+
* into the worktree's `openspec/changes/<name>/` (core ≥4.12 — previously read
|
|
19
|
+
* by nothing). Harvest NEVER throws and never blocks settle: every missing or
|
|
20
|
+
* malformed source degrades to an absent value on the evidence record, so the
|
|
21
|
+
* packet can always state honestly what it does and does not know.
|
|
22
|
+
*/
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
/** Bounded tail of the verify step's raw output persisted per unit. */
|
|
26
|
+
exports.VERIFY_TAIL_CAP = 4096;
|
|
27
|
+
/** Raw confidence-score.json payloads are capped before persistence. */
|
|
28
|
+
exports.CONFIDENCE_RAW_CAP = 8192;
|
|
29
|
+
/** Last sentinel wins: the verify prompt itself quotes the format, so earlier
|
|
30
|
+
* mentions in assistant reasoning must not shadow the final verdict line. */
|
|
31
|
+
const SENTINEL_RE = /VERIFICATION:\s*(PASS|FAIL)\b[ \t]*(?:[—:-][ \t]*)?([^\n]*)/gi;
|
|
32
|
+
function parseVerificationSentinel(text) {
|
|
33
|
+
let verdict = 'absent';
|
|
34
|
+
let detail = null;
|
|
35
|
+
for (const match of text.matchAll(SENTINEL_RE)) {
|
|
36
|
+
verdict = match[1].toUpperCase() === 'PASS' ? 'pass' : 'fail';
|
|
37
|
+
const tail = (match[2] ?? '').trim();
|
|
38
|
+
detail = verdict === 'fail' && tail ? tail.slice(0, 512) : null;
|
|
39
|
+
}
|
|
40
|
+
return { verdict, detail };
|
|
41
|
+
}
|
|
42
|
+
function parseLoopStepPayload(payload) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(payload);
|
|
45
|
+
return {
|
|
46
|
+
index: typeof parsed.index === 'number' ? parsed.index : null,
|
|
47
|
+
nodeId: typeof parsed.nodeId === 'string' ? parsed.nodeId : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Extract human-readable text from one persisted event row. Assistant rows
|
|
55
|
+
* hold the raw provider JSONL line; log rows hold plain text. Everything else
|
|
56
|
+
* contributes nothing (structured/loop events are boundaries, not output). */
|
|
57
|
+
function eventText(event) {
|
|
58
|
+
if (event.event_type === 'log')
|
|
59
|
+
return event.payload;
|
|
60
|
+
if (event.event_type !== 'assistant')
|
|
61
|
+
return '';
|
|
62
|
+
try {
|
|
63
|
+
const frame = JSON.parse(event.payload);
|
|
64
|
+
const blocks = frame.message?.content;
|
|
65
|
+
if (!Array.isArray(blocks))
|
|
66
|
+
return '';
|
|
67
|
+
return blocks
|
|
68
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
69
|
+
.map((block) => block.text)
|
|
70
|
+
.join('\n');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Text of the LAST `verify` step in the run (loop_step nodeId === 'verify'
|
|
78
|
+
* through its loop_step_end / next step / stream end). Runs without a verify
|
|
79
|
+
* step boundary (non-loop jobs, legacy runs) fall back to the whole stream's
|
|
80
|
+
* text — still raw output, just unscoped.
|
|
81
|
+
*/
|
|
82
|
+
function extractVerifyStepText(events) {
|
|
83
|
+
let start = -1;
|
|
84
|
+
let end = events.length;
|
|
85
|
+
let stepIndex = null;
|
|
86
|
+
for (let i = 0; i < events.length; i++) {
|
|
87
|
+
const event = events[i];
|
|
88
|
+
if (event.event_type === 'loop_step') {
|
|
89
|
+
const step = parseLoopStepPayload(event.payload);
|
|
90
|
+
if (step?.nodeId === 'verify') {
|
|
91
|
+
start = i;
|
|
92
|
+
stepIndex = step.index;
|
|
93
|
+
end = events.length;
|
|
94
|
+
}
|
|
95
|
+
else if (start >= 0 && i > start) {
|
|
96
|
+
// A later non-verify step closes the previous verify range unless a
|
|
97
|
+
// later verify step reopens it (loop iterations: last verify wins).
|
|
98
|
+
if (end === events.length)
|
|
99
|
+
end = i;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (start >= 0 && end === events.length && event.event_type === 'loop_step_end' &&
|
|
103
|
+
stepIndex !== null && parseLoopStepPayload(event.payload)?.index === stepIndex) {
|
|
104
|
+
end = i;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const range = start >= 0 ? events.slice(start, end) : events;
|
|
108
|
+
const text = range.map(eventText).filter(Boolean).join('\n');
|
|
109
|
+
return { text, scoped: start >= 0 };
|
|
110
|
+
}
|
|
111
|
+
function toNumberRecord(value) {
|
|
112
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
113
|
+
return {};
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
116
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
117
|
+
out[key] = raw;
|
|
118
|
+
else if (raw && typeof raw === 'object' && typeof raw.score === 'number') {
|
|
119
|
+
out[key] = raw.score;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Tolerant parse of a confidence-score.json payload. Returns null only when
|
|
125
|
+
* the payload is not a JSON object at all. */
|
|
126
|
+
function parseConfidenceScore(rawJson, changeName) {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(rawJson);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
135
|
+
return null;
|
|
136
|
+
const obj = parsed;
|
|
137
|
+
return {
|
|
138
|
+
changeName,
|
|
139
|
+
overall: typeof obj.overall === 'number' && Number.isFinite(obj.overall) ? obj.overall : null,
|
|
140
|
+
aspects: toNumberRecord(obj.aspects ?? obj.scores),
|
|
141
|
+
flags: Array.isArray(obj.flags) ? obj.flags.filter((f) => typeof f === 'string').slice(0, 20) : [],
|
|
142
|
+
raw: rawJson.length > exports.CONFIDENCE_RAW_CAP ? null : parsed,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Newest confidence-score.json under `<worktree>/openspec/changes/<name>/`.
|
|
146
|
+
* A worktree hosts at most a handful of change dirs; when several carry a
|
|
147
|
+
* score (unusual), the lexicographically last dir wins deterministically. */
|
|
148
|
+
function readConfidenceFromWorktree(worktreePath, io) {
|
|
149
|
+
const changesDir = path_1.default.join(worktreePath, 'openspec', 'changes');
|
|
150
|
+
let dirs;
|
|
151
|
+
try {
|
|
152
|
+
dirs = io.listDir(changesDir);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
let found = null;
|
|
158
|
+
for (const dir of [...dirs].sort()) {
|
|
159
|
+
const scorePath = path_1.default.join(changesDir, dir, 'confidence-score.json');
|
|
160
|
+
try {
|
|
161
|
+
if (!io.fileExists(scorePath))
|
|
162
|
+
continue;
|
|
163
|
+
const raw = io.readFile(scorePath);
|
|
164
|
+
const parsed = parseConfidenceScore(raw, dir);
|
|
165
|
+
if (parsed)
|
|
166
|
+
found = parsed;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* unreadable file = absent, not fatal */
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Harvest evidence for every unit of a settling delivery. Never throws; the
|
|
176
|
+
* per-unit try/catch turns internal errors into `partial`/`failed` harvest
|
|
177
|
+
* status while absence of a source stays `ok` (absence is honest data).
|
|
178
|
+
*/
|
|
179
|
+
function harvestDeliveryEvidence(io, units) {
|
|
180
|
+
const readFile = io.readFile ?? ((p) => fs_1.default.readFileSync(p, 'utf8'));
|
|
181
|
+
const listDir = io.listDir ?? ((p) => fs_1.default.readdirSync(p));
|
|
182
|
+
const fileExists = io.fileExists ?? ((p) => fs_1.default.existsSync(p));
|
|
183
|
+
const fsIo = { readFile, listDir, fileExists };
|
|
184
|
+
let errored = 0;
|
|
185
|
+
const harvested = units.map((unit) => {
|
|
186
|
+
const evidence = {
|
|
187
|
+
ticketId: unit.ticketId,
|
|
188
|
+
runId: unit.runId,
|
|
189
|
+
sentinel: 'absent',
|
|
190
|
+
sentinelDetail: null,
|
|
191
|
+
verifyTail: null,
|
|
192
|
+
confidence: null,
|
|
193
|
+
};
|
|
194
|
+
let unitErrored = false;
|
|
195
|
+
try {
|
|
196
|
+
if (unit.runId) {
|
|
197
|
+
const events = io.readEvents(unit.runId);
|
|
198
|
+
const { text } = extractVerifyStepText(events);
|
|
199
|
+
if (text) {
|
|
200
|
+
const sentinel = parseVerificationSentinel(text);
|
|
201
|
+
evidence.sentinel = sentinel.verdict;
|
|
202
|
+
evidence.sentinelDetail = sentinel.detail;
|
|
203
|
+
evidence.verifyTail = text.slice(-exports.VERIFY_TAIL_CAP);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
unitErrored = true;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
if (unit.worktreePath) {
|
|
212
|
+
evidence.confidence = readConfidenceFromWorktree(unit.worktreePath, fsIo);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
unitErrored = true;
|
|
217
|
+
}
|
|
218
|
+
if (unitErrored)
|
|
219
|
+
errored++;
|
|
220
|
+
return evidence;
|
|
221
|
+
});
|
|
222
|
+
return {
|
|
223
|
+
schemaVersion: 1,
|
|
224
|
+
harvest: units.length > 0 && errored === units.length ? 'failed' : errored > 0 ? 'partial' : 'ok',
|
|
225
|
+
harvestedAt: (io.now?.() ?? new Date()).toISOString(),
|
|
226
|
+
units: harvested,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Parse a persisted settle_evidence column value (null-tolerant). */
|
|
230
|
+
function readSettleEvidence(raw) {
|
|
231
|
+
if (!raw)
|
|
232
|
+
return null;
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.units))
|
|
236
|
+
return null;
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -17,6 +17,7 @@ const specrails_tech_client_1 = require("./specrails-tech-client");
|
|
|
17
17
|
const core_compat_1 = require("./core-compat");
|
|
18
18
|
const providers_1 = require("./providers");
|
|
19
19
|
const provider_detection_1 = require("./provider-detection");
|
|
20
|
+
const agent_defaults_1 = require("./agent-defaults");
|
|
20
21
|
const workspace_manager_1 = require("./workspace-manager");
|
|
21
22
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
22
23
|
const desktop_analytics_1 = require("./desktop-analytics");
|
|
@@ -1130,6 +1131,28 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
1130
1131
|
const monthlyBudgetUsd = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5.0;
|
|
1131
1132
|
res.json({ language, monthlyBudgetUsd });
|
|
1132
1133
|
});
|
|
1134
|
+
// ─── Specrails Agents defaults (global per-provider agent model/effort) ───
|
|
1135
|
+
// App-level layer read AT SPAWN TIME by QueueManager / rails-router / loop
|
|
1136
|
+
// executors — a change applies to the next run with zero restart.
|
|
1137
|
+
router.get('/agent-defaults', (_req, res) => {
|
|
1138
|
+
res.json({
|
|
1139
|
+
settings: (0, agent_defaults_1.readAgentDefaultsSettings)(registry.desktopDb),
|
|
1140
|
+
catalog: (0, agent_defaults_1.buildAgentDefaultsCatalog)(),
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
router.patch('/agent-defaults', (req, res) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const settings = (0, agent_defaults_1.applyAgentDefaultsPatch)(registry.desktopDb, req.body);
|
|
1146
|
+
res.json({ settings, catalog: (0, agent_defaults_1.buildAgentDefaultsCatalog)() });
|
|
1147
|
+
}
|
|
1148
|
+
catch (err) {
|
|
1149
|
+
if (err instanceof agent_defaults_1.AgentDefaultsValidationError) {
|
|
1150
|
+
res.status(400).json({ error: err.code, message: err.message });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
res.status(500).json({ error: 'agent_defaults_failed', message: err.message });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1133
1156
|
// ─── specrails-core update channel (app-global) ─────────────────────────────
|
|
1134
1157
|
// Detect + apply a specrails-core framework update independently of the desktop
|
|
1135
1158
|
// app update. See server/core-update-manager.ts. A single manager instance
|
|
@@ -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
|