specrails-desktop 2.29.2 → 2.29.4
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-CDMzpGXd.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-BVTacapZ.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-D9bhFyuy.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-DAjN8Au5.js} +1 -1
- package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-CebykajE.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BiPrVvgv.js} +1 -1
- package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Dw9eQFRS.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-cCG_JAEi.js} +1 -1
- package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-C1kZ_mRP.js} +1 -1
- package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-3GivxWJm.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-KuZXGdr8.js} +1 -1
- package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DWpRALkB.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-mrXDKafj.js} +1 -1
- package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-CUYXjcoc.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-CwjBD5Gm.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-CO9ton0-.js} +1 -1
- package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-CLL5UuvN.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext-DJAkRYps.js} +1 -1
- package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-DZvCC5Ri.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-C7Hesadg.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-DczCzUzP.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-CFh7GOv5.js} +4 -4
- package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-Cm9rhdnG.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-CHCEkds4.js +1 -0
- package/client/dist/assets/dashboard-Cqmmbh3l2.js +1 -0
- package/client/dist/assets/dashboard-CrlTV1ms.js +1 -0
- package/client/dist/assets/dashboard-Cslu_GCH.js +1 -0
- package/client/dist/assets/dashboard-DUU8-_Rg.js +1 -0
- package/client/dist/assets/dashboard-DweT3Y4R.js +1 -0
- package/client/dist/assets/dashboard-Dy2Um-US2.js +1 -0
- package/client/dist/assets/dashboard-WhzOQw1S.js +1 -0
- package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-BERxOT0X.js} +1 -1
- package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-Di8lflPo.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-BAXZRbEO.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DwzZXHtg.js} +1 -1
- package/client/dist/assets/{i18n-CgaRxpII.js → i18n-cK5pPbj8.js} +2 -2
- package/client/dist/assets/index-BT6xDQjB.css +2 -0
- package/client/dist/assets/index-CCq_kIrF.js +76 -0
- package/client/dist/assets/{spending-C4p06nen.js → spending-BLb5pj6G.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-DR1_jL56.js} +1 -1
- package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-D4DxBGPU.js} +1 -1
- package/client/dist/index.html +14 -14
- package/docs/internals/safe-pr-review-flow.md +250 -38
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +352 -53
- package/server/dist/agent-chat-manager.js +29 -12
- package/server/dist/agent-operator-prompt.js +9 -0
- package/server/dist/agent-store.js +19 -4
- package/server/dist/db.js +21 -0
- package/server/dist/mcp/tools/rails.js +8 -0
- package/server/dist/pr-lifecycle.js +100 -8
- package/server/dist/project-git.js +68 -3
- package/server/dist/rail-isolated-launch.js +719 -124
- package/server/dist/rail-pr-decision.js +1073 -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 +376 -4
- package/server/dist/rails-router.js +196 -10
- package/server/dist/worktree-manager.js +149 -18
- 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,329 @@
|
|
|
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;
|
|
38
|
+
exports.durableSettlementIgnoredPaths = durableSettlementIgnoredPaths;
|
|
3
39
|
exports.releaseRailWorktrees = releaseRailWorktrees;
|
|
4
40
|
const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
5
41
|
const worktree_manager_1 = require("./worktree-manager");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const worktree_overlay_1 = require("./worktree-overlay");
|
|
45
|
+
const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
46
|
+
/** Collapse per-unit settlement evidence into one unambiguous immutable HEAD per
|
|
47
|
+
* branch. Conflicting or malformed records intentionally yield no authorization. */
|
|
48
|
+
function durableBranchHeads(records) {
|
|
49
|
+
const heads = new Map();
|
|
50
|
+
const conflicted = new Set();
|
|
51
|
+
for (const record of records) {
|
|
52
|
+
const sha = record.finalSha?.trim().toLowerCase() ?? '';
|
|
53
|
+
if (!record.branch || !COMMIT_SHA_RE.test(sha) || conflicted.has(record.branch))
|
|
54
|
+
continue;
|
|
55
|
+
const existing = heads.get(record.branch);
|
|
56
|
+
if (existing && existing !== sha) {
|
|
57
|
+
heads.delete(record.branch);
|
|
58
|
+
conflicted.add(record.branch);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
heads.set(record.branch, sha);
|
|
62
|
+
}
|
|
63
|
+
return heads;
|
|
64
|
+
}
|
|
65
|
+
function safeRelativeOverlayPath(value) {
|
|
66
|
+
if (process.platform === 'win32' && value.includes('\\'))
|
|
67
|
+
return null;
|
|
68
|
+
if (value.split('/').includes('..'))
|
|
69
|
+
return null;
|
|
70
|
+
const normalized = path.posix.normalize(value);
|
|
71
|
+
if (!normalized || normalized === '.' || normalized.startsWith('../') ||
|
|
72
|
+
path.posix.isAbsolute(normalized) || normalized.includes('\0'))
|
|
73
|
+
return null;
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
/** Collapse persisted overlay fingerprints. Conflicting evidence for the same
|
|
77
|
+
* branch/path grants no exclusion permission. */
|
|
78
|
+
function durableOverlayCleanupEvidence(records) {
|
|
79
|
+
const byBranch = new Map();
|
|
80
|
+
const conflicted = new Map();
|
|
81
|
+
for (const record of records) {
|
|
82
|
+
if (!record.branch || !Array.isArray(record.overlayCleanupEvidence))
|
|
83
|
+
continue;
|
|
84
|
+
const branchEvidence = byBranch.get(record.branch) ?? new Map();
|
|
85
|
+
const branchConflicts = conflicted.get(record.branch) ?? new Set();
|
|
86
|
+
for (const value of record.overlayCleanupEvidence) {
|
|
87
|
+
if (!value || typeof value.path !== 'string' || typeof value.digest !== 'string')
|
|
88
|
+
continue;
|
|
89
|
+
if (value.kind !== 'symlink' && value.kind !== 'file' && value.kind !== 'directory')
|
|
90
|
+
continue;
|
|
91
|
+
const safe = safeRelativeOverlayPath(value.path);
|
|
92
|
+
if (!safe || !/^[0-9a-f]{64}$/.test(value.digest) || branchConflicts.has(safe))
|
|
93
|
+
continue;
|
|
94
|
+
const normalized = { path: safe, kind: value.kind, digest: value.digest };
|
|
95
|
+
const existing = branchEvidence.get(safe);
|
|
96
|
+
if (existing && (existing.kind !== normalized.kind || existing.digest !== normalized.digest)) {
|
|
97
|
+
branchEvidence.delete(safe);
|
|
98
|
+
branchConflicts.add(safe);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
branchEvidence.set(safe, normalized);
|
|
102
|
+
}
|
|
103
|
+
byBranch.set(record.branch, branchEvidence);
|
|
104
|
+
conflicted.set(record.branch, branchConflicts);
|
|
105
|
+
}
|
|
106
|
+
return new Map([...byBranch].map(([branch, values]) => [branch, [...values.values()]]));
|
|
107
|
+
}
|
|
108
|
+
/** Collapse persisted settlement ignored-path snapshots per branch. The same
|
|
109
|
+
* conservative rules as overlay evidence: malformed paths, a failed capture
|
|
110
|
+
* (null), or conflicting per-branch snapshots grant NO authorization. */
|
|
111
|
+
function durableSettlementIgnoredPaths(records) {
|
|
112
|
+
const byBranch = new Map();
|
|
113
|
+
for (const record of records) {
|
|
114
|
+
if (!record.branch || record.settlementIgnoredPaths === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
const prior = byBranch.get(record.branch);
|
|
117
|
+
if (prior === null)
|
|
118
|
+
continue; // already poisoned
|
|
119
|
+
if (record.settlementIgnoredPaths === null) {
|
|
120
|
+
byBranch.set(record.branch, null);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(record.settlementIgnoredPaths)) {
|
|
124
|
+
byBranch.set(record.branch, null);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const safe = [];
|
|
128
|
+
let valid = true;
|
|
129
|
+
for (const value of record.settlementIgnoredPaths) {
|
|
130
|
+
const normalized = typeof value === 'string' ? safeRelativeOverlayPath(value.replace(/\/$/, '')) : null;
|
|
131
|
+
if (!normalized) {
|
|
132
|
+
valid = false;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
safe.push(normalized);
|
|
136
|
+
}
|
|
137
|
+
if (!valid) {
|
|
138
|
+
byBranch.set(record.branch, null);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const sorted = [...new Set(safe)].sort();
|
|
142
|
+
if (prior === undefined) {
|
|
143
|
+
byBranch.set(record.branch, sorted);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (prior.join('\0') !== sorted.join('\0'))
|
|
147
|
+
byBranch.set(record.branch, null);
|
|
148
|
+
}
|
|
149
|
+
return new Map([...byBranch].flatMap(([branch, paths]) => (paths === null ? [] : [[branch, paths]])));
|
|
150
|
+
}
|
|
151
|
+
function coveredBySnapshot(livePath, snapshot) {
|
|
152
|
+
const live = livePath.replace(/\/$/, '');
|
|
153
|
+
return snapshot.some((entry) => live === entry || live.startsWith(`${entry}/`));
|
|
154
|
+
}
|
|
155
|
+
async function verifyReleaseEvidence(input, worktree) {
|
|
156
|
+
const expected = input.expectedHeadByBranch.get(worktree.branch)?.toLowerCase();
|
|
157
|
+
if (!expected || !COMMIT_SHA_RE.test(expected)) {
|
|
158
|
+
return { failure: 'no durable settled HEAD is recorded for its branch', overlayEntries: [] };
|
|
159
|
+
}
|
|
160
|
+
const overlayEntries = (input.overlayEvidenceByBranch.get(worktree.branch) ?? [])
|
|
161
|
+
.filter((entry) => (0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry));
|
|
162
|
+
const overlayExcludes = overlayEntries.map((entry) => entry.path);
|
|
163
|
+
try {
|
|
164
|
+
const status = await input.git.run([
|
|
165
|
+
'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
|
|
166
|
+
...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
|
|
167
|
+
], worktree.worktree_path);
|
|
168
|
+
if (status.code !== 0) {
|
|
169
|
+
if (!pathStillExists(worktree.worktree_path))
|
|
170
|
+
return { failure: null, overlayEntries: [], gone: true };
|
|
171
|
+
return { failure: 'Git could not verify worktree cleanliness', overlayEntries: [] };
|
|
172
|
+
}
|
|
173
|
+
// Partition: tracked/untracked entries are unconditional dirt (unchanged
|
|
174
|
+
// strictness); gitignored entries are release-safe ONLY when the immutable
|
|
175
|
+
// settlement snapshot already contained them — run-created build caches
|
|
176
|
+
// release cleanly, anything ignored that APPEARED after settlement (a
|
|
177
|
+
// user-parked file) preserves the worktree exactly as before.
|
|
178
|
+
const statusLines = status.stdout.split('\n').map((line) => line.trimEnd()).filter(Boolean);
|
|
179
|
+
const dirtyLines = statusLines.filter((line) => !line.startsWith('!! '));
|
|
180
|
+
if (dirtyLines.length > 0)
|
|
181
|
+
return { failure: 'the worktree contains changes made after settlement', overlayEntries: [] };
|
|
182
|
+
const ignoredPaths = statusLines.filter((line) => line.startsWith('!! ')).map((line) => line.slice(3));
|
|
183
|
+
if (ignoredPaths.length > 0) {
|
|
184
|
+
const snapshot = input.settlementIgnoredByBranch?.get(worktree.branch);
|
|
185
|
+
if (!snapshot) {
|
|
186
|
+
return { failure: 'the worktree contains ignored paths and no settlement snapshot authorizes them', overlayEntries: [] };
|
|
187
|
+
}
|
|
188
|
+
const uncovered = ignoredPaths.filter((livePath) => !coveredBySnapshot(livePath, snapshot));
|
|
189
|
+
if (uncovered.length > 0) {
|
|
190
|
+
return { failure: `the worktree contains ignored paths that appeared after settlement (${uncovered.slice(0, 3).join(', ')})`, overlayEntries: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const [head, branchRef] = await Promise.all([
|
|
194
|
+
input.git.run(['rev-parse', '--verify', 'HEAD'], worktree.worktree_path),
|
|
195
|
+
input.git.run(['rev-parse', '--verify', `refs/heads/${worktree.branch}`], input.repoDir),
|
|
196
|
+
]);
|
|
197
|
+
const headSha = head.code === 0 ? head.stdout.trim().toLowerCase() : '';
|
|
198
|
+
const branchSha = branchRef.code === 0 ? branchRef.stdout.trim().toLowerCase() : '';
|
|
199
|
+
if (!COMMIT_SHA_RE.test(headSha) || !COMMIT_SHA_RE.test(branchSha)) {
|
|
200
|
+
return { failure: 'Git could not verify the worktree HEAD and recorded branch ref', overlayEntries: [] };
|
|
201
|
+
}
|
|
202
|
+
if (headSha !== expected || branchSha !== expected) {
|
|
203
|
+
return { failure: `HEAD/ref moved after settlement (expected ${expected.slice(0, 12)})`, overlayEntries: [] };
|
|
204
|
+
}
|
|
205
|
+
return { failure: null, overlayEntries };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (!pathStillExists(worktree.worktree_path))
|
|
209
|
+
return { failure: null, overlayEntries: [], gone: true };
|
|
210
|
+
return { failure: `verification failed: ${err instanceof Error ? err.message : String(err)}`, overlayEntries: [] };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function pathStillExists(target) {
|
|
214
|
+
try {
|
|
215
|
+
fs.lstatSync(target);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function quarantineLocationNotes(entries) {
|
|
223
|
+
return entries
|
|
224
|
+
.filter((entry) => pathStillExists(entry.quarantinePath))
|
|
225
|
+
.map((entry) => `overlay data is preserved at ${entry.quarantinePath}`);
|
|
226
|
+
}
|
|
227
|
+
/** Atomically move fingerprint-matching, untracked overlay roots out of the
|
|
228
|
+
* worktree. Quarantine is a unique sibling on the same filesystem. It is never
|
|
229
|
+
* automatically deleted or restored: writes through an already-open fd after
|
|
230
|
+
* rename remain recoverable, and a concurrently recreated source can never be
|
|
231
|
+
* overwritten by rollback. */
|
|
232
|
+
async function quarantineVerifiedOverlayEntries(input, worktree, evidence) {
|
|
233
|
+
const removable = [];
|
|
234
|
+
for (const entry of evidence) {
|
|
235
|
+
const target = path.join(worktree.worktree_path, ...entry.path.split('/'));
|
|
236
|
+
if (!pathStillExists(target))
|
|
237
|
+
continue;
|
|
238
|
+
const tracked = await input.git.run(['ls-files', '--error-unmatch', '--', entry.path], worktree.worktree_path);
|
|
239
|
+
if (tracked.code === 0)
|
|
240
|
+
continue;
|
|
241
|
+
if (tracked.code !== 1) {
|
|
242
|
+
return { failure: `Git could not verify ownership of overlay path ${entry.path}`, entries: [] };
|
|
243
|
+
}
|
|
244
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry)) {
|
|
245
|
+
return { failure: `overlay path ${entry.path} changed after cleanup verification`, entries: [] };
|
|
246
|
+
}
|
|
247
|
+
removable.push(entry);
|
|
248
|
+
}
|
|
249
|
+
const cleanupRoots = [];
|
|
250
|
+
for (const entry of [...removable].sort((a, b) => a.path.split('/').length - b.path.split('/').length)) {
|
|
251
|
+
if (cleanupRoots.some((root) => root.kind === 'directory' && entry.path.startsWith(`${root.path}/`)))
|
|
252
|
+
continue;
|
|
253
|
+
cleanupRoots.push(entry);
|
|
254
|
+
}
|
|
255
|
+
if (cleanupRoots.length === 0)
|
|
256
|
+
return { failure: null, entries: [] };
|
|
257
|
+
let quarantineRoot;
|
|
258
|
+
try {
|
|
259
|
+
// mkdtemp creates the batch directory atomically beside the worktree. The
|
|
260
|
+
// ensuing rename therefore remains within one filesystem.
|
|
261
|
+
quarantineRoot = fs.mkdtempSync(`${worktree.worktree_path}.specrails-overlay-quarantine-`);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
return {
|
|
265
|
+
failure: `could not create overlay quarantine beside ${worktree.worktree_path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
266
|
+
entries: [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await input.onSafetyArchive?.(quarantineRoot);
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
return {
|
|
274
|
+
failure: `could not record safety archive ${quarantineRoot}: ${err instanceof Error ? err.message : String(err)}`,
|
|
275
|
+
entries: [],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const quarantined = [];
|
|
279
|
+
for (const entry of cleanupRoots) {
|
|
280
|
+
const sourcePath = path.join(worktree.worktree_path, ...entry.path.split('/'));
|
|
281
|
+
if (!pathStillExists(sourcePath))
|
|
282
|
+
continue;
|
|
283
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidenceAtPath)(sourcePath, entry)) {
|
|
284
|
+
return { failure: `overlay path ${entry.path} changed before quarantine`, entries: quarantined };
|
|
285
|
+
}
|
|
286
|
+
const quarantinePath = path.join(quarantineRoot, ...entry.path.split('/'));
|
|
287
|
+
try {
|
|
288
|
+
fs.mkdirSync(path.dirname(quarantinePath), { recursive: true });
|
|
289
|
+
await input.beforeOverlayQuarantine?.(sourcePath);
|
|
290
|
+
fs.renameSync(sourcePath, quarantinePath);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
return {
|
|
294
|
+
failure: `could not quarantine overlay path ${entry.path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
295
|
+
entries: quarantined,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const moved = { sourcePath, quarantinePath, evidence: entry };
|
|
299
|
+
quarantined.push(moved);
|
|
300
|
+
try {
|
|
301
|
+
await input.afterOverlayRename?.(sourcePath, quarantinePath);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
failure: `overlay post-rename hook failed at ${quarantinePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
306
|
+
entries: quarantined,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (!(0, worktree_overlay_1.matchesOverlayCleanupEvidenceAtPath)(quarantinePath, entry)) {
|
|
310
|
+
return {
|
|
311
|
+
failure: `overlay path ${entry.path} changed during atomic quarantine at ${quarantinePath}`,
|
|
312
|
+
entries: quarantined,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await input.afterOverlayQuarantine?.(quarantinePath);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return {
|
|
320
|
+
failure: `overlay quarantine hook failed at ${quarantinePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
321
|
+
entries: quarantined,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { failure: null, entries: quarantined };
|
|
326
|
+
}
|
|
6
327
|
/** Remove Specrails-managed linked worktrees while keeping their local branches.
|
|
7
328
|
* PR deliveries use this once the work has a durable remote branch/PR: the
|
|
8
329
|
* branch remains available for local checkout, but the internal worktree stops
|
|
@@ -15,19 +336,65 @@ async function releaseRailWorktrees(input) {
|
|
|
15
336
|
const wt = (0, rail_worktrees_store_1.getRailWorktree)(input.db, wtId);
|
|
16
337
|
if (!wt)
|
|
17
338
|
continue;
|
|
18
|
-
if (wt.merge_state === 'needs-review' && input.allowNeedsReview !== true)
|
|
19
|
-
continue;
|
|
20
339
|
// Successful removal already terminalized these rows. Re-running `git
|
|
21
340
|
// worktree remove` against their now-missing paths turns ordinary Publish /
|
|
22
341
|
// Poll / Discard follow-ups into false cleanup_incomplete warnings.
|
|
23
342
|
if (wt.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
|
|
24
343
|
continue;
|
|
344
|
+
const evidence = await verifyReleaseEvidence(input, wt);
|
|
345
|
+
// The mount is already GONE (the user ran `git worktree remove` manually,
|
|
346
|
+
// or an external prune cleaned it): Git could not even inspect the path
|
|
347
|
+
// AND the filesystem confirms it is absent. There are no bytes left to
|
|
348
|
+
// preserve — failing verification forever would permanently block Checkout
|
|
349
|
+
// on a branch that is actually free. Prune the stale registration
|
|
350
|
+
// (best-effort; prune only drops entries whose dir is missing) and
|
|
351
|
+
// terminalize the ledger row honestly.
|
|
352
|
+
if (evidence.gone) {
|
|
353
|
+
try {
|
|
354
|
+
await input.git.run(['worktree', 'prune'], input.repoDir);
|
|
355
|
+
}
|
|
356
|
+
catch { /* a leftover registration surfaces at checkout with git's own error */ }
|
|
357
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, state);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (evidence.failure) {
|
|
361
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
362
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${evidence.failure}`;
|
|
363
|
+
warnings.push(warning);
|
|
364
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const quarantine = await quarantineVerifiedOverlayEntries(input, wt, evidence.overlayEntries);
|
|
368
|
+
if (quarantine.failure) {
|
|
369
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
370
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${[
|
|
371
|
+
quarantine.failure,
|
|
372
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
373
|
+
].join('; ')}`;
|
|
374
|
+
warnings.push(warning);
|
|
375
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
// Quarantining changes the worktree. Re-prove both cleanliness and HEAD/ref
|
|
379
|
+
// identity immediately before asking Git for its final non-force guard.
|
|
380
|
+
const finalEvidence = await verifyReleaseEvidence(input, wt);
|
|
381
|
+
if (finalEvidence.failure) {
|
|
382
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
383
|
+
const warning = `worktree ${wt.worktree_path}: preserved because ${[
|
|
384
|
+
finalEvidence.failure,
|
|
385
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
386
|
+
].join('; ')}`;
|
|
387
|
+
warnings.push(warning);
|
|
388
|
+
console.warn(`[rail-worktree-release] ${warning}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
25
391
|
try {
|
|
26
392
|
await remove(input.git, {
|
|
27
393
|
repoDir: input.repoDir,
|
|
28
394
|
worktreePath: wt.worktree_path,
|
|
29
395
|
branch: wt.branch,
|
|
30
396
|
deleteBranch: false,
|
|
397
|
+
force: false,
|
|
31
398
|
});
|
|
32
399
|
}
|
|
33
400
|
catch (err) {
|
|
@@ -35,13 +402,18 @@ async function releaseRailWorktrees(input) {
|
|
|
35
402
|
if (current && current.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(current.merge_state)) {
|
|
36
403
|
continue;
|
|
37
404
|
}
|
|
38
|
-
|
|
405
|
+
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
406
|
+
const warning = `worktree ${wt.worktree_path}: ${[
|
|
407
|
+
err instanceof Error ? err.message : String(err),
|
|
408
|
+
...quarantineLocationNotes(quarantine.entries),
|
|
409
|
+
].join('; ')}`;
|
|
39
410
|
warnings.push(warning);
|
|
40
411
|
console.warn(`[rail-worktree-release] failed to remove ${warning}`);
|
|
41
412
|
continue;
|
|
42
413
|
}
|
|
43
|
-
if (!(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
|
|
414
|
+
if (wt.merge_state === 'needs-review' || !(0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state)) {
|
|
44
415
|
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, state);
|
|
416
|
+
}
|
|
45
417
|
}
|
|
46
418
|
return warnings;
|
|
47
419
|
}
|