specrails-desktop 2.62.0 → 2.63.0

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 (161) hide show
  1. package/cli/dist/specrails-desktop.js +39 -5
  2. package/client/dist/assets/{ActivityFeedPage-C_pqc281.js → ActivityFeedPage-Bn2MpG5M.js} +1 -1
  3. package/client/dist/assets/{AgentBrowserCapture-vz6rbDtu.js → AgentBrowserCapture-CHlXWdXq.js} +1 -1
  4. package/client/dist/assets/AgentModeAnalyticsPane-yj-YCgrK.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-Xczcyghb.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-99fBBLri.js → AgentModeJobsPane-DtOW01ln.js} +1 -1
  7. package/client/dist/assets/AgentsPage-BMfJ4mzU.js +118 -0
  8. package/client/dist/assets/AnalyticsPage-CGn5cgtc.js +1 -0
  9. package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
  10. package/client/dist/assets/CodePage-C8NIginr.js +3 -0
  11. package/client/dist/assets/DesktopAnalyticsPage-C87r4y26.js +1 -0
  12. package/client/dist/assets/{DocsDialog-XUCYKoq0.js → DocsDialog-Z50a_KPv.js} +1 -1
  13. package/client/dist/assets/{DocsPage-dOLC8bXA.js → DocsPage-DIArbEPl.js} +1 -1
  14. package/client/dist/assets/{ExportDropdown-BvKQgwM9.js → ExportDropdown-CJdCqYd7.js} +1 -1
  15. package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
  16. package/client/dist/assets/InteractiveJobComposer-BxLh4me-.js +19 -0
  17. package/client/dist/assets/{JobDetailModal-CkiubrkZ.js → JobDetailModal-C7dWoRu6.js} +1 -1
  18. package/client/dist/assets/{JobDetailPage-CNe4Wn7l.js → JobDetailPage-Ilu294WS.js} +1 -1
  19. package/client/dist/assets/JobsPage-fPq1hefb.js +1 -0
  20. package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +1 -0
  21. package/client/dist/assets/{LoopPreviewModal-Dv3dxHr_.js → LoopPreviewModal-ChI3NQC7.js} +1 -1
  22. package/client/dist/assets/{LoopWindowSurface-DN2lbSnL.js → LoopWindowSurface-D1WB9i-9.js} +1 -1
  23. package/client/dist/assets/LoopsPage-CCwc0Biu.js +2 -0
  24. package/client/dist/assets/MinimizedChatsContext-eH_KaAZ1.js +1 -0
  25. package/client/dist/assets/PluginsPage-DfDfEfbW.js +2 -0
  26. package/client/dist/assets/ProjectSettingsDialog-9NHnG8JQ.js +1 -0
  27. package/client/dist/assets/RepositoryDeliveries-DgzNnJmn.js +5 -0
  28. package/client/dist/assets/{RepositoryScopeSelector-DrtPqvc_.js → RepositoryScopeSelector-BSgUqk_u.js} +1 -1
  29. package/client/dist/assets/ReviewPacketPage-D34bGpSA.js +4 -0
  30. package/client/dist/assets/{TemplatePreviewModal-DcFlhUxy.js → TemplatePreviewModal-CcfSxWQ7.js} +1 -1
  31. package/client/dist/assets/TicketDetailModalContext-BoafEcm4.js +16 -0
  32. package/client/dist/assets/Trans-CVSryAL1.js +1 -0
  33. package/client/dist/assets/ban-wqfdzoXt.js +1 -0
  34. package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
  35. package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
  36. package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
  37. package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
  38. package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
  39. package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
  40. package/client/dist/assets/download-C9wP_42h.js +1 -0
  41. package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
  42. package/client/dist/assets/file-text-C0blZHLh.js +1 -0
  43. package/client/dist/assets/{formatDistanceToNow-DlQY1Lm_.js → formatDistanceToNow-NVM6rK3i.js} +1 -1
  44. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-COUeq65G.js → getTimezoneOffsetInMilliseconds-ZHg7WpdH.js} +1 -1
  45. package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
  46. package/client/dist/assets/{index-CONHvEJw.js → index-Culan6y7.js} +40 -40
  47. package/client/dist/assets/{index-BacrO_ub.css → index-D1v33giW.css} +1 -1
  48. package/client/dist/assets/jira-api-Bl8xEIpk.js +1 -0
  49. package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
  50. package/client/dist/assets/layers-BCLTgHXO.js +1 -0
  51. package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
  52. package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
  53. package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
  54. package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
  55. package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
  56. package/client/dist/assets/plus-fHDdjBk1.js +1 -0
  57. package/client/dist/assets/{project-repositories-Cc3oNv8X.js → project-repositories-CvJf1a6H.js} +1 -1
  58. package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
  59. package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
  60. package/client/dist/assets/save-LmPSfsvZ.js +1 -0
  61. package/client/dist/assets/search-BDVp3fkH.js +1 -0
  62. package/client/dist/assets/{settings-BKJZ2KEM.js → settings-BO2N-oPf.js} +1 -1
  63. package/client/dist/assets/{settings-LfFnl1UH.js → settings-BQChAs_w.js} +1 -1
  64. package/client/dist/assets/{settings-BV3jocIx.js → settings-BzmYlcQB.js} +1 -1
  65. package/client/dist/assets/{settings-BjM2Dw8_.js → settings-CGjXb6TX.js} +1 -1
  66. package/client/dist/assets/{settings-DA77M48m.js → settings-CMxEWY46.js} +1 -1
  67. package/client/dist/assets/{settings-P1IQvhnL.js → settings-C_dRSgI2.js} +1 -1
  68. package/client/dist/assets/{settings-eIMJjyK_.js → settings-Hw7av-B1.js} +1 -1
  69. package/client/dist/assets/{settings-CKu0LTib.js → settings-nryN6wuS.js} +1 -1
  70. package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
  71. package/client/dist/assets/spending-BfjSJjBd.js +1 -0
  72. package/client/dist/assets/square-D0JTbRSR.js +1 -0
  73. package/client/dist/assets/trash-D8zYUmTg.js +1 -0
  74. package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
  75. package/client/dist/assets/upload-CCHvUkRs.js +1 -0
  76. package/client/dist/assets/{useDesktop-9Lwoel_8.js → useDesktop-C5JVBeur.js} +1 -1
  77. package/client/dist/assets/{useRuntimeRuns-DL0xYoZ2.js → useRuntimeRuns-DohBmeg8.js} +1 -1
  78. package/client/dist/assets/useSharedWebSocket-CNOvmgTr.js +2 -0
  79. package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
  80. package/client/dist/assets/x-Bu7X_eR2.js +1 -0
  81. package/client/dist/assets/zap-bugHyWzQ.js +1 -0
  82. package/client/dist/index.html +43 -44
  83. package/docs/ci-cd.md +8 -1
  84. package/docs/customizing.md +1 -0
  85. package/docs/getting-started.md +1 -1
  86. package/docs/guide/de/settings/5-project-environment.md +27 -0
  87. package/docs/guide/en/settings/5-project-environment.md +27 -0
  88. package/docs/guide/es/settings/5-project-environment.md +27 -0
  89. package/docs/guide/fr/settings/5-project-environment.md +27 -0
  90. package/docs/guide/it/settings/5-project-environment.md +27 -0
  91. package/docs/guide/ja/settings/5-project-environment.md +27 -0
  92. package/docs/guide/pt/settings/5-project-environment.md +27 -0
  93. package/docs/guide/zh/settings/5-project-environment.md +27 -0
  94. package/docs/internals/api-reference.md +3 -1
  95. package/docs/internals/configuration.md +46 -5
  96. package/docs/internals/safe-pr-review-flow.md +15 -0
  97. package/docs/internals/source-map.md +3 -1
  98. package/docs/mcp.md +1 -1
  99. package/docs/platforms/macos.md +2 -0
  100. package/docs/platforms/windows-parity.md +4 -4
  101. package/docs/platforms/windows.md +9 -9
  102. package/local-runner/dist/specrails-local-runner.js +1 -1
  103. package/package.json +8 -9
  104. package/server/dist/mcp/tools/env.js +38 -3
  105. package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
  106. package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
  107. package/server/dist/modules/loops/runtime/loop-run-manager.js +7 -0
  108. package/server/dist/modules/project-settings/adapters/http.js +24 -0
  109. package/server/dist/path-resolver.js +135 -20
  110. package/server/dist/project-env.js +265 -17
  111. package/server/dist/project-registry.js +14 -0
  112. package/server/dist/project-router-settings.js +17 -0
  113. package/server/dist/worktree-node-modules.js +95 -1
  114. package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +0 -2
  115. package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +0 -2
  116. package/client/dist/assets/AgentsPage-CTwm_6T8.js +0 -118
  117. package/client/dist/assets/AnalyticsPage-YD8MLoPa.js +0 -1
  118. package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
  119. package/client/dist/assets/CodePage-CLJn2F2e.js +0 -3
  120. package/client/dist/assets/DesktopAnalyticsPage-MWGQVbwp.js +0 -1
  121. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
  122. package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +0 -19
  123. package/client/dist/assets/JobsPage-DSts_L_d.js +0 -1
  124. package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +0 -1
  125. package/client/dist/assets/LoopsPage-DHgIreEW.js +0 -2
  126. package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +0 -1
  127. package/client/dist/assets/PluginsPage-D1EJ9oja.js +0 -2
  128. package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +0 -1
  129. package/client/dist/assets/RepositoryDeliveries-CW4eKG3x.js +0 -5
  130. package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +0 -4
  131. package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +0 -16
  132. package/client/dist/assets/Trans-CAY2Rd2f.js +0 -1
  133. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  134. package/client/dist/assets/chevron-up-CwGUHd6D.js +0 -1
  135. package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
  136. package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
  137. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
  138. package/client/dist/assets/dist-Bq10nS2m.js +0 -1
  139. package/client/dist/assets/download-CFHyyHcw.js +0 -1
  140. package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
  141. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  142. package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
  143. package/client/dist/assets/jira-api-C94VNHNY.js +0 -1
  144. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
  145. package/client/dist/assets/layers-eDMjG41I.js +0 -1
  146. package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
  147. package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
  148. package/client/dist/assets/plus-CsnjfkmX.js +0 -1
  149. package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
  150. package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
  151. package/client/dist/assets/save-B6wGWwBR.js +0 -1
  152. package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
  153. package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
  154. package/client/dist/assets/spending-DemeFfrH.js +0 -1
  155. package/client/dist/assets/square-Ci-PFcFe.js +0 -1
  156. package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
  157. package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
  158. package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
  159. package/client/dist/assets/useSharedWebSocket-BEHPeU1Y.js +0 -2
  160. package/client/dist/assets/x-CBMI9ejn.js +0 -1
  161. package/client/dist/assets/zap-CHo3aCD1.js +0 -1
@@ -6,8 +6,8 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
6
6
 
7
7
  | Feature | Windows implementation and checks |
8
8
  | --- | --- |
9
- | Installation, native runtimes | Native x64/ARM64 NSIS and MSI builds; embedded WebView2 offline provisioning; bundled Node/Git/core/OpenSpec; staged SQLite and full ConPTY/WinPTY dependency validation. |
10
- | Desktop updates | Installer-specific Tauri targets keep NSIS and MSI separate. Missing, empty or ambiguous artifact/signature pairs fail manifest generation. Generic NSIS entries remain for older clients. Publication is serialized, and previous download installers are retained until the remote manifest matches the new release. |
9
+ | Installation, native runtimes | Native x64/ARM64 NSIS builds; WebView2 provisioning through the embedded bootstrapper; bundled Node/Git/core/OpenSpec; staged SQLite and full ConPTY/WinPTY dependency validation. |
10
+ | Desktop updates | NSIS-specific and generic Tauri targets both reference the NSIS installer; earlier MSI installs fall back to the generic entry and the NSIS installer removes the WiX installation. Missing, empty or ambiguous artifact/signature pairs fail manifest generation. Publication is serialized, and previous download installers are retained until the remote manifest matches the new release. |
11
11
  | Startup and project catalog | Recover the actual Windows profile rather than inventing `C:\Users\Default`; SQLite catalog/repository IDs reopen under the same Unicode/spaced profile. |
12
12
  | Projects and multi-repo | Repository membership and canonical identity use real paths; junction and case aliases cannot add the same repository twice. Shared backlog and per-repository jobs retain their existing regression coverage. |
13
13
  | Core setup and updates | Offline assembly from the shipped core; preserve and restore the previous active framework if Windows junction replacement fails. Installed-package smoke waits for the real workspace marker. |
@@ -28,7 +28,7 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
28
28
 
29
29
  `.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every pull request and push to `main`, adding `windows-11-arm` on the `main` pushes. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
30
30
 
31
- `.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts. For each NSIS and MSI package, the script installs into a temporary path containing spaces and drives `scripts/smoke-installed-windows.mjs` with the installed Node runtime. The driver uses an isolated user profile and tests:
31
+ `.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts. The script fails on any MSI bundle, then installs the NSIS package into a temporary path containing spaces and drives `scripts/smoke-installed-windows.mjs` with the installed Node runtime. The driver uses an isolated user profile and tests:
32
32
 
33
33
  1. The installed pkg sidecar boots with bundled resources and authenticates API access.
34
34
  2. A temporary Git repository is registered and core assembly completes offline.
@@ -53,7 +53,7 @@ The detachable-window implementation has passed native macOS fixtures locally, i
53
53
 
54
54
  Before claiming complete Windows parity, record successful x64 and ARM64 CI/release runs and exercise the actual installed UI on supported Windows versions:
55
55
 
56
- - Clean NSIS and MSI installs, in-place upgrades from the previous release, relaunch, tray quit and uninstall. Verify one installation entry and preserved projects/logs.
56
+ - Clean NSIS installs, in-place upgrades from the previous release (including an MSI-installed release migrating through the updater), relaunch, tray quit and uninstall. Verify one installation entry and preserved projects/logs.
57
57
  - Native browsing and capture at 100%, 150% and 200% display scaling; resize, maximize, multi-monitor movement, keyboard/clipboard, popup/self-close, and the relevant Okta/SSO tenant. Detach two active missions, minimize main independently, move them between monitors, and reattach into a window with its own open browser; verify both sessions and pending inputs remain intact.
58
58
  - Provider authentication and one real mission/rail per enabled provider; steering during a tool call; cancellation; multi-repo integration/checkout with clean and conflicting working trees.
59
59
  - Long-running frontend/backend processes, immediate startup failure, stopping nested processes, app quit/update during execution, and persisted logs after restart.
@@ -15,14 +15,14 @@ Installers are published on every release under:
15
15
 
16
16
  > 📥 `https://specrails.dev/downloads/specrails-desktop/latest/`
17
17
 
18
- Pick the pair that matches your CPU architecture:
18
+ Pick the installer that matches your CPU architecture:
19
19
 
20
- | Architecture | NSIS installer (recommended) | MSI installer (enterprise/group-policy) |
21
- | --- | --- | --- |
22
- | x64 | `specrails-desktop-<version>-x64-setup.exe` | `specrails-desktop-<version>-x64.msi` |
23
- | ARM64 | `specrails-desktop-<version>-arm64-setup.exe` | `specrails-desktop-<version>-arm64.msi` |
20
+ | Architecture | Installer |
21
+ | --- | --- |
22
+ | x64 | `specrails-desktop-<version>-x64-setup.exe` |
23
+ | ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
24
24
 
25
- If you are unsure which to use, the NSIS `-setup.exe` is the right choice for individual installs; the MSI exists for enterprise deployment. Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
25
+ Windows ships only the NSIS installer. For unattended or managed deployment, run it silently with `/S` (optionally `/D=<install dir>` as the last argument). Releases up to 2.62 also published an `.msi`; see [Updates](#updates) for how those installations migrate. The installer provisions Microsoft Edge WebView2 when it is missing, downloading it through a small embedded bootstrapper (Windows 11 and current Windows 10 already include it). Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
26
26
 
27
27
  A machine-readable `manifest.json` in `latest/` describes the current release (version, sha256, size) per platform, including `windows-x64` and `windows-arm64` entries — consumers can read it to build download links without hardcoding a version.
28
28
 
@@ -59,7 +59,7 @@ When the bundle is present, the Tauri host sets `SPECRAILS_IS_DESKTOP=1` and `SP
59
59
 
60
60
  The desktop app self-updates via the Tauri updater plugin. It checks a GitHub Releases `latest.json` endpoint and, on Windows, applies updates with `installMode: "passive"` — the update runs with a minimal progress UI and the app relaunches into the new version.
61
61
 
62
- Updates preserve the installation format: NSIS installs receive a signed `-setup.exe` updater artifact, and MSI installs receive a signed `.msi` artifact for the same architecture. The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication; the release no longer silently substitutes MSI for NSIS.
62
+ Updates use the signed NSIS `-setup.exe` for the same architecture. Installations made from an earlier `.msi` receive that NSIS update too: the installer detects the MSI installation and removes it before installing, so a single installation remains (Windows may ask for administrator approval to remove the machine-wide MSI). The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication.
63
63
 
64
64
  Normal quit and updates request an authenticated graceful shutdown of the owned sidecar first, giving processes and persistent logs time to close before a bounded force-stop fallback.
65
65
 
@@ -73,8 +73,8 @@ Reserved paths (`.specrails/profiles/**`, `.claude/agents/custom-*.md`) are pres
73
73
 
74
74
  ## Uninstall
75
75
 
76
- - NSIS: use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
77
- - MSI: use *Settings → Apps* or `msiexec /x <msi-path>`.
76
+ - Use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
77
+ - An installation from an earlier `.msi` release that has not updated yet: *Settings → Apps* or `msiexec /x <msi-path>`.
78
78
 
79
79
  ## Known limitations
80
80
 
@@ -4036,7 +4036,7 @@ var require_core = __commonJS({
4036
4036
  constructor(opts = {}) {
4037
4037
  this.schemas = {};
4038
4038
  this.refs = {};
4039
- this.formats = {};
4039
+ this.formats = /* @__PURE__ */ Object.create(null);
4040
4040
  this._compilations = /* @__PURE__ */ new Set();
4041
4041
  this._loading = {};
4042
4042
  this._cache = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.62.0",
3
+ "version": "2.63.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -56,7 +56,7 @@
56
56
  "dependencies": {
57
57
  "@modelcontextprotocol/sdk": "^1.29.0",
58
58
  "@tailwindcss/typography": "^0.5.19",
59
- "ajv": "^8.17.1",
59
+ "ajv": "^8.20.0",
60
60
  "better-sqlite3": "^12.8.0",
61
61
  "chokidar": "^5.0.0",
62
62
  "cross-spawn": "^7.0.6",
@@ -71,29 +71,28 @@
71
71
  "selfsigned": "^5.5.0",
72
72
  "tree-kill": "^1.2.2",
73
73
  "werift": "^0.23.0",
74
- "ws": "^8.21.0",
74
+ "ws": "^8.22.0",
75
75
  "zod": "^3.25.76"
76
76
  },
77
77
  "devDependencies": {
78
+ "@babel/parser": "^7.29.9",
78
79
  "@tauri-apps/cli": "^2.0.0",
79
80
  "@types/better-sqlite3": "^7.6.0",
80
- "@types/chokidar": "^1.7.5",
81
81
  "@types/cross-spawn": "^6.0.6",
82
- "@types/diff": "^7.0.2",
83
82
  "@types/express": "^5.0.6",
84
83
  "@types/js-yaml": "^4.0.9",
85
84
  "@types/multer": "^2.1.0",
86
- "@types/node": "^25.5.0",
85
+ "@types/node": "^26.6.5",
87
86
  "@types/pdf-parse": "^1.1.5",
88
87
  "@types/supertest": "^7.2.0",
89
88
  "@types/ws": "^8.5.0",
90
- "@vitest/coverage-v8": "^4.1.10",
89
+ "@vitest/coverage-v8": "^5.0.3",
91
90
  "concurrently": "^9.2.1",
92
91
  "esbuild": "^0.28.1",
93
92
  "supertest": "^7.2.2",
94
93
  "tsx": "^4.23.0",
95
- "typescript": "^5.4.0",
96
- "vitest": "^4.1.10"
94
+ "typescript": "^7.0.2",
95
+ "vitest": "^5.0.3"
97
96
  },
98
97
  "overrides": {
99
98
  "ip": "npm:@eggjs/ip@2.1.0",
@@ -15,6 +15,26 @@ function uniq(names) {
15
15
  }
16
16
  return out;
17
17
  }
18
+ /** Names and states only: rebuild each record field by field so nothing but
19
+ * the documented, value-free fields can ever reach an MCP client. */
20
+ function sanitizeStatus(raw) {
21
+ const report = (raw && typeof raw === 'object' ? raw : {});
22
+ const names = Array.isArray(report.names) ? report.names : [];
23
+ return {
24
+ checking: report.checking === true,
25
+ loginShellRecovery: report.loginShellRecovery !== false,
26
+ timeoutMs: typeof report.timeoutMs === 'number' ? report.timeoutMs : null,
27
+ statuses: names
28
+ .filter((entry) => typeof entry?.name === 'string' && typeof entry?.status === 'string')
29
+ .map((entry) => ({
30
+ name: entry.name,
31
+ status: entry.status,
32
+ shell: typeof entry.shell === 'string' ? entry.shell : null,
33
+ checkedAt: typeof entry.checkedAt === 'string' ? entry.checkedAt : null,
34
+ exitCode: typeof entry.exitCode === 'number' ? entry.exitCode : null,
35
+ })),
36
+ };
37
+ }
18
38
  function envTools() {
19
39
  return [
20
40
  {
@@ -22,11 +42,12 @@ function envTools() {
22
42
  title: 'Project worktree environment',
23
43
  description: 'Read, scan, and configure the per-project environment variable NAME allowlist used for rail jobs and isolated loop worktrees. ' +
24
44
  'Values are never read or stored by this tool; runs read them from the server environment and can recover configured names from the login shell when the desktop process is missing them. scan inspects safe repo config files such as package.json, .npmrc, .yarnrc.yml and pnpm-workspace.yaml for ${VAR} / process.env.VAR references and private scoped package auth hints. ' +
25
- 'Actions: get (read current names), scan (discover candidates), set (write names), auto_configure (scan then merge discovered names into settings).',
45
+ 'Each configured name reports a value-free resolution status: inherited, recovered (from the login shell), not-defined, probe-timeout or probe-failed. ' +
46
+ 'Actions: get (read current names and their statuses), status (statuses only), recheck (probe the login shell now, then return statuses), scan (discover candidates), set (write names), auto_configure (scan then merge discovered names into settings).',
26
47
  hintTier: 'read',
27
48
  tier: (args) => (args.action === 'set' || args.action === 'auto_configure' ? 'write' : 'read'),
28
49
  inputSchema: {
29
- action: zod_1.z.enum(['get', 'scan', 'set', 'auto_configure']).describe('Operation'),
50
+ action: zod_1.z.enum(['get', 'status', 'recheck', 'scan', 'set', 'auto_configure']).describe('Operation'),
30
51
  projectId: zod_1.z.string().optional().describe('Project id (defaults to the active project)'),
31
52
  names: zod_1.z.array(zod_1.z.string()).optional().describe('set: env var names to configure'),
32
53
  merge: zod_1.z.boolean().optional().describe('set: merge with existing names instead of replacing them (default true)'),
@@ -36,7 +57,21 @@ function envTools() {
36
57
  const base = (0, types_1.projectPath)(ctx, args.projectId);
37
58
  if (action === 'get') {
38
59
  const settings = await (0, types_1.apiCall)(ctx, 'GET', `${base}/settings`);
39
- return { names: Array.isArray(settings.worktreeEnvPassthrough) ? settings.worktreeEnvPassthrough : [] };
60
+ const names = Array.isArray(settings.worktreeEnvPassthrough) ? settings.worktreeEnvPassthrough : [];
61
+ // Statuses are best-effort: an older server without the route still answers names.
62
+ try {
63
+ const { statuses } = sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
64
+ return { names, statuses };
65
+ }
66
+ catch {
67
+ return { names };
68
+ }
69
+ }
70
+ if (action === 'status') {
71
+ return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
72
+ }
73
+ if (action === 'recheck') {
74
+ return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'POST', `${base}/env-passthrough/recheck`));
40
75
  }
41
76
  if (action === 'scan') {
42
77
  const project = (0, types_1.requireProject)(ctx, args.projectId).project;
@@ -2217,8 +2217,12 @@ class QueueManager {
2217
2217
  // values are read from the server env (with login-shell recovery for missing
2218
2218
  // names) at spawn time. Apply this before Specrails' own env overlays so
2219
2219
  // internal SPECRAILS_* control-plane values always win.
2220
+ let environmentNotice = null;
2220
2221
  if (this._db) {
2221
2222
  spawnEnv = (0, project_env_1.applyWorktreeEnvPassthrough)(this._db, spawnEnv);
2223
+ // One value-free line naming unresolved configured names (written to the
2224
+ // job log once the readers exist). The run proceeds without them.
2225
+ environmentNotice = (0, project_env_1.worktreeEnvPassthroughWarning)(this._db, spawnEnv);
2222
2226
  }
2223
2227
  const telemetryEnabled = !!(this._projectId && this._db && (0, db_1.getProjectSettings)(this._db).pipelineTelemetryEnabled);
2224
2228
  // Resolve the framework version ONCE at spawn time — `framework/current`
@@ -2605,6 +2609,10 @@ class QueueManager {
2605
2609
  const stdoutReader = (0, readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
2606
2610
  const stderrReader = (0, readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
2607
2611
  this._jobReaders.set(jobId, { stdout: stdoutReader, stderr: stderrReader });
2612
+ if (environmentNotice) {
2613
+ persistEvent({ event_type: 'log', source: 'stderr', payload: JSON.stringify({ line: environmentNotice }) }, false);
2614
+ emitLine('stderr', environmentNotice);
2615
+ }
2608
2616
  stdoutReader.on('line', (line) => {
2609
2617
  if (this._disposed)
2610
2618
  return;
@@ -181,6 +181,7 @@ function createLoopExecutors(opts = {}) {
181
181
  const completionContexts = new Map();
182
182
  const runtimeConfigPath = (cwd) => (0, node_path_1.join)(opts.pluginScope?.().stateRoot ?? cwd, '.specrails', 'agent-runtime.json');
183
183
  return {
184
+ ...(opts.environmentNotice ? { environmentNotice: opts.environmentNotice } : {}),
184
185
  async cancelDefinition({ runId, contextPath, requestId }) {
185
186
  const host = (0, agent_runtime_bridge_1.readFrozenRuntimeHost)(contextPath, resolveEnv(), runId);
186
187
  await (0, agent_runtime_bridge_1.runAgentRuntimeControl)({ kind: 'cancel', runId, contextPath, requestId, ...host });
@@ -1009,6 +1009,13 @@ class LoopRunManager {
1009
1009
  logLine(`▶ Loop "${req.loopName ?? req.loopId}" started${req.spec?.title ? ` — spec: ${req.spec.title}` : ''}`);
1010
1010
  if (req.isolation)
1011
1011
  logLine(`⎇ Isolated worktree: ${req.isolation.worktreePath} (branch ${req.isolation.branch})`);
1012
+ let environmentNotice = null;
1013
+ try {
1014
+ environmentNotice = this.executors.environmentNotice?.() ?? null;
1015
+ }
1016
+ catch { /* diagnostic only */ }
1017
+ if (environmentNotice)
1018
+ logLine(environmentNotice, 'stderr');
1012
1019
  if (!definitionEngine && !usageTelemetryAvailable) {
1013
1020
  logLine(`⚠️ ${adapter.displayName} does not report token/cost usage in headless mode; usage totals will remain unavailable.` +
1014
1021
  (configuredMaxCostUsd !== undefined
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerProjectSettingsHttp = registerProjectSettingsHttp;
4
+ exports.registerEnvPassthroughStatusHttp = registerEnvPassthroughStatusHttp;
4
5
  const __1 = require("..");
5
6
  /** Inbound adapter: translate HTTP into use cases and domain errors into HTTP. */
6
7
  function registerProjectSettingsHttp(router, serviceForRequest,
@@ -34,3 +35,26 @@ onUpdated) {
34
35
  }
35
36
  });
36
37
  }
38
+ /** Inbound adapter for `GET /:projectId/env-passthrough/status` and
39
+ * `POST /:projectId/env-passthrough/recheck`. Responses carry names and
40
+ * states only; values never reach this adapter. */
41
+ function registerEnvPassthroughStatusHttp(router, port) {
42
+ router.get('/:projectId/env-passthrough/status', async (req, res) => {
43
+ try {
44
+ res.json(await port.read(req));
45
+ }
46
+ catch (error) {
47
+ console.error('[project-router] env status error:', error);
48
+ res.status(500).json({ error: 'Failed to read environment status' });
49
+ }
50
+ });
51
+ router.post('/:projectId/env-passthrough/recheck', async (req, res) => {
52
+ try {
53
+ res.json(await port.recheck(req));
54
+ }
55
+ catch (error) {
56
+ console.error('[project-router] env recheck error:', error);
57
+ res.status(500).json({ error: 'Failed to recheck environment' });
58
+ }
59
+ });
60
+ }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AUTH_ENV_VARS = void 0;
6
+ exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = exports.AUTH_ENV_VARS = void 0;
7
7
  exports.ensureWindowsBaseEnv = ensureWindowsBaseEnv;
8
8
  exports.resolveBundledRuntimePath = resolveBundledRuntimePath;
9
9
  exports.resolveBundledNodeExe = resolveBundledNodeExe;
@@ -12,6 +12,10 @@ exports.parseLoginShellOutput = parseLoginShellOutput;
12
12
  exports.augmentPathFromLoginShell = augmentPathFromLoginShell;
13
13
  exports.parseLoginShellEnv = parseLoginShellEnv;
14
14
  exports.augmentEnvFromLoginShell = augmentEnvFromLoginShell;
15
+ exports.resolveLoginShellProbeTimeoutMs = resolveLoginShellProbeTimeoutMs;
16
+ exports.hasCompleteLoginShellEnvBlock = hasCompleteLoginShellEnvBlock;
17
+ exports.readEnvFromLoginShell = readEnvFromLoginShell;
18
+ exports.readEnvFromLoginShellSyncDetailed = readEnvFromLoginShellSyncDetailed;
15
19
  exports.readEnvFromLoginShellSync = readEnvFromLoginShellSync;
16
20
  exports.augmentEnvFromLoginShellSync = augmentEnvFromLoginShellSync;
17
21
  exports.augmentAuthEnvFromLoginShell = augmentAuthEnvFromLoginShell;
@@ -580,17 +584,130 @@ async function augmentEnvFromLoginShell(names, opts = {}) {
580
584
  });
581
585
  });
582
586
  }
583
- /** Read missing configured names without mutating the caller's or global env.
584
- * Callers own any cache so project credentials cannot leak across scopes. */
585
- function readEnvFromLoginShellSync(names, opts = {}) {
587
+ /** Default budget for the asynchronous project-env probe. Real interactive
588
+ * profiles (oh-my-zsh, nvm, powerlevel10k, conda) routinely exceed 1.5 s. */
589
+ exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = 10_000;
590
+ const LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS = 120_000;
591
+ const LOGIN_SHELL_OUTPUT_CAP = 1024 * 1024;
592
+ /** `SPECRAILS_LOGIN_SHELL_TIMEOUT_MS` overrides the asynchronous probe budget.
593
+ * Invalid or non-positive values fall back to the 10 s default; values are
594
+ * capped at 2 minutes so a typo cannot park a probe forever. */
595
+ function resolveLoginShellProbeTimeoutMs(env = process.env) {
596
+ const raw = env.SPECRAILS_LOGIN_SHELL_TIMEOUT_MS?.trim();
597
+ if (!raw || !/^\d+$/.test(raw))
598
+ return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
599
+ const value = Number(raw);
600
+ if (!Number.isFinite(value) || value <= 0)
601
+ return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
602
+ return Math.min(value, LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS);
603
+ }
604
+ /** True when the probe output contains the complete env sentinel block. The
605
+ * block is the proof that the printf ran, independent of the shell's status. */
606
+ function hasCompleteLoginShellEnvBlock(stdout) {
607
+ const begin = stdout.indexOf(ENV_BEGIN);
608
+ return begin !== -1 && stdout.indexOf(ENV_END, begin + ENV_BEGIN.length) !== -1;
609
+ }
610
+ function loginShellProbeSkipped(names = []) {
611
+ return { values: {}, status: 'skipped', exitCode: null, shell: null, names };
612
+ }
613
+ function pickRecovered(stdout, wanted) {
614
+ const recovered = parseLoginShellEnv(stdout);
615
+ const out = {};
616
+ for (const key of wanted) {
617
+ const val = recovered[key];
618
+ if (val)
619
+ out[key] = val;
620
+ }
621
+ return out;
622
+ }
623
+ function settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut) {
624
+ if (hasCompleteLoginShellEnvBlock(stdout)) {
625
+ return { values: pickRecovered(stdout, wanted), status: 'ok', exitCode, shell, names: wanted };
626
+ }
627
+ return { values: {}, status: timedOut ? 'timeout' : 'failed', exitCode, shell, names: wanted };
628
+ }
629
+ function wantedLoginShellNames(names, env) {
586
630
  if (process.platform === 'win32')
587
- return {};
631
+ return null;
588
632
  if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true')
589
- return {};
633
+ return null;
634
+ return normalizeLoginShellEnvNames(names).filter((name) => !env[name]);
635
+ }
636
+ /** Asynchronously read missing configured names from the login shell without
637
+ * blocking the event loop or mutating any environment. Values from a complete
638
+ * sentinel block are accepted regardless of the shell's exit status. Output is
639
+ * capped at 1 MiB; the child is killed at the timeout. Callers own any cache. */
640
+ function readEnvFromLoginShell(names, opts = {}) {
590
641
  const env = opts.env ?? process.env;
591
- const wanted = normalizeLoginShellEnvNames(names).filter((name) => !env[name]);
642
+ const wanted = wantedLoginShellNames(names, env);
643
+ if (wanted === null)
644
+ return Promise.resolve(loginShellProbeSkipped());
592
645
  if (wanted.length === 0)
593
- return {};
646
+ return Promise.resolve(loginShellProbeSkipped(wanted));
647
+ const spawnFn = opts.spawnFn ?? child_process_1.spawn;
648
+ const timeoutMs = opts.timeoutMs ?? resolveLoginShellProbeTimeoutMs(env);
649
+ const shell = resolveLoginShell(env);
650
+ const command = buildLoginShellEnvCommand(wanted);
651
+ return new Promise((resolve) => {
652
+ let child;
653
+ try {
654
+ child = spawnFn(shell, ['-l', '-i', '-c', command], { env, stdio: ['ignore', 'pipe', 'pipe'] });
655
+ }
656
+ catch {
657
+ resolve({ values: {}, status: 'failed', exitCode: null, shell, names: wanted });
658
+ return;
659
+ }
660
+ let stdout = '';
661
+ let timedOut = false;
662
+ let settled = false;
663
+ const kill = () => { try {
664
+ child.kill('SIGKILL');
665
+ }
666
+ catch { /* ignore */ } };
667
+ const timer = setTimeout(() => {
668
+ timedOut = true;
669
+ kill();
670
+ // Settle now: a background job started by the profile can keep the pipes
671
+ // (and so 'close') open long after the shell itself was killed. A complete
672
+ // block printed before a hung exit hook is still a success.
673
+ finish(null);
674
+ }, timeoutMs);
675
+ const finish = (exitCode) => {
676
+ if (settled)
677
+ return;
678
+ settled = true;
679
+ clearTimeout(timer);
680
+ resolve(settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut));
681
+ };
682
+ child.stdout?.on('data', (chunk) => {
683
+ if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP)
684
+ return;
685
+ stdout += chunk.toString('utf-8');
686
+ if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP) {
687
+ stdout = stdout.slice(0, LOGIN_SHELL_OUTPUT_CAP);
688
+ if (!hasCompleteLoginShellEnvBlock(stdout))
689
+ kill();
690
+ }
691
+ });
692
+ child.stderr?.on('data', () => { });
693
+ child.on('error', () => finish(null));
694
+ child.on('close', (code) => finish(typeof code === 'number' ? code : null));
695
+ });
696
+ }
697
+ function isSpawnSyncTimeout(res) {
698
+ const error = res.error;
699
+ return Boolean(error && (error.code === 'ETIMEDOUT' || /ETIMEDOUT/.test(error.message)));
700
+ }
701
+ /** Synchronous, detailed variant used only as the cold fallback at spawn time.
702
+ * Keeps the short 1.5 s budget so a cold spawn never freezes the server for
703
+ * long, and accepts a complete sentinel block regardless of exit status. */
704
+ function readEnvFromLoginShellSyncDetailed(names, opts = {}) {
705
+ const env = opts.env ?? process.env;
706
+ const wanted = wantedLoginShellNames(names, env);
707
+ if (wanted === null)
708
+ return loginShellProbeSkipped();
709
+ if (wanted.length === 0)
710
+ return loginShellProbeSkipped(wanted);
594
711
  const spawnSyncFn = opts.spawnSyncFn ?? child_process_1.spawnSync;
595
712
  const timeoutMs = opts.timeoutMs ?? LOGIN_SHELL_TIMEOUT_MS;
596
713
  const shell = resolveLoginShell(env);
@@ -602,23 +719,21 @@ function readEnvFromLoginShellSync(names, opts = {}) {
602
719
  encoding: 'utf8',
603
720
  stdio: ['ignore', 'pipe', 'pipe'],
604
721
  timeout: timeoutMs,
605
- maxBuffer: 1024 * 1024,
722
+ // An interactive shell can ignore SIGTERM; the cold fallback must stay bounded.
723
+ killSignal: 'SIGKILL',
724
+ maxBuffer: LOGIN_SHELL_OUTPUT_CAP,
606
725
  });
607
726
  }
608
727
  catch {
609
- return {};
728
+ return { values: {}, status: 'failed', exitCode: null, shell, names: wanted };
610
729
  }
611
- if (res.error || res.status !== 0)
612
- return {};
613
730
  const stdout = typeof res.stdout === 'string' ? res.stdout : res.stdout?.toString('utf8') ?? '';
614
- const recovered = parseLoginShellEnv(stdout);
615
- const out = {};
616
- for (const key of wanted) {
617
- const val = recovered[key];
618
- if (val)
619
- out[key] = val;
620
- }
621
- return out;
731
+ return settleLoginShellProbe(stdout, wanted, shell, typeof res.status === 'number' ? res.status : null, isSpawnSyncTimeout(res));
732
+ }
733
+ /** Read missing configured names without mutating the caller's or global env.
734
+ * Callers own any cache so project credentials cannot leak across scopes. */
735
+ function readEnvFromLoginShellSync(names, opts = {}) {
736
+ return readEnvFromLoginShellSyncDetailed(names, opts).values;
622
737
  }
623
738
  /** Compatibility backfill for callers that intentionally augment process.env.
624
739
  * Failed probes remain retryable; successful names are already present. */