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.
Files changed (101) hide show
  1. package/client/dist/assets/{ActivityFeedPage-ClpD5CFj.js → ActivityFeedPage-CDMzpGXd.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-BVTacapZ.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-D9bhFyuy.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-DAjN8Au5.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-CebykajE.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BiPrVvgv.js} +1 -1
  7. package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Dw9eQFRS.js} +1 -1
  8. package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-cCG_JAEi.js} +1 -1
  9. package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-C1kZ_mRP.js} +1 -1
  10. package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-3GivxWJm.js} +1 -1
  11. package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-KuZXGdr8.js} +1 -1
  12. package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DWpRALkB.js} +1 -1
  13. package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-mrXDKafj.js} +1 -1
  14. package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-CUYXjcoc.js} +1 -1
  15. package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-CwjBD5Gm.js} +1 -1
  16. package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-CO9ton0-.js} +1 -1
  17. package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-CLL5UuvN.js} +1 -1
  18. package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext-DJAkRYps.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-DZvCC5Ri.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-C7Hesadg.js} +1 -1
  21. package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-DczCzUzP.js} +1 -1
  22. package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-CFh7GOv5.js} +4 -4
  23. package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-Cm9rhdnG.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-CHCEkds4.js +1 -0
  41. package/client/dist/assets/dashboard-Cqmmbh3l2.js +1 -0
  42. package/client/dist/assets/dashboard-CrlTV1ms.js +1 -0
  43. package/client/dist/assets/dashboard-Cslu_GCH.js +1 -0
  44. package/client/dist/assets/dashboard-DUU8-_Rg.js +1 -0
  45. package/client/dist/assets/dashboard-DweT3Y4R.js +1 -0
  46. package/client/dist/assets/dashboard-Dy2Um-US2.js +1 -0
  47. package/client/dist/assets/dashboard-WhzOQw1S.js +1 -0
  48. package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-BERxOT0X.js} +1 -1
  49. package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-Di8lflPo.js} +1 -1
  50. package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-BAXZRbEO.js} +1 -1
  51. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DwzZXHtg.js} +1 -1
  52. package/client/dist/assets/{i18n-CgaRxpII.js → i18n-cK5pPbj8.js} +2 -2
  53. package/client/dist/assets/index-BT6xDQjB.css +2 -0
  54. package/client/dist/assets/index-CCq_kIrF.js +76 -0
  55. package/client/dist/assets/{spending-C4p06nen.js → spending-BLb5pj6G.js} +1 -1
  56. package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-DR1_jL56.js} +1 -1
  57. package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-D4DxBGPU.js} +1 -1
  58. package/client/dist/index.html +14 -14
  59. package/docs/internals/safe-pr-review-flow.md +250 -38
  60. package/package.json +1 -1
  61. package/server/dist/active-pr-continuation.js +352 -53
  62. package/server/dist/agent-chat-manager.js +29 -12
  63. package/server/dist/agent-operator-prompt.js +9 -0
  64. package/server/dist/agent-store.js +19 -4
  65. package/server/dist/db.js +21 -0
  66. package/server/dist/mcp/tools/rails.js +8 -0
  67. package/server/dist/pr-lifecycle.js +100 -8
  68. package/server/dist/project-git.js +68 -3
  69. package/server/dist/rail-isolated-launch.js +719 -124
  70. package/server/dist/rail-pr-decision.js +1073 -181
  71. package/server/dist/rail-pr-recovery-git.js +235 -0
  72. package/server/dist/rail-pr-store.js +120 -9
  73. package/server/dist/rail-worktree-release.js +376 -4
  74. package/server/dist/rails-router.js +196 -10
  75. package/server/dist/worktree-manager.js +149 -18
  76. package/server/dist/worktree-overlay.js +166 -4
  77. package/client/dist/assets/agent-4rmXoo_p.js +0 -1
  78. package/client/dist/assets/agent-BxZuNGih.js +0 -1
  79. package/client/dist/assets/agent-C3uXOYv_.js +0 -1
  80. package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
  81. package/client/dist/assets/agent-CASKrhG12.js +0 -1
  82. package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
  83. package/client/dist/assets/agent-DitUxYh2.js +0 -1
  84. package/client/dist/assets/agent-Doow4X7o.js +0 -1
  85. package/client/dist/assets/common-CD1QHriA2.js +0 -1
  86. package/client/dist/assets/common-CwwPaRxx.js +0 -1
  87. package/client/dist/assets/common-Dg2jaX7d.js +0 -1
  88. package/client/dist/assets/common-DuBwj6tK.js +0 -1
  89. package/client/dist/assets/common-MRj9MHcc.js +0 -1
  90. package/client/dist/assets/common-O4ZpnMLp.js +0 -1
  91. package/client/dist/assets/common-fyeF0v7C2.js +0 -1
  92. package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
  93. package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
  94. package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
  95. package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
  96. package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
  97. package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
  98. package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
  99. package/client/dist/assets/dashboard-owcDucDi.js +0 -1
  100. package/client/dist/assets/index-BflYQpWm.css +0 -2
  101. package/client/dist/assets/index-C1_-BTXy.js +0 -72
@@ -1 +1 @@
1
- import{o as e}from"./i18n-CgaRxpII.js";var t={job:`analytics:surfaces.job`,"quick-spec":`analytics:surfaces.quickSpec`,"explore-spec":`analytics:surfaces.exploreSpec`,"ai-edit":`analytics:surfaces.aiEdit`,smash:`analytics:surfaces.smash`,"file-summary":`analytics:surfaces.fileSummary`,loop:`analytics:surfaces.loop`,"chat-sidebar":`analytics:surfaces.chatSidebar`,"spec-launcher":`analytics:surfaces.specLauncher`,proposal:`analytics:surfaces.proposal`,"agent-studio":`analytics:surfaces.agentStudio`,setup:`analytics:surfaces.setup`},n={bg:`bg-muted/40`,text:`text-muted-foreground`,ring:`ring-border/40`,dot:`bg-muted-foreground`},r={job:{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"quick-spec":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},"explore-spec":{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"ai-edit":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},smash:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"file-summary":{bg:`bg-accent-warning/15`,text:`text-accent-warning`,ring:`ring-accent-warning/40`,dot:`bg-accent-warning`},loop:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`},"chat-sidebar":{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"spec-launcher":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},proposal:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"agent-studio":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},setup:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`}};function i(e,t){return new Proxy(e,{get(e,n,r){return typeof n==`string`&&!(n in e)?t(n):Reflect.get(e,n,r)}})}var a=i(Object.defineProperties({},Object.fromEntries(Object.keys(t).map(n=>[n,{get:()=>e.t(t[n]),enumerable:!0}]))),e=>e),o=i(r,()=>n);export{a as n,o as t};
1
+ import{o as e}from"./i18n-cK5pPbj8.js";var t={job:`analytics:surfaces.job`,"quick-spec":`analytics:surfaces.quickSpec`,"explore-spec":`analytics:surfaces.exploreSpec`,"ai-edit":`analytics:surfaces.aiEdit`,smash:`analytics:surfaces.smash`,"file-summary":`analytics:surfaces.fileSummary`,loop:`analytics:surfaces.loop`,"chat-sidebar":`analytics:surfaces.chatSidebar`,"spec-launcher":`analytics:surfaces.specLauncher`,proposal:`analytics:surfaces.proposal`,"agent-studio":`analytics:surfaces.agentStudio`,setup:`analytics:surfaces.setup`},n={bg:`bg-muted/40`,text:`text-muted-foreground`,ring:`ring-border/40`,dot:`bg-muted-foreground`},r={job:{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"quick-spec":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},"explore-spec":{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"ai-edit":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},smash:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"file-summary":{bg:`bg-accent-warning/15`,text:`text-accent-warning`,ring:`ring-accent-warning/40`,dot:`bg-accent-warning`},loop:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`},"chat-sidebar":{bg:`bg-accent-info/15`,text:`text-accent-info`,ring:`ring-accent-info/40`,dot:`bg-accent-info`},"spec-launcher":{bg:`bg-accent-secondary/15`,text:`text-accent-secondary`,ring:`ring-accent-secondary/40`,dot:`bg-accent-secondary`},proposal:{bg:`bg-accent-highlight/15`,text:`text-accent-highlight`,ring:`ring-accent-highlight/40`,dot:`bg-accent-highlight`},"agent-studio":{bg:`bg-accent-success/15`,text:`text-accent-success`,ring:`ring-accent-success/40`,dot:`bg-accent-success`},setup:{bg:`bg-accent-primary/15`,text:`text-accent-primary`,ring:`ring-accent-primary/40`,dot:`bg-accent-primary`}};function i(e,t){return new Proxy(e,{get(e,n,r){return typeof n==`string`&&!(n in e)?t(n):Reflect.get(e,n,r)}})}var a=i(Object.defineProperties({},Object.fromEntries(Object.keys(t).map(n=>[n,{get:()=>e.t(t[n]),enumerable:!0}]))),e=>e),o=i(r,()=>n);export{a as n,o as t};
@@ -1 +1 @@
1
- import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t}from"./jsx-runtime-zu2_FqZY.js";import{o as n}from"./i18n-CgaRxpII.js";import{t as r}from"./createLucideIcon-Bjbbp_KS.js";import{a as i,n as a}from"./auth-D_5bQv5W.js";import{t as o}from"./api-B2M7CtDw.js";var s=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),c=r(`globe`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]]),l=r(`rotate-cw`,[[`path`,{d:`M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8`,key:`1p45f6`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}]]),u=e(t(),1);function d(){return typeof window<`u`&&`__TAURI_INTERNALS__`in window||typeof window<`u`&&window.location.protocol!==`http:`&&window.location.protocol!==`https:`?`ws://localhost:4200`:`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`}var f=d();function p(){let e={BASE_URL:`/`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1}.VITE_FEATURE_BROWSER_CAPTURE;return typeof e!=`string`||e!==`false`}var m={desktop:{width:1280,height:800},tablet:{width:768,height:1024},mobile:{width:375,height:667}},h=class extends Error{},g=class extends Error{};function _(e){if(!e)throw Error(`A project owner is required for browser sessions`);return`${i}/api/projects/${encodeURIComponent(e)}`}async function v(e,t){let n=await fetch(`${_(e)}/browser/sessions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({initialUrl:t})});if(n.status===409)throw new h(`Too many open browser sessions`);if(n.status===502)throw new g(`The browser failed to launch`);if(!n.ok)throw Error(`Failed to open browser (${n.status})`);return(await n.json()).session}async function y(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/navigate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,url:r})});if(!i.ok)throw Error(`Navigation failed (${i.status})`);return await i.json()}async function b(e,t,n,r,i){let a=await fetch(`${_(e)}/browser/sessions/${t}/capture`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,pendingSpecId:r,captureNetwork:i?.captureNetwork??!0})});if(!a.ok)throw Error(await x(a));return await a.json()}async function x(e){let t=await e.json().then(e=>e.error).catch(()=>void 0);return t?`Capture failed (${e.status}): ${t}`:`Capture failed (${e.status})`}async function S(e,t,n,r,i,a=m){let o=await fetch(`${_(e)}/browser/sessions/${t}/capture-breakpoints`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,anchorPoint:r,pendingSpecId:i,breakpoints:a})});if(!o.ok)throw Error(await x(o));return await o.json()}async function C(e,t,n){let r=new FormData;r.append(`file`,t,n);let i=await fetch(`${o()}/tickets/${e}/attachments`,{method:`POST`,body:r});if(!i.ok)throw Error(`Upload failed (${i.status})`);return(await i.json()).attachment}async function w(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/clipboard`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,text:r})});if(!i.ok)throw Error(`Clipboard failed (${i.status})`);return await i.json()}async function T(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/element`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({selector:n,direction:r})});if(!i.ok)throw Error(`Element navigate failed (${i.status})`);return(await i.json()).probe}async function E(e,t,n){try{await fetch(`${_(e)}/browser/sessions/${t}/popup-view`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({target:n})})}catch{}}function D(e){if(!e||e===`about:blank`)return`…`;try{return new URL(e).hostname||`…`}catch{return`…`}}async function O(e,t){try{await fetch(`${_(e)}/browser/sessions/${t}`,{method:`DELETE`})}catch{}}function k(e,t){return`${f||(typeof window<`u`?`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`:`ws://localhost:4200`)}/ws/browser/${e}?projectId=${encodeURIComponent(t)}`}function A(e,t){let n=k(e,t),r=a(),i=r?new WebSocket(n,[`specrails-desktop`,r]):new WebSocket(n);return i.binaryType=`arraybuffer`,i}function j(e,t=e=>requestAnimationFrame(e),n=e=>cancelAnimationFrame(e)){let r=null,i=null,a=null,o=!1,s=()=>{a!=null&&(n(a),a=null);let t=r,o=i;r=null,i=null,t&&(!o||t.x!==o.x||t.y!==o.y)&&e({type:`mouse`,action:`move`,x:t.x,y:t.y}),o&&e({type:`wheel`,x:o.x,y:o.y,deltaX:o.deltaX,deltaY:o.deltaY})},c=()=>{a==null&&!o&&(a=t(()=>{a=null,s()}))};return{move(e,t){o||(r={x:e,y:t},c())},wheel(e,t,n,r){o||(i?(i.x=e,i.y=t,i.deltaX+=n,i.deltaY+=r):i={x:e,y:t,deltaX:n,deltaY:r},c())},flush(){o||s()},dispose(){o||(o=!0,a!=null&&(n(a),a=null),r=null,i=null)}}}function M(e,t,n){let r=t.width>0?(e.x-t.left)/t.width:0,i=t.height>0?(e.y-t.top)/t.height:0,a=Math.min(Math.max(r,0),1)*n.width,o=Math.min(Math.max(i,0),1)*n.height;return{x:Math.round(a),y:Math.round(o)}}function N(e,t,n){let r=n.width>0?t.width/n.width:0,i=n.height>0?t.height/n.height:0;return{left:t.left+e.x*r,top:t.top+e.y*i,width:e.width*r,height:e.height*i}}function P(e,t){return{x:Math.min(e.x,t.x),y:Math.min(e.y,t.y),width:Math.abs(e.x-t.x),height:Math.abs(e.y-t.y)}}function F(e,t=8){return e.width>=t&&e.height>=t}function I(e,t){let n=Math.max(1,t.width),r=Math.max(1,t.height),i=Math.max(0,Math.min(e.x,n-1)),a=Math.max(0,Math.min(e.y,r-1)),o=Math.min(n,Math.max(i+1,e.x+e.width)),s=Math.min(r,Math.max(a+1,e.y+e.height));return{x:Math.round(i),y:Math.round(a),width:Math.round(o-i),height:Math.round(s-a)}}function L(e){return{nodeCount:e.nodes.length,htmlBytes:e.html.length,truncated:e.htmlTruncated,networkCount:e.networkRequests?.length??0}}function R(e){let t=e.schedule??(e=>requestAnimationFrame(e)),n=e.cancel??(e=>cancelAnimationFrame(e)),r=e.now??(()=>typeof performance<`u`?performance.now():Date.now()),i=e.release??(()=>{}),a={received:0,decoded:0,drawn:0,droppedStale:0,droppedUndrawn:0,decodeMsTotal:0,drawMsTotal:0},o=!1,s=!1,c=null,l=null,u=null,d=()=>{u=null;let t=l;if(l=null,t==null||o){t!=null&&f(t);return}let n=r();try{e.draw(t),a.drawn++,a.drawMsTotal+=r()-n}catch{}finally{f(t)}},f=e=>{try{i(e)}catch{}},p=()=>{u==null&&!o&&(u=t(d))},m=t=>{s=!0;let n=r();e.decode(t).then(e=>{if(o){f(e);return}a.decoded++,a.decodeMsTotal+=r()-n,l!=null&&(a.droppedUndrawn++,f(l)),l=e,p(),h()},()=>{h()})},h=()=>{if(o){s=!1;return}let e=c;c=null,e==null?s=!1:m(e)};return{push(e){if(!o){if(a.received++,s){c!=null&&a.droppedStale++,c=e;return}m(e)}},stats(){return{...a}},dispose(){if(!o){if(o=!0,c=null,u!=null){try{n(u)}catch{}u=null}l!=null&&(f(l),l=null)}}}}var z=`specrails-desktop:browser-capture-debug`;function B(){try{return typeof localStorage<`u`&&localStorage.getItem(z)===`1`}catch{return!1}}function V(e,t,n){let r=Math.max(1,n),i=t.decoded-e.decoded,a=t.drawn-e.drawn,o=e=>Math.round(e*10)/10;return{fpsReceived:o((t.received-e.received)*1e3/r),fpsDrawn:o(a*1e3/r),droppedStale:t.droppedStale-e.droppedStale,droppedUndrawn:t.droppedUndrawn-e.droppedUndrawn,avgDecodeMs:i>0?o((t.decodeMsTotal-e.decodeMsTotal)/i):0,avgDrawMs:a>0?o((t.drawMsTotal-e.drawMsTotal)/a):0}}function H(e,t=e=>console.table([e]),n=2e3,r=()=>Date.now()){let i=e(),a=r(),o=setInterval(()=>{let n=e(),o=r();t(V(i,n,o-a)),i=n,a=o},n);return()=>clearInterval(o)}function U(e){let{projectId:t,open:r,initialUrl:i}=e,a=(0,u.useRef)(null),o=(0,u.useRef)(null),s=(0,u.useRef)(null),c=(0,u.useRef)(0),l=(0,u.useRef)(new Map),d=(0,u.useCallback)(()=>{if(c.current!==0||l.current.size===0)return;let e=[...l.current.values()];l.current.clear();for(let t of e)O(t.projectId,t.sessionId)},[]),[f,p]=(0,u.useState)({status:`connecting`,errorMsg:null,url:null,title:null,viewport:{width:1280,height:800},hoverRect:null,hoverSelector:null,hoverPath:null,popup:null});(0,u.useEffect)(()=>{if(!r||!t)return;let e=!1,u=null;p(e=>({...e,status:`connecting`,errorMsg:null,popup:null}));let d=R({decode:e=>createImageBitmap(new Blob([e],{type:`image/jpeg`})),draw:e=>{let t=a.current;t&&(t.width!==e.width&&(t.width=e.width),t.height!==e.height&&(t.height=e.height),t.getContext(`2d`)?.drawImage(e,0,0))},release:e=>e.close()}),f=B()?H(()=>d.stats()):null;return(async()=>{try{let r=await v(t,i);if(e){O(t,r.id);return}u=r.id,s.current={id:r.id,projectId:t},p(e=>({...e,url:r.url,title:r.title,viewport:{width:r.viewportWidth,height:r.viewportHeight}}));let a=A(r.id,t);o.current=a,a.onmessage=e=>{if(typeof e.data==`string`){try{let t=JSON.parse(e.data);if(t.type===`ready`)p(e=>({...e,status:`ready`,url:t.url??e.url,title:t.title??e.title,viewport:t.viewport??e.viewport}));else if(t.type===`nav`)p(e=>({...e,url:t.url??e.url,title:t.title??e.title}));else if(t.type===`hover`)p(e=>({...e,hoverRect:t.rect??null,hoverSelector:t.selector??null,hoverPath:t.path??null}));else if(t.type===`popup`){let e=typeof t.count==`number`?t.count:0;p(n=>({...n,popup:e>0?{count:e,active:t.active===!0,url:t.url??null}:null}))}}catch{}return}d.push(e.data)},a.onopen=()=>{e||p(e=>({...e,status:`ready`}))},a.onerror=()=>{e||p(e=>({...e,status:`error`,errorMsg:n.t(`browser:session.connectionError`)}))},a.onclose=()=>{e||p(e=>e.status===`ready`?{...e,status:`error`,errorMsg:n.t(`browser:session.lostConnection`)}:e)}}catch(t){if(e)return;let r=t instanceof h?n.t(`browser:session.tooManySessions`):t instanceof g?n.t(`browser:session.launchFailed`):n.t(`browser:session.openFailed`);p(e=>({...e,status:`error`,errorMsg:r}))}})(),()=>{e=!0,f?.(),d.dispose();let n=o.current;if(n){n.onmessage=null,n.onopen=null,n.onerror=null,n.onclose=null;try{n.close()}catch{}}o.current=null;let r=u;r&&s.current?.id===r&&s.current.projectId===t&&(s.current=null),r&&(c.current>0?l.current.set(`${t}\0${r}`,{sessionId:r,projectId:t}):O(t,r))}},[r,t,i,d]);let m=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`input`,event:e}))}catch{}},[]),_=(0,u.useCallback)(async(e,t)=>{let n=s.current;if(n)try{let r=await y(n.projectId,n.id,e,t);if(s.current?.id!==n.id||s.current.projectId!==n.projectId)return;p(e=>({...e,url:r.url,title:r.title}))}catch{}},[]),x=(0,u.useCallback)(async(e,t,n)=>{let r=s.current;if(!r)throw Error(`No active browser session`);c.current+=1;try{return await b(r.projectId,r.id,e,t,n)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),C=(0,u.useCallback)(async(e,t,n,r)=>{let i=s.current;if(!i)throw Error(`No active browser session`);c.current+=1;try{return await S(i.projectId,i.id,e,t,n,r)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),D=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?w(n.projectId,n.id,e,t):{text:``}},[]),k=(0,u.useCallback)((e,t)=>{let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t));p(e=>({...e,viewport:{width:n,height:r}})),m({type:`resize`,width:n,height:r})},[m]),j=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`probe`,x:e.x,y:e.y}))}catch{}},[]),M=(0,u.useCallback)(()=>{p(e=>e.hoverRect||e.hoverPath?{...e,hoverRect:null,hoverSelector:null,hoverPath:null}:e)},[]),N=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?T(n.projectId,n.id,e,t):null},[]),P=(0,u.useCallback)(e=>{let t=s.current;t&&(p(t=>t.popup?{...t,popup:{...t.popup,active:e===`popup`}}:t),E(t.projectId,t.id,e))},[]);return{...f,canvasRef:a,forwardInput:m,navigate:_,capture:x,captureBreakpoints:C,clipboard:D,navigateElement:N,setViewport:k,probe:j,clearHover:M,setPopupView:P}}export{L as a,M as c,P as d,C as f,s as g,c as h,j as i,N as l,l as m,m as n,p as o,f as p,I as r,F as s,U as t,D as u};
1
+ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t}from"./jsx-runtime-zu2_FqZY.js";import{o as n}from"./i18n-cK5pPbj8.js";import{t as r}from"./createLucideIcon-Bjbbp_KS.js";import{a as i,n as a}from"./auth-D_5bQv5W.js";import{t as o}from"./api-B2M7CtDw.js";var s=r(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),c=r(`globe`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20`,key:`13o1zl`}],[`path`,{d:`M2 12h20`,key:`9i4pu4`}]]),l=r(`rotate-cw`,[[`path`,{d:`M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8`,key:`1p45f6`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}]]),u=e(t(),1);function d(){return typeof window<`u`&&`__TAURI_INTERNALS__`in window||typeof window<`u`&&window.location.protocol!==`http:`&&window.location.protocol!==`https:`?`ws://localhost:4200`:`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`}var f=d();function p(){let e={BASE_URL:`/`,DEV:!1,MODE:`production`,PROD:!0,SSR:!1}.VITE_FEATURE_BROWSER_CAPTURE;return typeof e!=`string`||e!==`false`}var m={desktop:{width:1280,height:800},tablet:{width:768,height:1024},mobile:{width:375,height:667}},h=class extends Error{},g=class extends Error{};function _(e){if(!e)throw Error(`A project owner is required for browser sessions`);return`${i}/api/projects/${encodeURIComponent(e)}`}async function v(e,t){let n=await fetch(`${_(e)}/browser/sessions`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({initialUrl:t})});if(n.status===409)throw new h(`Too many open browser sessions`);if(n.status===502)throw new g(`The browser failed to launch`);if(!n.ok)throw Error(`Failed to open browser (${n.status})`);return(await n.json()).session}async function y(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/navigate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,url:r})});if(!i.ok)throw Error(`Navigation failed (${i.status})`);return await i.json()}async function b(e,t,n,r,i){let a=await fetch(`${_(e)}/browser/sessions/${t}/capture`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,pendingSpecId:r,captureNetwork:i?.captureNetwork??!0})});if(!a.ok)throw Error(await x(a));return await a.json()}async function x(e){let t=await e.json().then(e=>e.error).catch(()=>void 0);return t?`Capture failed (${e.status}): ${t}`:`Capture failed (${e.status})`}async function S(e,t,n,r,i,a=m){let o=await fetch(`${_(e)}/browser/sessions/${t}/capture-breakpoints`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({rect:n,anchorPoint:r,pendingSpecId:i,breakpoints:a})});if(!o.ok)throw Error(await x(o));return await o.json()}async function C(e,t,n){let r=new FormData;r.append(`file`,t,n);let i=await fetch(`${o()}/tickets/${e}/attachments`,{method:`POST`,body:r});if(!i.ok)throw Error(`Upload failed (${i.status})`);return(await i.json()).attachment}async function w(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/clipboard`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:n,text:r})});if(!i.ok)throw Error(`Clipboard failed (${i.status})`);return await i.json()}async function T(e,t,n,r){let i=await fetch(`${_(e)}/browser/sessions/${t}/element`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({selector:n,direction:r})});if(!i.ok)throw Error(`Element navigate failed (${i.status})`);return(await i.json()).probe}async function E(e,t,n){try{await fetch(`${_(e)}/browser/sessions/${t}/popup-view`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({target:n})})}catch{}}function D(e){if(!e||e===`about:blank`)return`…`;try{return new URL(e).hostname||`…`}catch{return`…`}}async function O(e,t){try{await fetch(`${_(e)}/browser/sessions/${t}`,{method:`DELETE`})}catch{}}function k(e,t){return`${f||(typeof window<`u`?`${window.location.protocol===`https:`?`wss:`:`ws:`}//${window.location.host}`:`ws://localhost:4200`)}/ws/browser/${e}?projectId=${encodeURIComponent(t)}`}function A(e,t){let n=k(e,t),r=a(),i=r?new WebSocket(n,[`specrails-desktop`,r]):new WebSocket(n);return i.binaryType=`arraybuffer`,i}function j(e,t=e=>requestAnimationFrame(e),n=e=>cancelAnimationFrame(e)){let r=null,i=null,a=null,o=!1,s=()=>{a!=null&&(n(a),a=null);let t=r,o=i;r=null,i=null,t&&(!o||t.x!==o.x||t.y!==o.y)&&e({type:`mouse`,action:`move`,x:t.x,y:t.y}),o&&e({type:`wheel`,x:o.x,y:o.y,deltaX:o.deltaX,deltaY:o.deltaY})},c=()=>{a==null&&!o&&(a=t(()=>{a=null,s()}))};return{move(e,t){o||(r={x:e,y:t},c())},wheel(e,t,n,r){o||(i?(i.x=e,i.y=t,i.deltaX+=n,i.deltaY+=r):i={x:e,y:t,deltaX:n,deltaY:r},c())},flush(){o||s()},dispose(){o||(o=!0,a!=null&&(n(a),a=null),r=null,i=null)}}}function M(e,t,n){let r=t.width>0?(e.x-t.left)/t.width:0,i=t.height>0?(e.y-t.top)/t.height:0,a=Math.min(Math.max(r,0),1)*n.width,o=Math.min(Math.max(i,0),1)*n.height;return{x:Math.round(a),y:Math.round(o)}}function N(e,t,n){let r=n.width>0?t.width/n.width:0,i=n.height>0?t.height/n.height:0;return{left:t.left+e.x*r,top:t.top+e.y*i,width:e.width*r,height:e.height*i}}function P(e,t){return{x:Math.min(e.x,t.x),y:Math.min(e.y,t.y),width:Math.abs(e.x-t.x),height:Math.abs(e.y-t.y)}}function F(e,t=8){return e.width>=t&&e.height>=t}function I(e,t){let n=Math.max(1,t.width),r=Math.max(1,t.height),i=Math.max(0,Math.min(e.x,n-1)),a=Math.max(0,Math.min(e.y,r-1)),o=Math.min(n,Math.max(i+1,e.x+e.width)),s=Math.min(r,Math.max(a+1,e.y+e.height));return{x:Math.round(i),y:Math.round(a),width:Math.round(o-i),height:Math.round(s-a)}}function L(e){return{nodeCount:e.nodes.length,htmlBytes:e.html.length,truncated:e.htmlTruncated,networkCount:e.networkRequests?.length??0}}function R(e){let t=e.schedule??(e=>requestAnimationFrame(e)),n=e.cancel??(e=>cancelAnimationFrame(e)),r=e.now??(()=>typeof performance<`u`?performance.now():Date.now()),i=e.release??(()=>{}),a={received:0,decoded:0,drawn:0,droppedStale:0,droppedUndrawn:0,decodeMsTotal:0,drawMsTotal:0},o=!1,s=!1,c=null,l=null,u=null,d=()=>{u=null;let t=l;if(l=null,t==null||o){t!=null&&f(t);return}let n=r();try{e.draw(t),a.drawn++,a.drawMsTotal+=r()-n}catch{}finally{f(t)}},f=e=>{try{i(e)}catch{}},p=()=>{u==null&&!o&&(u=t(d))},m=t=>{s=!0;let n=r();e.decode(t).then(e=>{if(o){f(e);return}a.decoded++,a.decodeMsTotal+=r()-n,l!=null&&(a.droppedUndrawn++,f(l)),l=e,p(),h()},()=>{h()})},h=()=>{if(o){s=!1;return}let e=c;c=null,e==null?s=!1:m(e)};return{push(e){if(!o){if(a.received++,s){c!=null&&a.droppedStale++,c=e;return}m(e)}},stats(){return{...a}},dispose(){if(!o){if(o=!0,c=null,u!=null){try{n(u)}catch{}u=null}l!=null&&(f(l),l=null)}}}}var z=`specrails-desktop:browser-capture-debug`;function B(){try{return typeof localStorage<`u`&&localStorage.getItem(z)===`1`}catch{return!1}}function V(e,t,n){let r=Math.max(1,n),i=t.decoded-e.decoded,a=t.drawn-e.drawn,o=e=>Math.round(e*10)/10;return{fpsReceived:o((t.received-e.received)*1e3/r),fpsDrawn:o(a*1e3/r),droppedStale:t.droppedStale-e.droppedStale,droppedUndrawn:t.droppedUndrawn-e.droppedUndrawn,avgDecodeMs:i>0?o((t.decodeMsTotal-e.decodeMsTotal)/i):0,avgDrawMs:a>0?o((t.drawMsTotal-e.drawMsTotal)/a):0}}function H(e,t=e=>console.table([e]),n=2e3,r=()=>Date.now()){let i=e(),a=r(),o=setInterval(()=>{let n=e(),o=r();t(V(i,n,o-a)),i=n,a=o},n);return()=>clearInterval(o)}function U(e){let{projectId:t,open:r,initialUrl:i}=e,a=(0,u.useRef)(null),o=(0,u.useRef)(null),s=(0,u.useRef)(null),c=(0,u.useRef)(0),l=(0,u.useRef)(new Map),d=(0,u.useCallback)(()=>{if(c.current!==0||l.current.size===0)return;let e=[...l.current.values()];l.current.clear();for(let t of e)O(t.projectId,t.sessionId)},[]),[f,p]=(0,u.useState)({status:`connecting`,errorMsg:null,url:null,title:null,viewport:{width:1280,height:800},hoverRect:null,hoverSelector:null,hoverPath:null,popup:null});(0,u.useEffect)(()=>{if(!r||!t)return;let e=!1,u=null;p(e=>({...e,status:`connecting`,errorMsg:null,popup:null}));let d=R({decode:e=>createImageBitmap(new Blob([e],{type:`image/jpeg`})),draw:e=>{let t=a.current;t&&(t.width!==e.width&&(t.width=e.width),t.height!==e.height&&(t.height=e.height),t.getContext(`2d`)?.drawImage(e,0,0))},release:e=>e.close()}),f=B()?H(()=>d.stats()):null;return(async()=>{try{let r=await v(t,i);if(e){O(t,r.id);return}u=r.id,s.current={id:r.id,projectId:t},p(e=>({...e,url:r.url,title:r.title,viewport:{width:r.viewportWidth,height:r.viewportHeight}}));let a=A(r.id,t);o.current=a,a.onmessage=e=>{if(typeof e.data==`string`){try{let t=JSON.parse(e.data);if(t.type===`ready`)p(e=>({...e,status:`ready`,url:t.url??e.url,title:t.title??e.title,viewport:t.viewport??e.viewport}));else if(t.type===`nav`)p(e=>({...e,url:t.url??e.url,title:t.title??e.title}));else if(t.type===`hover`)p(e=>({...e,hoverRect:t.rect??null,hoverSelector:t.selector??null,hoverPath:t.path??null}));else if(t.type===`popup`){let e=typeof t.count==`number`?t.count:0;p(n=>({...n,popup:e>0?{count:e,active:t.active===!0,url:t.url??null}:null}))}}catch{}return}d.push(e.data)},a.onopen=()=>{e||p(e=>({...e,status:`ready`}))},a.onerror=()=>{e||p(e=>({...e,status:`error`,errorMsg:n.t(`browser:session.connectionError`)}))},a.onclose=()=>{e||p(e=>e.status===`ready`?{...e,status:`error`,errorMsg:n.t(`browser:session.lostConnection`)}:e)}}catch(t){if(e)return;let r=t instanceof h?n.t(`browser:session.tooManySessions`):t instanceof g?n.t(`browser:session.launchFailed`):n.t(`browser:session.openFailed`);p(e=>({...e,status:`error`,errorMsg:r}))}})(),()=>{e=!0,f?.(),d.dispose();let n=o.current;if(n){n.onmessage=null,n.onopen=null,n.onerror=null,n.onclose=null;try{n.close()}catch{}}o.current=null;let r=u;r&&s.current?.id===r&&s.current.projectId===t&&(s.current=null),r&&(c.current>0?l.current.set(`${t}\0${r}`,{sessionId:r,projectId:t}):O(t,r))}},[r,t,i,d]);let m=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`input`,event:e}))}catch{}},[]),_=(0,u.useCallback)(async(e,t)=>{let n=s.current;if(n)try{let r=await y(n.projectId,n.id,e,t);if(s.current?.id!==n.id||s.current.projectId!==n.projectId)return;p(e=>({...e,url:r.url,title:r.title}))}catch{}},[]),x=(0,u.useCallback)(async(e,t,n)=>{let r=s.current;if(!r)throw Error(`No active browser session`);c.current+=1;try{return await b(r.projectId,r.id,e,t,n)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),C=(0,u.useCallback)(async(e,t,n,r)=>{let i=s.current;if(!i)throw Error(`No active browser session`);c.current+=1;try{return await S(i.projectId,i.id,e,t,n,r)}finally{c.current=Math.max(0,c.current-1),d()}},[d]),D=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?w(n.projectId,n.id,e,t):{text:``}},[]),k=(0,u.useCallback)((e,t)=>{let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t));p(e=>({...e,viewport:{width:n,height:r}})),m({type:`resize`,width:n,height:r})},[m]),j=(0,u.useCallback)(e=>{let t=o.current;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify({type:`probe`,x:e.x,y:e.y}))}catch{}},[]),M=(0,u.useCallback)(()=>{p(e=>e.hoverRect||e.hoverPath?{...e,hoverRect:null,hoverSelector:null,hoverPath:null}:e)},[]),N=(0,u.useCallback)(async(e,t)=>{let n=s.current;return n?T(n.projectId,n.id,e,t):null},[]),P=(0,u.useCallback)(e=>{let t=s.current;t&&(p(t=>t.popup?{...t,popup:{...t.popup,active:e===`popup`}}:t),E(t.projectId,t.id,e))},[]);return{...f,canvasRef:a,forwardInput:m,navigate:_,capture:x,captureBreakpoints:C,clipboard:D,navigateElement:N,setViewport:k,probe:j,clearHover:M,setPopupView:P}}export{L as a,M as c,P as d,C as f,s as g,c as h,j as i,N as l,l as m,m as n,p as o,f as p,I as r,F as s,U as t,D as u};
@@ -1 +1 @@
1
- import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-zu2_FqZY.js";import{o as r}from"./i18n-CgaRxpII.js";import{n as i}from"./dist-bVqfYmby.js";import{a}from"./auth-D_5bQv5W.js";import{n as o}from"./useSharedWebSocket-DaPh7QBL.js";import{n as s}from"./api-B2M7CtDw.js";var c=e(t(),1),l=new Map;function u(e,t){return`${e}:${t}`}function d(e){return(e instanceof Error?e.message:e==null?``:String(e)).trim()||`Request failed`}function f(e){if(!e)return;let t=`${e}:`;for(let e of l.keys())e.startsWith(t)&&l.delete(e)}function p({namespace:e,projectId:t,initialValue:n,fetcher:r,pollInterval:i=0}){let a=t?u(t,e):null,[o,s]=(0,c.useState)(()=>a&&l.has(a)?l.get(a):n),[f,p]=(0,c.useState)(()=>!a||!l.has(a)),[m,h]=(0,c.useState)(f),[g,_]=(0,c.useState)(null),v=(0,c.useRef)(r);v.current=r;let y=(0,c.useRef)(n);y.current=n;let b=(0,c.useRef)(a);b.current=a;let x=(0,c.useRef)(0),S=(0,c.useRef)(null),C=(0,c.useCallback)(async(e,t,n)=>{let r=++x.current;S.current?.abort();let i=new AbortController;S.current=i,n&&b.current===e&&(h(!0),_(null));try{let n=await v.current({projectId:t,signal:i.signal});if(i.signal.aborted||r!==x.current)return;l.set(e,n),b.current===e&&(s(n),_(null))}catch(t){if(i.signal.aborted||r!==x.current)return;b.current===e&&_(d(t))}finally{r===x.current&&(S.current===i&&(S.current=null),b.current===e&&(h(!1),p(!1)))}},[]);return(0,c.useEffect)(()=>{if(x.current+=1,S.current?.abort(),S.current=null,_(null),!a||!t){s(y.current),p(!0),h(!1);return}let e=l.has(a);e?(s(l.get(a)),p(!1),h(!1)):(s(y.current),p(!0),h(!0)),C(a,t,!e);let n;return i>0&&(n=setInterval(()=>{C(a,t,!1)},i)),()=>{x.current+=1,S.current?.abort(),S.current=null,n&&clearInterval(n)}},[a,t,i,C]),{data:o,isLoading:m,isFirstLoad:f,error:g,refresh:(0,c.useCallback)(async()=>{!a||!t||await C(a,t,!0)},[a,t,C])}}var m=n();function h(e){return e.providers&&e.providers.length>0?e.providers:[e.provider]}var g=(0,c.createContext)(null),_=`specrails-desktop:activeProjectId`;function v(e){try{e?localStorage.setItem(_,e):localStorage.removeItem(_)}catch{}}function y(){try{return localStorage.getItem(_)}catch{return null}}function b({children:e}){let[t,n]=(0,c.useState)([]),[l,u]=(0,c.useState)(null),[d,p]=(0,c.useState)(!0),[h,_]=(0,c.useState)(!1),[b,x]=(0,c.useState)(new Set),S=(0,c.useRef)(null),C=(0,c.useRef)(new Set),w=(0,c.useCallback)(e=>{v(e),s(e),u(t=>(t!==null&&t!==e&&(S.current&&clearTimeout(S.current),_(!0),S.current=setTimeout(()=>_(!1),400)),e))},[]),{registerHandler:T,unregisterHandler:E}=o();(0,c.useEffect)(()=>{async function e(){try{let e=await fetch(`${a}/api/projects`);if(!e.ok)return;let t=await e.json();n(t.projects),t.setupProjectIds&&t.setupProjectIds.length>0&&x(new Set(t.setupProjectIds))}catch{}finally{p(!1)}}e()},[]);let D=(0,c.useCallback)(e=>{let t=e;if(typeof t.type==`string`){if(t.type===`desktop.projects`){if(!Array.isArray(t.projects))return;let e=t.projects;n(e),u(t=>{let n;if(t&&e.find(e=>e.id===t))n=t;else{let t=y();n=t&&e.find(e=>e.id===t)?t:e.length>0?e[0].id:null}return v(n),s(n),n}),p(!1)}else if(t.type===`desktop.project_added`){let e=t.project;n(t=>t.find(t=>t.id===e.id)?t:[...t,e]),C.current.has(e.id)?C.current.delete(e.id):(i.success(r.t(`nav:projects.added`,{name:e.name})),w(e.id))}else if(t.type===`desktop.project_removed`){let e=t.projectId;i.success(r.t(`nav:projects.removed`)),f(e),n(t=>t.filter(t=>t.id!==e)),u(t=>t===e?(v(null),s(null),null):t)}}},[]);(0,c.useLayoutEffect)(()=>(T(`desktop`,D),()=>E(`desktop`)),[D,T,E]);let O=(0,c.useCallback)(async(e,t,r=[`claude`])=>{try{let i={path:e,providers:r.length>0?r:[`claude`]};t&&(i.name=t);let a=await fetch(`/api/projects`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i)});if(!a.ok){let e=await a.json();throw Error(e.error??`HTTP ${a.status}`)}let o=await a.json();return C.current.add(o.project.id),n(e=>e.find(e=>e.id===o.project.id)?e:[...e,o.project]),w(o.project.id),o}catch(e){throw console.error(`[useDesktop] addProject error:`,e),e}},[w]),k=(0,c.useCallback)(async e=>{try{let t=await fetch(`${a}/api/projects/${e}`,{method:`DELETE`});if(!t.ok){let e=await t.json();throw Error(e.error??`HTTP ${t.status}`)}f(e),n(t=>t.filter(t=>t.id!==e)),x(t=>{if(!t.has(e))return t;let n=new Set(t);return n.delete(e),n}),u(t=>t===e?(v(null),s(null),null):t)}catch(e){throw console.error(`[useDesktop] removeProject error:`,e),e}},[]),A=(0,c.useCallback)(e=>{x(t=>new Set([...t,e]))},[]),j=(0,c.useCallback)(e=>{x(t=>{let n=new Set(t);return n.delete(e),n})},[]),M=(0,c.useMemo)(()=>({projects:t,activeProjectId:l,setActiveProjectId:w,addProject:O,removeProject:k,isLoading:d,isSwitchingProject:h,setupProjectIds:b,startSetupWizard:A,completeSetupWizard:j}),[t,l,w,O,k,d,h,b,A,j]);return(0,m.jsx)(g.Provider,{value:M,children:e})}var x={projects:[],activeProjectId:null,setActiveProjectId:()=>{},addProject:async()=>null,removeProject:async()=>{},isLoading:!1,isSwitchingProject:!1,setupProjectIds:new Set,startSetupWizard:()=>{},completeSetupWizard:()=>{}};function S(){return(0,c.useContext)(g)??x}export{p as i,h as n,S as r,b as t};
1
+ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-zu2_FqZY.js";import{o as r}from"./i18n-cK5pPbj8.js";import{n as i}from"./dist-bVqfYmby.js";import{a}from"./auth-D_5bQv5W.js";import{n as o}from"./useSharedWebSocket-DaPh7QBL.js";import{n as s}from"./api-B2M7CtDw.js";var c=e(t(),1),l=new Map;function u(e,t){return`${e}:${t}`}function d(e){return(e instanceof Error?e.message:e==null?``:String(e)).trim()||`Request failed`}function f(e){if(!e)return;let t=`${e}:`;for(let e of l.keys())e.startsWith(t)&&l.delete(e)}function p({namespace:e,projectId:t,initialValue:n,fetcher:r,pollInterval:i=0}){let a=t?u(t,e):null,[o,s]=(0,c.useState)(()=>a&&l.has(a)?l.get(a):n),[f,p]=(0,c.useState)(()=>!a||!l.has(a)),[m,h]=(0,c.useState)(f),[g,_]=(0,c.useState)(null),v=(0,c.useRef)(r);v.current=r;let y=(0,c.useRef)(n);y.current=n;let b=(0,c.useRef)(a);b.current=a;let x=(0,c.useRef)(0),S=(0,c.useRef)(null),C=(0,c.useCallback)(async(e,t,n)=>{let r=++x.current;S.current?.abort();let i=new AbortController;S.current=i,n&&b.current===e&&(h(!0),_(null));try{let n=await v.current({projectId:t,signal:i.signal});if(i.signal.aborted||r!==x.current)return;l.set(e,n),b.current===e&&(s(n),_(null))}catch(t){if(i.signal.aborted||r!==x.current)return;b.current===e&&_(d(t))}finally{r===x.current&&(S.current===i&&(S.current=null),b.current===e&&(h(!1),p(!1)))}},[]);return(0,c.useEffect)(()=>{if(x.current+=1,S.current?.abort(),S.current=null,_(null),!a||!t){s(y.current),p(!0),h(!1);return}let e=l.has(a);e?(s(l.get(a)),p(!1),h(!1)):(s(y.current),p(!0),h(!0)),C(a,t,!e);let n;return i>0&&(n=setInterval(()=>{C(a,t,!1)},i)),()=>{x.current+=1,S.current?.abort(),S.current=null,n&&clearInterval(n)}},[a,t,i,C]),{data:o,isLoading:m,isFirstLoad:f,error:g,refresh:(0,c.useCallback)(async()=>{!a||!t||await C(a,t,!0)},[a,t,C])}}var m=n();function h(e){return e.providers&&e.providers.length>0?e.providers:[e.provider]}var g=(0,c.createContext)(null),_=`specrails-desktop:activeProjectId`;function v(e){try{e?localStorage.setItem(_,e):localStorage.removeItem(_)}catch{}}function y(){try{return localStorage.getItem(_)}catch{return null}}function b({children:e}){let[t,n]=(0,c.useState)([]),[l,u]=(0,c.useState)(null),[d,p]=(0,c.useState)(!0),[h,_]=(0,c.useState)(!1),[b,x]=(0,c.useState)(new Set),S=(0,c.useRef)(null),C=(0,c.useRef)(new Set),w=(0,c.useCallback)(e=>{v(e),s(e),u(t=>(t!==null&&t!==e&&(S.current&&clearTimeout(S.current),_(!0),S.current=setTimeout(()=>_(!1),400)),e))},[]),{registerHandler:T,unregisterHandler:E}=o();(0,c.useEffect)(()=>{async function e(){try{let e=await fetch(`${a}/api/projects`);if(!e.ok)return;let t=await e.json();n(t.projects),t.setupProjectIds&&t.setupProjectIds.length>0&&x(new Set(t.setupProjectIds))}catch{}finally{p(!1)}}e()},[]);let D=(0,c.useCallback)(e=>{let t=e;if(typeof t.type==`string`){if(t.type===`desktop.projects`){if(!Array.isArray(t.projects))return;let e=t.projects;n(e),u(t=>{let n;if(t&&e.find(e=>e.id===t))n=t;else{let t=y();n=t&&e.find(e=>e.id===t)?t:e.length>0?e[0].id:null}return v(n),s(n),n}),p(!1)}else if(t.type===`desktop.project_added`){let e=t.project;n(t=>t.find(t=>t.id===e.id)?t:[...t,e]),C.current.has(e.id)?C.current.delete(e.id):(i.success(r.t(`nav:projects.added`,{name:e.name})),w(e.id))}else if(t.type===`desktop.project_removed`){let e=t.projectId;i.success(r.t(`nav:projects.removed`)),f(e),n(t=>t.filter(t=>t.id!==e)),u(t=>t===e?(v(null),s(null),null):t)}}},[]);(0,c.useLayoutEffect)(()=>(T(`desktop`,D),()=>E(`desktop`)),[D,T,E]);let O=(0,c.useCallback)(async(e,t,r=[`claude`])=>{try{let i={path:e,providers:r.length>0?r:[`claude`]};t&&(i.name=t);let a=await fetch(`/api/projects`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i)});if(!a.ok){let e=await a.json();throw Error(e.error??`HTTP ${a.status}`)}let o=await a.json();return C.current.add(o.project.id),n(e=>e.find(e=>e.id===o.project.id)?e:[...e,o.project]),w(o.project.id),o}catch(e){throw console.error(`[useDesktop] addProject error:`,e),e}},[w]),k=(0,c.useCallback)(async e=>{try{let t=await fetch(`${a}/api/projects/${e}`,{method:`DELETE`});if(!t.ok){let e=await t.json();throw Error(e.error??`HTTP ${t.status}`)}f(e),n(t=>t.filter(t=>t.id!==e)),x(t=>{if(!t.has(e))return t;let n=new Set(t);return n.delete(e),n}),u(t=>t===e?(v(null),s(null),null):t)}catch(e){throw console.error(`[useDesktop] removeProject error:`,e),e}},[]),A=(0,c.useCallback)(e=>{x(t=>new Set([...t,e]))},[]),j=(0,c.useCallback)(e=>{x(t=>{let n=new Set(t);return n.delete(e),n})},[]),M=(0,c.useMemo)(()=>({projects:t,activeProjectId:l,setActiveProjectId:w,addProject:O,removeProject:k,isLoading:d,isSwitchingProject:h,setupProjectIds:b,startSetupWizard:A,completeSetupWizard:j}),[t,l,w,O,k,d,h,b,A,j]);return(0,m.jsx)(g.Provider,{value:M,children:e})}var x={projects:[],activeProjectId:null,setActiveProjectId:()=>{},addProject:async()=>null,removeProject:async()=>{},isLoading:!1,isSwitchingProject:!1,setupProjectIds:new Set,startSetupWizard:()=>{},completeSetupWizard:()=>{}};function S(){return(0,c.useContext)(g)??x}export{p as i,h as n,S as r,b as t};
@@ -204,7 +204,7 @@
204
204
  .specrails-splash__mark .pill { opacity: 1; }
205
205
  }
206
206
  </style>
207
- <script type="module" crossorigin src="/assets/index-C1_-BTXy.js"></script>
207
+ <script type="module" crossorigin src="/assets/index-CCq_kIrF.js"></script>
208
208
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
209
209
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
210
210
  <link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
@@ -213,7 +213,7 @@
213
213
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-Bjbbp_KS.js">
214
214
  <link rel="modulepreload" crossorigin href="/assets/activity-DiyjsgQV.js">
215
215
  <link rel="modulepreload" crossorigin href="/assets/addspec-Rw8uVBC1.js">
216
- <link rel="modulepreload" crossorigin href="/assets/agent-4rmXoo_p.js">
216
+ <link rel="modulepreload" crossorigin href="/assets/agent-BqEfA5u5.js">
217
217
  <link rel="modulepreload" crossorigin href="/assets/agents-DhG7mg6-.js">
218
218
  <link rel="modulepreload" crossorigin href="/assets/agentstudio-BIUe1Im2.js">
219
219
  <link rel="modulepreload" crossorigin href="/assets/aiedit-iIjz2_Qk.js">
@@ -223,8 +223,8 @@
223
223
  <link rel="modulepreload" crossorigin href="/assets/chat-BGD7MXAy.js">
224
224
  <link rel="modulepreload" crossorigin href="/assets/code-BDXEM4ml.js">
225
225
  <link rel="modulepreload" crossorigin href="/assets/commands-DyUZQ1FI.js">
226
- <link rel="modulepreload" crossorigin href="/assets/common-DVQuhs-m.js">
227
- <link rel="modulepreload" crossorigin href="/assets/dashboard-CpCmJvMB.js">
226
+ <link rel="modulepreload" crossorigin href="/assets/common-DHsv9N5d.js">
227
+ <link rel="modulepreload" crossorigin href="/assets/dashboard-Cslu_GCH.js">
228
228
  <link rel="modulepreload" crossorigin href="/assets/explore-DgBACn39.js">
229
229
  <link rel="modulepreload" crossorigin href="/assets/integrations-mB-7zoyT.js">
230
230
  <link rel="modulepreload" crossorigin href="/assets/jira-CmKWERE0.js">
@@ -237,19 +237,19 @@
237
237
  <link rel="modulepreload" crossorigin href="/assets/specs-DII0C8gw.js">
238
238
  <link rel="modulepreload" crossorigin href="/assets/terminal-BFRZnMPP.js">
239
239
  <link rel="modulepreload" crossorigin href="/assets/tickets-D_yDquEw.js">
240
- <link rel="modulepreload" crossorigin href="/assets/i18n-CgaRxpII.js">
241
- <link rel="modulepreload" crossorigin href="/assets/Trans-B_vKZ-Hb.js">
240
+ <link rel="modulepreload" crossorigin href="/assets/i18n-cK5pPbj8.js">
241
+ <link rel="modulepreload" crossorigin href="/assets/Trans-Cm9rhdnG.js">
242
242
  <link rel="modulepreload" crossorigin href="/assets/dist-bVqfYmby.js">
243
243
  <link rel="modulepreload" crossorigin href="/assets/arrow-left-CpIR4j7G.js">
244
244
  <link rel="modulepreload" crossorigin href="/assets/auth-D_5bQv5W.js">
245
245
  <link rel="modulepreload" crossorigin href="/assets/api-B2M7CtDw.js">
246
- <link rel="modulepreload" crossorigin href="/assets/useBrowserCaptureSession-2OqX0vap.js">
246
+ <link rel="modulepreload" crossorigin href="/assets/useBrowserCaptureSession-DR1_jL56.js">
247
247
  <link rel="modulepreload" crossorigin href="/assets/arrow-up-right-9DoMn7fD.js">
248
248
  <link rel="modulepreload" crossorigin href="/assets/sparkles-DrY8hb0X.js">
249
249
  <link rel="modulepreload" crossorigin href="/assets/x-CBMI9ejn.js">
250
250
  <link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-DaPh7QBL.js">
251
- <link rel="modulepreload" crossorigin href="/assets/useDesktop-X8YB9xzv.js">
252
- <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-DiFvZDPW.js">
251
+ <link rel="modulepreload" crossorigin href="/assets/useDesktop-D4DxBGPU.js">
252
+ <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-DJAkRYps.js">
253
253
  <link rel="modulepreload" crossorigin href="/assets/external-link-BcU5EWZp.js">
254
254
  <link rel="modulepreload" crossorigin href="/assets/chevron-right-CViMxucY.js">
255
255
  <link rel="modulepreload" crossorigin href="/assets/circle-x-CMYsN4ZH.js">
@@ -272,11 +272,11 @@
272
272
  <link rel="modulepreload" crossorigin href="/assets/dialog-CzIekCw0.js">
273
273
  <link rel="modulepreload" crossorigin href="/assets/jira-api-BgFIr8eD.js">
274
274
  <link rel="modulepreload" crossorigin href="/assets/lib-CGdyxIWg.js">
275
- <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-BxHAksaU.js">
276
- <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-ByHIauTQ.js">
277
- <link rel="modulepreload" crossorigin href="/assets/spending-C4p06nen.js">
278
- <link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-9UEyK974.js">
279
- <link rel="stylesheet" crossorigin href="/assets/index-BflYQpWm.css">
275
+ <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-DwzZXHtg.js">
276
+ <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-BAXZRbEO.js">
277
+ <link rel="modulepreload" crossorigin href="/assets/spending-BLb5pj6G.js">
278
+ <link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-CFh7GOv5.js">
279
+ <link rel="stylesheet" crossorigin href="/assets/index-BT6xDQjB.css">
280
280
  </head>
281
281
  <body>
282
282
  <div id="specrails-splash" aria-hidden="false" role="status" aria-label="Loading Specrails">
@@ -9,13 +9,24 @@
9
9
 
10
10
  ## The single source of truth
11
11
 
12
- `rail_pr_deliveries` (per-project `jobs.sqlite`, migrations 36/38/48–50; CRUD in
12
+ `rail_pr_deliveries` (per-project `jobs.sqlite`, migrations 36/38/48–54; CRUD in
13
13
  `server/rail-pr-store.ts`) stores one durable **generation** per implementation launch. A partial
14
14
  unique index enforces one non-terminal generation per rail. Reimplementing an attached PR creates
15
- generation B and atomically moves generation A to terminal `superseded`; A can never reappear after
16
- B closes. `supersedes_delivery_id` preserves that lineage and `is_continuation` records that the
15
+ generation B and atomically moves generation A to terminal `superseded`; A cannot reappear through
16
+ ordinary replay after B closes. The sole exception is an allocation rollback committed in the same
17
+ transaction: B becomes terminal and restored A carries `restored_from_delivery_id=B` as exact causal
18
+ proof. `supersedes_delivery_id` preserves forward lineage and `is_continuation` records that the
17
19
  PR/head branch is borrowed review state rather than a resource this generation owns.
18
20
 
21
+ Every projected snapshot also carries `createdAt`/`updatedAt`; claim, transition, release, recovery,
22
+ and supersession writes advance the latter with a strictly monotonic millisecond UTC logical clock.
23
+ Consumers replace a different generation only through an explicit supersedes link or a strictly
24
+ later valid `createdAt`, and advance the same id only with newer update evidence under a deterministic fail-closed tie policy.
25
+ Delivery ids already observed terminal or superseded are tombstoned against non-terminal replay.
26
+ Only the exact paired rollback above may consume such a tombstone; a delayed predecessor without
27
+ that durable proof remains rejected. Missing or ambiguous ordering evidence fails closed instead of
28
+ letting arrival order resurrect an old card.
29
+
19
30
  The important rule is that no single enum carries all truth. The row has orthogonal axes:
20
31
 
21
32
  - `implementation_outcome`: `running | succeeded | partially_succeeded | failed | unknown` —
@@ -27,6 +38,10 @@ The important rule is that no single enum carries all truth. The row has orthogo
27
38
  `merged`, `discarded`, `superseded`).
28
39
  - `status_code` + bounded `status_detail`: localized stage/reason and secondary diagnostics.
29
40
  - `delivery_sha`: the exact verified object used by an existing-PR push or retry.
41
+ - `supersedes_delivery_id` / `restored_from_delivery_id`: forward replacement and exact failed-
42
+ replacement rollback lineage.
43
+ - `safety_archives`: deduplicated roots for persistent overlay quarantine batches; shown as neutral recovery
44
+ evidence rather than cleanup failures.
30
45
  - `pr_state`: `none | local-only | pushed | pr-created`, independent of all three axes.
31
46
 
32
47
  `branches` is the durable per-unit record: ticket/run, branch, actual implementation result,
@@ -44,7 +59,10 @@ engine succeeded + no branch delta → no_changes
44
59
  mixed clean units → on_review / partial (explicit subset)
45
60
  engine succeeded + commit/ref unsafe → pr_failed / blocked (worktree preserved)
46
61
  verified existing-PR push failed → pr_failed / retryable_failure (exact SHA retained)
47
- GitHub reports CLOSED, not merged → pr_closed
62
+ exact OPEN head/base contains SHA → attached delivery verified
63
+ exact CLOSED head/base contains SHA → pr_closed
64
+ exact MERGED in a decision action → merged immediately (terminal)
65
+ stale/retargeted/terminal missing SHA → on_review, PR detached, SHA retained
48
66
  new generation replaces prior one → prior generation superseded (terminal)
49
67
  fresh no-change accepted → completed (terminal; no merge claimed)
50
68
  ```
@@ -54,10 +72,30 @@ truthfully with “delivery needs attention”. A fresh no-op never offers Creat
54
72
  no-op leaves the existing PR untouched. Partial cards name the included and failed units before
55
73
  delivery.
56
74
 
57
- Continuation cleanup is ownership-aware: **Dismiss** clears the follow-up card/worktree but never
75
+ Continuation cleanup is ownership-aware: **Dismiss** clears the follow-up card/worktree when its
76
+ final live preflight proves that removal is lossless, but never
58
77
  closes the pre-existing PR, deletes its head branch, or returns its tickets from review. A blocked
59
- dirty follow-up requires an explicit destructive “Discard local result” confirmation, while still
60
- preserving the external PR/branch. Fresh deliveries keep the full discard semantics.
78
+ dirty follow-up requires an explicit “Discard local result” confirmation, while still preserving
79
+ the external PR/branch. That confirmation terminalizes the workflow result; it does not authorize
80
+ forced byte deletion. New tracked, untracked, or ignored files keep the worktree and its checked-out
81
+ branch mounted with an actionable cleanup warning. If this computer has no authenticated live
82
+ worktree path, the blocked continuation shows ownership-safe **Dismiss** instead of claiming that a
83
+ local result will be discarded; the existing PR/branch/review state remains untouched. Fresh deliveries keep the full PR/ticket discard
84
+ semantics under the same lossless local preflight.
85
+
86
+ When a terminal dashboard action removes the card while recovery data remains, the cleanup warning
87
+ stays visible indefinitely with the retained location and a localized Copy recovery details action.
88
+ Agent Chat keeps the same evidence in terminal history.
89
+
90
+ A dismissed continuation remains a strict historical anchor for the next local-only iteration. With
91
+ no active delivery, the resolver considers only the newest terminal generation that overlaps any
92
+ requested ticket. Reuse requires that row itself to be a discarded continuation for the exact full
93
+ ticket set, with one valid PR/head/base/SHA; GitHub must still report that exact PR `OPEN` at that SHA,
94
+ every available ref must agree, and no prior worktree may remain mounted or unsettled. A newer subset,
95
+ superset, fresh, malformed, merged, completed, or superseded generation shadows older history. Failed
96
+ historical proof blocks Jira/PR heuristics instead of silently selecting a different review. The next
97
+ generation stores the verified baseline SHA and terminal predecessor lineage before worktree
98
+ allocation so a crash is retryable and same-timestamp clients still elect the replacement.
61
99
 
62
100
  Fresh no-change cards offer explicit **Mark done** and **Refine** outcomes. Mark done uses terminal
63
101
  `completed` and moves review tickets to Done without claiming a merge; Refine explicitly returns
@@ -74,19 +112,69 @@ actionable.
74
112
  - `prMode` is captured once. Admission is rechecked inside the per-repository allocation lock;
75
113
  two requests cannot both create a generation or reuse the ticket-keyed worktree.
76
114
  - A continuation supersedes its prior generation and creates the new `building` row in one DB
77
- transaction. Allocation failure closes the new row and restores the prior generation atomically.
115
+ transaction. Allocation failure closes the new row and restores the prior generation atomically,
116
+ recording the failed replacement id so every client can safely undo its tombstone for that pair.
78
117
  - Isolation units remain `per-ticket` or one `all`-scope batch unit. Initial/final SHAs plus commit
79
118
  verification distinguish changed, resumed, and no-change results.
80
119
  - Per-unit settlement returns structured execution + delivery results. `onLoopRunFinished` receives
81
120
  the engine outcome only; commit/status/ref/provenance/push failures cannot rewrite it.
82
- - Clean, committed worktrees may be released. Dirty, unknown, or ref-mismatched worktrees become
83
- `needs-review` and are never force-removed automatically. Push failure retains the exact SHA.
121
+ - Clean, committed worktrees may be released only after a final live preflight proves tracked and
122
+ untracked status (apart from durably recorded Specrails overlay paths), plus exact worktree HEAD
123
+ and branch ref. Gitignored paths are release-safe only when covered by the IMMUTABLE settlement
124
+ snapshot (`DeliverBranchRecord.settlementIgnoredPaths`, captured the moment the worktree was
125
+ proven clean) — run-created build caches (`__pycache__`, `node_modules`) release cleanly, while an
126
+ ignored path that APPEARS after settlement, a failed/oversized capture (>400 paths → null), a
127
+ legacy row without a snapshot, or conflicting per-branch snapshots all preserve the worktree
128
+ (`durableSettlementIgnoredPaths`, cleanup-and-checkout-tolerate-run-artifacts). Authenticated overlay roots are atomically renamed to a unique
129
+ persistent same-filesystem quarantine batch and revalidated there; automatic cleanup never unlinks that
130
+ quarantine, so a raced replacement or open-descriptor write remains recoverable. The batch root is
131
+ persisted before the first move and pointers are never evicted while their bytes remain on disk. A recreated
132
+ original path makes final non-force removal fail closed. Each quarantine batch root is persisted and shown
133
+ in a collapsed card section without changing an otherwise successful delivery to
134
+ `cleanup_incomplete`. If a terminal dashboard action creates an archive, a persistent notification
135
+ exposes its exact paths and a Copy paths action before the card is removed. Dirty, unknown,
136
+ ignored-user-data, concurrent-write, or ref-
137
+ mismatched worktrees become retryable `needs-review`; push failure retains the exact SHA.
84
138
  - Project admission remains closed while startup reconciliation runs under the repo lock. Every
85
139
  stale `building` shape becomes actionable; successful interrupted work is preserved and labelled
86
140
  `settlement_interrupted`, while only all-failed runs become `implementation_failed`.
87
141
  - Operation leases are process capabilities. Startup clears every prior-process lease before card
88
142
  projection, marks the still-active delivery `operation_interrupted`, and leaves its durable SHA,
89
143
  PR and unit evidence intact so the user can safely retry.
144
+ - Migrated successful `settlement_interrupted` and `recovery_unavailable` rows with an existing PR
145
+ are re-examined at startup. Startup and the explicit recovery action share one bounded,
146
+ fail-closed scan over refs, reflogs, and unreachable objects; a command/read failure, malformed
147
+ output, excessive combined candidate set, or ambiguous marker can never become false proof of a
148
+ unique result. Every refs/reflogs, unreachable-object, or recorded-branch candidate must carry the unique `(run <id>)` settlement
149
+ marker in its **commit subject**; branch/worktree association or a marker found only in the body
150
+ is never causal proof. The unreachable-object scan runs at most once per serialized reconciliation,
151
+ accepts only a bounded, unambiguous set of commit objects, and fails closed on malformed or excessive
152
+ output. A terminal `released`/legacy `failed` branch tip is only a fallback under
153
+ that same subject rule. One unambiguous run-owned commit becomes `delivery_sha` and a Retry push action
154
+ when absent from the recorded open PR. If GitHub already exposes that exact causal commit, the
155
+ row is classified as delivered without a no-op push. Startup audits earlier legacy recoveries
156
+ that already reached draft/ready too, replacing a frozen old PR-head SHA with the run-owned
157
+ commit when needed. Existing-PR recovery repairs `is_continuation=true` before exposing actions;
158
+ per-unit `branchOwnership=borrowed-pr|preexisting` independently prevents branch deletion without
159
+ incorrectly making every fresh replacement PR a borrowed continuation.
160
+ Before persisting or pushing a recovered orphan, Specrails pins it under a delivery-specific
161
+ `refs/specrails/recovery/*` ref. The ref survives crashes and protects the object from Git garbage
162
+ collection until exact remote delivery is proved or the user explicitly discards the result.
163
+ Discovery still runs when a ledger path exists but is dirty, recreated, or unauthenticated; if a
164
+ distinct worktree result and run-owned orphan coexist, both are preserved and neither is selected,
165
+ bundled, or pushed automatically. Restart preserves a `recover-and-retry` recovery-family status,
166
+ and explicit discard may remove a pre-persistence ref only after its exact run/final ownership is
167
+ re-proven. If the ref was advanced from marker-bearing ancestor A to descendant B before a crash,
168
+ that exact delivery ref selects B rather than letting the global scan manufacture self-ambiguity.
169
+ Successful successor delivery and startup settlement retry clean up exact ancestor refs.
170
+ A later unrelated branch tip is never substituted for the durable final SHA or unique run-marked
171
+ object, and all matching units are updated together. Dirty, missing, mismatched, unmarked or
172
+ ambiguous evidence stays blocked and preserved. Once the exact causal SHA is proven, a transient
173
+ GitHub lookup instead stays retryable and Retry push revalidates before mutation.
174
+ - A blocked legacy card exposes a worktree path only after the path is proved absolute,
175
+ non-symlink, outside the main checkout, uniquely present in this repository's live worktree
176
+ registry, and checked out on the recorded branch/HEAD. Historical or other-device paths are
177
+ removed from the snapshot instead of being presented as available.
90
178
  - `GET /rails` is hydration-only and never invokes crash recovery. This avoids misclassifying the
91
179
  normal live window between durable loop completion and commit/ref/push settlement.
92
180
  - Startup retries pending terminal ticket effects in-process and re-projects active and terminal
@@ -116,12 +204,17 @@ entries. Each allocated worktree therefore gets `applyWorktreeOverlay(...)` at l
116
204
  `worktrees/` entry are never linked (nested pipeline worktrees stay local). `.mcp.json`
117
205
  and the instruction file (CLAUDE.md/AGENTS.md/GEMINI.md per provider) are COPIED when
118
206
  the checkout lacks them. Windows: junction → dereferencing-copy fallback.
119
- - **Never on the PR**: every overlay-owned path lands in a worktree-local manifest
120
- (`.sr-rail-overlay.json`, unioned across passes so a RESUMED worktree keeps prior
121
- entries claimed) and is excluded from `commitWorktree`'s stage via `:(exclude)`
122
- pathspecs. Excluded paths are untracked by construction, so no real work can be lost;
123
- a no-op overlay keeps the byte-identical `git add -A`. Worktree removal (`git worktree
124
- remove --force`) deletes the symlinks as entries without following them.
207
+ - **Never on the PR**: every allocator-created overlay path is excluded from staging with a literal
208
+ top-level pathspec. The writable worktree manifest is not cleanup authority; settlement persists
209
+ fingerprints captured from the trusted source/created entries. Cleanup atomically renames each
210
+ authenticated root into a unique sibling quarantine, revalidates it after the move, discloses
211
+ raced content there, and never deletes or restores quarantined paths automatically over a
212
+ possibly recreated source. A modified or
213
+ concurrently extended overlay is preserved as user data, while a no-op
214
+ overlay keeps the byte-identical ordinary staging behavior.
215
+ Successful quarantine batches deliberately remain beside the former worktree; automatic GC is
216
+ out of scope because deleting a path still writable through an open descriptor would reintroduce
217
+ the data-loss race. A future cleanup flow must be explicit and inspectable.
125
218
  - **Degrade, don't abort**: entry-level failures produce `warnings` — surfaced as a
126
219
  server log line plus the project-scoped `rail.overlay_degraded` WS event — and the
127
220
  spawn proceeds (a partial surface beats an aborted rail). `applyWorktreeOverlay` never
@@ -153,6 +246,24 @@ same repository lock as recovery, launch allocation and checkout, revalidates it
153
246
  after waiting for that lock, and uses bounded Git/GitHub command timeouts so a wedged child cannot
154
247
  block the repository forever.
155
248
 
249
+ Every path that observes an attached PR—continuation admission, Retry push, post-push
250
+ verification, Verify PR/poll, and Reopen—uses the same causal check: recorded PR identity, exact
251
+ head/base, lifecycle, and inclusion of immutable `delivery_sha`. Admission is read-only and accepts
252
+ only exact `OPEN` evidence; otherwise it refuses the launch without superseding the preserved card.
253
+ Immediately before any existing-PR push, including automatic second-iteration settlement, the local
254
+ `origin` must resolve to exactly one push URL identifying the repository that owns the same-repository
255
+ PR. A fork, multiple URLs, ambiguous URL, missing remote, or mismatch preserves the immutable SHA and
256
+ fails retryably without mutation. The push targets that already-verified raw URL, not the mutable
257
+ `origin` alias, closing the verification/use window. URLs with credential-bearing userinfo,
258
+ passwords, query strings, or fragments are rejected before they can enter process arguments or
259
+ persisted diagnostics; Git/`gh` credential helpers remain the authentication channel.
260
+ Exact `MERGED` evidence containing that SHA terminalizes immediately in a Retry/Reopen/Verify
261
+ decision action. A definitively stale/retargeted PR, or a `CLOSED`/`MERGED` PR without that SHA, is
262
+ detached by that action to `on_review` with the SHA and local branch preserved for a new PR.
263
+ Transient observation failure keeps the evidence and fails retryably.
264
+ Initial settlement records exact post-push `CLOSED` as `pr_closed`; exact post-push `MERGED` stays
265
+ attached and ready for Verify so that explicit decision can commit terminal ticket intent atomically.
266
+
156
267
  - **create-pr** — deferred `deliverRailAsPr` (`server/rail-pr-delivery.ts`: 1 unit → its
157
268
  branch; N → assembled onto the conventional batch branch off the integration branch, one PR
158
269
  covering every ticket). The PR **title and canonical body are composed here** (see "Branch
@@ -167,24 +278,50 @@ block the repository forever.
167
278
  labelled retryable. Tickets stay `on_review`.
168
279
  - **publish** — `gh pr ready <url>` → `pr_ready` (the draft opens for the team's normal review).
169
280
  Requires a real `pr_url`.
170
- - **discard** — for a fresh delivery, best-effort `gh pr close <url> --delete-branch`; remove the launch's worktrees
281
+ - **discard** — for a fresh delivery, best-effort `gh pr close <url>` without unleased remote-head
282
+ deletion; remove the launch's worktrees
171
283
  (ledger rows closed as `failed`); delete only branches durably recorded as created by this
172
- delivery (owned per-unit sources plus its exact assembled head; preferred names are never
284
+ delivery only while each live local tip still equals its frozen SHA (owned per-unit sources plus
285
+ its exact assembled head; advanced or remote heads are preserved, preferred names are never
173
286
  recomputed, and the integration branch is NEVER deleted); → `discarded`; revert
174
287
  still-`on_review` tickets → `todo` (a manually re-triaged spec is respected) + Jira
175
288
  `onRailDiscard`. Failures are retained in `cleanup_warnings` and disclosed. For a continuation,
176
289
  discard can remove only the explicitly confirmed blocked local iteration: it preserves the
177
- borrowed PR/head and review ticket state.
290
+ borrowed PR/head and review ticket state. Startup repairs migrated continuation ownership before
291
+ exposing actions, and per-unit `borrowed-pr` evidence independently keeps stale surfaces safe.
178
292
  - **dismiss** — continuation acknowledgement. Clears the generation and owned clean
179
293
  worktree while preserving the existing PR, head branch and `on_review` tickets.
180
294
  - **acknowledge-no-changes** — fresh no-change acceptance → terminal `completed`, tickets Done,
181
295
  with no PR/merge claim. Jira receives its own no-PR completion comment. Refine remains the
182
296
  explicitly backlog-returning path and always uses Jira `todo`; it never inherits the configured
183
297
  discard/cancellation status.
184
- - **poll-merge** — observes state, exact head/base/head OID, merge commit and PR commit set;
185
- `MERGED` becomes `merged` only when that immutable evidence contains `delivery_sha`, then tickets
186
- → `done` + Jira `onRailMerged`. `OPEN` is observation-only; `CLOSED` without merge → `pr_closed`.
187
- - **reopen** — `gh pr reopen` from `pr_closed`, returning to draft or ready according to GitHub.
298
+ - **recover-and-retry** — for one exact successful `settlement_interrupted` continuation, confirms
299
+ use of its recorded run/unit and authenticated isolated worktree. Authentication is repeated
300
+ immediately before staging; symlinks, the main checkout, reused directories and unregistered
301
+ paths fail closed. Private/overlay paths are excluded from add and from the final `commit --only`
302
+ pathspec; pre-staged forbidden entries are safely unstaged, hooks are bypassed, and the index is
303
+ re-audited. The candidate must fast-forward the live PR head. GitHub must prove a same-repository
304
+ PR and the local `origin` push URL must identify that exact PR repository before the immutable SHA
305
+ is pushed without force. A merge racing recovery returns verified success rather than a warning.
306
+ The user's main checkout is never switched, staged, stashed, committed, or cleaned by this action.
307
+ If the worktree was already removed, the same action scans refs, reflogs, and unreachable commits
308
+ before declaring the result absent. Baseline equality means “already delivered” only with an exact
309
+ durable final SHA/run marker, and means “no changes” only when every matching unit durably records
310
+ `changed=false` and `initialSha=finalSha=live PR head`. Otherwise the row becomes
311
+ `recovery_unavailable`: the card offers a secondary Recheck action, keeps Inspect/log access and
312
+ explains that Git evidence is local to the computer where the run executed instead of repeating
313
+ an impossible Commit or Checkout action.
314
+ - **poll-merge / Verify PR** — observes state, exact head/base/head OID, merge commit and PR commit
315
+ set. `OPEN` remains delivery-verified only while its exact head is `delivery_sha`; a missing
316
+ remote commit restores Retry push instead of merely saying “not merged”. `MERGED` becomes
317
+ `merged` immediately when immutable evidence contains `delivery_sha`, then tickets → `done` + Jira
318
+ `onRailMerged`. Exact `CLOSED` containing the SHA → `pr_closed`; missing-SHA terminal evidence
319
+ detaches to `on_review`. Successful retry/check responses carry the verified SHA, remote head and
320
+ whether a push actually ran for immediate user feedback.
321
+ - **reopen** — before and after `gh pr reopen`, revalidates exact PR/head/base and inclusion of
322
+ `delivery_sha`; mismatch or missing-SHA evidence detaches to `on_review` instead of retaining or
323
+ reopening the stale attachment. A valid reopen returns to draft or ready according to GitHub;
324
+ an exact merge racing the action terminalizes, while an exact still-closed result reports failure.
188
325
  - **merge-local** — builds the complete merge in an isolated temporary checkout and advances the
189
326
  user's revalidated clean base only after all branches succeed.
190
327
 
@@ -250,32 +387,86 @@ the `expectedDecision` they rendered.
250
387
  one-shot `rail.pr_delivered`): broadcast at row INSERT (`building`), build-settle, and every
251
388
  decision mutation. **`GET /rails`** returns `prDeliveries: Record<railIndex, snapshot>`
252
389
  (newest non-terminal row per slot) for late-join hydration.
390
+ - **Monotonic generation arbitration.** Dashboard and Agent Chat retain terminal/superseded id
391
+ tombstones, accept a different id only through explicit lineage, strictly later `createdAt`, or
392
+ the exact `A.restoredFromDeliveryId=B` rollback after B terminalizes, and advance the same id only
393
+ through newer `updatedAt` evidence. A delayed A snapshot without that paired proof cannot replace
394
+ B, and a non-terminal replay cannot ordinarily reopen an id already observed terminal.
253
395
  - **Option A — dashboard.** `client/src/context/RailPrDecisionContext.tsx` (registerHandler on
254
396
  the shared socket + `GET /rails` seed) feeds `RailPrDecisionStrip` on `RailRow` (both density
255
397
  branches): on_review → Create PR / Discard; pr_draft → Open PR + Publish / Discard (degraded →
256
- retry); pr_ready → Check merge; no_changes → Done/refine; pr_closed → Reopen; pr_failed derives
257
- retry-vs-blocked actions from `delivery_outcome`. Logs remain available after settle. Buttons
398
+ retry); pr_ready → Verify PR; no_changes → Done/refine; pr_closed → Reopen; pr_failed derives
399
+ retry-vs-blocked actions from `delivery_outcome`. A blocked legacy continuation without
400
+ `delivery_sha` never offers Checkout: it exposes any live preserved worktree through Inspect local
401
+ result and offers the confirmed Commit & retry push action above. A `recovery_unavailable` row
402
+ replaces that primary action with Check again; a clone without the original worktree/object never
403
+ claims that the implementation failed or that local progress was discarded. Ordinary Checkout is restricted
404
+ to a delivery with an immutable SHA; the server repeats that guard under the repo lock and refuses
405
+ a dirty or unreadable main checkout before releasing a worktree or changing any ref. Checkout
406
+ re-reads and uses only the current attached PR branch and immutable `delivery_sha` after acquiring
407
+ the repository lock, never the pre-lock snapshot. A same-named local/remote branch must equal that
408
+ SHA before switching, and the final branch plus HEAD are verified afterward; divergence is
409
+ preserved and a failed fast-forward can never be reported as success. Logs remain available after settle. Buttons
258
410
  disable in flight and apply the authoritative HTTP snapshot immediately.
259
411
  - **Option B — agent chat.** When the row carries `origin_conversation_id`, settle posts a
260
412
  **persisted inline card**: a `'system'`-role `agent_messages` row (role is unconstrained TEXT —
261
413
  no migration; TS unions widened) whose content is the `PrDecisionCardEnvelope` JSON.
262
- `AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (update-in-place on every
263
- transition, so cold-load shows terminal states correctly) are reached from rails code via the
414
+ `AgentChatManager.postPrDecisionCard` / `updatePrDecisionCard` (idempotent update-in-place on
415
+ every transition, transactionally consolidating legacy duplicate rows by delivery id) are reached via the
264
416
  process-wide `server/agent-chat-registry.ts` singleton (null-safe: tests/disabled builds).
265
417
  Live updates ride the app-global `agent_pr_decision` WS event; the client renders
266
- `AgentPrDecisionCard` and POSTs the same project-scoped `/rails/pr-decision`.
267
- - **Race:** a stale generation or occupied operation lease returns 409 before effects. Clients
418
+ `AgentPrDecisionCard` and POSTs the same project-scoped `/rails/pr-decision`. Client hydration,
419
+ WS upsert and rendering also dedupe by delivery id, so stale blocked and current ready cards can
420
+ never expose two contradictory Checkout/Discard action sets.
421
+ - **Race / hydration ABA guard:** a stale generation or occupied operation lease returns 409 before effects. Clients
268
422
  distinguish `operation_in_progress` from a stale decision, show the active operation, disable
269
423
  actions, apply the response snapshot, ignore terminal events for an older delivery id, and
270
- hydrate again on focus/reconnect.
424
+ hydrate again on focus/reconnect. Each dashboard GET captures accepted per-rail mutation versions;
425
+ a live replacement or terminal deletion wins over its stale seed. Only accepted snapshots advance
426
+ that guard, so an ignored terminal for old A cannot make the same GET drop valid B.
427
+ Confirmation dialogs are bound to the exact delivery id/decision that opened them and disappear
428
+ when that generation changes; Checkout carries the same exact id through client and server, and
429
+ a rejected stale action never mutates or produces a success toast for B.
271
430
 
272
431
  ## Relaunch guard
273
432
 
274
433
  `POST /rails/:i/launch` returns **409 `{ error: 'pr_decision_pending', prDeliveryId }`** for an
275
434
  unresolved fresh delivery. An active `pr_draft`/`pr_ready` generation covering the same tickets is
276
- a verified continuation contract instead: launch revalidates it again under the repo lock,
277
- supersedes it atomically and runs only on that PR head. A stale concurrent request receives 409
278
- before it can allocate or claim anything. Legacy / non-isolated launches are unaffected.
435
+ a continuation contract only after exact `OPEN` identity, head/base, and `delivery_sha` evidence is
436
+ revalidated again under the repo lock, and its allocated worktree/local ref both equal that frozen
437
+ SHA. External PR discovery requires an explicit PR number or authoritative Jira key; the list match
438
+ is only discovery, and an exact authoritative PR view must freeze its live `headRefOid` before local
439
+ allocation. Local ticket numbers, title similarity, and unrelated `Fixes #<id>` text are not
440
+ authority. Any other evidence refuses admission and preserves
441
+ the existing card for Retry push or Verify PR; those decision actions terminalize exact `MERGED`
442
+ evidence or detach stale/missing-SHA terminal evidence to `on_review`. Only a verified continuation
443
+ is superseded atomically and run on that PR head. A stale
444
+ concurrent request receives 409 before it can allocate or claim anything. Legacy / non-isolated
445
+ launches are unaffected.
446
+
447
+ ## Explicit target PR (deliver-rail-into-existing-pr)
448
+
449
+ The launch body accepts an optional `targetPrNumber` — the user's explicit designation of an
450
+ existing open PR as the delivery destination ("extend PR #151"). It bypasses the automatic-inference
451
+ status gates (which never probe GitHub for a `todo` ticket) but keeps the authoritative validation
452
+ ladder: `resolveExplicitPrTarget` (`server/active-pr-continuation.ts`) resolves the number to its
453
+ canonical URL, observes the lifecycle, and throws `ExplicitPrTargetError` with a distinct code —
454
+ `target_pr_not_found` (404) / `target_pr_not_open` / `target_pr_fork` (`isCrossRepository !== false`)
455
+ / `target_pr_invalid` / `target_pr_unfetchable` (all 409) — rejecting the launch BEFORE any delivery
456
+ row or worktree exists; there is never a silent fallback to a fresh integration-branch run. The
457
+ router also 400s `invalid_target_pr` (shape) and `target_pr_requires_pr_mode` (loops/PR-delivery off,
458
+ non-isolated launch, or isolation unavailable). A valid target becomes one `ActivePrContinuationTarget`
459
+ (`source: 'explicit-target'`) applied to every launch ticket — the existing single-checkout collapse,
460
+ born-attached generation (`pr_url`/`pr_number` at insert, `isContinuation`, `borrowed-pr` ownership),
461
+ and attached-PR decision card all reuse the automatic-continuation machinery byte-identically. The
462
+ PR's `baseRefName` wins over the configured integration branch. When the slot has a continuable
463
+ delivery for the SAME PR the continuation contract drives instead (explicit target dropped as
464
+ redundant); a DIFFERENT number 409s `pr_decision_pending`. Surfaces: dashboard rail header
465
+ `RailTargetPrSelector` (candidates from `GET /rails/:i/pr-candidates` — the automatic matchers
466
+ without the status gate, display-only, fork rows disabled — plus manual number entry; selection is
467
+ one-shot, consumed by the launch) with `TargetPrLaunchDialog` confirming number/title/head branch;
468
+ MCP `specrails_rails(launch, targetPrNumber)` with operator-prompt teaching ("user names a PR →
469
+ pass it; never create a duplicate"). Fork PR push support and multi-PR batches remain out of scope.
279
470
 
280
471
  ## Ticket lifecycle — `on_review`
281
472
 
@@ -400,14 +591,25 @@ the launch INSERT. As-built:
400
591
  - **D11 — premium decision UI**: disable-on-click reconciled to authoritative snapshots, tooltips
401
592
  (base branch, PR url), designed degraded/failed states, distinct stale-vs-operation-busy 409 UX;
402
593
  English source strings, all 8 locales, locale-parity green.
403
- - **D12 — no automatic data loss**: dirty, unknown and ref-mismatched worktrees are
404
- `needs-review`; only explicit consequence-specific cleanup may force-remove them.
594
+ - **D12 — no automatic data loss**: live tracked/untracked/ignored state, HEAD, and branch ref are
595
+ revalidated before non-force automatic release; overlay roots are atomically moved into persistent
596
+ quarantine rather than unlinked; dirty, concurrent, unknown, user-ignored, ref-
597
+ mismatched, or advanced worktrees/branches are preserved.
405
598
  - **D13 — one active generation**: migration 48 supersedes historical duplicates and a partial
406
- unique index enforces one active row per rail.
599
+ unique index enforces one active row per rail; snapshot consumers enforce lineage with
600
+ `createdAt`, same-id order with `updatedAt`, terminal/superseded tombstones, exact-generation
601
+ confirmations, an accepted-mutation ABA guard, and one exact failed-replacement rollback proof.
407
602
  - **D14 — operation lease before effects**: a decision claims before Git/GitHub/ticket work;
408
603
  the loser has no external side effects and HTTP returns the authoritative snapshot.
409
- - **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`; ambiguous PR
410
- creation resolves exact open head/base identity; CLOSED is distinct and reopenable.
604
+ - **D15 — exact/idempotent delivery**: continuation retry pushes `delivery_sha`; admission,
605
+ retry/post-push, poll and reopen all validate exact PR/head/base plus SHA inclusion. Admission is
606
+ read-only and refuses non-exact continuations; inferred external PRs freeze the authoritative live
607
+ head before worktree allocation, post-run verification permits only a matched HEAD/ref advance,
608
+ and pre-push settlement freezes the new exact SHA and proves `origin` owns the attached PR before
609
+ every automatic or user-triggered push. Decision actions terminalize exact MERGED evidence
610
+ or detach stale/retargeted/terminal-without-SHA attachments to `on_review` with the SHA preserved.
611
+ Legacy recovery accepts only a unique run marker in the commit
612
+ subject. Ambiguous PR creation resolves exact open head/base identity.
411
613
  - **D16 — crash recovery before admission**: startup preserves successful interrupted work and
412
614
  makes every stale `building` generation actionable before another launch can reuse its path.
413
615
  - **D17 — ownership-aware continuation cleanup**: dismiss/discard never closes or deletes the
@@ -417,6 +619,16 @@ the launch INSERT. As-built:
417
619
  mutating newer work, and terminal Agent cards are re-projected.
418
620
  - **D19 — truthful no-change completion**: fresh no-change has Mark done (`completed`) and Refine;
419
621
  a continuation only dismisses its borrowed follow-up.
622
+ - **D20 — preserved-result recovery**: startup also recovers one uniquely run-marked unreachable
623
+ commit; otherwise both cards hide misleading Checkout, reveal live delivery-owned worktrees, and
624
+ offer a lease-guarded Commit & retry push that is exact-baseline, fast-forward, immutable-SHA and
625
+ non-force while leaving the user's main checkout untouched. Recovery worktrees, commit index
626
+ pathspecs, same-repository PR ownership, origin identity, and checkout cleanliness all fail closed.
627
+ - **D21 — orphan protection and truthful device-local fallback**: startup and manual Recheck share
628
+ one bounded refs/reflogs/unreachable scan; an exact orphan is pinned under an internal recovery ref
629
+ before durable/network mutation. No evidence, ambiguous evidence, or a later unrelated branch tip
630
+ stays non-destructive and renders localized other-computer guidance instead of looping Commit or
631
+ Checkout.
420
632
 
421
633
  ## Kill-switch
422
634
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.29.2",
3
+ "version": "2.29.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",