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
@@ -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. */
@@ -1,37 +1,237 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROJECT_ENV_FAILURE_TTL_MS = exports.PROJECT_ENV_SUCCESS_TTL_MS = void 0;
4
+ exports.refreshProjectEnv = refreshProjectEnv;
5
+ exports.ensureProjectEnvFresh = ensureProjectEnvFresh;
6
+ exports.startProjectEnvWarmup = startProjectEnvWarmup;
7
+ exports.stopProjectEnvWarmup = stopProjectEnvWarmup;
3
8
  exports.resolveWorktreeEnvPassthrough = resolveWorktreeEnvPassthrough;
4
9
  exports.applyWorktreeEnvPassthrough = applyWorktreeEnvPassthrough;
10
+ exports.getProjectEnvStatus = getProjectEnvStatus;
11
+ exports.worktreeEnvPassthroughWarning = worktreeEnvPassthroughWarning;
5
12
  const db_1 = require("./db");
6
13
  const path_resolver_1 = require("./path-resolver");
7
- // Values stay in the owning project's memory, never process.env or SQLite.
8
- // Both successful and missing lookups expire so profiles can recover/rotate.
9
- const LOGIN_SHELL_CACHE_MS = 30_000;
10
- const shellValues = new WeakMap();
14
+ /** Successful recoveries stay valid for 10 minutes; the background refresh
15
+ * rotates them well inside that window. */
16
+ exports.PROJECT_ENV_SUCCESS_TTL_MS = 10 * 60_000;
17
+ /** Failed or empty lookups are retried after 30 seconds. */
18
+ exports.PROJECT_ENV_FAILURE_TTL_MS = 30_000;
19
+ /** The background refresh runs this long before a success expires. */
20
+ const PROJECT_ENV_REFRESH_LEAD_MS = 60_000;
21
+ const states = new WeakMap();
22
+ function shellIdentity() {
23
+ return JSON.stringify([process.env.SHELL, process.env.HOME, process.env.ZDOTDIR]);
24
+ }
25
+ function stateFor(db) {
26
+ const identity = shellIdentity();
27
+ let state = states.get(db);
28
+ if (!state) {
29
+ state = { identity, records: new Map(), values: new Map(), inflight: null, queued: null, timer: null, active: false };
30
+ states.set(db, state);
31
+ }
32
+ else if (state.identity !== identity) {
33
+ state.identity = identity;
34
+ state.records.clear();
35
+ state.values.clear();
36
+ }
37
+ return state;
38
+ }
39
+ function configuredNames(db) {
40
+ try {
41
+ return (0, db_1.getProjectSettings)(db).worktreeEnvPassthrough;
42
+ }
43
+ catch {
44
+ // A closed/unloaded project connection has no configured names.
45
+ return [];
46
+ }
47
+ }
48
+ function missingNames(names) {
49
+ return names.filter(name => !process.env[name]);
50
+ }
51
+ /** Drop records for names no longer configured or now inherited. */
52
+ function prune(state, names) {
53
+ for (const name of [...state.records.keys()]) {
54
+ if (!names.includes(name) || process.env[name]) {
55
+ state.records.delete(name);
56
+ state.values.delete(name);
57
+ }
58
+ }
59
+ }
60
+ function isValid(record, now) {
61
+ return Boolean(record && now < record.expiresAt);
62
+ }
63
+ function statusFromProbe(probe, value) {
64
+ if (value)
65
+ return 'recovered';
66
+ if (probe.status === 'timeout')
67
+ return 'probe-timeout';
68
+ if (probe.status === 'failed')
69
+ return 'probe-failed';
70
+ // `ok` without a value, or `skipped` (Windows / no login-shell recovery).
71
+ return 'not-defined';
72
+ }
73
+ function recordProbe(state, probe, names, now) {
74
+ for (const name of names) {
75
+ const value = probe.values[name];
76
+ const status = statusFromProbe(probe, value);
77
+ const previous = state.records.get(name);
78
+ // A transient probe failure never discards a still-valid recovered value.
79
+ if (status !== 'recovered' && previous?.status === 'recovered' && isValid(previous, now) && probe.status !== 'ok')
80
+ continue;
81
+ state.records.set(name, {
82
+ status,
83
+ shell: probe.shell,
84
+ checkedAt: now,
85
+ expiresAt: now + (status === 'recovered' ? exports.PROJECT_ENV_SUCCESS_TTL_MS : exports.PROJECT_ENV_FAILURE_TTL_MS),
86
+ exitCode: probe.exitCode,
87
+ });
88
+ if (value)
89
+ state.values.set(name, value);
90
+ else
91
+ state.values.delete(name);
92
+ }
93
+ }
94
+ function scheduleRefresh(db, state) {
95
+ if (state.timer) {
96
+ clearTimeout(state.timer);
97
+ state.timer = null;
98
+ }
99
+ if (!state.active || states.get(db) !== state)
100
+ return;
101
+ let earliest = Infinity;
102
+ for (const record of state.records.values()) {
103
+ if (record.status === 'recovered')
104
+ earliest = Math.min(earliest, record.expiresAt);
105
+ }
106
+ if (earliest === Infinity)
107
+ return;
108
+ const now = Date.now();
109
+ const at = Math.max(now + exports.PROJECT_ENV_FAILURE_TTL_MS, earliest - PROJECT_ENV_REFRESH_LEAD_MS);
110
+ state.timer = setTimeout(() => {
111
+ state.timer = null;
112
+ if (state.active && states.get(db) === state)
113
+ void refreshProjectEnv(db);
114
+ }, at - now);
115
+ state.timer.unref?.();
116
+ }
117
+ /** Probe the login shell asynchronously for every configured name missing from
118
+ * the process environment and update this project's records. De-duplicated
119
+ * while a probe is in flight: a call during a probe resolves after one more
120
+ * probe that starts when the current one ends, so a settings change or an
121
+ * explicit recheck is never answered with stale results. Never rejects. */
122
+ function refreshProjectEnv(db) {
123
+ const state = stateFor(db);
124
+ if (state.inflight) {
125
+ // A request during a probe (settings change, recheck) gets a fresh probe
126
+ // after the current one; concurrent requests share that single follow-up.
127
+ if (!state.queued) {
128
+ state.queued = state.inflight.then(() => {
129
+ state.queued = null;
130
+ return refreshProjectEnv(db);
131
+ });
132
+ }
133
+ return state.queued;
134
+ }
135
+ const names = configuredNames(db);
136
+ prune(state, names);
137
+ const missing = missingNames(names);
138
+ if (missing.length === 0) {
139
+ scheduleRefresh(db, state);
140
+ return Promise.resolve();
141
+ }
142
+ const run = (0, path_resolver_1.readEnvFromLoginShell)(missing, { timeoutMs: (0, path_resolver_1.resolveLoginShellProbeTimeoutMs)() })
143
+ .then(probe => {
144
+ if (states.get(db) !== state)
145
+ return;
146
+ const current = configuredNames(db);
147
+ prune(state, current);
148
+ // Only names still configured and still missing are recorded.
149
+ const stillMissing = missingNames(current);
150
+ recordProbe(state, probe, missing.filter(name => stillMissing.includes(name)), Date.now());
151
+ })
152
+ .catch(() => { })
153
+ .finally(() => {
154
+ state.inflight = null;
155
+ scheduleRefresh(db, state);
156
+ });
157
+ state.inflight = run;
158
+ return run;
159
+ }
160
+ /** Probe only when a configured missing name has no valid record yet. */
161
+ function ensureProjectEnvFresh(db) {
162
+ const state = stateFor(db);
163
+ if (state.inflight)
164
+ return state.inflight;
165
+ const names = configuredNames(db);
166
+ prune(state, names);
167
+ const now = Date.now();
168
+ const stale = missingNames(names).some(name => !isValid(state.records.get(name), now));
169
+ return stale ? refreshProjectEnv(db) : Promise.resolve();
170
+ }
171
+ /** Start background warm-up for a loaded project: probe now, then refresh
172
+ * before successful results expire, until `stopProjectEnvWarmup`. */
173
+ function startProjectEnvWarmup(db) {
174
+ const state = stateFor(db);
175
+ state.active = true;
176
+ void refreshProjectEnv(db);
177
+ }
178
+ /** Stop the background refresh and forget this project's recovered values. */
179
+ function stopProjectEnvWarmup(db) {
180
+ const state = states.get(db);
181
+ if (!state)
182
+ return;
183
+ state.active = false;
184
+ if (state.timer) {
185
+ clearTimeout(state.timer);
186
+ state.timer = null;
187
+ }
188
+ states.delete(db);
189
+ }
11
190
  function recoverProjectEnv(db, names) {
12
- const missing = names.filter(name => !process.env[name]);
13
- if (!missing.length) {
14
- shellValues.delete(db);
191
+ const state = stateFor(db);
192
+ prune(state, names);
193
+ const missing = missingNames(names);
194
+ if (!missing.length)
15
195
  return {};
196
+ const now = Date.now();
197
+ // Cold fallback: a name with no record (or an expired success) gets the
198
+ // short synchronous probe. Expired failures are retried asynchronously.
199
+ const cold = missing.filter(name => {
200
+ const record = state.records.get(name);
201
+ return !record || (record.status === 'recovered' && !isValid(record, now));
202
+ });
203
+ let retry = missing.some(name => {
204
+ const record = state.records.get(name);
205
+ return record !== undefined && record.status !== 'recovered' && !isValid(record, now);
206
+ });
207
+ if (cold.length) {
208
+ const probe = (0, path_resolver_1.readEnvFromLoginShellSyncDetailed)(cold);
209
+ recordProbe(state, probe, cold, now);
210
+ // The async probe has a larger budget than the cold sync one.
211
+ if (probe.status === 'timeout' || probe.status === 'failed')
212
+ retry = true;
213
+ }
214
+ if (retry)
215
+ void refreshProjectEnv(db);
216
+ const out = {};
217
+ for (const name of missing) {
218
+ const value = state.values.get(name);
219
+ if (value && isValid(state.records.get(name), now))
220
+ out[name] = value;
16
221
  }
17
- const identity = JSON.stringify([names, missing, process.env.SHELL, process.env.HOME, process.env.ZDOTDIR]);
18
- const cached = shellValues.get(db);
19
- if (cached?.identity === identity && Date.now() < cached.expiresAt)
20
- return cached.env;
21
- const env = (0, path_resolver_1.readEnvFromLoginShellSync)(missing);
22
- shellValues.set(db, { identity, expiresAt: Date.now() + LOGIN_SHELL_CACHE_MS, env });
23
- return env;
222
+ return out;
24
223
  }
25
224
  /** Resolve the per-project env passthrough overlay for a spawn.
26
225
  *
27
226
  * The project setting stores names only. Values are read from `sourceEnv` at
28
227
  * spawn time so credentials can rotate without touching SQLite. When the
29
- * source is `process.env`, missing configured names get bounded login-shell
30
- * recovery with a short, project-owned cache. Unresolved names remain absent.
228
+ * source is `process.env`, missing configured names come from the project's
229
+ * warm login-shell cache; the short synchronous probe runs only when cold.
230
+ * Unresolved names remain absent.
31
231
  */
32
232
  function resolveWorktreeEnvPassthrough(db, sourceEnv = process.env) {
33
233
  const out = {};
34
- const names = (0, db_1.getProjectSettings)(db).worktreeEnvPassthrough;
234
+ const names = configuredNames(db);
35
235
  const recovered = sourceEnv === process.env ? recoverProjectEnv(db, names) : {};
36
236
  for (const name of names) {
37
237
  const value = sourceEnv[name] || recovered[name] || sourceEnv[name];
@@ -44,3 +244,51 @@ function applyWorktreeEnvPassthrough(db, env, sourceEnv = process.env) {
44
244
  const passthrough = resolveWorktreeEnvPassthrough(db, sourceEnv);
45
245
  return Object.keys(passthrough).length === 0 ? env : { ...env, ...passthrough };
46
246
  }
247
+ /** Value-free status of every configured name for this project. */
248
+ function getProjectEnvStatus(db) {
249
+ const state = stateFor(db);
250
+ const names = configuredNames(db);
251
+ prune(state, names);
252
+ const nowIso = new Date(Date.now()).toISOString();
253
+ return {
254
+ loginShellRecovery: process.platform !== 'win32',
255
+ timeoutMs: (0, path_resolver_1.resolveLoginShellProbeTimeoutMs)(),
256
+ checking: state.inflight !== null,
257
+ names: names.map((name) => {
258
+ if (process.env[name])
259
+ return { name, status: 'inherited', shell: null, checkedAt: nowIso, exitCode: null };
260
+ const record = state.records.get(name);
261
+ if (!record)
262
+ return { name, status: 'pending', shell: null, checkedAt: null, exitCode: null };
263
+ return {
264
+ name,
265
+ status: record.status,
266
+ shell: record.shell,
267
+ checkedAt: new Date(record.checkedAt).toISOString(),
268
+ exitCode: record.exitCode,
269
+ };
270
+ }),
271
+ };
272
+ }
273
+ /** One aggregated, value-free run-log line naming every configured name that
274
+ * is absent from `env`, or null when all configured names resolved. */
275
+ function worktreeEnvPassthroughWarning(db, env) {
276
+ const names = configuredNames(db);
277
+ const unresolved = names.filter(name => !env[name]);
278
+ if (!unresolved.length)
279
+ return null;
280
+ const state = states.get(db);
281
+ const now = Date.now();
282
+ const parts = unresolved.map(name => {
283
+ const record = state?.records.get(name);
284
+ // An expired success no longer counts as recovered; failures keep their cause.
285
+ const status = !record ? 'not-defined' : record.status === 'recovered' && now >= record.expiresAt ? 'not-defined' : record.status;
286
+ return { name, status };
287
+ });
288
+ const hint = process.platform === 'win32'
289
+ ? 'set it in the environment Specrails starts with'
290
+ : 'configure it in the login shell or launch from a terminal';
291
+ if (parts.length === 1)
292
+ return `[environment] ${parts[0].name} not available (${parts[0].status}); ${hint}`;
293
+ return `[environment] ${parts.map(p => `${p.name} (${p.status})`).join(', ')} not available; ${hint.replace(' it ', ' them ')}`;
294
+ }
@@ -390,6 +390,11 @@ class ProjectRegistry {
390
390
  (0, telemetry_receiver_1.dropBlobStatesForProject)(id);
391
391
  }
392
392
  catch { /* ignore */ }
393
+ // Stop the login-shell env refresh and forget recovered values.
394
+ try {
395
+ (0, project_env_1.stopProjectEnvWarmup)(ctx.db);
396
+ }
397
+ catch { /* ignore */ }
393
398
  // Close the DB connection BEFORE removing the project's data dir below.
394
399
  try {
395
400
  ctx.db.close();
@@ -639,6 +644,10 @@ class ProjectRegistry {
639
644
  ctx.fileSummaryManager.dispose();
640
645
  }
641
646
  catch { /* ignore */ }
647
+ try {
648
+ (0, project_env_1.stopProjectEnvWarmup)(ctx.db);
649
+ }
650
+ catch { /* ignore */ }
642
651
  ctx.ticketWatcher.close().catch(() => { });
643
652
  }
644
653
  // Close the shared browser context once, after every per-project manager has
@@ -1152,6 +1161,8 @@ class ProjectRegistry {
1152
1161
  sourcePath: () => (0, project_repositories_1.resolveProjectRepository)(project).path,
1153
1162
  workspacePaths: () => { const primary = (0, project_repositories_1.resolveProjectRepository)(project); return primary.workspacePaths ?? (primary.workspacePath ? [primary.workspacePath] : undefined); },
1154
1163
  env: () => (0, workspace_resolution_1.resolveLoopBaseEnv)({ slug: project.slug, path: project.path }, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
1164
+ // One value-free run-log line when configured names are unresolved.
1165
+ environmentNotice: () => (0, project_env_1.worktreeEnvPassthroughWarning)(db, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
1155
1166
  // Named rail profiles and global per-agent defaults ride the same
1156
1167
  // SPECRAILS_PROFILE_PATH snapshot in both AI transports. An explicit null
1157
1168
  // rail selection opts out; omitted selections keep Core's default rules.
@@ -1486,6 +1497,9 @@ class ProjectRegistry {
1486
1497
  const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, stuckRunDetector, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb, milestoneProgress };
1487
1498
  ctx.milestoneChains = milestoneChains;
1488
1499
  this._contexts.set(project.id, ctx);
1500
+ // Warm the project's login-shell env cache off the event loop so the first
1501
+ // rail/loop spawn reads recovered credentials instead of a cold 1.5 s probe.
1502
+ (0, project_env_1.startProjectEnvWarmup)(db);
1489
1503
  const recoverAfterProbe = (definitionStates) => {
1490
1504
  if (this._contexts.get(project.id) !== ctx)
1491
1505
  return;
@@ -9,6 +9,7 @@ exports.registerSettingsRoutes = registerSettingsRoutes;
9
9
  const project_settings_1 = require("./modules/project-settings");
10
10
  const sqlite_1 = require("./modules/project-settings/adapters/sqlite");
11
11
  const http_1 = require("./modules/project-settings/adapters/http");
12
+ const project_env_1 = require("./project-env");
12
13
  const fs_1 = __importDefault(require("fs"));
13
14
  const path_1 = __importDefault(require("path"));
14
15
  const db_1 = require("./db");
@@ -35,6 +36,22 @@ function registerSettingsRoutes(deps) {
35
36
  const runtimeChanged = JSON.stringify(settings.subagentRuntime) !== JSON.stringify(previous.subagentRuntime);
36
37
  if (settings.allowSubagents !== previous.allowSubagents || runtimeChanged)
37
38
  registry.notifySettingsChanged({ projectId: String(req.params.projectId) });
39
+ // Configured env names changed: re-warm this project's login-shell cache.
40
+ if (JSON.stringify(settings.worktreeEnvPassthrough) !== JSON.stringify(previous.worktreeEnvPassthrough))
41
+ void (0, project_env_1.refreshProjectEnv)(ctx(req).db);
42
+ });
43
+ // Value-free resolution status of the configured env names (+ recheck).
44
+ (0, http_1.registerEnvPassthroughStatusHttp)(router, {
45
+ read: async (req) => {
46
+ const { db } = ctx(req);
47
+ await (0, project_env_1.ensureProjectEnvFresh)(db);
48
+ return (0, project_env_1.getProjectEnvStatus)(db);
49
+ },
50
+ recheck: async (req) => {
51
+ const { db } = ctx(req);
52
+ await (0, project_env_1.refreshProjectEnv)(db);
53
+ return (0, project_env_1.getProjectEnvStatus)(db);
54
+ },
38
55
  });
39
56
  // ─── Per-project Quick mode Contract Refine last-used value ─────────────────
40
57
  router.get('/:projectId/add-spec-quick-contract-refine-last', (req, res) => {
@@ -12,7 +12,7 @@
12
12
  "planning": { "enum": ["full", "proportional"] }, "acceptDeveloperChecks": { "type": "boolean" },
13
13
  "verification": { "type": "object", "additionalProperties": false, "properties": { "maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 4 } } }
14
14
  } },
15
- "guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" } } },
15
+ "guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" }, "verification-output-adoption": { "type": "boolean" } } },
16
16
  "rolePrompts": { "type": "object", "propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,63}$" }, "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" }, "properties": {
17
17
  "architect": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
18
18
  "developer": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },