specrails-desktop 2.62.0 → 2.63.1

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 (181) hide show
  1. package/cli/dist/specrails-desktop.js +39 -5
  2. package/client/dist/assets/{ActivityFeedPage-C_pqc281.js → ActivityFeedPage-B1bpTy7O.js} +1 -1
  3. package/client/dist/assets/{AgentBrowserCapture-vz6rbDtu.js → AgentBrowserCapture-BussBbN3.js} +1 -1
  4. package/client/dist/assets/AgentModeAnalyticsPane-BiT1n3BU.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CUjNfeKa.js +2 -0
  6. package/client/dist/assets/{AgentModeJobsPane-99fBBLri.js → AgentModeJobsPane-RVEwyS59.js} +1 -1
  7. package/client/dist/assets/AgentsPage-DKE-eMC5.js +118 -0
  8. package/client/dist/assets/AnalyticsPage-Bser2Ly3.js +1 -0
  9. package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
  10. package/client/dist/assets/CodePage-BK0grm6e.js +3 -0
  11. package/client/dist/assets/DesktopAnalyticsPage-DEBidZbs.js +1 -0
  12. package/client/dist/assets/{DocsDialog-XUCYKoq0.js → DocsDialog-Dhyo57n9.js} +1 -1
  13. package/client/dist/assets/{DocsPage-dOLC8bXA.js → DocsPage-BcEkPXd6.js} +1 -1
  14. package/client/dist/assets/{ExportDropdown-BvKQgwM9.js → ExportDropdown-mRmqdkjr.js} +1 -1
  15. package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
  16. package/client/dist/assets/InteractiveJobComposer-BlCe3vrU.js +19 -0
  17. package/client/dist/assets/{JobDetailModal-CkiubrkZ.js → JobDetailModal-CkAejD1F.js} +1 -1
  18. package/client/dist/assets/{JobDetailPage-CNe4Wn7l.js → JobDetailPage-P-FS_lKz.js} +1 -1
  19. package/client/dist/assets/JobsPage-CoUXgYse.js +1 -0
  20. package/client/dist/assets/LoopBuilderPage-iFylNk68.js +1 -0
  21. package/client/dist/assets/{LoopPreviewModal-Dv3dxHr_.js → LoopPreviewModal-4obS_Ptg.js} +1 -1
  22. package/client/dist/assets/{LoopWindowSurface-DN2lbSnL.js → LoopWindowSurface-CLr5OPqj.js} +1 -1
  23. package/client/dist/assets/LoopsPage-eKpWFtCp.js +2 -0
  24. package/client/dist/assets/MinimizedChatsContext-BHIUPVmC.js +1 -0
  25. package/client/dist/assets/PluginsPage-BgTC2CQx.js +2 -0
  26. package/client/dist/assets/ProjectSettingsDialog-B4chTfU8.js +1 -0
  27. package/client/dist/assets/RepositoryDeliveries-DAIxIYsl.js +5 -0
  28. package/client/dist/assets/{RepositoryScopeSelector-DrtPqvc_.js → RepositoryScopeSelector-DeCoUsRN.js} +1 -1
  29. package/client/dist/assets/ReviewPacketPage-Cktv0-9b.js +4 -0
  30. package/client/dist/assets/{TemplatePreviewModal-DcFlhUxy.js → TemplatePreviewModal-BZTV1TM1.js} +1 -1
  31. package/client/dist/assets/TicketDetailModalContext-DiPngt2g.js +16 -0
  32. package/client/dist/assets/Trans-ZiXIfj50.js +1 -0
  33. package/client/dist/assets/{agentRuntime-TKQyg137.js → agentRuntime-BDvbJMmS.js} +1 -1
  34. package/client/dist/assets/{agentRuntime-DvhEe1um.js → agentRuntime-BKJfv5YD.js} +1 -1
  35. package/client/dist/assets/{agentRuntime-DxC6-9rN.js → agentRuntime-BxF81z52.js} +1 -1
  36. package/client/dist/assets/{agentRuntime-C_yLPKDy.js → agentRuntime-BxYS5V7u.js} +1 -1
  37. package/client/dist/assets/{agentRuntime-WSv8G0pW.js → agentRuntime-BzuwQQgn.js} +1 -1
  38. package/client/dist/assets/{agentRuntime-D2kYQqNO.js → agentRuntime-Dc9HG3BA.js} +1 -1
  39. package/client/dist/assets/{agentRuntime-Bjt7Ij6_.js → agentRuntime-DiCldqGG.js} +1 -1
  40. package/client/dist/assets/{agentRuntime-B-myNBUP.js → agentRuntime-es2ymSl-.js} +1 -1
  41. package/client/dist/assets/ban-wqfdzoXt.js +1 -0
  42. package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
  43. package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
  44. package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
  45. package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
  46. package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
  47. package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
  48. package/client/dist/assets/download-C9wP_42h.js +1 -0
  49. package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
  50. package/client/dist/assets/file-text-C0blZHLh.js +1 -0
  51. package/client/dist/assets/{formatDistanceToNow-DlQY1Lm_.js → formatDistanceToNow-CdN5lTyD.js} +1 -1
  52. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-COUeq65G.js → getTimezoneOffsetInMilliseconds-62h40I4t.js} +1 -1
  53. package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
  54. package/client/dist/assets/index-BgbCbIua.css +2 -0
  55. package/client/dist/assets/{index-CONHvEJw.js → index-CECj3joE.js} +40 -40
  56. package/client/dist/assets/jira-api-C_D_RDhl.js +1 -0
  57. package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
  58. package/client/dist/assets/layers-BCLTgHXO.js +1 -0
  59. package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
  60. package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
  61. package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
  62. package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
  63. package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
  64. package/client/dist/assets/plus-fHDdjBk1.js +1 -0
  65. package/client/dist/assets/{project-repositories-Cc3oNv8X.js → project-repositories-Be0CWklo.js} +1 -1
  66. package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
  67. package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
  68. package/client/dist/assets/save-LmPSfsvZ.js +1 -0
  69. package/client/dist/assets/search-BDVp3fkH.js +1 -0
  70. package/client/dist/assets/{settings-BKJZ2KEM.js → settings-BO2N-oPf.js} +1 -1
  71. package/client/dist/assets/{settings-LfFnl1UH.js → settings-BQChAs_w.js} +1 -1
  72. package/client/dist/assets/{settings-BV3jocIx.js → settings-BzmYlcQB.js} +1 -1
  73. package/client/dist/assets/{settings-BjM2Dw8_.js → settings-CGjXb6TX.js} +1 -1
  74. package/client/dist/assets/{settings-DA77M48m.js → settings-CMxEWY46.js} +1 -1
  75. package/client/dist/assets/{settings-P1IQvhnL.js → settings-C_dRSgI2.js} +1 -1
  76. package/client/dist/assets/{settings-eIMJjyK_.js → settings-Hw7av-B1.js} +1 -1
  77. package/client/dist/assets/{settings-CKu0LTib.js → settings-nryN6wuS.js} +1 -1
  78. package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
  79. package/client/dist/assets/spending-eWiZGhC_.js +1 -0
  80. package/client/dist/assets/square-D0JTbRSR.js +1 -0
  81. package/client/dist/assets/trash-D8zYUmTg.js +1 -0
  82. package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
  83. package/client/dist/assets/upload-CCHvUkRs.js +1 -0
  84. package/client/dist/assets/{useDesktop-9Lwoel_8.js → useDesktop-C0mRIlHT.js} +1 -1
  85. package/client/dist/assets/{useRuntimeRuns-DL0xYoZ2.js → useRuntimeRuns-B1a6GChu.js} +1 -1
  86. package/client/dist/assets/useSharedWebSocket-CcsE5986.js +2 -0
  87. package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
  88. package/client/dist/assets/x-Bu7X_eR2.js +1 -0
  89. package/client/dist/assets/zap-bugHyWzQ.js +1 -0
  90. package/client/dist/index.html +44 -45
  91. package/docs/ci-cd.md +8 -1
  92. package/docs/customizing.md +1 -0
  93. package/docs/getting-started.md +1 -1
  94. package/docs/guide/de/settings/5-project-environment.md +27 -0
  95. package/docs/guide/en/settings/5-project-environment.md +27 -0
  96. package/docs/guide/es/settings/5-project-environment.md +27 -0
  97. package/docs/guide/fr/settings/5-project-environment.md +27 -0
  98. package/docs/guide/it/settings/5-project-environment.md +27 -0
  99. package/docs/guide/ja/settings/5-project-environment.md +27 -0
  100. package/docs/guide/pt/settings/5-project-environment.md +27 -0
  101. package/docs/guide/zh/settings/5-project-environment.md +27 -0
  102. package/docs/internals/api-reference.md +3 -1
  103. package/docs/internals/configuration.md +46 -5
  104. package/docs/internals/core-runtime-updates.md +3 -3
  105. package/docs/internals/desktop-owned-workflows.md +10 -0
  106. package/docs/internals/programmatic-agent-runtime.md +16 -0
  107. package/docs/internals/safe-pr-review-flow.md +15 -0
  108. package/docs/internals/source-map.md +6 -1
  109. package/docs/mcp.md +1 -1
  110. package/docs/platforms/macos.md +2 -0
  111. package/docs/platforms/windows-parity.md +4 -4
  112. package/docs/platforms/windows.md +9 -9
  113. package/local-runner/dist/specrails-local-runner.js +1 -1
  114. package/package.json +8 -9
  115. package/server/dist/mcp/tools/env.js +38 -3
  116. package/server/dist/mcp/tools/loops.js +1 -1
  117. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +13 -3
  118. package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +1 -1
  119. package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
  120. package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
  121. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +13 -6
  122. package/server/dist/modules/loops/runtime/loop-run-manager.js +7 -0
  123. package/server/dist/modules/loops/runtime/loop-turn-limits.js +64 -0
  124. package/server/dist/modules/loops/runtime/loops-router.js +3 -1
  125. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -0
  126. package/server/dist/modules/project-settings/adapters/http.js +24 -0
  127. package/server/dist/path-resolver.js +135 -20
  128. package/server/dist/project-env.js +265 -17
  129. package/server/dist/project-registry.js +14 -0
  130. package/server/dist/project-router-settings.js +17 -0
  131. package/server/dist/schemas/agent-runtime.schema.json +1 -1
  132. package/server/dist/worktree-node-modules.js +95 -1
  133. package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +0 -2
  134. package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +0 -2
  135. package/client/dist/assets/AgentsPage-CTwm_6T8.js +0 -118
  136. package/client/dist/assets/AnalyticsPage-YD8MLoPa.js +0 -1
  137. package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
  138. package/client/dist/assets/CodePage-CLJn2F2e.js +0 -3
  139. package/client/dist/assets/DesktopAnalyticsPage-MWGQVbwp.js +0 -1
  140. package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
  141. package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +0 -19
  142. package/client/dist/assets/JobsPage-DSts_L_d.js +0 -1
  143. package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +0 -1
  144. package/client/dist/assets/LoopsPage-DHgIreEW.js +0 -2
  145. package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +0 -1
  146. package/client/dist/assets/PluginsPage-D1EJ9oja.js +0 -2
  147. package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +0 -1
  148. package/client/dist/assets/RepositoryDeliveries-CW4eKG3x.js +0 -5
  149. package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +0 -4
  150. package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +0 -16
  151. package/client/dist/assets/Trans-CAY2Rd2f.js +0 -1
  152. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  153. package/client/dist/assets/chevron-up-CwGUHd6D.js +0 -1
  154. package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
  155. package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
  156. package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
  157. package/client/dist/assets/dist-Bq10nS2m.js +0 -1
  158. package/client/dist/assets/download-CFHyyHcw.js +0 -1
  159. package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
  160. package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
  161. package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
  162. package/client/dist/assets/index-BacrO_ub.css +0 -2
  163. package/client/dist/assets/jira-api-C94VNHNY.js +0 -1
  164. package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
  165. package/client/dist/assets/layers-eDMjG41I.js +0 -1
  166. package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
  167. package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
  168. package/client/dist/assets/plus-CsnjfkmX.js +0 -1
  169. package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
  170. package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
  171. package/client/dist/assets/save-B6wGWwBR.js +0 -1
  172. package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
  173. package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
  174. package/client/dist/assets/spending-DemeFfrH.js +0 -1
  175. package/client/dist/assets/square-Ci-PFcFe.js +0 -1
  176. package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
  177. package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
  178. package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
  179. package/client/dist/assets/useSharedWebSocket-BEHPeU1Y.js +0 -2
  180. package/client/dist/assets/x-CBMI9ejn.js +0 -1
  181. package/client/dist/assets/zap-CHo3aCD1.js +0 -1
@@ -133,6 +133,21 @@ Both temporary checkouts use non-force cleanup, including after a Git runner exc
133
133
  including proofs for historical misplaced links; symlinked destination ancestors confer no
134
134
  authority. Registration paths, frozen manifests and Core scope remain unchanged. A failed run
135
135
  keeps its old checkout; after updating Desktop, a fresh Relaunch receives the corrected layout.
136
+ - Warm reuse is gated by dependency freshness. Before preparing a new link for a package
137
+ directory, Desktop compares its dependency inputs in the base checkout and in the worktree: the
138
+ nearest lockfile found walking up to the Git top level (first of `pnpm-lock.yaml`, `yarn.lock`,
139
+ `package-lock.json`, `npm-shrinkwrap.json`, `bun.lock`, at the same relative position) and a
140
+ canonical digest of the `package.json` dependency, devDependency, optionalDependency and
141
+ peerDependency fields (scripts and other metadata are ignored). If either input differs, or one
142
+ cannot be read, the directory gets no `node_modules` and the run reports, on the
143
+ `rail.overlay_degraded` channel, for example
144
+ `apps/catalog: yarn.lock differs from the base checkout; dependencies will be installed in the worktree`.
145
+ Core's environment preparation then installs that directory cold from the worktree's own lockfile.
146
+ This prevents a base checkout on another branch, or behind the remote, from supplying packages
147
+ the run's code does not declare. Desktop never checks out, pulls or installs anything in the
148
+ user's checkout to make warm reuse possible. Links already present for a resumed run keep their
149
+ authentication; the guard only applies when a link is prepared. `SPECRAILS_WORKTREE_NODE_MODULES=false`
150
+ still disables warm reuse entirely.
136
151
  - Per-unit settlement returns structured execution + delivery results. `onLoopRunFinished` receives
137
152
  the engine outcome only; commit/status/ref/provenance/push failures cannot rewrite it.
138
153
  - Before staging, settlement blocks active OpenSpec change directories introduced relative to
@@ -5,7 +5,7 @@ architectural dependency declaration. Search a heading or filename and read only
5
5
  the relevant source and tests. Feature prefixes group the legacy server files;
6
6
  new bounded modules live under `server/modules/`.
7
7
 
8
- Includes 1042 source/build files. Nearby tests are linked where names
8
+ Includes 1047 source/build files. Nearby tests are linked where names
9
9
  match; integration suites may live elsewhere. Use `rg` to find other consumers.
10
10
 
11
11
  ## cli
@@ -384,6 +384,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
384
384
  - [LoopMigrationPanel.tsx](../../client/src/features/loops/components/LoopMigrationPanel.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopMigrationPanel.test.tsx)
385
385
  - [LoopPreviewModal.tsx](../../client/src/features/loops/components/LoopPreviewModal.tsx)
386
386
  - [LoopRunModal.tsx](../../client/src/features/loops/components/LoopRunModal.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopRunModal.test.tsx)
387
+ - [LoopTurnLimits.tsx](../../client/src/features/loops/components/LoopTurnLimits.tsx) · [test](../../client/src/features/loops/components/LoopTurnLimits.test.tsx)
387
388
  - [LoopWindowSurface.tsx](../../client/src/features/loops/components/LoopWindowSurface.tsx) · [test](../../client/src/features/loops/components/__tests__/LoopWindowSurface.test.tsx)
388
389
  - [TemplatePreviewModal.tsx](../../client/src/features/loops/components/TemplatePreviewModal.tsx) · [test](../../client/src/features/loops/components/__tests__/TemplatePreviewModal.test.tsx)
389
390
 
@@ -413,6 +414,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
413
414
  - [loop-export.ts](../../client/src/features/loops/lib/loop-export.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-export.test.ts)
414
415
  - [loop-graph-rf.ts](../../client/src/features/loops/lib/loop-graph-rf.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-graph-rf.test.ts)
415
416
  - [loop-layout.ts](../../client/src/features/loops/lib/loop-layout.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-layout.test.ts)
417
+ - [loop-role-agents.ts](../../client/src/features/loops/lib/loop-role-agents.ts)
416
418
  - [loop-run-models.ts](../../client/src/features/loops/lib/loop-run-models.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-run-models.test.ts)
417
419
  - [loop-template-filter.ts](../../client/src/features/loops/lib/loop-template-filter.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-template-filter.test.ts)
418
420
  - [loop-ticket-need.ts](../../client/src/features/loops/lib/loop-ticket-need.ts) · [test](../../client/src/features/loops/lib/__tests__/loop-ticket-need.test.ts)
@@ -848,7 +850,9 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
848
850
  - [generate-icons.mjs](../../scripts/generate-icons.mjs)
849
851
  - [generate-mobile-types.mjs](../../scripts/generate-mobile-types.mjs)
850
852
  - [generate-source-map.mjs](../../scripts/generate-source-map.mjs) · [test](../../scripts/generate-source-map.test.mjs)
853
+ - [module-references.mjs](../../scripts/module-references.mjs) · [test](../../scripts/module-references.test.mjs)
851
854
  - [obfuscate-chromium.mjs](../../scripts/obfuscate-chromium.mjs)
855
+ - [paired-core-shards.mjs](../../scripts/paired-core-shards.mjs) · [test](../../scripts/paired-core-shards.test.mjs)
852
856
  - [patch-node-pty.mjs](../../scripts/patch-node-pty.mjs) · [test](../../scripts/patch-node-pty.test.mjs)
853
857
  - [recover-desktop-channel.mjs](../../scripts/recover-desktop-channel.mjs) · [test](../../scripts/recover-desktop-channel.test.mjs)
854
858
  - [release-policy.mjs](../../scripts/release-policy.mjs) · [test](../../scripts/release-policy.test.mjs)
@@ -1330,6 +1334,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
1330
1334
  - [loop-shell-invocation.ts](../../server/modules/loops/runtime/loop-shell-invocation.ts)
1331
1335
  - [loop-step-idle.ts](../../server/modules/loops/runtime/loop-step-idle.ts) · [test](../../server/modules/loops/runtime/loop-step-idle.test.ts)
1332
1336
  - [loop-templates.ts](../../server/modules/loops/runtime/loop-templates.ts) · [test](../../server/modules/loops/runtime/loop-templates.test.ts)
1337
+ - [loop-turn-limits.ts](../../server/modules/loops/runtime/loop-turn-limits.ts) · [test](../../server/modules/loops/runtime/loop-turn-limits.test.ts)
1333
1338
  - [loops-router.ts](../../server/modules/loops/runtime/loops-router.ts) · [test](../../server/modules/loops/runtime/loops-router.test.ts)
1334
1339
  - [loops-store.ts](../../server/modules/loops/runtime/loops-store.ts) · [test](../../server/modules/loops/runtime/loops-store.test.ts)
1335
1340
 
package/docs/mcp.md CHANGED
@@ -48,7 +48,7 @@ reading one description. The tools are:
48
48
  | `specrails_loops` | Saved loop workflows |
49
49
  | `specrails_code` | File discovery, literal content search, bounded line reads, summaries and provenance; AI summary regeneration is capability-gated |
50
50
  | `specrails_git` | Repository/branch/worktree information, status, diffs and PR lookup |
51
- | `specrails_env` | Project environment readiness and environment-file management |
51
+ | `specrails_env` | Project environment readiness and environment-file management; `get`/`status`/`recheck` report each configured name's value-free resolution status |
52
52
  | `specrails_support` | Support triage, local diagnostics and specrails-core update workflows |
53
53
  | `specrails_setup` | Add-project setup wizard surface |
54
54
  | `specrails_analytics` | Per-project spending analytics + budget |
@@ -77,6 +77,8 @@ When the app runs without an active bundle (a runtimes-less build, or `npm run d
77
77
 
78
78
  2. **Login-shell merge (async)** — right after the HTTP server starts listening, `augmentPathFromLoginShell()` spawns `$SHELL -l -i` once (1500 ms timeout) and merges whatever `PATH` that login + interactive shell exposes. This recovers the segments that Volta, nvm, fnm, asdf, etc. add only inside your interactive shell — whatever your `$SHELL` produces, no specific rc file is assumed. On timeout or non-zero exit, the fast-path `PATH` stays in effect and a single warning is logged.
79
79
 
80
+ Project environment names configured in Project Settings (for example `NODE_AUTH_TOKEN`) use a separate, per-project login-shell probe with a 10 s budget (`SPECRAILS_LOGIN_SHELL_TIMEOUT_MS`). It runs in the background, never writes to `process.env`, and reports a status per name. See [configuration](../internals/configuration.md#login-shell-recovery-and-resolution-status).
81
+
80
82
  The resolved `PATH` is stored on `process.env.PATH`, so every downstream spawn inherits it (`QueueManager` → provider CLI, `SetupManager` → `npx specrails-core`, `terminalManager` PTYs, etc.).
81
83
 
82
84
  ## Broken-symlink detection
@@ -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.1",
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;
@@ -30,7 +30,7 @@ function loopsTools() {
30
30
  'For a standalone ticket-less run use the run action. ' +
31
31
  'Actions: ' +
32
32
  'list (all saved loops), ' +
33
- 'get (one loop by id), ' +
33
+ 'get (one loop by id, plus turnLimits: each AI step\'s role and effective Maximum turns — set graph.config.agents.roles.<role>.maxTurns, or agents.<role> for built-in roles, then publish; project Agent Runtime settings never change loop runs), ' +
34
34
  'templates (built-in template catalog with graphs), ' +
35
35
  'factory (the built-in loops — factory:implement, factory:freestyle, factory:sdd-quick-openspec — with the graph a rail launch runs; ' +
36
36
  'built-ins are ordinary editable loops whose loopId IS the factory id: change them with update + publish and the change applies to every rail, chat launch and companion that uses that built-in), ' +
@@ -23,6 +23,7 @@ const agent_runtime_package_1 = require("./agent-runtime-package");
23
23
  const agent_runtime_settings_1 = require("./agent-runtime-settings");
24
24
  const core_node_runtime_1 = require("../../../core-node-runtime");
25
25
  const win_spawn_1 = require("../../../util/win-spawn");
26
+ const loop_turn_limits_1 = require("../../loops/runtime/loop-turn-limits");
26
27
  function runtimeChangeName(runId) {
27
28
  return 'runtime-' + (0, node_crypto_1.createHash)('sha256').update(runId).digest('hex').slice(0, 20);
28
29
  }
@@ -265,9 +266,17 @@ async function runAgentRuntimeInvocation(options) {
265
266
  }
266
267
  if ((0, node_fs_1.existsSync)(frozenPath)) {
267
268
  const frozen = JSON.parse((0, node_fs_1.readFileSync)(frozenPath, 'utf8'));
268
- for (const [role, assignment] of [...Object.entries(frozen.agents), ...(frozen.fixer ? [['fixer', frozen.fixer]] : [])]) {
269
+ // A definition names the roles its steps run (Implement: plan/build/assess/correct); list exactly those with their turn ceiling.
270
+ const definitionRoles = definitionEngine ? JSON.parse((0, node_fs_1.readFileSync)(definitionFile, 'utf8')).roles : undefined;
271
+ const used = Array.isArray(definitionRoles) ? new Set(definitionRoles.filter((role) => typeof role === 'string')) : undefined;
272
+ if (used?.has('developer'))
273
+ used.add('fixer');
274
+ const assignments = [...Object.entries(frozen.agents), ...(frozen.fixer ? [['fixer', frozen.fixer]] : []), ...Object.entries(frozen.roles ?? {})]
275
+ .filter(([role]) => used ? used.has(role) : role in frozen.agents || role === 'fixer');
276
+ for (const [role, assignment] of assignments) {
277
+ const turns = assignment.maxTurns === undefined ? `max ${loop_turn_limits_1.CORE_DEFAULT_MAX_TURNS} turns (Core default)` : `max ${assignment.maxTurns} turns`;
269
278
  try {
270
- options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'}\n`);
279
+ options.onLine?.(`[runtime] ${role}: ${assignment.provider}/${assignment.model ?? 'provider default'} · ${turns}\n`);
271
280
  }
272
281
  catch { /* Log observers cannot prevent execution. */ }
273
282
  }
@@ -378,7 +387,8 @@ async function runAgentRuntimeInvocation(options) {
378
387
  const kind = payload?.type === 'step_succeeded' && payload.outcome === 'fail' ? 'verification_failed' : payload?.type;
379
388
  const completionDetail = payload?.type === 'workflow_failed' && Array.isArray(payload.reasons)
380
389
  ? payload.reasons.filter(reason => typeof reason === 'string' && reason.trim()).join('; ').slice(0, 4_000) : undefined;
381
- const detail = payload?.error?.message ?? payload?.message ?? completionDetail;
390
+ const rawDetail = payload?.error?.message ?? payload?.message ?? completionDetail;
391
+ const detail = payload?.type === 'step_failed' && typeof rawDetail === 'string' ? (0, loop_turn_limits_1.withTurnLimitHint)(rawDetail, payload.stepId) : rawDetail;
382
392
  const stepKey = JSON.stringify([payload?.scopeId ?? null, payload?.nodePath ?? payload?.stepId ?? null]);
383
393
  if (payload?.type === 'step_failed' && typeof detail === 'string' && detail.trim()) {
384
394
  stepFailures.delete(stepKey);
@@ -447,7 +447,7 @@ function createIsolatedRunSettlement(ports) {
447
447
  run: a, implementationOutcome, deliveryOutcome: 'blocked',
448
448
  initialSha: a.initialSha, finalSha: await readHeadSha(git, a.handle.worktreePath),
449
449
  failureCode: 'settlement_interrupted',
450
- failureDetail: 'Programmatic workflow needs attention. Resume it from Agent Runtime settings in its original worktree.',
450
+ failureDetail: 'Programmatic workflow stopped before delivery; partial changes stay in its original worktree. The run log names the failing step. A resumable step offers Resume on the job page; a raised loop limit (such as a step\'s Maximum turns) needs a new launch.',
451
451
  safeToRelease: false,
452
452
  };
453
453
  }
@@ -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 });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.supportsHostBlockers = exports.HOST_BLOCKER_KINDS = void 0;
3
+ exports.supportsHostBlockers = exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS = exports.HOST_BLOCKER_KINDS = void 0;
4
4
  exports.configurableImplementGraph = configurableImplementGraph;
5
5
  const agent_runtime_settings_1 = require("../../agent-runtime/runtime/agent-runtime-settings");
6
6
  const loop_agents_1 = require("./loop-agents");
@@ -12,6 +12,8 @@ const aspects = { type_correctness: 60, pattern_adherence: 60, test_coverage: 60
12
12
  const reviewPolicy = 'approved == true; issues must be empty; score >= 70; ' + Object.entries(aspects).map(([aspect, threshold]) => `${aspect} >= ${threshold}`).join('; ');
13
13
  exports.HOST_BLOCKER_KINDS = ['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope'];
14
14
  /** Fixer/developer structured `blocker`: the host ends the run on it instead of counting a correction round. */
15
+ /** Write roles of a large spec routinely exceed Core's default of 100 turns (one cross-repo Developer used 113). */
16
+ exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS = 300;
15
17
  const blocker = objectSchema({ kind: { enum: [...exports.HOST_BLOCKER_KINDS] }, command: string, cwd: string, evidence: string, requiredAction: string }, ['kind', 'requiredAction']);
16
18
  /** Host-owned environment repair and structured blockers need the paired Core generation. */
17
19
  const supportsHostBlockers = (capabilities) => capabilities?.hostBlockers === 1;
@@ -24,9 +26,9 @@ function configurableImplementGraph(capabilities) {
24
26
  const agents = (0, loop_agents_1.defaultLoopAgents)();
25
27
  agents.roles = {
26
28
  plan: { ...agents.agents.architect, access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'architect') },
27
- build: { ...agents.agents.developer, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'developer') },
29
+ build: { ...agents.agents.developer, maxTurns: exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'developer') },
28
30
  assess: { ...agents.agents.reviewer, access: 'read', artifacts: 'none', openspecSkill: 'openspec-verify-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'reviewer') },
29
- correct: { ...agents.fixer, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'fixer') },
31
+ correct: { ...agents.fixer, maxTurns: exports.IMPLEMENT_WRITE_ROLE_MAX_TURNS, access: 'write', artifacts: 'tasks-checkboxes', openspecSkill: 'openspec-apply-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'fixer') },
30
32
  };
31
33
  const nodes = [{ id: 'start', type: 'start', position: { x: 0, y: 0 } }], edges = [];
32
34
  const node = (id, label, kind, params, outcomes) => {
@@ -48,7 +50,7 @@ function configurableImplementGraph(capabilities) {
48
50
  node('reset-confidence', 'Reset confidence after answer', 'assign', { set: { planInvestigated: false } }, { next: 'architect', failed: 'failed' });
49
51
  node('validate', 'Validate OpenSpec', 'openspec-validate', { change }, { pass: 'freeze', fail: 'architect', failed: 'failed' });
50
52
  node('freeze', 'Freeze approved artifacts', 'artifact-contract', { change, contractId: 'plan', action: 'freeze' }, { pass: 'initialize-corrections', fail: 'architect', failed: 'failed' });
51
- node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3 } }, { next: 'developer', failed: 'failed' });
53
+ node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3, implementationAttempts: 0, implementationLimit: 3, archiveRetries: 0 } }, { next: 'developer', failed: 'failed' });
52
54
  const implementation = objectSchema({ summary: string, incomplete: strings, blocker }, ['summary', 'incomplete']);
53
55
  turn('developer', 'Developer', 'build', `Implement every approved task for change ${change} using the bound official apply skill. Preserve proposal, design and specs; only tick tasks you completed. Report summary and incomplete task descriptions. If a task cannot be finished because of a host precondition or environment blocker outside this change (for example the sandbox forbids starting a local server, a credential or tool is missing), keep the work done, leave that task unchecked and return the structured blocker (kind, command, cwd, evidence, requiredAction). A failing test, a test you cannot make pass, or unexpected browser or tool behaviour is NOT a host blocker: change the test or the implementation instead. The host confirms a blocker by running its own verification before ending the run; a failure it reproduces comes back as a correction. Prior execution evidence and human answers: {{history.text}}`, implementation, 'developer-blocker', writes);
54
56
  node('developer-blocker', 'Developer reported a host blocker', 'condition', { expr: 'exists($outputs.developer.structured.blocker) && $outputs.developer.structured.blocker != null' }, { true: 'developer-blocker-kind', false: 'tasks' });
@@ -76,9 +78,14 @@ function configurableImplementGraph(capabilities) {
76
78
  node('correction-progress', 'Check correction progress', 'condition', { expr: 'exists($outputs.fixer.candidateHash) && $outputs.fixer.candidateHash != null && $outputs.fixer.candidateHash != $outputs.verify.candidateHash' }, { true: 'tasks', false: 'correction-stalled' });
77
79
  node('approve', 'Approve archive', 'approval', { reason: `Approve archiving the reviewed and verified candidate for change ${change}.`, bindCandidate: true, enabled: agents.approvalBeforeArchive === true }, { next: 'approval-policy' });
78
80
  node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
79
- node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'verify' });
81
+ node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'archive-retry' });
82
+ // Only a stale or unverified candidate is worth re-verifying before another archive attempt.
83
+ // An OpenSpec rejection (e.g. a requirement without SHALL/MUST) repeats identically, so it ends the run
84
+ // instead of cycling verify → review → archive until the transition budget runs out.
85
+ node('archive-retry', 'Archive needs a fresh verification', 'condition', { expr: 'exists($outputs.archive.reason) && ($outputs.archive.reason == "unverified" || $outputs.archive.reason == "candidate_changed") && $vars.archiveRetries < 2' }, { true: 'count-archive-retry', false: 'archive-failed' });
86
+ node('count-archive-retry', 'Count archive re-verification', 'assign', { increment: { archiveRetries: 1 } }, { next: 'verify', failed: 'failed' });
80
87
  node('verify-archive', 'Verify archived candidate', 'verify', verifyParams, verifyEnds('done', 'failed', 'host-blocked-archive'));
81
- nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'implementation-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'The developer made no changes while approved tasks are still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'implementation-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Implementation attempt limit reached ({{run.implementationAttempts}}/{{run.implementationLimit}}) with approved tasks still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'developer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Developer reported a host blocker ({{outputs.developer.structured.blocker.kind}}): {{outputs.developer.structured.blocker.requiredAction}} Evidence: {{outputs.developer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'developer' } : {}) } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
88
+ nodes.push({ id: 'done', type: 'end', position: { x: 0, y: nodes.length * 140 }, data: { outcome: 'success', requiresVerified: true } }, { id: 'failed', type: 'end', position: { x: 360, y: nodes.length * 140 }, data: { outcome: 'failure' } }, { id: 'archive-failed', type: 'end', position: { x: 360, y: (nodes.length + 1) * 140 }, data: { outcome: 'failure', reason: `OpenSpec could not archive change ${change}. The implementation was verified and reviewed; the archive step in the run log shows OpenSpec's reason (for example a requirement whose first body line has no SHALL or MUST). Fix the spec in the original worktree, then archive it there or relaunch.` } }, { id: 'correction-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction made no candidate changes. Host verification is valid: {{outputs.verify.valid}}. Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}` } }, { id: 'correction-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: `Automatic correction limit reached ({{run.correctionAttempts}}/{{run.correctionLimit}}). Acceptance policy: ${reviewPolicy}. Latest review findings: {{run.latestReview}}. Fixer diagnosis: {{outputs.fixer.structured.summary}}. Host verification: {{outputs.verify}}` } }, { id: 'implementation-stalled', type: 'end', position: { x: 720, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'The developer made no changes while approved tasks are still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'implementation-exhausted', type: 'end', position: { x: 1080, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Implementation attempt limit reached ({{run.implementationAttempts}}/{{run.implementationLimit}}) with approved tasks still incomplete. Incomplete: {{outputs.developer.structured.incomplete}}. Developer summary: {{outputs.developer.structured.summary}}' } }, { id: 'developer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Developer reported a host blocker ({{outputs.developer.structured.blocker.kind}}): {{outputs.developer.structured.blocker.requiredAction}} Evidence: {{outputs.developer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'developer' } : {}) } }, { id: 'fixer-blocked', type: 'end', position: { x: 1440, y: nodes.length * 140 }, data: { outcome: 'failure', reason: 'Fixer reported a host blocker ({{outputs.fixer.structured.blocker.kind}}): {{outputs.fixer.structured.blocker.requiredAction}} Evidence: {{outputs.fixer.structured.blocker.evidence}}', ...(hostBlockers ? { blockerFrom: 'fixer' } : {}) } });
82
89
  if (hostBlockers) {
83
90
  const hostBlocked = (id, source) => ({ id, type: 'end', position: { x: 1800, y: nodes.length * 140 }, data: { outcome: 'failure', blockerFrom: source, reason: `Host blocker ({{outputs.${source}.blocker.kind}}): {{outputs.${source}.blocker.reason}} Required action: {{outputs.${source}.blocker.requiredAction}}` } });
84
91
  nodes.push(hostBlocked('host-blocked', 'verify'), hostBlocked('host-blocked-archive', 'verify-archive'));
@@ -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
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CORE_DEFAULT_MAX_TURNS = void 0;
4
+ exports.loopStepRole = loopStepRole;
5
+ exports.loopTurnLimits = loopTurnLimits;
6
+ exports.withTurnLimitHint = withTurnLimitHint;
7
+ /** Core's ceiling for one role turn when the role sets no `maxTurns` (Core `cli-executor`). */
8
+ exports.CORE_DEFAULT_MAX_TURNS = 100;
9
+ function object(value) {
10
+ return !!value && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+ /** The agent role a canvas step runs, mirroring the definition compiler. */
13
+ function loopStepRole(id, kind, params) {
14
+ if (kind === 'prompt')
15
+ return id === 'fix' ? 'fixer' : id === 'prepare' ? 'architect' : params.access === 'read' ? 'reviewer' : 'developer';
16
+ if ((kind === 'role-turn' || kind === 'decider') && typeof params.roleId === 'string')
17
+ return params.roleId;
18
+ if (kind === 'implementation-step' && ['architect', 'developer', 'reviewer', 'fixer'].includes(String(params.phase)))
19
+ return String(params.phase);
20
+ return undefined;
21
+ }
22
+ function roleEngine(agents, role) {
23
+ if (!agents)
24
+ return undefined;
25
+ if (role === 'fixer')
26
+ return agents.fixer ?? agents.agents.developer;
27
+ if (role === 'architect' || role === 'developer' || role === 'reviewer')
28
+ return agents.agents[role];
29
+ return agents.roles?.[role];
30
+ }
31
+ /** Effective per-step turn ceiling: step engine override, then the loop role, then Core's default. */
32
+ function loopTurnLimits(graph) {
33
+ const agents = graph.config.agents;
34
+ const limits = [];
35
+ const visit = (body) => {
36
+ for (const node of body.nodes) {
37
+ if (node.type !== 'core')
38
+ continue;
39
+ const params = object(node.data?.params) ? node.data.params : {};
40
+ const role = loopStepRole(node.id, node.data?.kind, params);
41
+ if (!role)
42
+ continue;
43
+ const label = typeof node.data?.label === 'string' ? node.data.label : undefined;
44
+ const stepTurns = object(params.engine) ? params.engine.maxTurns : undefined;
45
+ const roleTurns = roleEngine(agents, role)?.maxTurns;
46
+ const [maxTurns, source] = typeof stepTurns === 'number' ? [stepTurns, 'step']
47
+ : typeof roleTurns === 'number' ? [roleTurns, 'role'] : [exports.CORE_DEFAULT_MAX_TURNS, 'core-default'];
48
+ limits.push({ stepId: node.id, ...(label ? { label } : {}), role, maxTurns, source });
49
+ }
50
+ };
51
+ visit(graph);
52
+ for (const component of Object.values(graph.components ?? {}))
53
+ visit(component);
54
+ return limits;
55
+ }
56
+ const TURN_LIMIT = /reached the configured limit of (\d+) turns for ([\w:-]+)/;
57
+ /** Appends where to raise the limit to Core's turn-limit failure; other messages pass through. */
58
+ function withTurnLimitHint(message, stepId) {
59
+ const match = message.match(TURN_LIMIT);
60
+ if (!match)
61
+ return message;
62
+ const step = stepId ? `the "${stepId}" step` : `the step that runs role "${match[2]}"`;
63
+ return `${message} To raise it: Loops ▸ this loop ▸ select ${step} ▸ "Maximum turns" (role "${match[2]}", currently ${match[1]}), save and publish. Project Settings do not change loop runs.`;
64
+ }
@@ -17,6 +17,7 @@ const builtin_loops_1 = require("./builtin-loops");
17
17
  const loop_command_catalog_1 = require("./loop-command-catalog");
18
18
  const loop_constants_1 = require("./loop-constants");
19
19
  const loop_preview_1 = require("./loop-preview");
20
+ const loop_turn_limits_1 = require("./loop-turn-limits");
20
21
  const BUILTIN_DELETE_MESSAGE = 'Built-in loops cannot be deleted. Use Restore original to reset it, or Duplicate it to keep a separate copy.';
21
22
  async function factoryCapabilities() {
22
23
  try {
@@ -281,7 +282,8 @@ function registerLoopsRoutes(router, deps) {
281
282
  res.status(404).json({ error: 'Loop not found' });
282
283
  return;
283
284
  }
284
- res.json({ loop });
285
+ // Effective per-step turn ceilings, so readers (UI, MCP) never guess where the limit lives.
286
+ res.json({ loop, turnLimits: (0, loop_turn_limits_1.loopTurnLimits)(loop.graph) });
285
287
  });
286
288
  // ── Create ───────────────────────────────────────────────────────────────────
287
289
  router.post('/loops', (req, res) => {
@@ -81,6 +81,14 @@ repository members, and runs coordinated AI coding pipelines over their selected
81
81
  (\`factory:implement\`, \`factory:freestyle\`, \`factory:sdd-quick-openspec\`)
82
82
  are editable in place: \`update\` + \`publish\` changes the built-in for every
83
83
  rail that uses it, \`restore_builtin\` resets it; \`duplicate\` makes a copy.
84
+ **Turn limits live in the loop, never in Settings.** Each AI step runs an
85
+ agent role with a "Maximum turns" ceiling (Core default 100 when unset).
86
+ \`specrails_loops(get)\` returns \`turnLimits\` (step → role → effective
87
+ value). Implement steps run roles plan/build/assess/correct (Architect,
88
+ Developer, Reviewer, Fixer), stored at \`graph.config.agents.roles.<role>.maxTurns\`.
89
+ To raise one: Loops ▸ the loop ▸ select the step ▸ "Maximum turns", save and
90
+ publish (or \`update\` + \`publish\`). Project Agent Runtime settings do not
91
+ change loop runs, and a failed run keeps its frozen limit: relaunch.
84
92
  - **Profile** — per-project agent-chain config (which agents, which models,
85
93
  routing). Supported by Claude and Kimi; forced to null on Codex/Gemini rails.
86
94
  - **Provider / engine** — claude, codex, gemini or kimi. A project installs one