specrails-desktop 2.29.2 → 2.29.3
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-ClpD5CFj.js → ActivityFeedPage-CFB4UBhT.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-C-T0r16B.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-IljV5FOQ.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-B5miH10_.js} +1 -1
- package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-BgYQrAcH.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BtVCuNjL.js} +1 -1
- package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Cb_jAjSV.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-s_Cc_JL1.js} +1 -1
- package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-Cr47QI9L.js} +1 -1
- package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-C_WPRuG_.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-NfFOciRB.js} +1 -1
- package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DqEW7Z7K.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-D4ppEpDE.js} +1 -1
- package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-DStSoMyq.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-DltCwS4P.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-BWaiAbUq.js} +1 -1
- package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-BqjbXuBZ.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext--o3JAM9F.js} +1 -1
- package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-B-UOmzXw.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-DkGajIxE.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-5CUXlkw7.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-B_igqmti.js} +4 -4
- package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-DDMIBBUd.js} +1 -1
- package/client/dist/assets/agent-BXc59ARK2.js +1 -0
- package/client/dist/assets/agent-BbIFXdDq.js +1 -0
- package/client/dist/assets/agent-BqEfA5u5.js +1 -0
- package/client/dist/assets/agent-BtGzgTC4.js +1 -0
- package/client/dist/assets/agent-CIhys_5d.js +1 -0
- package/client/dist/assets/agent-CJPaqsVR.js +1 -0
- package/client/dist/assets/agent-n-hY4GuL.js +1 -0
- package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
- package/client/dist/assets/common-6MLbD-jM.js +1 -0
- package/client/dist/assets/common-BKGEu4rm.js +1 -0
- package/client/dist/assets/common-BNbnv8cE2.js +1 -0
- package/client/dist/assets/common-BqGWtDbt.js +1 -0
- package/client/dist/assets/common-DBtTzeYm2.js +1 -0
- package/client/dist/assets/common-DEW3Nr8n.js +1 -0
- package/client/dist/assets/{common-DVQuhs-m.js → common-DHsv9N5d.js} +1 -1
- package/client/dist/assets/common-mkrEcW-Y.js +1 -0
- package/client/dist/assets/dashboard-BYzxom0L.js +1 -0
- package/client/dist/assets/dashboard-BmDZmHyY.js +1 -0
- package/client/dist/assets/dashboard-CnNtJBQ5.js +1 -0
- package/client/dist/assets/dashboard-D4Uj-XWc2.js +1 -0
- package/client/dist/assets/dashboard-D57uRaGw2.js +1 -0
- package/client/dist/assets/dashboard-DAr6F9et.js +1 -0
- package/client/dist/assets/dashboard-vAcsn0L3.js +1 -0
- package/client/dist/assets/dashboard-vrjZvxWy.js +1 -0
- package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-BCUhGUXS.js} +1 -1
- package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-DcoXTnEI.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-DP3Ab6fX.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DdKKL8Fx.js} +1 -1
- package/client/dist/assets/{i18n-CgaRxpII.js → i18n-DREQCE2F.js} +2 -2
- package/client/dist/assets/index-B784m8RT.js +76 -0
- package/client/dist/assets/index-CQBIemiz.css +2 -0
- package/client/dist/assets/{spending-C4p06nen.js → spending-CtY7yYes.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-BFBdW59q.js} +1 -1
- package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-uWVZ8eia.js} +1 -1
- package/client/dist/index.html +14 -14
- package/docs/internals/safe-pr-review-flow.md +221 -38
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +237 -52
- package/server/dist/agent-chat-manager.js +29 -12
- package/server/dist/agent-store.js +19 -4
- package/server/dist/db.js +21 -0
- package/server/dist/pr-lifecycle.js +100 -8
- package/server/dist/project-git.js +62 -2
- package/server/dist/rail-isolated-launch.js +644 -108
- package/server/dist/rail-pr-decision.js +1065 -181
- package/server/dist/rail-pr-recovery-git.js +235 -0
- package/server/dist/rail-pr-store.js +120 -9
- package/server/dist/rail-worktree-release.js +290 -4
- package/server/dist/rails-router.js +97 -9
- package/server/dist/worktree-manager.js +120 -16
- package/server/dist/worktree-overlay.js +166 -4
- package/client/dist/assets/agent-4rmXoo_p.js +0 -1
- package/client/dist/assets/agent-BxZuNGih.js +0 -1
- package/client/dist/assets/agent-C3uXOYv_.js +0 -1
- package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
- package/client/dist/assets/agent-CASKrhG12.js +0 -1
- package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
- package/client/dist/assets/agent-DitUxYh2.js +0 -1
- package/client/dist/assets/agent-Doow4X7o.js +0 -1
- package/client/dist/assets/common-CD1QHriA2.js +0 -1
- package/client/dist/assets/common-CwwPaRxx.js +0 -1
- package/client/dist/assets/common-Dg2jaX7d.js +0 -1
- package/client/dist/assets/common-DuBwj6tK.js +0 -1
- package/client/dist/assets/common-MRj9MHcc.js +0 -1
- package/client/dist/assets/common-O4ZpnMLp.js +0 -1
- package/client/dist/assets/common-fyeF0v7C2.js +0 -1
- package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
- package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
- package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
- package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
- package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
- package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
- package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
- package/client/dist/assets/dashboard-owcDucDi.js +0 -1
- package/client/dist/assets/index-BflYQpWm.css +0 -2
- package/client/dist/assets/index-C1_-BTXy.js +0 -72
|
@@ -1,8 +1,258 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.durableBranchHeads = durableBranchHeads;
|
|
37
|
+
exports.durableOverlayCleanupEvidence = durableOverlayCleanupEvidence;
|
|
3
38
|
exports.releaseRailWorktrees = releaseRailWorktrees;
|
|
4
39
|
const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
5
40
|
const worktree_manager_1 = require("./worktree-manager");
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const worktree_overlay_1 = require("./worktree-overlay");
|
|
44
|
+
const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
45
|
+
/** Collapse per-unit settlement evidence into one unambiguous immutable HEAD per
|
|
46
|
+
* branch. Conflicting or malformed records intentionally yield no authorization. */
|
|
47
|
+
function durableBranchHeads(records) {
|
|
48
|
+
const heads = new Map();
|
|
49
|
+
const conflicted = new Set();
|
|
50
|
+
for (const record of records) {
|
|
51
|
+
const sha = record.finalSha?.trim().toLowerCase() ?? '';
|
|
52
|
+
if (!record.branch || !COMMIT_SHA_RE.test(sha) || conflicted.has(record.branch))
|
|
53
|
+
continue;
|
|
54
|
+
const existing = heads.get(record.branch);
|
|
55
|
+
if (existing && existing !== sha) {
|
|
56
|
+
heads.delete(record.branch);
|
|
57
|
+
conflicted.add(record.branch);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
heads.set(record.branch, sha);
|
|
61
|
+
}
|
|
62
|
+
return heads;
|
|
63
|
+
}
|
|
64
|
+
function safeRelativeOverlayPath(value) {
|
|
65
|
+
if (process.platform === 'win32' && value.includes('\\'))
|
|
66
|
+
return null;
|
|
67
|
+
if (value.split('/').includes('..'))
|
|
68
|
+
return null;
|
|
69
|
+
const normalized = path.posix.normalize(value);
|
|
70
|
+
if (!normalized || normalized === '.' || normalized.startsWith('../') ||
|
|
71
|
+
path.posix.isAbsolute(normalized) || normalized.includes('\0'))
|
|
72
|
+
return null;
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
/** Collapse persisted overlay fingerprints. Conflicting evidence for the same
|
|
76
|
+
* branch/path grants no exclusion permission. */
|
|
77
|
+
function durableOverlayCleanupEvidence(records) {
|
|
78
|
+
const byBranch = new Map();
|
|
79
|
+
const conflicted = new Map();
|
|
80
|
+
for (const record of records) {
|
|
81
|
+
if (!record.branch || !Array.isArray(record.overlayCleanupEvidence))
|
|
82
|
+
continue;
|
|
83
|
+
const branchEvidence = byBranch.get(record.branch) ?? new Map();
|
|
84
|
+
const branchConflicts = conflicted.get(record.branch) ?? new Set();
|
|
85
|
+
for (const value of record.overlayCleanupEvidence) {
|
|
86
|
+
if (!value || typeof value.path !== 'string' || typeof value.digest !== 'string')
|
|
87
|
+
continue;
|
|
88
|
+
if (value.kind !== 'symlink' && value.kind !== 'file' && value.kind !== 'directory')
|
|
89
|
+
continue;
|
|
90
|
+
const safe = safeRelativeOverlayPath(value.path);
|
|
91
|
+
if (!safe || !/^[0-9a-f]{64}$/.test(value.digest) || branchConflicts.has(safe))
|
|
92
|
+
continue;
|
|
93
|
+
const normalized = { path: safe, kind: value.kind, digest: value.digest };
|
|
94
|
+
const existing = branchEvidence.get(safe);
|
|
95
|
+
if (existing && (existing.kind !== normalized.kind || existing.digest !== normalized.digest)) {
|
|
96
|
+
branchEvidence.delete(safe);
|
|
97
|
+
branchConflicts.add(safe);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
branchEvidence.set(safe, normalized);
|
|
101
|
+
}
|
|
102
|
+
byBranch.set(record.branch, branchEvidence);
|
|
103
|
+
conflicted.set(record.branch, branchConflicts);
|
|
104
|
+
}
|
|
105
|
+
return new Map([...byBranch].map(([branch, values]) => [branch, [...values.values()]]));
|
|
106
|
+
}
|
|
107
|
+
async function verifyReleaseEvidence(input, worktree) {
|
|
108
|
+
const expected = input.expectedHeadByBranch.get(worktree.branch)?.toLowerCase();
|
|
109
|
+
if (!expected || !COMMIT_SHA_RE.test(expected)) {
|
|
110
|
+
return { failure: 'no durable settled HEAD is recorded for its branch', overlayEntries: [] };
|
|
111
|
+
}
|
|
112
|
+
const overlayEntries = (input.overlayEvidenceByBranch.get(worktree.branch) ?? [])
|
|
113
|
+
.filter((entry) => (0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry));
|
|
114
|
+
const overlayExcludes = overlayEntries.map((entry) => entry.path);
|
|
115
|
+
try {
|
|
116
|
+
const status = await input.git.run([
|
|
117
|
+
'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
|
|
118
|
+
...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
|
|
119
|
+
], worktree.worktree_path);
|
|
120
|
+
if (status.code !== 0)
|
|
121
|
+
return { failure: 'Git could not verify worktree cleanliness', overlayEntries: [] };
|
|
122
|
+
if (status.stdout.trim())
|
|
123
|
+
return { failure: 'the worktree contains changes made after settlement', overlayEntries: [] };
|
|
124
|
+
const [head, branchRef] = await Promise.all([
|
|
125
|
+
input.git.run(['rev-parse', '--verify', 'HEAD'], worktree.worktree_path),
|
|
126
|
+
input.git.run(['rev-parse', '--verify', `refs/heads/${worktree.branch}`], input.repoDir),
|
|
127
|
+
]);
|
|
128
|
+
const headSha = head.code === 0 ? head.stdout.trim().toLowerCase() : '';
|
|
129
|
+
const branchSha = branchRef.code === 0 ? branchRef.stdout.trim().toLowerCase() : '';
|
|
130
|
+
if (!COMMIT_SHA_RE.test(headSha) || !COMMIT_SHA_RE.test(branchSha)) {
|
|
131
|
+
return { failure: 'Git could not verify the worktree HEAD and recorded branch ref', overlayEntries: [] };
|
|
132
|
+
}
|
|
133
|
+
if (headSha !== expected || branchSha !== expected) {
|
|
134
|
+
return { failure: `HEAD/ref moved after settlement (expected ${expected.slice(0, 12)})`, overlayEntries: [] };
|
|
135
|
+
}
|
|
136
|
+
return { failure: null, overlayEntries };
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return { failure: `verification failed: ${err instanceof Error ? err.message : String(err)}`, overlayEntries: [] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function pathStillExists(target) {
|
|
143
|
+
try {
|
|
144
|
+
fs.lstatSync(target);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function quarantineLocationNotes(entries) {
|
|
152
|
+
return entries
|
|
153
|
+
.filter((entry) => pathStillExists(entry.quarantinePath))
|
|
154
|
+
.map((entry) => `overlay data is preserved at ${entry.quarantinePath}`);
|
|
155
|
+
}
|
|
156
|
+
/** Atomically move fingerprint-matching, untracked overlay roots out of the
|
|
157
|
+
* worktree. Quarantine is a unique sibling on the same filesystem. It is never
|
|
158
|
+
* automatically deleted or restored: writes through an already-open fd after
|
|
159
|
+
* rename remain recoverable, and a concurrently recreated source can never be
|
|
160
|
+
* overwritten by rollback. */
|
|
161
|
+
async function quarantineVerifiedOverlayEntries(input, worktree, evidence) {
|
|
162
|
+
const removable = [];
|
|
163
|
+
for (const entry of evidence) {
|
|
164
|
+
const target = path.join(worktree.worktree_path, ...entry.path.split('/'));
|
|
165
|
+
if (!pathStillExists(target))
|
|
166
|
+
continue;
|
|
167
|
+
const tracked = await input.git.run(['ls-files', '--error-unmatch', '--', entry.path], worktree.worktree_path);
|
|
168
|
+
if (tracked.code === 0)
|
|
169
|
+
continue;
|
|
170
|
+
if (tracked.code !== 1) {
|
|
171
|
+
return { failure: `Git could not verify ownership of overlay path ${entry.path}`, entries: [] };
|
|
172
|
+
}
|
|
173
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry)) {
|
|
174
|
+
return { failure: `overlay path ${entry.path} changed after cleanup verification`, entries: [] };
|
|
175
|
+
}
|
|
176
|
+
removable.push(entry);
|
|
177
|
+
}
|
|
178
|
+
const cleanupRoots = [];
|
|
179
|
+
for (const entry of [...removable].sort((a, b) => a.path.split('/').length - b.path.split('/').length)) {
|
|
180
|
+
if (cleanupRoots.some((root) => root.kind === 'directory' && entry.path.startsWith(`${root.path}/`)))
|
|
181
|
+
continue;
|
|
182
|
+
cleanupRoots.push(entry);
|
|
183
|
+
}
|
|
184
|
+
if (cleanupRoots.length === 0)
|
|
185
|
+
return { failure: null, entries: [] };
|
|
186
|
+
let quarantineRoot;
|
|
187
|
+
try {
|
|
188
|
+
// mkdtemp creates the batch directory atomically beside the worktree. The
|
|
189
|
+
// ensuing rename therefore remains within one filesystem.
|
|
190
|
+
quarantineRoot = fs.mkdtempSync(`${worktree.worktree_path}.specrails-overlay-quarantine-`);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return {
|
|
194
|
+
failure: `could not create overlay quarantine beside ${worktree.worktree_path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
195
|
+
entries: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await input.onSafetyArchive?.(quarantineRoot);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return {
|
|
203
|
+
failure: `could not record safety archive ${quarantineRoot}: ${err instanceof Error ? err.message : String(err)}`,
|
|
204
|
+
entries: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const quarantined = [];
|
|
208
|
+
for (const entry of cleanupRoots) {
|
|
209
|
+
const sourcePath = path.join(worktree.worktree_path, ...entry.path.split('/'));
|
|
210
|
+
if (!pathStillExists(sourcePath))
|
|
211
|
+
continue;
|
|
212
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidenceAtPath)(sourcePath, entry)) {
|
|
213
|
+
return { failure: `overlay path ${entry.path} changed before quarantine`, entries: quarantined };
|
|
214
|
+
}
|
|
215
|
+
const quarantinePath = path.join(quarantineRoot, ...entry.path.split('/'));
|
|
216
|
+
try {
|
|
217
|
+
fs.mkdirSync(path.dirname(quarantinePath), { recursive: true });
|
|
218
|
+
await input.beforeOverlayQuarantine?.(sourcePath);
|
|
219
|
+
fs.renameSync(sourcePath, quarantinePath);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
failure: `could not quarantine overlay path ${entry.path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
224
|
+
entries: quarantined,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const moved = { sourcePath, quarantinePath, evidence: entry };
|
|
228
|
+
quarantined.push(moved);
|
|
229
|
+
try {
|
|
230
|
+
await input.afterOverlayRename?.(sourcePath, quarantinePath);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return {
|
|
234
|
+
failure: `overlay post-rename hook failed at ${quarantinePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
235
|
+
entries: quarantined,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidenceAtPath)(quarantinePath, entry)) {
|
|
239
|
+
return {
|
|
240
|
+
failure: `overlay path ${entry.path} changed during atomic quarantine at ${quarantinePath}`,
|
|
241
|
+
entries: quarantined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
await input.afterOverlayQuarantine?.(quarantinePath);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return {
|
|
249
|
+
failure: `overlay quarantine hook failed at ${quarantinePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
250
|
+
entries: quarantined,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { failure: null, entries: quarantined };
|
|
255
|
+
}
|
|
6
256
|
/** Remove Specrails-managed linked worktrees while keeping their local branches.
|
|
7
257
|
* PR deliveries use this once the work has a durable remote branch/PR: the
|
|
8
258
|
* branch remains available for local checkout, but the internal worktree stops
|
|
@@ -15,19 +265,50 @@ async function releaseRailWorktrees(input) {
|
|
|
15
265
|
const wt = (0, rail_worktrees_store_1.getRailWorktree)(input.db, wtId);
|
|
16
266
|
if (!wt)
|
|
17
267
|
continue;
|
|
18
|
-
if (wt.merge_state === 'needs-review' && input.allowNeedsReview !== true)
|
|
19
|
-
continue;
|
|
20
268
|
// Successful removal already terminalized these rows. Re-running `git
|
|
21
269
|
// worktree remove` against their now-missing paths turns ordinary Publish /
|
|
22
270
|
// Poll / Discard follow-ups into false cleanup_incomplete warnings.
|
|
23
271
|
if (wt.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
|
|
24
272
|
continue;
|
|
273
|
+
const evidence = await verifyReleaseEvidence(input, wt);
|
|
274
|
+
if (evidence.failure) {
|
|
275
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
276
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${evidence.failure}`;
|
|
277
|
+
warnings.push(warning);
|
|
278
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const quarantine = await quarantineVerifiedOverlayEntries(input, wt, evidence.overlayEntries);
|
|
282
|
+
if (quarantine.failure) {
|
|
283
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
284
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${[
|
|
285
|
+
quarantine.failure,
|
|
286
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
287
|
+
].join('; ')}`;
|
|
288
|
+
warnings.push(warning);
|
|
289
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
// Quarantining changes the worktree. Re-prove both cleanliness and HEAD/ref
|
|
293
|
+
// identity immediately before asking Git for its final non-force guard.
|
|
294
|
+
const finalEvidence = await verifyReleaseEvidence(input, wt);
|
|
295
|
+
if (finalEvidence.failure) {
|
|
296
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
297
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${[
|
|
298
|
+
finalEvidence.failure,
|
|
299
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
300
|
+
].join('; ')}`;
|
|
301
|
+
warnings.push(warning);
|
|
302
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
25
305
|
try {
|
|
26
306
|
await remove(input.git, {
|
|
27
307
|
repoDir: input.repoDir,
|
|
28
308
|
worktreePath: wt.worktree_path,
|
|
29
309
|
branch: wt.branch,
|
|
30
310
|
deleteBranch: false,
|
|
311
|
+
force: false,
|
|
31
312
|
});
|
|
32
313
|
}
|
|
33
314
|
catch (err) {
|
|
@@ -35,13 +316,18 @@ async function releaseRailWorktrees(input) {
|
|
|
35
316
|
if (current && current.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(current.merge_state)) {
|
|
36
317
|
continue;
|
|
37
318
|
}
|
|
38
|
-
|
|
319
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
320
|
+
const warning = `worktree ${wt.worktree_path}: ${[
|
|
321
|
+
err instanceof Error ? err.message : String(err),
|
|
322
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
323
|
+
].join('; ')}`;
|
|
39
324
|
warnings.push(warning);
|
|
40
325
|
console.warn(`[rail-worktree-release] failed to remove ${warning}`);
|
|
41
326
|
continue;
|
|
42
327
|
}
|
|
43
|
-
if (!(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
|
|
328
|
+
if (wt.merge_state === 'needs-review' || !(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state)) {
|
|
44
329
|
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, state);
|
|
330
|
+
}
|
|
45
331
|
}
|
|
46
332
|
return warnings;
|
|
47
333
|
}
|
|
@@ -40,12 +40,21 @@ const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high']);
|
|
|
40
40
|
function prDeliveryContinuesTickets(delivery, ticketIds) {
|
|
41
41
|
if (delivery.decision !== 'pr_draft' && delivery.decision !== 'pr_ready')
|
|
42
42
|
return false;
|
|
43
|
-
if (!delivery.prUrl || !delivery.branch || delivery.prState !== 'pr-created')
|
|
43
|
+
if (!delivery.prUrl || !delivery.branch || !delivery.deliverySha || delivery.prState !== 'pr-created')
|
|
44
44
|
return false;
|
|
45
45
|
const covered = new Set(delivery.ticketIds);
|
|
46
46
|
const requested = new Set(ticketIds);
|
|
47
47
|
return requested.size > 0 && requested.size === covered.size && [...requested].every((id) => covered.has(id));
|
|
48
48
|
}
|
|
49
|
+
function prDeliveryCheckoutBlock(delivery) {
|
|
50
|
+
if (delivery.deliveryOutcome === 'blocked') {
|
|
51
|
+
return 'delivery is blocked and has no safely checkoutable result';
|
|
52
|
+
}
|
|
53
|
+
if (!delivery.deliverySha) {
|
|
54
|
+
return 'delivery has no verified commit available for checkout';
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
49
58
|
function emitPrDeliveryUpdate(c, prDeliveryId) {
|
|
50
59
|
const row = (0, rail_pr_store_1.getPrDelivery)(c.db, prDeliveryId);
|
|
51
60
|
if (!row)
|
|
@@ -603,8 +612,10 @@ function createRailsRouter() {
|
|
|
603
612
|
deliveryId: continuablePrDelivery.id,
|
|
604
613
|
decision: continuablePrDelivery.decision,
|
|
605
614
|
branch: continuablePrDelivery.branch,
|
|
615
|
+
baseBranch: continuablePrDelivery.baseBranch,
|
|
606
616
|
prUrl: continuablePrDelivery.prUrl,
|
|
607
617
|
prNumber: continuablePrDelivery.prNumber,
|
|
618
|
+
deliverySha: continuablePrDelivery.deliverySha,
|
|
608
619
|
},
|
|
609
620
|
} : {}),
|
|
610
621
|
});
|
|
@@ -881,7 +892,7 @@ function createRailsRouter() {
|
|
|
881
892
|
// POST /rails/pr-decision — the ONE decision action (safe-pr-review-flow) both
|
|
882
893
|
// surfaces (dashboard rail row + agent-chat card) call on a rail_pr_deliveries
|
|
883
894
|
// row: create-pr | publish | discard/dismiss | poll/reopen | merge-local |
|
|
884
|
-
// acknowledge-no-changes, compare-and-set-guarded by
|
|
895
|
+
// acknowledge-no-changes | recover-and-retry, compare-and-set-guarded by
|
|
885
896
|
// expectedDecision (a raced concurrent answer loses with 409 stale_decision).
|
|
886
897
|
// Replaces the stateless v1 POST /rails/pr-review passthrough. The route only
|
|
887
898
|
// validates and delegates — the action logic lives in rail-pr-decision.ts.
|
|
@@ -894,7 +905,7 @@ function createRailsRouter() {
|
|
|
894
905
|
return;
|
|
895
906
|
}
|
|
896
907
|
if (!(0, rail_pr_decision_1.isPrDecisionAction)(action)) {
|
|
897
|
-
res.status(400).json({ error:
|
|
908
|
+
res.status(400).json({ error: `action must be one of: ${rail_pr_decision_1.PR_DECISION_ACTIONS.join(', ')}` });
|
|
898
909
|
return;
|
|
899
910
|
}
|
|
900
911
|
if (typeof expectedDecision !== 'string' || !expectedDecision) {
|
|
@@ -968,7 +979,21 @@ function createRailsRouter() {
|
|
|
968
979
|
res.status(404).json({ error: 'Unknown prDeliveryId' });
|
|
969
980
|
return;
|
|
970
981
|
}
|
|
982
|
+
const active = (0, rail_pr_store_1.getActivePrDeliveryByRail)(c.db, row.rail_index);
|
|
983
|
+
if (!active || active.id !== row.id) {
|
|
984
|
+
res.status(409).json({
|
|
985
|
+
error: 'stale_decision',
|
|
986
|
+
current: active?.decision ?? null,
|
|
987
|
+
currentPrDeliveryId: active?.id ?? null,
|
|
988
|
+
});
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
971
991
|
const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
|
|
992
|
+
const checkoutBlock = prDeliveryCheckoutBlock(snap);
|
|
993
|
+
if (checkoutBlock) {
|
|
994
|
+
res.status(409).json({ error: 'checkout_not_deliverable', detail: checkoutBlock });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
972
997
|
if (!snap.branch || !snap.prUrl) {
|
|
973
998
|
res.status(409).json({ error: 'checkout_unavailable', detail: 'delivery has no PR branch' });
|
|
974
999
|
return;
|
|
@@ -976,20 +1001,83 @@ function createRailsRouter() {
|
|
|
976
1001
|
try {
|
|
977
1002
|
const outcome = await (0, repo_lock_1.withRepoLock)(c.project.path, async () => {
|
|
978
1003
|
admission.assertCurrent();
|
|
1004
|
+
const current = (0, rail_pr_store_1.getActivePrDeliveryByRail)(c.db, row.rail_index);
|
|
1005
|
+
if (!current || current.id !== row.id) {
|
|
1006
|
+
return {
|
|
1007
|
+
ok: false,
|
|
1008
|
+
status: 409,
|
|
1009
|
+
error: 'stale_decision',
|
|
1010
|
+
detail: 'The delivery generation changed before checkout.',
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const currentSnap = (0, rail_pr_store_1.toPrDeliverySnapshot)(current);
|
|
1014
|
+
const currentCheckoutBlock = prDeliveryCheckoutBlock(currentSnap);
|
|
1015
|
+
if (currentCheckoutBlock) {
|
|
1016
|
+
return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
status: 409,
|
|
1019
|
+
error: 'checkout_not_deliverable',
|
|
1020
|
+
detail: currentCheckoutBlock,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
if (!currentSnap.branch || !currentSnap.prUrl) {
|
|
1024
|
+
return {
|
|
1025
|
+
ok: false,
|
|
1026
|
+
status: 409,
|
|
1027
|
+
error: 'checkout_not_deliverable',
|
|
1028
|
+
detail: 'The current delivery no longer has an attached PR branch.',
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const cleanliness = await (0, project_git_1.inspectProjectCheckoutCleanliness)(c.project.path);
|
|
1032
|
+
if (!cleanliness.ok) {
|
|
1033
|
+
return {
|
|
1034
|
+
ok: false,
|
|
1035
|
+
status: 409,
|
|
1036
|
+
error: 'checkout_safety_unknown',
|
|
1037
|
+
detail: cleanliness.detail,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (!cleanliness.clean) {
|
|
1041
|
+
return { ok: false, status: 409, error: 'checkout_dirty', detail: 'Working tree has uncommitted changes. Commit or stash them before checkout.' };
|
|
1042
|
+
}
|
|
979
1043
|
const info = await (0, project_git_1.getProjectGitInfo)(c.project.path);
|
|
980
1044
|
if (!info.git) {
|
|
981
1045
|
return { ok: false, status: 409, error: 'checkout_unavailable', detail: 'project is not a git repository' };
|
|
982
1046
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
await (0, rail_worktree_release_1.releaseRailWorktrees)({
|
|
1047
|
+
let archived = false;
|
|
1048
|
+
const cleanupWarnings = await (0, rail_worktree_release_1.releaseRailWorktrees)({
|
|
987
1049
|
db: c.db,
|
|
988
1050
|
git: worktree_manager_1.defaultGitRunner,
|
|
989
1051
|
repoDir: c.project.path,
|
|
990
|
-
worktreeIds:
|
|
1052
|
+
worktreeIds: currentSnap.worktreeIds,
|
|
1053
|
+
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(currentSnap.branches),
|
|
1054
|
+
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(currentSnap.branches),
|
|
1055
|
+
onSafetyArchive: (archive) => {
|
|
1056
|
+
archived = true;
|
|
1057
|
+
if (!(0, rail_pr_store_1.appendPrDeliverySafetyArchive)(c.db, row.id, archive)) {
|
|
1058
|
+
throw new Error(`delivery ${row.id} disappeared while recording safety archive ${archive}`);
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
991
1061
|
});
|
|
992
|
-
|
|
1062
|
+
if (archived) {
|
|
1063
|
+
const archivedRow = (0, rail_pr_store_1.getPrDelivery)(c.db, row.id);
|
|
1064
|
+
if (archivedRow) {
|
|
1065
|
+
const archivedSnap = (0, rail_pr_store_1.toPrDeliverySnapshot)(archivedRow);
|
|
1066
|
+
c.broadcast((0, rail_pr_store_1.toRailPrStateMessage)(c.project.id, archivedSnap));
|
|
1067
|
+
if (archivedRow.origin_conversation_id) {
|
|
1068
|
+
(0, agent_chat_registry_1.getAgentChatManager)()?.updatePrDecisionCard(archivedRow.origin_conversation_id, (0, rail_pr_store_1.toPrDecisionCardEnvelope)(c.project.id, archivedSnap));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (cleanupWarnings.length > 0) {
|
|
1073
|
+
return {
|
|
1074
|
+
ok: false,
|
|
1075
|
+
status: 409,
|
|
1076
|
+
error: 'checkout_unavailable',
|
|
1077
|
+
detail: cleanupWarnings[0],
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
const checkedOut = await (0, project_git_1.checkoutProjectReviewBranch)(c.project.path, currentSnap.branch, currentSnap.deliverySha);
|
|
993
1081
|
if (!checkedOut.ok) {
|
|
994
1082
|
return { ok: false, status: 409, error: 'checkout_failed', detail: checkedOut.error };
|
|
995
1083
|
}
|