specrails-desktop 2.29.3 → 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-CFB4UBhT.js → ActivityFeedPage-CDMzpGXd.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-C-T0r16B.js → AgentBrowserCapture-BVTacapZ.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-IljV5FOQ.js → AgentModeCodePane-D9bhFyuy.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-B5miH10_.js → AgentModeJobsPane-DAjN8Au5.js} +1 -1
- package/client/dist/assets/{AgentsPage-BgYQrAcH.js → AgentsPage-CebykajE.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-BtVCuNjL.js → AnalyticsPage-BiPrVvgv.js} +1 -1
- package/client/dist/assets/{CodePage-Cb_jAjSV.js → CodePage-Dw9eQFRS.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-s_Cc_JL1.js → DesktopAnalyticsPage-cCG_JAEi.js} +1 -1
- package/client/dist/assets/{DocsDialog-Cr47QI9L.js → DocsDialog-C1kZ_mRP.js} +1 -1
- package/client/dist/assets/{DocsPage-C_WPRuG_.js → DocsPage-3GivxWJm.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-NfFOciRB.js → InteractiveJobComposer-KuZXGdr8.js} +1 -1
- package/client/dist/assets/{JobDetailModal-DqEW7Z7K.js → JobDetailModal-DWpRALkB.js} +1 -1
- package/client/dist/assets/{JobDetailPage-D4ppEpDE.js → JobDetailPage-mrXDKafj.js} +1 -1
- package/client/dist/assets/{JobsPage-DStSoMyq.js → JobsPage-CUYXjcoc.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-DltCwS4P.js → LoopBuilderPage-CwjBD5Gm.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-BWaiAbUq.js → LoopPreviewModal-CO9ton0-.js} +1 -1
- package/client/dist/assets/{LoopsPage-BqjbXuBZ.js → LoopsPage-CLL5UuvN.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext--o3JAM9F.js → MinimizedChatsContext-DJAkRYps.js} +1 -1
- package/client/dist/assets/{PluginsPage-B-UOmzXw.js → PluginsPage-DZvCC5Ri.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-DkGajIxE.js → ProjectSettingsDialog-C7Hesadg.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-5CUXlkw7.js → TemplatePreviewModal-DczCzUzP.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-B_igqmti.js → TicketDetailModal-CFh7GOv5.js} +3 -3
- package/client/dist/assets/{Trans-DDMIBBUd.js → Trans-Cm9rhdnG.js} +1 -1
- package/client/dist/assets/{dashboard-DAr6F9et.js → dashboard-CHCEkds4.js} +1 -1
- package/client/dist/assets/{dashboard-D4Uj-XWc2.js → dashboard-Cqmmbh3l2.js} +1 -1
- package/client/dist/assets/{dashboard-BYzxom0L.js → dashboard-CrlTV1ms.js} +1 -1
- package/client/dist/assets/dashboard-Cslu_GCH.js +1 -0
- package/client/dist/assets/{dashboard-CnNtJBQ5.js → dashboard-DUU8-_Rg.js} +1 -1
- package/client/dist/assets/{dashboard-vrjZvxWy.js → dashboard-DweT3Y4R.js} +1 -1
- package/client/dist/assets/{dashboard-D57uRaGw2.js → dashboard-Dy2Um-US2.js} +1 -1
- package/client/dist/assets/{dashboard-vAcsn0L3.js → dashboard-WhzOQw1S.js} +1 -1
- package/client/dist/assets/{dist-js-DcoXTnEI.js → dist-js-BERxOT0X.js} +1 -1
- package/client/dist/assets/{dist-js-BCUhGUXS.js → dist-js-Di8lflPo.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-DP3Ab6fX.js → formatDistanceToNow-BAXZRbEO.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-DdKKL8Fx.js → getTimezoneOffsetInMilliseconds-DwzZXHtg.js} +1 -1
- package/client/dist/assets/{i18n-DREQCE2F.js → i18n-cK5pPbj8.js} +2 -2
- package/client/dist/assets/{index-CQBIemiz.css → index-BT6xDQjB.css} +1 -1
- package/client/dist/assets/index-CCq_kIrF.js +76 -0
- package/client/dist/assets/{spending-CtY7yYes.js → spending-BLb5pj6G.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-BFBdW59q.js → useBrowserCaptureSession-DR1_jL56.js} +1 -1
- package/client/dist/assets/{useDesktop-uWVZ8eia.js → useDesktop-D4DxBGPU.js} +1 -1
- package/client/dist/index.html +12 -12
- package/docs/internals/safe-pr-review-flow.md +32 -3
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +115 -1
- package/server/dist/agent-operator-prompt.js +9 -0
- package/server/dist/mcp/tools/rails.js +8 -0
- package/server/dist/project-git.js +7 -2
- package/server/dist/rail-isolated-launch.js +82 -23
- package/server/dist/rail-pr-decision.js +8 -0
- package/server/dist/rail-worktree-release.js +88 -2
- package/server/dist/rails-router.js +99 -1
- package/server/dist/worktree-manager.js +32 -5
- package/client/dist/assets/dashboard-BmDZmHyY.js +0 -1
- package/client/dist/assets/index-B784m8RT.js +0 -76
|
@@ -347,6 +347,7 @@ async function rerouteFromStalePr(deps, row, detail, exactWasPushed) {
|
|
|
347
347
|
worktreeIds: releasableWorktreeIds(deps, snap),
|
|
348
348
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
349
349
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
350
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
350
351
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
351
352
|
});
|
|
352
353
|
const partial = row.implementation_outcome === 'partially_succeeded';
|
|
@@ -386,6 +387,7 @@ async function settleObservedExistingPr(deps, row, observation, pushed) {
|
|
|
386
387
|
worktreeIds: releasableWorktreeIds(deps, snap),
|
|
387
388
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
388
389
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
390
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
389
391
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
390
392
|
});
|
|
391
393
|
const conflict = casTransition(deps, row, next, {
|
|
@@ -420,6 +422,7 @@ async function settleObservedClosedPr(deps, row, observation) {
|
|
|
420
422
|
worktreeIds: releasableWorktreeIds(deps, snap),
|
|
421
423
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
422
424
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
425
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
423
426
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
424
427
|
});
|
|
425
428
|
const conflict = casTransition(deps, row, 'pr_closed', {
|
|
@@ -1397,6 +1400,7 @@ async function runCreatePr(deps, row) {
|
|
|
1397
1400
|
db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: releasableWorktreeIds(deps, snap),
|
|
1398
1401
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(effectiveBranches),
|
|
1399
1402
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(effectiveBranches),
|
|
1403
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(effectiveBranches),
|
|
1400
1404
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
1401
1405
|
});
|
|
1402
1406
|
if (patch.cleanupWarnings.length > 0)
|
|
@@ -1537,6 +1541,7 @@ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
|
|
|
1537
1541
|
state: 'failed',
|
|
1538
1542
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
1539
1543
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
1544
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
1540
1545
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
1541
1546
|
}));
|
|
1542
1547
|
const retainedWorktreeBranches = new Set(snap.worktreeIds
|
|
@@ -1628,6 +1633,7 @@ async function runAcknowledgeNoChanges(deps, row) {
|
|
|
1628
1633
|
worktreeIds: snap.worktreeIds,
|
|
1629
1634
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
1630
1635
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
1636
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
1631
1637
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
1632
1638
|
});
|
|
1633
1639
|
// A fresh no-change branch is disposable only while Git still proves that it
|
|
@@ -1683,6 +1689,7 @@ async function settleMergedPr(deps, row) {
|
|
|
1683
1689
|
worktreeIds: releasableWorktreeIds(deps, snap), state: 'merged',
|
|
1684
1690
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
1685
1691
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
1692
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
1686
1693
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
1687
1694
|
});
|
|
1688
1695
|
await deleteOwnedBranchesIfUnchanged(deps, row, snap, cleanupWarnings);
|
|
@@ -2049,6 +2056,7 @@ async function runMergeLocalLocked(deps, row) {
|
|
|
2049
2056
|
state: 'merged',
|
|
2050
2057
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(snap.branches),
|
|
2051
2058
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(snap.branches),
|
|
2059
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(snap.branches),
|
|
2052
2060
|
onSafetyArchive: safetyArchiveRecorder(deps, row),
|
|
2053
2061
|
}));
|
|
2054
2062
|
await deleteOwnedBranchesIfUnchanged(deps, row, snap, cleanupWarnings);
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.durableBranchHeads = durableBranchHeads;
|
|
37
37
|
exports.durableOverlayCleanupEvidence = durableOverlayCleanupEvidence;
|
|
38
|
+
exports.durableSettlementIgnoredPaths = durableSettlementIgnoredPaths;
|
|
38
39
|
exports.releaseRailWorktrees = releaseRailWorktrees;
|
|
39
40
|
const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
40
41
|
const worktree_manager_1 = require("./worktree-manager");
|
|
@@ -104,6 +105,53 @@ function durableOverlayCleanupEvidence(records) {
|
|
|
104
105
|
}
|
|
105
106
|
return new Map([...byBranch].map(([branch, values]) => [branch, [...values.values()]]));
|
|
106
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
|
+
}
|
|
107
155
|
async function verifyReleaseEvidence(input, worktree) {
|
|
108
156
|
const expected = input.expectedHeadByBranch.get(worktree.branch)?.toLowerCase();
|
|
109
157
|
if (!expected || !COMMIT_SHA_RE.test(expected)) {
|
|
@@ -117,10 +165,31 @@ async function verifyReleaseEvidence(input, worktree) {
|
|
|
117
165
|
'status', '--porcelain', '--untracked-files=all', '--ignored=matching', '--', '.',
|
|
118
166
|
...overlayExcludes.map((entry) => `:(top,exclude,literal)${entry}`),
|
|
119
167
|
], worktree.worktree_path);
|
|
120
|
-
if (status.code !== 0)
|
|
168
|
+
if (status.code !== 0) {
|
|
169
|
+
if (!pathStillExists(worktree.worktree_path))
|
|
170
|
+
return { failure: null, overlayEntries: [], gone: true };
|
|
121
171
|
return { failure: 'Git could not verify worktree cleanliness', overlayEntries: [] };
|
|
122
|
-
|
|
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)
|
|
123
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
|
+
}
|
|
124
193
|
const [head, branchRef] = await Promise.all([
|
|
125
194
|
input.git.run(['rev-parse', '--verify', 'HEAD'], worktree.worktree_path),
|
|
126
195
|
input.git.run(['rev-parse', '--verify', `refs/heads/${worktree.branch}`], input.repoDir),
|
|
@@ -136,6 +205,8 @@ async function verifyReleaseEvidence(input, worktree) {
|
|
|
136
205
|
return { failure: null, overlayEntries };
|
|
137
206
|
}
|
|
138
207
|
catch (err) {
|
|
208
|
+
if (!pathStillExists(worktree.worktree_path))
|
|
209
|
+
return { failure: null, overlayEntries: [], gone: true };
|
|
139
210
|
return { failure: `verification failed: ${err instanceof Error ? err.message : String(err)}`, overlayEntries: [] };
|
|
140
211
|
}
|
|
141
212
|
}
|
|
@@ -271,6 +342,21 @@ async function releaseRailWorktrees(input) {
|
|
|
271
342
|
if (wt.merge_state !== 'needs-review' && (0, rail_worktrees_store_1.isTerminalMergeState)(wt.merge_state))
|
|
272
343
|
continue;
|
|
273
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
|
+
}
|
|
274
360
|
if (evidence.failure) {
|
|
275
361
|
(0, rail_worktrees_store_1.updateRailWorktreeState)(input.db, wt.id, 'needs-review');
|
|
276
362
|
const warning = `worktree ${wt.worktree_path}: preserved because ${evidence.failure}`;
|
|
@@ -21,6 +21,7 @@ const rail_isolation_1 = require("./rail-isolation");
|
|
|
21
21
|
const rail_pr_store_1 = require("./rail-pr-store");
|
|
22
22
|
const loop_effect_1 = require("./loop-effect");
|
|
23
23
|
const rail_pr_decision_1 = require("./rail-pr-decision");
|
|
24
|
+
const active_pr_continuation_1 = require("./active-pr-continuation");
|
|
24
25
|
const rail_isolated_launch_1 = require("./rail-isolated-launch");
|
|
25
26
|
const worktree_manager_1 = require("./worktree-manager");
|
|
26
27
|
const rail_worktree_release_1 = require("./rail-worktree-release");
|
|
@@ -277,6 +278,40 @@ function createRailsRouter() {
|
|
|
277
278
|
res.status(500).json({ error: 'Failed to update rail profile' });
|
|
278
279
|
}
|
|
279
280
|
});
|
|
281
|
+
// GET /rails/:railIndex/pr-candidates — display-only open-PR suggestions for
|
|
282
|
+
// the "deliver into existing PR" launch affordance. Reuses the automatic-
|
|
283
|
+
// continuation matchers WITHOUT the ticket-status gate: suggestions are
|
|
284
|
+
// informational, selection is always an explicit user action. No side effects.
|
|
285
|
+
router.get('/:railIndex/pr-candidates', async (req, res) => {
|
|
286
|
+
const c = ctx(req);
|
|
287
|
+
const railIndex = parseInt(req.params.railIndex, 10);
|
|
288
|
+
if (isNaN(railIndex) || railIndex < 0 || railIndex >= rails_store_1.MAX_RAILS) {
|
|
289
|
+
res.status(400).json({ error: 'Invalid rail index' });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const rail = (0, rails_store_1.getRail)(c.db, railIndex);
|
|
294
|
+
const candidates = await (0, active_pr_continuation_1.listPrCandidatesForTickets)({
|
|
295
|
+
db: c.db,
|
|
296
|
+
exec: pr_publisher_1.defaultExec,
|
|
297
|
+
repoDir: c.project.path,
|
|
298
|
+
ticketIds: [...(rail?.ticketIds ?? [])],
|
|
299
|
+
getTicketSpec: (ticketId) => {
|
|
300
|
+
try {
|
|
301
|
+
return c.getTicketSpec(ticketId) ?? undefined;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
res.json({ candidates });
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
console.error('[rails-router] pr-candidates error:', err);
|
|
312
|
+
res.status(500).json({ error: 'Failed to list PR candidates' });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
280
315
|
// PUT /rails/:railIndex/engine — set the AI engine override for a rail
|
|
281
316
|
// Body: { aiEngine: string | null } (null = use the project's primary provider)
|
|
282
317
|
router.put('/:railIndex/engine', (req, res) => {
|
|
@@ -352,7 +387,16 @@ function createRailsRouter() {
|
|
|
352
387
|
return;
|
|
353
388
|
}
|
|
354
389
|
let { mode = 'implement' } = req.body ?? {};
|
|
355
|
-
const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface } = req.body ?? {};
|
|
390
|
+
const { profileName, aiEngine, model, loopId: rawLoopId, reasoning_effort, originConversationId, originSurface, targetPrNumber } = req.body ?? {};
|
|
391
|
+
// Explicit target PR (deliver-rail-into-existing-pr): the user names an
|
|
392
|
+
// existing open PR as the delivery destination. Shape-validated here;
|
|
393
|
+
// resolved and verified inside the isolated launch, fail-closed.
|
|
394
|
+
if (targetPrNumber !== undefined && targetPrNumber !== null) {
|
|
395
|
+
if (typeof targetPrNumber !== 'number' || !Number.isSafeInteger(targetPrNumber) || targetPrNumber <= 0 || targetPrNumber > 1_000_000_000) {
|
|
396
|
+
res.status(400).json({ error: 'invalid_target_pr', detail: 'targetPrNumber must be a positive integer' });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
356
400
|
let loopId = rawLoopId;
|
|
357
401
|
// Origin link (safe-pr-review-flow): an agent-chat/MCP launch tags itself so
|
|
358
402
|
// the PR decision can later be posted back into the launching conversation.
|
|
@@ -395,6 +439,13 @@ function createRailsRouter() {
|
|
|
395
439
|
res.status(403).json({ error: 'Loops are disabled on this server' });
|
|
396
440
|
return;
|
|
397
441
|
}
|
|
442
|
+
// An explicit target PR is only meaningful on the isolated PR-delivery
|
|
443
|
+
// path. Reject early when that path is structurally unavailable rather
|
|
444
|
+
// than silently launching fresh work next to the designated PR.
|
|
445
|
+
if (typeof targetPrNumber === 'number' && (!(0, feature_flags_1.isLoopsEnabled)() || !(0, rail_isolation_1.isRailPrDeliveryEnabled)())) {
|
|
446
|
+
res.status(400).json({ error: 'target_pr_requires_pr_mode', detail: 'targetPrNumber requires loops and PR delivery to be enabled' });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
398
449
|
// Freestyle model picker: optional, validated against the allow-list.
|
|
399
450
|
// Ignored for non-freestyle modes (they use the orchestrator model).
|
|
400
451
|
if (mode === 'freestyle' && model !== undefined && model !== null) {
|
|
@@ -571,11 +622,23 @@ function createRailsRouter() {
|
|
|
571
622
|
res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
|
|
572
623
|
return;
|
|
573
624
|
}
|
|
625
|
+
// Explicit target vs an undecided continuable delivery: the slot's
|
|
626
|
+
// active generation owns its PR. A DIFFERENT explicit target would
|
|
627
|
+
// append to the undecided branches → 409; the SAME PR is redundant
|
|
628
|
+
// (the continuation contract below already drives it) and is dropped.
|
|
629
|
+
if (pendingSnapshot && typeof targetPrNumber === 'number' && pendingSnapshot.prNumber !== targetPrNumber) {
|
|
630
|
+
res.status(409).json({ error: 'pr_decision_pending', prDeliveryId: pendingSnapshot.id });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
574
633
|
continuablePrDelivery = pendingSnapshot;
|
|
575
634
|
}
|
|
576
635
|
const useIsolation = (0, rail_isolation_1.isolationApplies)({
|
|
577
636
|
loopsEnabled: (0, feature_flags_1.isLoopsEnabled)(), scope, ticketCount: rail.ticketIds.length, readOnly: loopReadOnly,
|
|
578
637
|
});
|
|
638
|
+
if (typeof targetPrNumber === 'number' && !useIsolation) {
|
|
639
|
+
res.status(400).json({ error: 'target_pr_requires_pr_mode', detail: 'targetPrNumber requires an isolated (worktree) launch; this launch would run in the shared checkout' });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
579
642
|
if (continuablePrDelivery && !loopReadOnly && !useIsolation) {
|
|
580
643
|
res.status(409).json({
|
|
581
644
|
error: 'pr_continuation_isolation_required',
|
|
@@ -589,6 +652,13 @@ function createRailsRouter() {
|
|
|
589
652
|
// unborn HEAD can't be branched). Fall back (with a message) otherwise.
|
|
590
653
|
const status = await (0, worktree_manager_1.repoIsolationStatus)(worktree_manager_1.defaultGitRunner, c.project.path);
|
|
591
654
|
if (status !== 'ok') {
|
|
655
|
+
if (typeof targetPrNumber === 'number') {
|
|
656
|
+
res.status(400).json({
|
|
657
|
+
error: 'target_pr_requires_pr_mode',
|
|
658
|
+
detail: `targetPrNumber requires worktree isolation, which is unavailable (${status})`,
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
592
662
|
if (continuablePrDelivery) {
|
|
593
663
|
res.status(409).json({
|
|
594
664
|
error: 'pr_continuation_isolation_required',
|
|
@@ -618,6 +688,12 @@ function createRailsRouter() {
|
|
|
618
688
|
deliverySha: continuablePrDelivery.deliverySha,
|
|
619
689
|
},
|
|
620
690
|
} : {}),
|
|
691
|
+
// Explicit target only drives fresh launches; when the slot has
|
|
692
|
+
// a continuable delivery for the same PR, the continuation
|
|
693
|
+
// contract above is the (stricter) authority.
|
|
694
|
+
...(typeof targetPrNumber === 'number' && !continuablePrDelivery
|
|
695
|
+
? { explicitPrTarget: { prNumber: targetPrNumber } }
|
|
696
|
+
: {}),
|
|
621
697
|
});
|
|
622
698
|
res.status(202).json({ loopRunIds: ids, railIndex, mode, isolated: true });
|
|
623
699
|
return;
|
|
@@ -630,6 +706,16 @@ function createRailsRouter() {
|
|
|
630
706
|
});
|
|
631
707
|
return;
|
|
632
708
|
}
|
|
709
|
+
// Explicit-target validation failures are the USER's answer being
|
|
710
|
+
// unusable — fail closed with the machine-readable code; never
|
|
711
|
+
// fall back to a fresh shared-cwd run next to the designated PR.
|
|
712
|
+
if (err instanceof active_pr_continuation_1.ExplicitPrTargetError) {
|
|
713
|
+
res.status(err.code === 'target_pr_not_found' ? 404 : 409).json({
|
|
714
|
+
error: err.code,
|
|
715
|
+
detail: err.message,
|
|
716
|
+
});
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
633
719
|
if (continuablePrDelivery || err instanceof rail_isolated_launch_1.PrContinuationIsolationError) {
|
|
634
720
|
const detail = err instanceof Error ? err.message : String(err);
|
|
635
721
|
res.status(409).json({
|
|
@@ -1052,6 +1138,7 @@ function createRailsRouter() {
|
|
|
1052
1138
|
worktreeIds: currentSnap.worktreeIds,
|
|
1053
1139
|
expectedHeadByBranch: (0, rail_worktree_release_1.durableBranchHeads)(currentSnap.branches),
|
|
1054
1140
|
overlayEvidenceByBranch: (0, rail_worktree_release_1.durableOverlayCleanupEvidence)(currentSnap.branches),
|
|
1141
|
+
settlementIgnoredByBranch: (0, rail_worktree_release_1.durableSettlementIgnoredPaths)(currentSnap.branches),
|
|
1055
1142
|
onSafetyArchive: (archive) => {
|
|
1056
1143
|
archived = true;
|
|
1057
1144
|
if (!(0, rail_pr_store_1.appendPrDeliverySafetyArchive)(c.db, row.id, archive)) {
|
|
@@ -1077,6 +1164,17 @@ function createRailsRouter() {
|
|
|
1077
1164
|
detail: cleanupWarnings[0],
|
|
1078
1165
|
};
|
|
1079
1166
|
}
|
|
1167
|
+
// Cleanup just completed with ZERO warnings — clear any stale
|
|
1168
|
+
// cleanup_incomplete evidence persisted by earlier failed release
|
|
1169
|
+
// attempts, so the card stops claiming "Cleanup is incomplete" after
|
|
1170
|
+
// the condition has been resolved. Same-state patch + live broadcast.
|
|
1171
|
+
if (currentSnap.cleanupWarnings.length > 0 || currentSnap.statusCode === 'cleanup_incomplete') {
|
|
1172
|
+
(0, rail_pr_store_1.transitionDecision)(c.db, row.id, currentSnap.decision, currentSnap.decision, {
|
|
1173
|
+
cleanupWarnings: [],
|
|
1174
|
+
...(currentSnap.statusCode === 'cleanup_incomplete' ? { statusCode: null } : {}),
|
|
1175
|
+
});
|
|
1176
|
+
emitPrDeliveryUpdate(c, row.id);
|
|
1177
|
+
}
|
|
1080
1178
|
const checkedOut = await (0, project_git_1.checkoutProjectReviewBranch)(c.project.path, currentSnap.branch, currentSnap.deliverySha);
|
|
1081
1179
|
if (!checkedOut.ok) {
|
|
1082
1180
|
return { ok: false, status: 409, error: 'checkout_failed', detail: checkedOut.error };
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PR_NEVER_STAGE_PATHS = exports.defaultGitRunner = void 0;
|
|
36
|
+
exports.PR_NEVER_STAGE_PATHSPEC_ROOTS = exports.PR_NEVER_STAGE_PATHS = exports.defaultGitRunner = void 0;
|
|
37
37
|
exports.isGitRepo = isGitRepo;
|
|
38
38
|
exports.repoIsolationStatus = repoIsolationStatus;
|
|
39
39
|
exports.worktreeBranch = worktreeBranch;
|
|
@@ -243,7 +243,7 @@ async function gitRun(git, args, cwd) {
|
|
|
243
243
|
function commitPathspecs(excludePaths) {
|
|
244
244
|
return [
|
|
245
245
|
'--', '.',
|
|
246
|
-
...exports.
|
|
246
|
+
...exports.PR_NEVER_STAGE_PATHSPEC_ROOTS.map((p) => `:(exclude)${p}`),
|
|
247
247
|
// Overlay names originate in the workspace filesystem. Treat them as
|
|
248
248
|
// literal top-level paths so glob/pathspec metacharacters cannot broaden
|
|
249
249
|
// the never-commit surface.
|
|
@@ -261,6 +261,23 @@ function normalizedGitPath(value) {
|
|
|
261
261
|
function isPathAtOrBelow(candidate, root) {
|
|
262
262
|
return candidate === root || candidate.startsWith(`${root}/`);
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Collapse a path list to its minimal roots — drop any entry that is at or
|
|
266
|
+
* below another. Overlay-owned entries like `.claude/agent-memory` are
|
|
267
|
+
* SYMLINKS (the overlay links shared memory in). A git pathspec that descends
|
|
268
|
+
* INTO a symlink — `:(exclude).claude/agent-memory/**` or the `/explanations`
|
|
269
|
+
* subpaths — is rejected with `fatal: pathspec ... is beyond a symbolic link`,
|
|
270
|
+
* which aborts the entire `git add`/`git status` and blocks PR delivery. The
|
|
271
|
+
* symlink ROOT alone (`:(exclude).claude/agent-memory`) still excludes all of
|
|
272
|
+
* its contents (dir-level pathspec semantics) without traversing the link, and
|
|
273
|
+
* is equally correct for the copy-fallback case where it is a real directory.
|
|
274
|
+
*/
|
|
275
|
+
function minimalPathRoots(paths) {
|
|
276
|
+
const norm = [...new Set(paths.map((p) => (p.endsWith('/**') ? p.slice(0, -3) : p)))];
|
|
277
|
+
return norm.filter((p) => !norm.some((other) => other !== p && isPathAtOrBelow(p, other)));
|
|
278
|
+
}
|
|
279
|
+
/** Symlink-safe never-stage pathspec roots (see {@link minimalPathRoots}). */
|
|
280
|
+
exports.PR_NEVER_STAGE_PATHSPEC_ROOTS = minimalPathRoots(exports.PR_NEVER_STAGE_PATHS);
|
|
264
281
|
function forbiddenStagedPaths(staged, excludePaths) {
|
|
265
282
|
const overlayRoots = excludePaths.map(normalizedGitPath).filter(Boolean);
|
|
266
283
|
return staged.filter((candidate) => (PR_NEVER_STAGE_ROOTS.some((root) => isPathAtOrBelow(candidate, root))
|
|
@@ -307,7 +324,17 @@ function describeForbiddenPaths(paths) {
|
|
|
307
324
|
*/
|
|
308
325
|
async function commitWorktreeAndVerify(git, worktreePath, message, excludePaths = []) {
|
|
309
326
|
const pathspecs = commitPathspecs(excludePaths);
|
|
310
|
-
|
|
327
|
+
// The add is deliberately PLAIN — no exclude pathspecs. `git add` exits 1
|
|
328
|
+
// with "The following paths are ignored by one of your .gitignore files"
|
|
329
|
+
// whenever ANY pathspec item (exclude items included) names a git-ignored
|
|
330
|
+
// path, and our own info/exclude block (ensurePrNeverStageExcludes) plus repo
|
|
331
|
+
// .gitignore entries (e.g. `.claude/settings.local.json`) make exactly the
|
|
332
|
+
// excluded paths ignored. Ignored paths can never be staged without `-f`, so
|
|
333
|
+
// the plain add is safe for them; non-ignored overlay files DO get staged
|
|
334
|
+
// here and are unstaged by the audit → reset flow below before the commit,
|
|
335
|
+
// whose `--only` pathspecs (which git commit accepts without the
|
|
336
|
+
// ignored-path check) remain the authoritative exclusion.
|
|
337
|
+
const add = await gitRun(git, ['add', '-A', '--', '.'], worktreePath);
|
|
311
338
|
let indexSafe = false;
|
|
312
339
|
let indexError;
|
|
313
340
|
if (add.code === 0) {
|
|
@@ -319,8 +346,8 @@ async function commitWorktreeAndVerify(git, worktreePath, message, excludePaths
|
|
|
319
346
|
indexSafe = true;
|
|
320
347
|
}
|
|
321
348
|
else {
|
|
322
|
-
//
|
|
323
|
-
// earlier process may
|
|
349
|
+
// The plain add routinely stages non-ignored overlay files, and an
|
|
350
|
+
// earlier process may have put private paths in the index. Reset only the
|
|
324
351
|
// prohibited roots back to HEAD: working files remain intact for recovery.
|
|
325
352
|
const reset = await gitRun(git, ['reset', '--quiet', ...forbiddenResetPathspecs(excludePaths)], worktreePath);
|
|
326
353
|
if (reset.code !== 0) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as e}from"./rolldown-runtime-CNC7AqOf.js";var t=e({default:()=>d,freestyleDialog:()=>l,launchAll:()=>a,launchAllDialog:()=>o,railControls:()=>s,railPr:()=>r,railRow:()=>c,railsBoard:()=>i,toasts:()=>n,useRails:()=>u}),n={railAdded:`Rail {{n}} added`,railRemoved:`{{rail}} removed`,railLimitReached:`Rail limit reached ({{max}} max)`,railDeleteFailed:`Couldn't remove the rail — it holds specs, is running, or has a pending PR decision`,launchTicketsInFlight:`A spec on this rail is already being worked by an active run`,launchAllSummary_one:`{{count}} rail launched in parallel`,launchAllSummary_other:`{{count}} rails launched in parallel`,launchAllNoneEligible:`No rails ready to launch — assign specs to an idle rail first`,launchAllFailed:`No rails could be launched`,railCompleted:`Rail {{n}} completed`,railFailed:`Rail {{n}} failed — specs returned to Specs`,railEnded:`Rail {{n}} {{status}} — specs returned to Specs`,alreadyOnRail:`Already on {{rail}}`,movedToRail:`Moved to {{rail}}`,railRunningStopFirst:`{{rail}} is running — stop it before removing`,removedFromRail:`Removed from {{rail}}`,queuedOnRail:`Queued on {{rail}}`,queuedOnRailDescription:`Rail is currently running`,railStopped:`{{rail}} stopped`,stopFailed:`Failed to stop rail`,syncTicketsFailed:`Failed to sync rail tickets`,launchFailed:`Failed to launch rail`,launchNetworkError:`Network error launching rail`,railLaunched:`{{rail}} launched`,launchDescription_one:`{{mode}} with {{count}} spec`,launchDescription_other:`{{mode}} with {{count}} specs`,railWorktreeNeedsReview:`Rail {{n}}: spec #{{ticket}} needs review after merge`,railWorktreesNoGit:`Worktree isolation unavailable: this project isn't a git repository — running in shared mode.`,railWorktreesNoCommits:`Worktree isolation unavailable: the repository has no commits yet — make an initial commit, then relaunch.`,railWorktreesError:`Worktree isolation failed — this run continues WITHOUT the PR review flow (no implementation card). See the error detail below.`,onReviewCannotMoveToRail:`This spec is awaiting PR review — resolve the PR decision first`,onReviewNotLaunchable:`A spec on this rail is awaiting PR review — resolve the PR decision before launching`},r={operation:{inProgress:`Action in progress…`,"create-pr":`Creating PR…`,publish:`Publishing…`,discard:`Discarding…`,dismiss:`Dismissing follow-up…`,"poll-merge":`Checking merge…`,reopen:`Reopening…`,"merge-local":`Integrating locally…`,"recover-and-retry":`Committing and retrying the preserved result…`,"acknowledge-no-changes":`Marking done…`},readyForReview:`Ready for review`,draftPr:`Draft PR`,branchPushed:`Branch pushed — PR pending`,branchPushedHint:`The branch was pushed but the PR couldn't be created (GitHub CLI unavailable). Retry to create it.`,localOnly:`Changes available locally`,localOnlyHint:`The implementation completed, but the changes have not been pushed. Integrate them locally or retry publishing when the remote is available.`,noChanges:`No changes needed`,markDone:`Mark done`,refine:`Refine`,completedToast:`Confirmed — specs moved to Done`,markDoneConfirmTitle:`Mark these specs done?`,markDoneConfirmBody:`The implementation confirmed that no code changes are needed. The specs will move to Done and the no-change worktree will be cleaned up.`,refineConfirmTitle:`Refine these specs?`,refineConfirmBody:`The no-change result will be closed and the specs will return to the backlog so you can refine them. The run logs remain available.`,partial:`{{succeeded}} of {{total}} implementations ready`,createPartialPr:`Create PR with {{count}}`,deliveryBlocked:`Implementation complete — delivery needs attention`,updateFailed:`Implementation complete — update failed`,retryPush:`Retry push`,retryPushVerified:`Push verified · commit {{sha}} is in the PR`,commitAlreadyVerified:`Commit {{sha}} is already verified in the PR`,deliveryCommit:`commit {{sha}}`,prClosed:`PR closed without merge`,reopen:`Reopen`,iteratingPr:`Updating PR`,prReady:`PR ready`,implementationFailed:`Implementation failed`,implementationFailedHint:`The implementation run failed. Open the run log, then discard this card before relaunching.`,prFailed:`PR failed`,prFailedDetail:`PR failed: {{detail}}`,createDegraded:`Couldn't publish the PR — {{detail}}. Retry when resolved.`,createPr:`Create PR`,createPrTooltip:`→ {{base}}`,publish:`Publish`,publishTooltip:`Open this draft PR for team review`,checkout:`Checkout`,checkoutTooltip:`Check out {{branch}} in the main project folder`,checkoutSuccess:`Checked out {{branch}}`,checkoutFailed:`Could not check out the PR branch`,checkoutDirtyTitle:`Checkout blocked to protect your changes`,checkoutDirtyBody:`The main project folder has uncommitted changes. Commit or stash them, then retry. Nothing was changed.`,checkoutSafetyUnknownTitle:`Checkout paused because safety could not be verified`,checkoutSafetyUnknownBody:`Specrails could not read the main project folder’s Git status. Nothing was released or changed. Retry after Git is available.`,checkoutNotDeliverableTitle:`This PR branch is not the preserved implementation`,checkoutNotDeliverableBody:`No exact delivery commit has been verified yet. Inspect or recover the local result instead.`,inspectLocalResult:`Inspect local result`,inspectLocalResultTooltip:`Reveal the preserved isolated worktree and copy its path`,localResultPathCopied:`Local result path copied`,localResultPath:`Preserved local result`,localResultUnavailableHere:`No recoverable worktree is available on this computer. Review the run log from the machine where it executed.`,recoverAndRetry:`Commit & retry push`,recoverAndRetryConfirmTitle:`Commit and push the preserved result?`,recoverAndRetryConfirmBody:`Specrails will use only the isolated worktree or recorded fast-forward branch for {{branch}}. It will commit deliverable files if needed, then push the exact commit only if the PR is still open on the expected head and base. Your main project folder and its uncommitted changes will not be touched.`,recoverAndRetryVerified:`Recovered commit {{sha}} is verified in the PR`,recoveryStillBlocked:`Local recovery still needs attention`,recheckRecovery:`Check again`,recheckRecoveryConfirmTitle:`Check this computer again?`,recheckRecoveryConfirmBody:`Specrails will rescan this clone’s delivery-owned worktree, branch, refs, reflogs, and orphan Git objects for {{branch}}. If it proves one result, it may commit isolated changes and retry the exact push. Your main project folder will not be touched.`,recoveryUnavailable:`Recovery could not be verified on this computer`,recoveryUnavailableBody:`Specrails could not safely prove one delivery-owned result in this clone. Nothing was changed or removed. Inspect the stage detail and run logs. If the run executed on another computer, open Specrails there, then check again.`,recoveryDetailSummary:`Technical recovery detail`,discard:`Discard`,dismiss:`Dismiss follow-up`,discardLocalResult:`Discard local result`,discardTooltip:`Close the delivery and return its tickets to Specs; changed local work is preserved`,retryPr:`Retry PR`,retry:`Retry`,checkMerge:`Check merge`,verifyPr:`Verify PR`,verifyPrTooltip:`Verify the exact commit in GitHub and check whether the PR is merged`,checkMergeTooltip:`Ask GitHub whether the PR has been merged`,prLinkFallback:`PR`,openPrTitle:`Open the PR on GitHub`,notMergedYet:`Not merged yet`,commitMissingRetry:`The delivery commit is no longer in the PR. Retry push is available again.`,prVerifiedWaiting:`Commit {{sha}} verified · waiting for merge`,alreadyResolved:`Already resolved elsewhere`,ghFailed:`GitHub CLI failed: {{detail}}`,actionFailed:`Couldn't apply the PR decision`,mergedToast:`PR merged — tickets moved to Done`,discardedToast:`PR discarded — tickets returned to Specs`,decisionPending:`This rail has a pending PR decision — create, publish or discard it before relaunching`,discardConfirmTitle:`Discard this delivery?`,discardConfirmBody:`The PR will be closed without deleting its remote branch. Only SpecRails-owned local branches and worktrees that are still at their recorded commit will be removed; anything changed will be preserved with a warning. The tickets will return to the Specs backlog. Removed local resources cannot be recovered.`,implementationFailedDiscardTitle:`Discard this failed delivery?`,implementationFailedDiscardBody:`The card will be cleared and its tickets will return to Specs. Local work and branches will be kept for inspection. Only resources proven safe to clean up may be removed.`,dismissConfirmTitle:`Dismiss this follow-up?`,dismissConfirmBody:`This follow-up card will be cleared. Its owned worktree will be removed only if it is still safe; otherwise it will be preserved with a warning. The existing PR, its branch, and review state will remain unchanged.`,discardLocalConfirmTitle:`Discard the local result?`,discardLocalConfirmBody:`The recoverable local iteration will be removed only if it is still safe; if it changed, it will be preserved with a warning. Any existing PR and its branch will remain unchanged. A removed result cannot be recovered.`,mergeLocal:`Integrate locally`,mergeLocalTooltip:`Merge the delivered branches into {{base}} in your local checkout (no GitHub needed)`,mergeLocalConfirmTitle:`Integrate locally?`,mergeLocalConfirmBody:`The delivered branches will be merged into {{base}} in your local checkout. Your checkout must be on {{base}} with a clean working tree.`,mergeLocalBlockedDirty:`Cannot integrate: your working tree on {{base}} has uncommitted changes. Commit or stash them, then retry.`,mergeLocalBlockedBranch:`Cannot integrate: your checkout is on {{current}}, not {{base}}. Check out {{base}}, then retry.`,mergeLocalFailed:`Local merge failed: {{detail}}`,mergedLocally:`Integrated into {{base}} locally`,viewRunLog:`View run log for {{ref}}`,cleanupIncomplete_one:`Cleanup is incomplete ({{count}} warning)`,cleanupIncomplete_other:`Cleanup is incomplete ({{count}} warnings)`,cleanupIncompleteHint:`Local recovery data was preserved. Inspect these details before removing anything manually.`,copyCleanupDetails:`Copy recovery details`,safetyArchiveTitle:`Safety archive ({{count}})`,safetyArchiveHint:`Temporary workspace files were preserved outside the PR as a precaution. You can inspect or remove them manually.`,copySafetyArchive:`Copy paths`,unit:{failed:`implementation failed`,blocked:`delivery blocked`,noChanges:`no changes`,succeeded:`ready`}},i={title:`Rails`,runningCount_one:`{{count}} running`,runningCount_other:`{{count}} running`,launchAll:`Launch all`,launchAllTitle:`Launch every ready rail in parallel — each run is isolated in its own git worktree`},a={skipRunning_one:`{{count}} already running`,skipRunning_other:`{{count}} already running`,skipEmpty_one:`{{count}} without specs`,skipEmpty_other:`{{count}} without specs`,skipPendingDecision_one:`{{count}} awaiting a PR decision`,skipPendingDecision_other:`{{count}} awaiting a PR decision`,skipOnReview_one:`{{count}} with specs on review`,skipOnReview_other:`{{count}} with specs on review`,skipInFlight_one:`{{count}} skipped (spec already in flight)`,skipInFlight_other:`{{count}} skipped (specs already in flight)`,failedCount_one:`{{count}} failed`,failedCount_other:`{{count}} failed`},o={title:`Launch all rails`,railsCount_one:`{{count}} rail`,railsCount_other:`{{count}} rails`,specsCount_one:`{{count}} spec`,specsCount_other:`{{count}} specs`,isolationTitle:`Parallel & isolated.`,isolationBody:`Each rail runs in its own git worktree — concurrent launches never collide.`,pipelinesTitle_one:`{{count}} AI pipeline.`,pipelinesTitle_other:`{{count}} AI pipelines.`,pipelinesBody:`One launch per rail, each with its configured mode, engine and profile.`,costTitle:`Multiplied cost.`,costBody:`Every rail spawns its own AI run — spend scales with the number of rails.`,launch_one:`Launch {{count}} rail`,launch_other:`Launch {{count}} rails`},s={viewJobLog:`View job log`,log:`Log`,implement:`Implement`,batch:`Batch`,freestyle:`Freestyle`,freestyleTitle:`Freestyle — Claude implements the spec autonomously, no pipeline`,loop:`Loop`,loopTitle:`Loop — run a published automation loop against the rail's specs`,pickLoop:`Pick a loop…`,effortTitle:`Reasoning effort`,stop:`Stop`,play:`Play`,failedRetry:`Job failed — click to retry`,addSpecsFirst:`Add specs to this rail first`,builtInLoops:`Built-in`,customLoops:`Custom loops`,worktreesMerged:`{{count}} merged`,worktreesNeedsReview:`{{count}} need review`,execSteps:`{{count}} steps`,execLines:`{{count}} lines`,openspec:`OpenSpec`},c={moveToSpecs:`Move to Specs`,deleteRail:`Delete {{label}}`,dropHere:`Drop here`,dropOnThisRail:`Drop on this rail`,dragSpecsHere:`Drag specs here`},l={title:`Launch in Freestyle mode`,context_one:`{{rail}} · {{count}} spec`,context_other:`{{rail}} · {{count}} specs`,noPipelineTitle:`No OpenSpec pipeline.`,noPipelineBody:`The structured architect → developer → reviewer flow is skipped entirely.`,autonomyTitle:`Native Claude autonomy.`,autonomyBody:`Claude works on its own using native agents and dynamic workflows.`,costTitle:`Variable cost.`,costBody:`Spend depends on what Claude decides to do — it can be higher than expected.`,continue:`Continue`},u={fetchFailed:`Failed to fetch rails: {{status}}`},d={toasts:n,railPr:r,railsBoard:i,launchAll:a,launchAllDialog:o,railControls:s,railRow:c,freestyleDialog:l,useRails:u};export{t};
|