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.
Files changed (99) hide show
  1. package/client/dist/assets/{ActivityFeedPage-ClpD5CFj.js → ActivityFeedPage-CFB4UBhT.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-C-T0r16B.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-IljV5FOQ.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-B5miH10_.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-BgYQrAcH.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BtVCuNjL.js} +1 -1
  7. package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Cb_jAjSV.js} +1 -1
  8. package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-s_Cc_JL1.js} +1 -1
  9. package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-Cr47QI9L.js} +1 -1
  10. package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-C_WPRuG_.js} +1 -1
  11. package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-NfFOciRB.js} +1 -1
  12. package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DqEW7Z7K.js} +1 -1
  13. package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-D4ppEpDE.js} +1 -1
  14. package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-DStSoMyq.js} +1 -1
  15. package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-DltCwS4P.js} +1 -1
  16. package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-BWaiAbUq.js} +1 -1
  17. package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-BqjbXuBZ.js} +1 -1
  18. package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext--o3JAM9F.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-B-UOmzXw.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-DkGajIxE.js} +1 -1
  21. package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-5CUXlkw7.js} +1 -1
  22. package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-B_igqmti.js} +4 -4
  23. package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-DDMIBBUd.js} +1 -1
  24. package/client/dist/assets/agent-BXc59ARK2.js +1 -0
  25. package/client/dist/assets/agent-BbIFXdDq.js +1 -0
  26. package/client/dist/assets/agent-BqEfA5u5.js +1 -0
  27. package/client/dist/assets/agent-BtGzgTC4.js +1 -0
  28. package/client/dist/assets/agent-CIhys_5d.js +1 -0
  29. package/client/dist/assets/agent-CJPaqsVR.js +1 -0
  30. package/client/dist/assets/agent-n-hY4GuL.js +1 -0
  31. package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
  32. package/client/dist/assets/common-6MLbD-jM.js +1 -0
  33. package/client/dist/assets/common-BKGEu4rm.js +1 -0
  34. package/client/dist/assets/common-BNbnv8cE2.js +1 -0
  35. package/client/dist/assets/common-BqGWtDbt.js +1 -0
  36. package/client/dist/assets/common-DBtTzeYm2.js +1 -0
  37. package/client/dist/assets/common-DEW3Nr8n.js +1 -0
  38. package/client/dist/assets/{common-DVQuhs-m.js → common-DHsv9N5d.js} +1 -1
  39. package/client/dist/assets/common-mkrEcW-Y.js +1 -0
  40. package/client/dist/assets/dashboard-BYzxom0L.js +1 -0
  41. package/client/dist/assets/dashboard-BmDZmHyY.js +1 -0
  42. package/client/dist/assets/dashboard-CnNtJBQ5.js +1 -0
  43. package/client/dist/assets/dashboard-D4Uj-XWc2.js +1 -0
  44. package/client/dist/assets/dashboard-D57uRaGw2.js +1 -0
  45. package/client/dist/assets/dashboard-DAr6F9et.js +1 -0
  46. package/client/dist/assets/dashboard-vAcsn0L3.js +1 -0
  47. package/client/dist/assets/dashboard-vrjZvxWy.js +1 -0
  48. package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-BCUhGUXS.js} +1 -1
  49. package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-DcoXTnEI.js} +1 -1
  50. package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-DP3Ab6fX.js} +1 -1
  51. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DdKKL8Fx.js} +1 -1
  52. package/client/dist/assets/{i18n-CgaRxpII.js → i18n-DREQCE2F.js} +2 -2
  53. package/client/dist/assets/index-B784m8RT.js +76 -0
  54. package/client/dist/assets/index-CQBIemiz.css +2 -0
  55. package/client/dist/assets/{spending-C4p06nen.js → spending-CtY7yYes.js} +1 -1
  56. package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-BFBdW59q.js} +1 -1
  57. package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-uWVZ8eia.js} +1 -1
  58. package/client/dist/index.html +14 -14
  59. package/docs/internals/safe-pr-review-flow.md +221 -38
  60. package/package.json +1 -1
  61. package/server/dist/active-pr-continuation.js +237 -52
  62. package/server/dist/agent-chat-manager.js +29 -12
  63. package/server/dist/agent-store.js +19 -4
  64. package/server/dist/db.js +21 -0
  65. package/server/dist/pr-lifecycle.js +100 -8
  66. package/server/dist/project-git.js +62 -2
  67. package/server/dist/rail-isolated-launch.js +644 -108
  68. package/server/dist/rail-pr-decision.js +1065 -181
  69. package/server/dist/rail-pr-recovery-git.js +235 -0
  70. package/server/dist/rail-pr-store.js +120 -9
  71. package/server/dist/rail-worktree-release.js +290 -4
  72. package/server/dist/rails-router.js +97 -9
  73. package/server/dist/worktree-manager.js +120 -16
  74. package/server/dist/worktree-overlay.js +166 -4
  75. package/client/dist/assets/agent-4rmXoo_p.js +0 -1
  76. package/client/dist/assets/agent-BxZuNGih.js +0 -1
  77. package/client/dist/assets/agent-C3uXOYv_.js +0 -1
  78. package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
  79. package/client/dist/assets/agent-CASKrhG12.js +0 -1
  80. package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
  81. package/client/dist/assets/agent-DitUxYh2.js +0 -1
  82. package/client/dist/assets/agent-Doow4X7o.js +0 -1
  83. package/client/dist/assets/common-CD1QHriA2.js +0 -1
  84. package/client/dist/assets/common-CwwPaRxx.js +0 -1
  85. package/client/dist/assets/common-Dg2jaX7d.js +0 -1
  86. package/client/dist/assets/common-DuBwj6tK.js +0 -1
  87. package/client/dist/assets/common-MRj9MHcc.js +0 -1
  88. package/client/dist/assets/common-O4ZpnMLp.js +0 -1
  89. package/client/dist/assets/common-fyeF0v7C2.js +0 -1
  90. package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
  91. package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
  92. package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
  93. package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
  94. package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
  95. package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
  96. package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
  97. package/client/dist/assets/dashboard-owcDucDi.js +0 -1
  98. package/client/dist/assets/index-BflYQpWm.css +0 -2
  99. package/client/dist/assets/index-C1_-BTXy.js +0 -72
@@ -1,16 +1,97 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PR_LIFECYCLE_JSON_FIELDS = void 0;
4
+ exports.verifyPushRemoteForPr = verifyPushRemoteForPr;
4
5
  exports.observePrLifecycle = observePrLifecycle;
6
+ exports.matchesRecordedPrIdentity = matchesRecordedPrIdentity;
5
7
  exports.isExactOpenPr = isExactOpenPr;
6
8
  const SHA_RE = /^[0-9a-f]{40,64}$/i;
7
- exports.PR_LIFECYCLE_JSON_FIELDS = 'state,isDraft,headRefName,baseRefName,headRefOid,mergeCommit,commits';
9
+ exports.PR_LIFECYCLE_JSON_FIELDS = 'state,isDraft,headRefName,baseRefName,isCrossRepository,headRefOid,mergeCommit,commits';
8
10
  function firstLine(result) {
9
11
  return (result.stderr.trim() || result.stdout.trim()).split('\n')[0].trim().slice(0, 512);
10
12
  }
11
13
  function oid(value) {
12
14
  return typeof value === 'string' && SHA_RE.test(value) ? value.toLowerCase() : null;
13
15
  }
16
+ function githubRepositoryIdentity(rawUrl, kind) {
17
+ const raw = rawUrl.trim();
18
+ if (!raw || raw.includes('\n') || raw.includes('\r'))
19
+ return null;
20
+ let host = '';
21
+ let pathname = '';
22
+ try {
23
+ const parsed = new URL(raw);
24
+ if (!['http:', 'https:', 'ssh:', 'git:'].includes(parsed.protocol))
25
+ return null;
26
+ // The verified target is later passed as an argv element. Never carry
27
+ // embedded credentials or token-like URL suffixes into a child process or
28
+ // its timeout/error diagnostics; credential helpers remain the only
29
+ // supported authentication channel. SSH's conventional `git` username is
30
+ // identity, not a secret, and is safe to retain.
31
+ if (parsed.password || parsed.search || parsed.hash)
32
+ return null;
33
+ if (parsed.username && !(parsed.protocol === 'ssh:' && parsed.username === 'git'))
34
+ return null;
35
+ host = parsed.hostname.toLowerCase();
36
+ pathname = parsed.pathname;
37
+ }
38
+ catch {
39
+ // SCP-style Git URL: git@github.com:owner/repository.git
40
+ const scp = /^(?:git@)?([^@:/\s]+):(.+)$/.exec(raw);
41
+ if (!scp)
42
+ return null;
43
+ host = scp[1].toLowerCase();
44
+ pathname = scp[2];
45
+ if (pathname.includes('?') || pathname.includes('#'))
46
+ return null;
47
+ }
48
+ const segments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
49
+ if (kind === 'pr') {
50
+ if (segments.length !== 4 || segments[2] !== 'pull' || !/^\d+$/.test(segments[3]))
51
+ return null;
52
+ }
53
+ else if (segments.length !== 2) {
54
+ return null;
55
+ }
56
+ const owner = segments[0]?.toLowerCase();
57
+ const repository = segments[1]?.replace(/\.git$/i, '').toLowerCase();
58
+ return host && owner && repository ? `${host}/${owner}/${repository}` : null;
59
+ }
60
+ /**
61
+ * Prove that the local push remote names the repository that owns a same-repo
62
+ * PR. Branch names alone are insufficient: another clone may call its fork
63
+ * `origin`, which would otherwise receive the exact recovery refspec.
64
+ */
65
+ async function verifyPushRemoteForPr(exec, repoDir, prUrl, remote = 'origin') {
66
+ const expected = githubRepositoryIdentity(prUrl, 'pr');
67
+ if (!expected)
68
+ return { ok: false, detail: 'the recorded PR URL does not identify an exact GitHub repository' };
69
+ let result;
70
+ try {
71
+ result = await exec.run('git', ['remote', 'get-url', '--push', '--all', remote], repoDir);
72
+ }
73
+ catch (err) {
74
+ return { ok: false, detail: `the ${remote} push remote could not be inspected: ${err instanceof Error ? err.message : String(err)}` };
75
+ }
76
+ if (result.code !== 0) {
77
+ return { ok: false, detail: `the ${remote} push remote could not be inspected: ${firstLine(result) || `exit ${result.code}`}` };
78
+ }
79
+ const pushTargets = result.stdout.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
80
+ if (pushTargets.length !== 1) {
81
+ return {
82
+ ok: false,
83
+ detail: `the ${remote} push remote must resolve to exactly one URL; found ${pushTargets.length}`,
84
+ };
85
+ }
86
+ const pushTarget = pushTargets[0];
87
+ const actual = githubRepositoryIdentity(pushTarget, 'remote');
88
+ if (!actual)
89
+ return { ok: false, detail: `the ${remote} push remote is not an unambiguous GitHub repository URL` };
90
+ if (actual !== expected) {
91
+ return { ok: false, detail: `the ${remote} push remote does not own the recorded PR repository` };
92
+ }
93
+ return { ok: true, identity: actual, pushTarget };
94
+ }
14
95
  /**
15
96
  * Observe one exact PR and, when requested, whether its immutable commit/merge
16
97
  * evidence contains a delivery SHA. The helper never guesses from a ledger row:
@@ -42,11 +123,15 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
42
123
  // A merged PR's live head branch can advance after the merge. Its current
43
124
  // headRefOid is therefore NOT proof that the merge included that object.
44
125
  // Only the PR's commit snapshot / merge commit are valid MERGED evidence.
45
- const observedOids = new Set([
46
- ...(normalizedState === 'OPEN' ? [headRefOid] : []),
47
- mergeCommitOid,
48
- ...commitOids,
49
- ].filter((value) => value !== null));
126
+ // While OPEN, delivery verification is intentionally stronger than mere
127
+ // ancestry: the remote PR head itself must still be the frozen delivery
128
+ // object. Otherwise a concurrent writer could advance/substitute the head
129
+ // and a stale card would incorrectly report our exact push as verified.
130
+ // CLOSED/MERGED no longer have a mutable review head, so their immutable PR
131
+ // commit snapshot (or merge object) is the relevant inclusion evidence.
132
+ const observedOids = new Set((normalizedState === 'OPEN'
133
+ ? [headRefOid]
134
+ : [mergeCommitOid, ...commitOids]).filter((value) => value !== null));
50
135
  const includesExpectedSha = expected === null
51
136
  ? null
52
137
  : observedOids.has(expected)
@@ -60,6 +145,7 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
60
145
  isDraft: parsed.isDraft === true,
61
146
  headRefName: typeof parsed.headRefName === 'string' ? parsed.headRefName : null,
62
147
  baseRefName: typeof parsed.baseRefName === 'string' ? parsed.baseRefName : null,
148
+ isCrossRepository: typeof parsed.isCrossRepository === 'boolean' ? parsed.isCrossRepository : null,
63
149
  headRefOid,
64
150
  mergeCommitOid,
65
151
  commitOids,
@@ -70,8 +156,14 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
70
156
  return { ok: false, detail: 'unparseable gh pr view output' };
71
157
  }
72
158
  }
159
+ /** Head/base identity is required in every lifecycle state. A PR retargeted to
160
+ * another base must never satisfy delivery merely because it still lists the
161
+ * expected commit. */
162
+ function matchesRecordedPrIdentity(observation, branch, baseBranch) {
163
+ return observation.ok && observation.isCrossRepository === false &&
164
+ observation.headRefName === branch && observation.baseRefName === baseBranch;
165
+ }
73
166
  /** OPEN is continuation-safe only for the same recorded head/base identity. */
74
167
  function isExactOpenPr(observation, branch, baseBranch) {
75
- return observation.ok && observation.state === 'OPEN' &&
76
- observation.headRefName === branch && observation.baseRefName === baseBranch;
168
+ return observation.ok && matchesRecordedPrIdentity(observation, branch, baseBranch) && observation.state === 'OPEN';
77
169
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseWorktreePorcelain = parseWorktreePorcelain;
4
+ exports.inspectProjectCheckoutCleanliness = inspectProjectCheckoutCleanliness;
4
5
  exports.getProjectGitInfo = getProjectGitInfo;
5
6
  exports.compactCheckoutError = compactCheckoutError;
6
7
  exports.checkoutProjectBranch = checkoutProjectBranch;
@@ -17,6 +18,7 @@ const integration_branch_1 = require("./integration-branch");
17
18
  // against `git branch` output, so no user string can smuggle flags or refs.
18
19
  const GIT_TIMEOUT_MS = 10_000;
19
20
  const GIT_MAX_BUFFER = 4 * 1024 * 1024;
21
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
20
22
  function git(repoDir, args) {
21
23
  return new Promise((resolve, reject) => {
22
24
  (0, child_process_1.execFile)('git', args, { cwd: repoDir, env: file_provenance_1.GIT_EXEC_ENV, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
@@ -57,6 +59,21 @@ async function listLocalBranches(repoDir) {
57
59
  const out = await git(repoDir, ['branch', '--format=%(refname:short)']);
58
60
  return out.split('\n').map((b) => b.trim()).filter(Boolean);
59
61
  }
62
+ /** Lossless checkout preflight. Unlike the dashboard-oriented aggregate info,
63
+ * this result is tri-state: an unreadable status is never interpreted as a
64
+ * clean checkout. */
65
+ async function inspectProjectCheckoutCleanliness(repoDir) {
66
+ try {
67
+ const status = await git(repoDir, ['status', '--porcelain', '--untracked-files=all']);
68
+ return { ok: true, clean: status === '' };
69
+ }
70
+ catch (err) {
71
+ return {
72
+ ok: false,
73
+ detail: `Working tree cleanliness could not be verified: ${err instanceof Error ? err.message : String(err)}`,
74
+ };
75
+ }
76
+ }
60
77
  async function getProjectGitInfo(repoDir) {
61
78
  try {
62
79
  await git(repoDir, ['rev-parse', '--is-inside-work-tree']);
@@ -159,11 +176,18 @@ async function remoteBranchExists(repoDir, branch) {
159
176
  /** Checkout a PR/review branch into the user's main repo. Unlike the small
160
177
  * AgentGitBar switcher, this path may materialize a local tracking branch from
161
178
  * `origin/<branch>` because a PR delivery can be remote-only after worktree
162
- * cleanup or a fresh clone. It never overwrites dirty local work. */
163
- async function checkoutProjectReviewBranch(repoDir, branch) {
179
+ * cleanup or a fresh clone. When supplied, `expectedSha` is an immutable
180
+ * delivery lease: a divergent local/remote branch is preserved and checkout
181
+ * succeeds only when the final branch HEAD equals that exact object. It never
182
+ * overwrites dirty local work. */
183
+ async function checkoutProjectReviewBranch(repoDir, branch, expectedSha) {
164
184
  const target = branch.trim();
165
185
  if (!(0, integration_branch_1.isValidBranchName)(target))
166
186
  return { ok: false, error: `Invalid branch name: ${branch}` };
187
+ const expected = expectedSha?.trim().toLowerCase() ?? null;
188
+ if (expected !== null && !COMMIT_SHA_RE.test(expected)) {
189
+ return { ok: false, error: 'The verified delivery commit is invalid; checkout was not attempted.' };
190
+ }
167
191
  let dirty = false;
168
192
  try {
169
193
  dirty = (await git(repoDir, ['status', '--porcelain'])) !== '';
@@ -185,9 +209,27 @@ async function checkoutProjectReviewBranch(repoDir, branch) {
185
209
  }
186
210
  try {
187
211
  if (branches.includes(target)) {
212
+ if (expected) {
213
+ const localSha = (await git(repoDir, ['rev-parse', '--verify', `refs/heads/${target}`])).toLowerCase();
214
+ if (localSha !== expected) {
215
+ return {
216
+ ok: false,
217
+ error: `Local branch '${target}' points to ${localSha.slice(0, 8)}, not verified delivery ${expected.slice(0, 8)}. It was preserved and not checked out.`,
218
+ };
219
+ }
220
+ }
188
221
  await git(repoDir, ['checkout', target]);
189
222
  }
190
223
  else if (await remoteBranchExists(repoDir, target)) {
224
+ if (expected) {
225
+ const remoteSha = (await git(repoDir, ['rev-parse', '--verify', `refs/remotes/origin/${target}`])).toLowerCase();
226
+ if (remoteSha !== expected) {
227
+ return {
228
+ ok: false,
229
+ error: `Remote branch 'origin/${target}' points to ${remoteSha.slice(0, 8)}, not verified delivery ${expected.slice(0, 8)}. Checkout was not attempted.`,
230
+ };
231
+ }
232
+ }
191
233
  await git(repoDir, ['checkout', '-b', target, '--track', `origin/${target}`]);
192
234
  }
193
235
  else {
@@ -197,6 +239,24 @@ async function checkoutProjectReviewBranch(repoDir, branch) {
197
239
  catch (err) {
198
240
  return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout failed') };
199
241
  }
242
+ if (expected) {
243
+ try {
244
+ const [currentBranch, currentSha] = await Promise.all([
245
+ git(repoDir, ['rev-parse', '--abbrev-ref', 'HEAD']),
246
+ git(repoDir, ['rev-parse', '--verify', 'HEAD']),
247
+ ]);
248
+ if (currentBranch !== target || currentSha.toLowerCase() !== expected) {
249
+ return {
250
+ ok: false,
251
+ error: `Checkout changed concurrently and did not land on verified delivery ${expected.slice(0, 8)}. No reset or cleanup was performed.`,
252
+ };
253
+ }
254
+ return { ok: true };
255
+ }
256
+ catch (err) {
257
+ return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout verification failed') };
258
+ }
259
+ }
200
260
  // A failed ff-only pull should not undo a successful checkout; the branch is
201
261
  // already where the user can inspect/fix it. Surface only hard checkout errors.
202
262
  await git(repoDir, ['pull', '--ff-only']).catch(() => { });