specrails-desktop 2.63.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 (63) hide show
  1. package/client/dist/assets/{ActivityFeedPage-Bn2MpG5M.js → ActivityFeedPage-B1bpTy7O.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-CHlXWdXq.js → AgentBrowserCapture-BussBbN3.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-yj-YCgrK.js → AgentModeAnalyticsPane-BiT1n3BU.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-Xczcyghb.js → AgentModeCodePane-CUjNfeKa.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-DtOW01ln.js → AgentModeJobsPane-RVEwyS59.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-BMfJ4mzU.js → AgentsPage-DKE-eMC5.js} +1 -1
  7. package/client/dist/assets/{AnalyticsPage-CGn5cgtc.js → AnalyticsPage-Bser2Ly3.js} +1 -1
  8. package/client/dist/assets/{CodePage-C8NIginr.js → CodePage-BK0grm6e.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-C87r4y26.js → DesktopAnalyticsPage-DEBidZbs.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-Z50a_KPv.js → DocsDialog-Dhyo57n9.js} +1 -1
  11. package/client/dist/assets/{DocsPage-DIArbEPl.js → DocsPage-BcEkPXd6.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-CJdCqYd7.js → ExportDropdown-mRmqdkjr.js} +1 -1
  13. package/client/dist/assets/{InteractiveJobComposer-BxLh4me-.js → InteractiveJobComposer-BlCe3vrU.js} +1 -1
  14. package/client/dist/assets/{JobDetailModal-C7dWoRu6.js → JobDetailModal-CkAejD1F.js} +1 -1
  15. package/client/dist/assets/{JobDetailPage-Ilu294WS.js → JobDetailPage-P-FS_lKz.js} +1 -1
  16. package/client/dist/assets/{JobsPage-fPq1hefb.js → JobsPage-CoUXgYse.js} +1 -1
  17. package/client/dist/assets/LoopBuilderPage-iFylNk68.js +1 -0
  18. package/client/dist/assets/{LoopPreviewModal-ChI3NQC7.js → LoopPreviewModal-4obS_Ptg.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-D1WB9i-9.js → LoopWindowSurface-CLr5OPqj.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-CCwc0Biu.js → LoopsPage-eKpWFtCp.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-eH_KaAZ1.js → MinimizedChatsContext-BHIUPVmC.js} +1 -1
  22. package/client/dist/assets/{PluginsPage-DfDfEfbW.js → PluginsPage-BgTC2CQx.js} +1 -1
  23. package/client/dist/assets/{ProjectSettingsDialog-9NHnG8JQ.js → ProjectSettingsDialog-B4chTfU8.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-DgzNnJmn.js → RepositoryDeliveries-DAIxIYsl.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-BSgUqk_u.js → RepositoryScopeSelector-DeCoUsRN.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-D34bGpSA.js → ReviewPacketPage-Cktv0-9b.js} +1 -1
  27. package/client/dist/assets/{TemplatePreviewModal-CcfSxWQ7.js → TemplatePreviewModal-BZTV1TM1.js} +1 -1
  28. package/client/dist/assets/{TicketDetailModalContext-BoafEcm4.js → TicketDetailModalContext-DiPngt2g.js} +1 -1
  29. package/client/dist/assets/{Trans-CVSryAL1.js → Trans-ZiXIfj50.js} +1 -1
  30. package/client/dist/assets/{agentRuntime-TKQyg137.js → agentRuntime-BDvbJMmS.js} +1 -1
  31. package/client/dist/assets/{agentRuntime-DvhEe1um.js → agentRuntime-BKJfv5YD.js} +1 -1
  32. package/client/dist/assets/{agentRuntime-DxC6-9rN.js → agentRuntime-BxF81z52.js} +1 -1
  33. package/client/dist/assets/{agentRuntime-C_yLPKDy.js → agentRuntime-BxYS5V7u.js} +1 -1
  34. package/client/dist/assets/{agentRuntime-WSv8G0pW.js → agentRuntime-BzuwQQgn.js} +1 -1
  35. package/client/dist/assets/{agentRuntime-D2kYQqNO.js → agentRuntime-Dc9HG3BA.js} +1 -1
  36. package/client/dist/assets/{agentRuntime-Bjt7Ij6_.js → agentRuntime-DiCldqGG.js} +1 -1
  37. package/client/dist/assets/{agentRuntime-B-myNBUP.js → agentRuntime-es2ymSl-.js} +1 -1
  38. package/client/dist/assets/{formatDistanceToNow-NVM6rK3i.js → formatDistanceToNow-CdN5lTyD.js} +1 -1
  39. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-ZHg7WpdH.js → getTimezoneOffsetInMilliseconds-62h40I4t.js} +1 -1
  40. package/client/dist/assets/index-BgbCbIua.css +2 -0
  41. package/client/dist/assets/{index-Culan6y7.js → index-CECj3joE.js} +7 -7
  42. package/client/dist/assets/{jira-api-Bl8xEIpk.js → jira-api-C_D_RDhl.js} +1 -1
  43. package/client/dist/assets/{project-repositories-CvJf1a6H.js → project-repositories-Be0CWklo.js} +1 -1
  44. package/client/dist/assets/{spending-BfjSJjBd.js → spending-eWiZGhC_.js} +1 -1
  45. package/client/dist/assets/{useDesktop-C5JVBeur.js → useDesktop-C0mRIlHT.js} +1 -1
  46. package/client/dist/assets/{useRuntimeRuns-DohBmeg8.js → useRuntimeRuns-B1a6GChu.js} +1 -1
  47. package/client/dist/assets/{useSharedWebSocket-CNOvmgTr.js → useSharedWebSocket-CcsE5986.js} +2 -2
  48. package/client/dist/index.html +16 -16
  49. package/docs/internals/core-runtime-updates.md +3 -3
  50. package/docs/internals/desktop-owned-workflows.md +10 -0
  51. package/docs/internals/programmatic-agent-runtime.md +16 -0
  52. package/docs/internals/source-map.md +4 -1
  53. package/package.json +1 -1
  54. package/server/dist/mcp/tools/loops.js +1 -1
  55. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +13 -3
  56. package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +1 -1
  57. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +13 -6
  58. package/server/dist/modules/loops/runtime/loop-turn-limits.js +64 -0
  59. package/server/dist/modules/loops/runtime/loops-router.js +3 -1
  60. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -0
  61. package/server/dist/schemas/agent-runtime.schema.json +1 -1
  62. package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +0 -1
  63. package/client/dist/assets/index-D1v33giW.css +0 -2
@@ -204,7 +204,7 @@
204
204
  .specrails-splash__mark .pill { opacity: 1; }
205
205
  }
206
206
  </style>
207
- <script type="module" crossorigin src="/assets/index-Culan6y7.js"></script>
207
+ <script type="module" crossorigin src="/assets/index-CECj3joE.js"></script>
208
208
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
209
209
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-BbYvbtY8.js">
210
210
  <link rel="modulepreload" crossorigin href="/assets/react-dom-DDD8FYaU.js">
@@ -214,7 +214,7 @@
214
214
  <link rel="modulepreload" crossorigin href="/assets/activity-DiyjsgQV.js">
215
215
  <link rel="modulepreload" crossorigin href="/assets/addspec-BOvgmcgk.js">
216
216
  <link rel="modulepreload" crossorigin href="/assets/agent-Dip4h2YE.js">
217
- <link rel="modulepreload" crossorigin href="/assets/agentRuntime-C_yLPKDy.js">
217
+ <link rel="modulepreload" crossorigin href="/assets/agentRuntime-BxYS5V7u.js">
218
218
  <link rel="modulepreload" crossorigin href="/assets/agents-Doe1sHY5.js">
219
219
  <link rel="modulepreload" crossorigin href="/assets/agentstudio-BzugqB_N.js">
220
220
  <link rel="modulepreload" crossorigin href="/assets/aiedit-iIjz2_Qk.js">
@@ -243,16 +243,16 @@
243
243
  <link rel="modulepreload" crossorigin href="/assets/terminal-BAzpHtpY.js">
244
244
  <link rel="modulepreload" crossorigin href="/assets/tickets-D41ZMQpf.js">
245
245
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DtODIU64.js">
246
- <link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-CNOvmgTr.js">
247
- <link rel="modulepreload" crossorigin href="/assets/Trans-CVSryAL1.js">
248
- <link rel="modulepreload" crossorigin href="/assets/useDesktop-C5JVBeur.js">
246
+ <link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-CcsE5986.js">
247
+ <link rel="modulepreload" crossorigin href="/assets/Trans-ZiXIfj50.js">
248
+ <link rel="modulepreload" crossorigin href="/assets/useDesktop-C0mRIlHT.js">
249
249
  <link rel="modulepreload" crossorigin href="/assets/provider-capabilities-lePfeT_Y.js">
250
250
  <link rel="modulepreload" crossorigin href="/assets/save-LmPSfsvZ.js">
251
- <link rel="modulepreload" crossorigin href="/assets/spending-BfjSJjBd.js">
251
+ <link rel="modulepreload" crossorigin href="/assets/spending-eWiZGhC_.js">
252
252
  <link rel="modulepreload" crossorigin href="/assets/chevron-up-6EFqEHBj.js">
253
253
  <link rel="modulepreload" crossorigin href="/assets/sparkles-CCfUdFGY.js">
254
254
  <link rel="modulepreload" crossorigin href="/assets/x-Bu7X_eR2.js">
255
- <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-eH_KaAZ1.js">
255
+ <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-BHIUPVmC.js">
256
256
  <link rel="modulepreload" crossorigin href="/assets/ban-wqfdzoXt.js">
257
257
  <link rel="modulepreload" crossorigin href="/assets/file-text-C0blZHLh.js">
258
258
  <link rel="modulepreload" crossorigin href="/assets/circle-x-D2ttKotB.js">
@@ -268,23 +268,23 @@
268
268
  <link rel="modulepreload" crossorigin href="/assets/search-BDVp3fkH.js">
269
269
  <link rel="modulepreload" crossorigin href="/assets/trash-D8zYUmTg.js">
270
270
  <link rel="modulepreload" crossorigin href="/assets/triangle-alert-Cxpn8bqr.js">
271
- <link rel="modulepreload" crossorigin href="/assets/jira-api-Bl8xEIpk.js">
271
+ <link rel="modulepreload" crossorigin href="/assets/jira-api-C_D_RDhl.js">
272
272
  <link rel="modulepreload" crossorigin href="/assets/zap-bugHyWzQ.js">
273
273
  <link rel="modulepreload" crossorigin href="/assets/clsx-CjueKrWZ.js">
274
274
  <link rel="modulepreload" crossorigin href="/assets/utils-BC4DHlz2.js">
275
275
  <link rel="modulepreload" crossorigin href="/assets/dialog-DtdR05v1.js">
276
276
  <link rel="modulepreload" crossorigin href="/assets/lib-DWJfei6y.js">
277
- <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-ZHg7WpdH.js">
278
- <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-NVM6rK3i.js">
277
+ <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-62h40I4t.js">
278
+ <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-CdN5lTyD.js">
279
279
  <link rel="modulepreload" crossorigin href="/assets/button-4FS1ZDTp.js">
280
- <link rel="modulepreload" crossorigin href="/assets/project-repositories-CvJf1a6H.js">
281
- <link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-BSgUqk_u.js">
282
- <link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-BoafEcm4.js">
280
+ <link rel="modulepreload" crossorigin href="/assets/project-repositories-Be0CWklo.js">
281
+ <link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-DeCoUsRN.js">
282
+ <link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-DiPngt2g.js">
283
283
  <link rel="modulepreload" crossorigin href="/assets/core-B3mKBIh5.js">
284
- <link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-DgzNnJmn.js">
285
- <link rel="modulepreload" crossorigin href="/assets/useRuntimeRuns-DohBmeg8.js">
284
+ <link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-DAIxIYsl.js">
285
+ <link rel="modulepreload" crossorigin href="/assets/useRuntimeRuns-B1a6GChu.js">
286
286
  <link rel="modulepreload" crossorigin href="/assets/event-Bt7f3wXe.js">
287
- <link rel="stylesheet" crossorigin href="/assets/index-D1v33giW.css">
287
+ <link rel="stylesheet" crossorigin href="/assets/index-BgbCbIua.css">
288
288
  </head>
289
289
  <body>
290
290
  <div id="specrails-splash" aria-hidden="false" role="status" aria-label="Loading Specrails">
@@ -13,7 +13,7 @@ framework is rejected rather than silently replacing an update.
13
13
 
14
14
  ## Persistence and publication
15
15
 
16
- The release bundle pins Core 6.5.0 in `desktop-release.yml` and
16
+ The release bundle pins Core 6.6.2 in `desktop-release.yml` and
17
17
  `scripts/assemble-bundled-core.lock.json`. Core 6 is the Desktop-only engine: it
18
18
  publishes integration contract 5.1 (no standalone `update`) and ships only the
19
19
  implement and retry workflows (Batch was folded into implement; Desktop no
@@ -27,8 +27,8 @@ dependency leaks into its tarball (today `@langchain/langgraph-sdk` ships
27
27
  module still loads while every staged path stays inside the MSI budget
28
28
  (`MAX_STAGED_RELATIVE_PATH`, 120 characters).
29
29
 
30
- The paired CI checkout uses the immutable Core 6.5.0 release commit
31
- `187f9d24533a99fb1f37c7805b3c5b1375040d25`. Bundle smoke checks use Node 22.22.3,
30
+ The paired CI checkout uses the immutable Core 6.6.2 release commit
31
+ `98b5d3c515967aed170dcb8a31a7462390cc420f`. Bundle smoke checks use Node 22.22.3,
32
32
  matching Core's minimum supported Node version and the Desktop release runtime.
33
33
 
34
34
  Desktop updates retain the complete npm installation, including dependencies,
@@ -54,6 +54,16 @@ real committed full verification and the reviewed/approved candidate before
54
54
  publishing the recoverable write set. The final host check certifies the archived
55
55
  candidate. Reviewer assertions remain AI evidence and cannot create verification.
56
56
 
57
+ A failed archive returns to verification only when the gate reports a stale or
58
+ unverified candidate (`unverified`/`candidate_changed`), at most twice
59
+ (`archiveRetries`). Any other archive failure, such as OpenSpec rejecting a rebuilt
60
+ main spec, repeats identically. Implement ends at `archive-failed` with
61
+ the run log pointing at OpenSpec's reason, instead of looping through verification,
62
+ review and archive until the transition budget runs out. Core's `openspec-validate`
63
+ dry-runs the archive before the plan freezes, so most such specs return to the
64
+ Architect with the offending requirement named. Edited built-in loops keep their
65
+ saved graph: restore or re-edit them to pick up recipe changes.
66
+
57
67
  Implement checks the host candidate hash after each correction. When verification
58
68
  failed and the correction leaves that candidate unchanged, it ends unsuccessfully
59
69
  with the corrector's explanation instead of rerunning the same failed checks.
@@ -69,6 +69,22 @@ Desktop negotiates `runtime api` and sends configuration to `runtime validate --
69
69
  may only tighten Core's floors (70 overall, 75 security, 60 other aspects).
70
70
  Archive approval and low-confidence architect behavior are loop policies.
71
71
 
72
+ ### Turn limits
73
+
74
+ Every AI step runs an agent role with a **Maximum turns** ceiling; each tool call
75
+ counts as a turn. Unset roles use Core's default of 100. The Implement recipe runs
76
+ roles `plan`, `build`, `assess` and `correct` (Architect, Developer, Reviewer,
77
+ Fixer) and ships `build`/`correct` at 300. Read and set every ceiling in one place
78
+ under the loop's **Advanced settings → Turn limits**, or per step in its agent
79
+ panel; steps sharing a role share the value. Save and publish: a run freezes its
80
+ limits at launch, so a failed run needs a new launch, not a resume.
81
+
82
+ `GET /api/loops/:id` (and MCP `specrails_loops get`) returns `turnLimits` with each
83
+ step's role, effective value and source (`step`, `role` or `core-default`). The run
84
+ log prints each used role as `[runtime] build: claude/opus · max 300 turns`, and a
85
+ turn-limit failure names the step and the field to raise. Project agent-runtime
86
+ settings never change loop runs.
87
+
72
88
  Only verification is read from the project's `.specrails/agent-runtime.json` for
73
89
  new loop-owned recipes. Historical roles remain available for explicit import
74
90
  and old saved graphs. The editor never silently imports a project's settings
@@ -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 1044 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)
@@ -1332,6 +1334,7 @@ match; integration suites may live elsewhere. Use `rg` to find other consumers.
1332
1334
  - [loop-shell-invocation.ts](../../server/modules/loops/runtime/loop-shell-invocation.ts)
1333
1335
  - [loop-step-idle.ts](../../server/modules/loops/runtime/loop-step-idle.ts) · [test](../../server/modules/loops/runtime/loop-step-idle.test.ts)
1334
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)
1335
1338
  - [loops-router.ts](../../server/modules/loops/runtime/loops-router.ts) · [test](../../server/modules/loops/runtime/loops-router.test.ts)
1336
1339
  - [loops-store.ts](../../server/modules/loops/runtime/loops-store.ts) · [test](../../server/modules/loops/runtime/loops-store.test.ts)
1337
1340
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.63.0",
3
+ "version": "2.63.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
  }
@@ -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'));
@@ -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
@@ -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" },