specrails-desktop 2.60.0 → 2.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/client/dist/assets/{ActivityFeedPage-3J7fR8yB.js → ActivityFeedPage-Wi5XNgoG.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-CkNWDZT3.js} +1 -1
  3. package/client/dist/assets/{AgentModeAnalyticsPane-Dps30tk9.js → AgentModeAnalyticsPane-CXz2j-Bt.js} +2 -2
  4. package/client/dist/assets/{AgentModeCodePane-CvLimey8.js → AgentModeCodePane-DnsBPtpS.js} +2 -2
  5. package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-U1J6PaBo.js} +1 -1
  6. package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CXrqu5vo.js} +1 -1
  7. package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-Cvtt-LOO.js} +1 -1
  8. package/client/dist/assets/{CodePage-D981zRHy.js → CodePage-D35o-MFO.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-CsDFy5rp.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-CSlMPemn.js} +1 -1
  11. package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-BKlYAvWq.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-CweP_1GP.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-CFKvbQeJ.js +19 -0
  14. package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-DYyGq1Vp.js} +1 -1
  15. package/client/dist/assets/{JobDetailPage-1TUQ9vlE.js → JobDetailPage-C6gpdYvj.js} +1 -1
  16. package/client/dist/assets/{JobsPage-BSrl8P7u.js → JobsPage-CgS0pnKd.js} +1 -1
  17. package/client/dist/assets/LoopBuilderPage-jUp20DSZ.js +1 -0
  18. package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-DkLRfsYX.js} +1 -1
  19. package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-CuACkXFV.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-ClSrWzDd.js} +1 -1
  21. package/client/dist/assets/{MinimizedChatsContext-BD2bsRBe.js → MinimizedChatsContext-CroAs6Kh.js} +1 -1
  22. package/client/dist/assets/{PluginsPage-B8f49Mvn.js → PluginsPage-nCKQOwRL.js} +1 -1
  23. package/client/dist/assets/{ProjectSettingsDialog-D7q-O6h3.js → ProjectSettingsDialog-BVDYwllU.js} +1 -1
  24. package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-DJCm1pZr.js} +1 -1
  25. package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-CeCr6Jz-.js} +1 -1
  26. package/client/dist/assets/{ReviewPacketPage-DpZSzF-Q.js → ReviewPacketPage-kTErrEjx.js} +1 -1
  27. package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-B8yd1AGU.js} +1 -1
  28. package/client/dist/assets/{TicketDetailModalContext-BHmtj1As.js → TicketDetailModalContext-YBJX3RmP.js} +1 -1
  29. package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-D5eOjfvG.js} +1 -1
  30. package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
  31. package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
  32. package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
  33. package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
  34. package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
  35. package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
  36. package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
  37. package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
  38. package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DZ3VWsHq.js} +1 -1
  39. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-9JiGlFrx.js} +1 -1
  40. package/client/dist/assets/{index-g8S62tpS.js → index-lTGJJTuk.js} +8 -8
  41. package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-Bp0zXwjg.js} +1 -1
  42. package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
  43. package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
  44. package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
  45. package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
  46. package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
  47. package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
  48. package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
  49. package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
  50. package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
  51. package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
  52. package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
  53. package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
  54. package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
  55. package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
  56. package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
  57. package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
  58. package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-CadLZOnr.js} +1 -1
  59. package/client/dist/assets/{spending-Oov7p52A.js → spending-DQDNtM8q.js} +1 -1
  60. package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-DKNbOF0u.js} +1 -1
  61. package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-D3YMw9qx.js} +1 -1
  62. package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-DaWuiO0S.js} +2 -2
  63. package/client/dist/index.html +17 -17
  64. package/docs/internals/configuration.md +2 -0
  65. package/docs/internals/core-runtime-updates.md +3 -3
  66. package/docs/internals/programmatic-agent-runtime.md +2 -0
  67. package/package.json +1 -1
  68. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
  69. package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
  70. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
  71. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
  72. package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
  73. package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
  74. package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
  75. package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
  76. package/server/dist/modules/loops/runtime/loop-implement-recipe.js +28 -9
  77. package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
  78. package/server/dist/schemas/agent-runtime.schema.json +9 -7
  79. package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
  80. package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
  81. package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
  82. package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
  83. package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
  84. package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
  85. package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
  86. package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
  87. package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
  88. package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
@@ -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-g8S62tpS.js"></script>
207
+ <script type="module" crossorigin src="/assets/index-lTGJJTuk.js"></script>
208
208
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
209
209
  <link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
210
210
  <link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
@@ -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-Z5s_GTP0.js">
217
- <link rel="modulepreload" crossorigin href="/assets/agentRuntime-BHVw9orA.js">
217
+ <link rel="modulepreload" crossorigin href="/assets/agentRuntime-C_yLPKDy.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">
@@ -231,9 +231,9 @@
231
231
  <link rel="modulepreload" crossorigin href="/assets/integrations-Dwl9KQp0.js">
232
232
  <link rel="modulepreload" crossorigin href="/assets/jira-FXr7u2lh.js">
233
233
  <link rel="modulepreload" crossorigin href="/assets/jobs-De3EqaO0.js">
234
- <link rel="modulepreload" crossorigin href="/assets/loops--KWr0X-w.js">
234
+ <link rel="modulepreload" crossorigin href="/assets/loops-WyWDkhok.js">
235
235
  <link rel="modulepreload" crossorigin href="/assets/mcp-CUAN6ORK.js">
236
- <link rel="modulepreload" crossorigin href="/assets/narration-n3hOA7LY.js">
236
+ <link rel="modulepreload" crossorigin href="/assets/narration-DbHnz0qV.js">
237
237
  <link rel="modulepreload" crossorigin href="/assets/nav--u7iPYlS.js">
238
238
  <link rel="modulepreload" crossorigin href="/assets/packet-B0H15elv.js">
239
239
  <link rel="modulepreload" crossorigin href="/assets/settings-Dbyd6gmj.js">
@@ -243,15 +243,15 @@
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-Bjbbp_KS.js">
246
- <link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-BZxKaVNT.js">
247
- <link rel="modulepreload" crossorigin href="/assets/Trans-wmTUDVyC.js">
248
- <link rel="modulepreload" crossorigin href="/assets/useDesktop-DpiS8Uov.js">
246
+ <link rel="modulepreload" crossorigin href="/assets/useSharedWebSocket-DaWuiO0S.js">
247
+ <link rel="modulepreload" crossorigin href="/assets/Trans-D5eOjfvG.js">
248
+ <link rel="modulepreload" crossorigin href="/assets/useDesktop-DKNbOF0u.js">
249
249
  <link rel="modulepreload" crossorigin href="/assets/provider-capabilities-lePfeT_Y.js">
250
250
  <link rel="modulepreload" crossorigin href="/assets/save-OsUqVn99.js">
251
- <link rel="modulepreload" crossorigin href="/assets/spending-Oov7p52A.js">
251
+ <link rel="modulepreload" crossorigin href="/assets/spending-DQDNtM8q.js">
252
252
  <link rel="modulepreload" crossorigin href="/assets/sparkles-DrY8hb0X.js">
253
253
  <link rel="modulepreload" crossorigin href="/assets/x-CBMI9ejn.js">
254
- <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-BD2bsRBe.js">
254
+ <link rel="modulepreload" crossorigin href="/assets/MinimizedChatsContext-CroAs6Kh.js">
255
255
  <link rel="modulepreload" crossorigin href="/assets/zap-CUOlvtT1.js">
256
256
  <link rel="modulepreload" crossorigin href="/assets/file-text-BcLCfcWZ.js">
257
257
  <link rel="modulepreload" crossorigin href="/assets/circle-x-CMYsN4ZH.js">
@@ -266,20 +266,20 @@
266
266
  <link rel="modulepreload" crossorigin href="/assets/search-Cq_gE3Ik.js">
267
267
  <link rel="modulepreload" crossorigin href="/assets/trash-2-BOU3WdZZ.js">
268
268
  <link rel="modulepreload" crossorigin href="/assets/triangle-alert-H9T4jAmh.js">
269
- <link rel="modulepreload" crossorigin href="/assets/jira-api-CLrIvXFm.js">
269
+ <link rel="modulepreload" crossorigin href="/assets/jira-api-Bp0zXwjg.js">
270
270
  <link rel="modulepreload" crossorigin href="/assets/clsx-CjueKrWZ.js">
271
271
  <link rel="modulepreload" crossorigin href="/assets/utils-BC4DHlz2.js">
272
272
  <link rel="modulepreload" crossorigin href="/assets/dialog-CMgps2MY.js">
273
273
  <link rel="modulepreload" crossorigin href="/assets/lib-CGdyxIWg.js">
274
- <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-dpIo42JZ.js">
275
- <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-Dax4zqPF.js">
274
+ <link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-9JiGlFrx.js">
275
+ <link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-DZ3VWsHq.js">
276
276
  <link rel="modulepreload" crossorigin href="/assets/button-D1xExSTF.js">
277
- <link rel="modulepreload" crossorigin href="/assets/project-repositories-BkyT8AjU.js">
278
- <link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-D-jb4bI_.js">
279
- <link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-BHmtj1As.js">
277
+ <link rel="modulepreload" crossorigin href="/assets/project-repositories-CadLZOnr.js">
278
+ <link rel="modulepreload" crossorigin href="/assets/RepositoryScopeSelector-CeCr6Jz-.js">
279
+ <link rel="modulepreload" crossorigin href="/assets/TicketDetailModalContext-YBJX3RmP.js">
280
280
  <link rel="modulepreload" crossorigin href="/assets/core-B3mKBIh5.js">
281
- <link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-NNorcGzT.js">
282
- <link rel="modulepreload" crossorigin href="/assets/useRuntimeRuns-DBm1mul0.js">
281
+ <link rel="modulepreload" crossorigin href="/assets/RepositoryDeliveries-DJCm1pZr.js">
282
+ <link rel="modulepreload" crossorigin href="/assets/useRuntimeRuns-D3YMw9qx.js">
283
283
  <link rel="modulepreload" crossorigin href="/assets/event-Bt7f3wXe.js">
284
284
  <link rel="stylesheet" crossorigin href="/assets/index-Bfunvi6w.css">
285
285
  </head>
@@ -74,6 +74,8 @@ Project settings apply to a single project. Open them from the project's **Setti
74
74
  | **Freestyle pre-prompt** | Text prepended to Freestyle-mode launches (Claude and Kimi advertise this autonomous rail mode). |
75
75
  | **Budget** | Per-project daily spend cap (with queue auto-pause) and a per-job cost alert threshold. |
76
76
  | **Terminal Settings** | Per-project overrides for the terminal panel defaults (project override → app default → built-in). |
77
+ | **Verification commands** | Repository checks Core runs after the developer finishes; failures go back to the developer (or the fixer). Stored under `verification` in `.specrails/agent-runtime.json`; an empty list lets the architect propose the project's own checks. |
78
+ | **Setup commands** | Idempotent commands Core runs inside the repository checkout before every verification plan, for example `npx playwright install chromium`. Same entry shape as `verification` (`repositoryId`, `command`, `args`, optional `cwd`, `env`, `timeoutMs`, `key`, `label`), stored under `setup`, no auto-detection. Credential-like `env` keys are refused and `key` values must be unique across `setup` and `verification`. A failing setup command ends the run as blocked by the host environment. The list reaches Core only when the installed Core advertises the `setupCommands` capability; older cores receive the configuration without it and the run log records a one-line note. |
77
79
 
78
80
  > The project's **provider(s)** — **Claude, Codex, Gemini, Kimi, or any
79
81
  > compatible combination** — are chosen when you add the project and are
@@ -13,15 +13,15 @@ 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.2.1 in `desktop-release.yml` and
16
+ The release bundle pins Core 6.4.0 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
20
20
  longer requires `batch-implement` but stays compatible with Cores that still ship it). Update both pins together and check compatibility against
21
21
  the staged published package; retained runs still use their original runtime.
22
22
 
23
- The paired CI checkout uses the immutable Core 6.2.1 release commit
24
- `ae52a5ff0ff9dce024008adf16115e7f04d1ef35`. Bundle smoke checks use Node 22.22.3,
23
+ The paired CI checkout uses the immutable Core 6.4.0 release commit
24
+ `aa00781e962a30c22bbcdecc632bc2abcf3c97d2`. Bundle smoke checks use Node 22.22.3,
25
25
  matching Core's minimum supported Node version and the Desktop release runtime.
26
26
 
27
27
  Desktop updates retain the complete npm installation, including dependencies,
@@ -119,6 +119,8 @@ All routes are under `/api/projects/:projectId`:
119
119
  | `POST /agent-runtime/runs/:runId/resume` | Accept `{}`, `{ "approve": ["archive"] }`, `{ "recover": ["developer"] }`, explicit `invalidate` phase IDs, or `{ "answer": "…" }` for a pending question (required while one is open) |
120
120
  | `POST /agent-runtime/runs/:runId/cancel` | Cancel a continuation owned by this controller |
121
121
 
122
+ Host blockers (Core `hostBlockers: 1` / `setupCommands: 1`): the Implement recipe opts its `verify` nodes into host-owned environment repair and the `blocked` outcome. Core emits `[environment] <command> (<repository>)` and `Environment: installed …` lines on the `verification-output` channel, so they reach the readable job log through the same projection as checks. A precondition Core cannot satisfy (no network for a browser download, registry credentials, a failing `setup` command, a missing toolchain) ends the run at `host-blocked` with `completion.blocker = { kind, reason, command, args, cwd, requiredAction, evidenceId? }`; a fixer-declared `blocker` ends it at `fixer-blocked`. The bridge maps that completion (`ok: false`) to `runtimeStatus: 'blocked'`, the run row shows the blocked badge and the loop log renders "Blocked by the host environment" with the localized kind, the failing command and cwd, the required action and a copy button. Older Core packages receive the pre-blocker definition and no `setup` list.
123
+
122
124
  Resume responds `202` after admission and continues asynchronously. Legacy phase IDs are `architect`, `developer`, `fixer`, `verify`, `reviewer` and `archive`. For engine v2, use `POST /loop-runs/:runId/resume` instead. `recover` contains exact attempt IDs from `GET /loop-runs/:runId/recovery`; approval and answer controls use pending interrupt IDs. Node paths are display context, not recovery identities. The saved-execution panel and job header expose explicit per-attempt selection. Core's lease and Desktop's execution/mount claim jointly prevent competing writers. Resident human pauses keep their original settlement callback; restart recovery reconnects to the frozen delivery allocation and terminal outbox.
123
125
 
124
126
  Desktop launches `node <Core>/dist/agent-runtime/cli.js` with structured argv (`--approve`, `--recover`, `--invalidate`, `--answer <text>` on resume) and consumes JSON lines for phase events (`workflow-event`), agent output (`agent-event`), verification output, trace spans (`span`: `{ traceId, spanId, name, stepId, attempt, visit, startedAt, endedAt, status, usage?, error? }`) and the terminal `runtime-result`. Spans are stored verbatim as job events for diagnostics and are not narrated in the log. `runtime status --compact` returns `state.traceId`, `pendingApproval`, `pendingQuestion` and per-step `{ status, visits }`. Desktop probes `runtime api` once per executable package content digest; `runtime validate --stdin` runs on every save. The job log shows phase transitions (`[runtime] step_started: developer`), live tool activity per role (`[developer] Read src/app.ts`, `[developer] Bash npm test`) and Core's own phase notes (architecture written, verification passed, review approved or corrections requested); the final JSON of architect and reviewer is not echoed. The narrated view (Relato) derives its milestones from the same events: each runtime phase, the tools used, correction loops and a stopped workflow with Core's structural reason. Accounting uses the invocation's new attempts, so resuming a completed phase does not bill its cumulative history twice. Status queries use `--compact`, are read-only and do not invoke providers; accumulated logs stay in Core's checkpoint instead of overflowing the process status response.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.60.0",
3
+ "version": "2.61.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -166,6 +166,18 @@ async function runAgentRuntimeInvocation(options) {
166
166
  config.providers = (0, agent_runtime_settings_1.stripDesktopConnectionFields)(config.providers, (0, agent_runtime_settings_1.coreConnectionFieldGates)(runtime.api?.capabilities));
167
167
  if (runtime.api?.capabilities?.configurableGuardrails !== 1)
168
168
  delete config.guardrails;
169
+ // Setup commands are a `setupCommands` capability: an older Core rejects the unknown key, so strip it and say so once in the run log.
170
+ if (config.setup !== undefined && !(0, agent_runtime_settings_1.coreSupportsSetupCommands)(runtime.api?.capabilities)) {
171
+ if (config.setup.length) {
172
+ try {
173
+ options.onLine?.(`[runtime] setup commands ignored: the installed Core does not advertise setupCommands (${config.setup.length} configured)\n`);
174
+ }
175
+ catch { /* Advisory log observer. */ }
176
+ }
177
+ delete config.setup;
178
+ }
179
+ else if (config.setup)
180
+ config.setup = scopedHostChecks(config.setup, admittedContext.repositories);
169
181
  if (definitionEngine && (runtime.api?.capabilities?.engineV2 !== 1 || runtime.api?.capabilities?.workflowDefinitions !== 1))
170
182
  throw new Error('engine_unsupported: Update Core to run workflow definitions');
171
183
  const compilationConfig = structuredClone(config);
@@ -76,6 +76,8 @@ function resolveEffectiveRuntimeConfig(input, options) {
76
76
  }
77
77
  fillDefaultRoleModels(config);
78
78
  config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
79
+ if (config.setup)
80
+ config.setup = config.setup.filter(check => options.repositoryIds.includes(check.repositoryId));
79
81
  return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
80
82
  }
81
83
  /**
@@ -21,6 +21,10 @@ function withDefaultRoleModels(body) {
21
21
  return { ...body, agents: filled.agents, ...(filled.roles ? { roles: filled.roles } : {}), ...(filled.fixer ? { fixer: filled.fixer } : {}) };
22
22
  }
23
23
  const loop_role_engines_1 = require("../../loops/runtime/loop-role-engines");
24
+ /** The settings API always answers `setup` as an array; the file keeps it absent until the user saves one. */
25
+ function withSetupList(config) {
26
+ return config ? { ...config, setup: config.setup ?? [] } : config;
27
+ }
24
28
  function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
25
29
  /** Guardrail catalog of the loaded Core (configurable-guardrails): ids + phase; the UI owns labels. `supported:false` on older cores. */
26
30
  router.get('/:projectId/agent-runtime/guardrails', async (_req, res) => {
@@ -70,7 +74,7 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
70
74
  openRolesAvailable = capabilities?.openRoles === 1;
71
75
  }
72
76
  catch { /* Ordinary settings remain readable without capability support. */ }
73
- res.json({ configured, config, runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null, efficiencyAvailable, openRolesAvailable,
77
+ res.json({ configured, config: withSetupList(config), runtimeAvailable: (0, agent_runtime_loader_1.findCoreAgentRuntimeEntry)() !== null, efficiencyAvailable, openRolesAvailable,
74
78
  workflowRoleDefaults: openRolesAvailable && config ? (0, agent_runtime_effective_config_1.workflowRoleDefaults)(config) : {} });
75
79
  }
76
80
  catch (err) {
@@ -147,7 +151,7 @@ function registerAgentRuntimeSettingsRoutes({ router, ctx }) {
147
151
  }
148
152
  }
149
153
  const saved = (0, agent_runtime_settings_1.saveAgentRuntimeConfig)(ctx(req).project, config);
150
- res.json({ configured: true, config: saved, runtimeAvailable, efficiencyAvailable, openRolesAvailable });
154
+ res.json({ configured: true, config: withSetupList(saved), runtimeAvailable, efficiencyAvailable, openRolesAvailable });
151
155
  }
152
156
  catch (err) {
153
157
  const validation = err instanceof agent_runtime_settings_1.AgentRuntimeConfigError;
@@ -25,6 +25,7 @@ exports.loadRuntimeRolePrompts = loadRuntimeRolePrompts;
25
25
  exports.saveRuntimeRolePrompts = saveRuntimeRolePrompts;
26
26
  exports.validateRuntimeProviderOverride = validateRuntimeProviderOverride;
27
27
  exports.forCoreRuntime = forCoreRuntime;
28
+ exports.coreSupportsSetupCommands = coreSupportsSetupCommands;
28
29
  const node_fs_1 = __importDefault(require("node:fs"));
29
30
  const node_path_1 = __importDefault(require("node:path"));
30
31
  const node_os_1 = __importDefault(require("node:os"));
@@ -136,17 +137,18 @@ function validateLoopRuntimeSettings(input, providers = loadRuntimeProviders())
136
137
  const config = validateAgentRuntimeConfig({ ...input, enabled: true, providers: portableProviders, verification: [] });
137
138
  if (['architect', 'developer', 'reviewer', 'fixer'].some(role => !config.rolePrompts?.[role]?.trim()))
138
139
  throw new AgentRuntimeConfigError('Loop agents require an explicit definition for architect, developer, reviewer and fixer');
139
- const { enabled: _enabled, providers: _providers, verification: _checks, ...settings } = config;
140
+ const { enabled: _enabled, providers: _providers, verification: _checks, setup: _setup, ...settings } = config;
140
141
  return settings;
141
142
  }
142
143
  /** Read only repository checks from the old project file. Role migration is explicit. */
143
144
  function loadLoopRuntimeConfig(file, input, selection = { provider: 'claude' }) {
144
- let verification = [];
145
+ let verification = [], setup = undefined;
145
146
  try {
146
147
  const saved = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
147
148
  if (!saved || typeof saved !== 'object' || Array.isArray(saved))
148
149
  throw new AgentRuntimeConfigError('Saved runtime configuration must be an object');
149
150
  verification = saved.verification ?? [];
151
+ setup = saved.setup;
150
152
  }
151
153
  catch (error) {
152
154
  if (error.code !== 'ENOENT')
@@ -163,7 +165,7 @@ function loadLoopRuntimeConfig(file, input, selection = { provider: 'claude' })
163
165
  settings.fixer = inherit(settings.fixer);
164
166
  for (const [role, agent] of Object.entries(settings.roles ?? {}))
165
167
  settings.roles[role] = inherit(agent);
166
- return validateAgentRuntimeConfig({ ...settings, providers, enabled: true, verification });
168
+ return validateAgentRuntimeConfig({ ...settings, providers, enabled: true, verification, ...(setup !== undefined ? { setup } : {}) });
167
169
  }
168
170
  const CLI_PROVIDERS = ['claude', 'codex', 'gemini', 'kimi'];
169
171
  const ROLES = ['architect', 'developer', 'reviewer'];
@@ -260,10 +262,12 @@ function validateAgentRuntimeConfig(input) {
260
262
  if (!Number.isFinite(value) || (key !== 'maxCostUsd' && !Number.isSafeInteger(value)))
261
263
  throw new AgentRuntimeConfigError('Workflow limits must be finite, safe numbers');
262
264
  }
263
- const keys = config.verification.flatMap(command => command.key ? [command.key] : []);
265
+ // `setup` entries share the verification entry shape and rules (Core validates both with one validator; keys are unique across the two lists).
266
+ const hostCommands = [...config.verification, ...(config.setup ?? [])];
267
+ const keys = hostCommands.flatMap(command => command.key ? [command.key] : []);
264
268
  if (new Set(keys).size !== keys.length)
265
269
  throw new AgentRuntimeConfigError('Verification check keys must be unique');
266
- for (const command of config.verification) {
270
+ for (const command of hostCommands) {
267
271
  for (const value of [command.label, ...(command.policy?.inputs ?? []), ...(command.policy?.toolchainInputs ?? []), ...(command.policy?.resources ?? [])])
268
272
  if (value !== undefined && (!value.trim() || value.includes('\0')))
269
273
  throw new AgentRuntimeConfigError('Invalid verification label or policy input');
@@ -482,7 +486,7 @@ function validateRuntimeProviderOverride(value) {
482
486
  throw new AgentRuntimeConfigError('Invalid runtime provider override effort');
483
487
  return structuredClone(raw);
484
488
  }
485
- /** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`. */
489
+ /** Older Core runtimes reject unknown top-level keys: drop `guardrails` unless the loaded Core advertises `configurableGuardrails`, and `setup` unless it advertises `setupCommands`. */
486
490
  function forCoreRuntime(config, capabilities) {
487
491
  let out = config;
488
492
  if (capabilities?.openRoles !== 1 && out.roles !== undefined) {
@@ -495,6 +499,10 @@ function forCoreRuntime(config, capabilities) {
495
499
  const { guardrails: _dropped, ...rest } = out;
496
500
  out = rest;
497
501
  }
502
+ if (!coreSupportsSetupCommands(capabilities) && out.setup !== undefined) {
503
+ const { setup: _dropped, ...rest } = out;
504
+ out = rest;
505
+ }
498
506
  // The per-role thinking switch is known only to cores advertising `roleThinkingControl`.
499
507
  if (capabilities?.roleThinkingControl !== 1 && ([...Object.values(out.agents ?? {}), ...Object.values(out.roles ?? {})].some(agent => agent?.thinking !== undefined) || out.fixer?.thinking !== undefined)) {
500
508
  const strip = (agent) => { const { thinking: _t, ...rest } = agent; return rest; };
@@ -502,3 +510,7 @@ function forCoreRuntime(config, capabilities) {
502
510
  }
503
511
  return out;
504
512
  }
513
+ /** Core advertises `setupCommands: 1` when its verify piece runs `RuntimeConfig.setup` before each verification plan. */
514
+ function coreSupportsSetupCommands(capabilities) {
515
+ return (capabilities?.setupCommands ?? 0) >= 1;
516
+ }
@@ -4,9 +4,9 @@ exports.coreFactoryGraph = coreFactoryGraph;
4
4
  const loop_implement_recipe_1 = require("./loop-implement-recipe");
5
5
  const loop_agents_1 = require("./loop-agents");
6
6
  /** App-owned definition graphs. Core supplies schemas, execution and canonical versions. */
7
- function coreFactoryGraph(mode, independent = false, configurable = false) {
7
+ function coreFactoryGraph(mode, independent = false, configurable = false, capabilities) {
8
8
  if (mode === 'implement' && configurable)
9
- return (0, loop_implement_recipe_1.configurableImplementGraph)();
9
+ return (0, loop_implement_recipe_1.configurableImplementGraph)(capabilities);
10
10
  const nodes = [{ id: 'start', type: 'start', position: { x: 0, y: 0 } }];
11
11
  const edges = [];
12
12
  const config = { maxIterations: 12, maxTransitions: 120, timeoutMinutes: 0, aiStepTimeoutMinutes: 0,
@@ -41,6 +41,7 @@ function compileLoopToDefinition(graph, launch) {
41
41
  : {}),
42
42
  ...(typeof node.data?.exit === 'string' ? { exit: node.data.exit } : {}),
43
43
  ...(typeof node.data?.reason === 'string' ? { reason: node.data.reason } : {}),
44
+ ...(typeof node.data?.blockerFrom === 'string' ? { blockerFrom: node.data.blockerFrom } : {}),
44
45
  }
45
46
  : node.data.params;
46
47
  const promptRole = node.id === 'fix' ? 'fixer' : node.id === 'prepare' ? 'architect' : rawParams.access === 'read' ? 'reviewer' : 'developer';
@@ -94,7 +94,7 @@ const FACTORY_BY_ID = new Map([
94
94
  function factoryLoopsForCapabilities(capabilities) {
95
95
  if (capabilities?.engineV2 !== 1 || capabilities.workflowDefinitions !== 1)
96
96
  return exports.FACTORY_LOOPS;
97
- return exports.FACTORY_LOOPS.map(factory => ({ ...factory, graph: (0, loop_core_factory_1.coreFactoryGraph)(factory.id === 'factory:implement' ? 'implement' : factory.id === 'factory:freestyle' ? 'freestyle' : 'quick-sdd', capabilities.implementationSteps === 1, capabilities.workflowAgentSteps === 1) }));
97
+ return exports.FACTORY_LOOPS.map(factory => ({ ...factory, graph: (0, loop_core_factory_1.coreFactoryGraph)(factory.id === 'factory:implement' ? 'implement' : factory.id === 'factory:freestyle' ? 'freestyle' : 'quick-sdd', capabilities.implementationSteps === 1, capabilities.workflowAgentSteps === 1, capabilities) }));
98
98
  }
99
99
  function getFactoryLoop(id, capabilities) {
100
100
  const legacy = FACTORY_BY_ID.get(id);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CORE_NODE_KINDS = void 0;
4
+ exports.effectivePieceOutcomes = effectivePieceOutcomes;
4
5
  exports.isDefinitionReviewerPath = isDefinitionReviewerPath;
5
6
  exports.isDefinitionGraph = isDefinitionGraph;
6
7
  exports.assertDefinitionGraph = assertDefinitionGraph;
@@ -16,6 +17,12 @@ exports.interpolateSpec = interpolateSpec;
16
17
  exports.CORE_NODE_KINDS = ['prompt', 'role-turn', 'decider', 'condition', 'assign', 'verify', 'shell',
17
18
  'artifact-contract', 'openspec-validate', 'openspec-archive', 'approval', 'question', 'gate', 'map', 'join',
18
19
  'component', 'implementation', 'implementation-step', 'end'];
20
+ /** Mirror of Core's `getOutcomes(params)`: `verify` advertises `blocked` only when the node opts into host blockers. */
21
+ function effectivePieceOutcomes(descriptor, params) {
22
+ if (descriptor.kind === 'verify' && params?.hostBlockers !== true)
23
+ return descriptor.outcomes.filter(outcome => outcome !== 'blocked');
24
+ return descriptor.outcomes;
25
+ }
19
26
  function isDefinitionReviewerPath(graph, value) {
20
27
  if (typeof value !== 'string' || !value || value.length > 2048)
21
28
  return false;
@@ -201,7 +208,7 @@ function validateLoopGraph(graph, catalog) {
201
208
  const labels = new Set();
202
209
  for (const edge of out) {
203
210
  if (typeof edge.label !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(edge.label) || labels.has(edge.label) ||
204
- (descriptor && !descriptor.outcomes.includes(edge.label))) {
211
+ (descriptor && !effectivePieceOutcomes(descriptor, node.data?.params).includes(edge.label))) {
205
212
  errors.push({ code: 'INVALID_BRANCH', nodeId: node.id, edgeId: edge.id, message: 'Core outcome edges need unique labels advertised by the piece.' });
206
213
  }
207
214
  if (edge.label)
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supportsHostBlockers = exports.HOST_BLOCKER_KINDS = void 0;
3
4
  exports.configurableImplementGraph = configurableImplementGraph;
4
5
  const agent_runtime_settings_1 = require("../../agent-runtime/runtime/agent-runtime-settings");
5
6
  const loop_agents_1 = require("./loop-agents");
@@ -9,8 +10,17 @@ const strings = { type: 'array', items: string, maxItems: 100 };
9
10
  const score = { type: 'number', minimum: 0, maximum: 100 };
10
11
  const aspects = { type_correctness: 60, pattern_adherence: 60, test_coverage: 60, security: 75, architectural_alignment: 60 };
11
12
  const reviewPolicy = 'approved == true; issues must be empty; score >= 70; ' + Object.entries(aspects).map(([aspect, threshold]) => `${aspect} >= ${threshold}`).join('; ');
12
- /** The recipe, roles, schemas and decisions are Desktop data, never Core phases. */
13
- function configurableImplementGraph() {
13
+ exports.HOST_BLOCKER_KINDS = ['network', 'credential', 'environment-variable', 'toolchain', 'setup', 'environment', 'scope'];
14
+ /** Fixer/developer structured `blocker`: the host ends the run on it instead of counting a correction round. */
15
+ const blocker = objectSchema({ kind: { enum: [...exports.HOST_BLOCKER_KINDS] }, command: string, cwd: string, evidence: string, requiredAction: string }, ['kind', 'requiredAction']);
16
+ /** Host-owned environment repair and structured blockers need the paired Core generation. */
17
+ const supportsHostBlockers = (capabilities) => capabilities?.hostBlockers === 1;
18
+ exports.supportsHostBlockers = supportsHostBlockers;
19
+ /** The recipe, roles, schemas and decisions are Desktop data, never Core phases.
20
+ * `capabilities` are the installed Core's advertised runtime capabilities: without `hostBlockers`
21
+ * the verify nodes compile exactly as before (no `setup`, no `blocked` edge, no `blockerFrom`). */
22
+ function configurableImplementGraph(capabilities) {
23
+ const hostBlockers = (0, exports.supportsHostBlockers)(capabilities);
14
24
  const agents = (0, loop_agents_1.defaultLoopAgents)();
15
25
  agents.roles = {
16
26
  plan: { ...agents.agents.architect, access: 'read', artifacts: 'all', openspecSkill: 'openspec-ff-change', prompt: (0, agent_runtime_settings_1.inheritedRolePrompt)(agents.rolePrompts, 'architect') },
@@ -24,7 +34,11 @@ function configurableImplementGraph() {
24
34
  for (const [outcome, target] of Object.entries(outcomes))
25
35
  edges.push({ id: `e-${id}-${outcome}`, source: id, target, label: outcome });
26
36
  };
27
- const turn = (id, label, roleId, prompt, schema, next) => node(id, label, 'role-turn', { roleId, prompt, structuredOutput: schema, sessionContinuity: 'none' }, { next, invalid: 'failed', failed: 'failed' });
37
+ const turn = (id, label, roleId, prompt, schema, next, extra = {}) => node(id, label, 'role-turn', { roleId, prompt, structuredOutput: schema, sessionContinuity: 'none', ...extra }, { next, invalid: 'failed', failed: 'failed' });
38
+ const verifyParams = { commands: 'configured', additionalCommandsFrom: 'architect', ...(hostBlockers ? { setup: 'configured', hostBlockers: true } : {}) };
39
+ const verifyEnds = (passed, failed, blockedEnd) => ({ pass: passed, fail: failed, failed: 'failed', ...(hostBlockers ? { blocked: blockedEnd } : {}) });
40
+ // Same Core generation as host blockers: older role-turn schemas reject unknown params.
41
+ const writes = hostBlockers ? { verificationProposalsFrom: 'architect' } : {};
28
42
  const change = '{{run.changeId}}';
29
43
  turn('architect', 'Architect', 'plan', `Plan the frozen ticket requirements for OpenSpec change ${change}. Use the bound official fast-forward skill. Author complete proposal, design, delta specs and actionable tasks. Propose existing repository verification commands for repositories with no configured checks. Return confidence, question (empty when none), and verification [{repositoryId,command,args,cwd?}]. When revisiting low confidence, investigate the missing repository evidence before asking again. Context and human answers: {{history.text}}`, objectSchema({ confidence: { enum: ['high', 'medium', 'low'] }, question: string, verification: { type: 'array', maxItems: 20, items: objectSchema({ repositoryId: string, command: string, args: strings, cwd: string }, ['repositoryId', 'command', 'args']) } }), 'confidence');
30
44
  node('confidence', 'Planning confidence', 'condition', { expr: '$outputs.architect.structured.confidence != "low"' }, { true: 'validate', false: 'investigate' });
@@ -35,10 +49,10 @@ function configurableImplementGraph() {
35
49
  node('validate', 'Validate OpenSpec', 'openspec-validate', { change }, { pass: 'freeze', fail: 'architect', failed: 'failed' });
36
50
  node('freeze', 'Freeze approved artifacts', 'artifact-contract', { change, contractId: 'plan', action: 'freeze' }, { pass: 'initialize-corrections', fail: 'architect', failed: 'failed' });
37
51
  node('initialize-corrections', 'Initialize automatic correction budget', 'assign', { set: { correctionAttempts: 0, correctionLimit: 3 } }, { next: 'developer', failed: 'failed' });
38
- const implementation = objectSchema({ summary: string, incomplete: strings });
39
- 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. Prior execution evidence and human answers: {{history.text}}`, implementation, 'tasks');
52
+ const implementation = objectSchema({ summary: string, incomplete: strings, blocker }, ['summary', 'incomplete']);
53
+ 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. Prior execution evidence and human answers: {{history.text}}`, implementation, 'tasks', writes);
40
54
  node('tasks', 'Check artifacts and completed tasks', 'artifact-contract', { change, contractId: 'plan', action: 'check', requireCompletedTasks: true }, { pass: 'verify', fail: 'developer', failed: 'failed' });
41
- node('verify', 'Verify candidate', 'verify', { commands: 'configured', additionalCommandsFrom: 'architect' }, { pass: 'reviewer', fail: 'correction-context', failed: 'failed' });
55
+ node('verify', 'Verify candidate', 'verify', verifyParams, verifyEnds('reviewer', 'correction-context', 'host-blocked'));
42
56
  turn('reviewer', 'Reviewer', 'assess', `Review change ${change} read-only against all approved artifacts and frozen ticket acceptance criteria. Inspect real code and the host verification evidence: {{outputs.verify}}. Return approved, summary, issues, score (0-100), and aspects. Acceptance policy: ${reviewPolicy}. For any rejection or below-threshold score, explain the concrete unmet requirement and actionable repair in summary and issues; passing commands do not prove every acceptance obligation. Prior execution: {{history.text}}`, objectSchema({ approved: { type: 'boolean' }, summary: string, issues: strings, score, aspects: objectSchema(Object.fromEntries(Object.keys(aspects).map(aspect => [aspect, score]))) }), 'review-policy');
43
57
  node('review-policy', 'Review acceptance policy', 'condition', { expr: ['$outputs.reviewer.structured.approved == true', '$outputs.reviewer.structured.issues.length == 0', '$outputs.reviewer.structured.score >= 70', ...Object.entries(aspects).map(([aspect, threshold]) => `$outputs.reviewer.structured.aspects.${aspect} >= ${threshold}`)].join(' && ') }, { true: 'approve', false: 'correction-context' });
44
58
  node('correction-context', 'Select current review findings', 'condition', { expr: '$outputs.verify.valid == true && exists($outputs.reviewer.candidateHash) && $outputs.reviewer.candidateHash == $outputs.verify.candidateHash' }, { true: 'capture-review', false: 'clear-review' });
@@ -46,13 +60,18 @@ function configurableImplementGraph() {
46
60
  node('clear-review', 'Clear outdated review findings', 'assign', { set: { latestReview: 'No reviewer findings for the current verified candidate. Diagnose the current host verification failure first.' } }, { next: 'correction-budget', failed: 'failed' });
47
61
  node('correction-budget', 'Check automatic correction budget', 'condition', { expr: '$vars.correctionAttempts < $vars.correctionLimit' }, { true: 'begin-correction', false: 'correction-exhausted' });
48
62
  node('begin-correction', 'Count automatic correction attempt', 'assign', { increment: { correctionAttempts: 1 } }, { next: 'fixer', failed: 'failed' });
49
- turn('fixer', 'Fixer', 'correct', `Correct the reported implementation, test or review failure for change ${change} using the bound apply skill and preserving approved artifacts. Read failureSummary, command, cwd, exitCode and evidenceId first; use read_verification_evidence for truncated evidence. If Host verification is valid, repair the concrete rejected review obligation or below-threshold aspect instead of only rerunning green commands. Acceptance policy: ${reviewPolicy}. Reproduce the exact focused failure, inspect the assertion and tested code, then apply the smallest repair that preserves the required behavior. An unchanged filename does not prove an unrelated failure. A mandatory test inside the admitted workspace may receive a proven formatting/compatibility repair while retaining every required guard. Report diagnosis, focused commands with original exit codes, repair and unresolved failures in summary and incomplete. If the cause cannot be repaired within scope, explain the evidence and required action; any unchanged correction stops this run even when host checks pass. Do not make unrelated edits merely to change the candidate hash. Host verification: {{outputs.verify}}. Latest review findings for this candidate: {{run.latestReview}}. Prior execution: {{history.text}}`, implementation, 'correction-progress');
63
+ turn('fixer', 'Fixer', 'correct', `Correct the reported implementation, test or review failure for change ${change} using the bound apply skill and preserving approved artifacts. Read failureSummary, command, cwd, exitCode and evidenceId first; use read_verification_evidence for truncated evidence. If Host verification is valid, repair the concrete rejected review obligation or below-threshold aspect instead of only rerunning green commands. Acceptance policy: ${reviewPolicy}. Reproduce the exact focused failure, inspect the assertion and tested code, then apply the smallest repair that preserves the required behavior. An unchanged filename does not prove an unrelated failure. A mandatory test inside the admitted workspace may receive a proven formatting/compatibility repair while retaining every required guard. Report diagnosis, focused commands with original exit codes, repair and unresolved failures in summary and incomplete. If the cause is a host precondition or environment blocker outside this change, make no edits and return the structured blocker (kind, command, cwd, evidence, requiredAction): the host ends the run with your required action. Any other unchanged correction stops this run even when host checks pass. Do not make unrelated edits merely to change the candidate hash. Host verification: {{outputs.verify}}. Latest review findings for this candidate: {{run.latestReview}}. Prior execution: {{history.text}}`, implementation, 'correction-blocker', writes);
64
+ node('correction-blocker', 'Fixer reported a host blocker', 'condition', { expr: 'exists($outputs.fixer.structured.blocker) && $outputs.fixer.structured.blocker != null' }, { true: 'fixer-blocked', false: 'correction-progress' });
50
65
  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' });
51
66
  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' });
52
67
  node('approval-policy', 'Approval decision', 'condition', { expr: '$outputs.approve.response.approved == true' }, { true: 'archive', false: 'failed' });
53
68
  node('archive', 'Archive OpenSpec', 'openspec-archive', { change, allowArchived: true, requiresVerified: true, reviewedCandidate: '{{outputs.reviewer.candidateHash}}', approvedCandidate: '{{outputs.approve.candidateHash}}' }, { next: 'verify-archive', failed: 'verify' });
54
- node('verify-archive', 'Verify archived candidate', 'verify', { commands: 'configured', additionalCommandsFrom: 'architect' }, { pass: 'done', fail: 'failed', failed: 'failed' });
55
- 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}}` } });
69
+ node('verify-archive', 'Verify archived candidate', 'verify', verifyParams, verifyEnds('done', 'failed', 'host-blocked-archive'));
70
+ 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: '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' } : {}) } });
71
+ if (hostBlockers) {
72
+ 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}}` } });
73
+ nodes.push(hostBlocked('host-blocked', 'verify'), hostBlocked('host-blocked-archive', 'verify-archive'));
74
+ }
56
75
  edges.unshift({ id: 'e-start', source: 'start', target: 'architect' });
57
76
  return { nodes, edges, config: { maxIterations: 12, maxTransitions: 200, timeoutMinutes: 0, aiStepTimeoutMinutes: 0, change: 'new', journal: 'ledger-only', ticketScope: 'all', agents, reviewerStepId: 'reviewer' } };
58
77
  }
@@ -341,10 +341,10 @@ const CORE_STARTER_SHAPES = {
341
341
  };
342
342
  /** Core definition graph for one of the eight starters. Deterministic: same id ⇒
343
343
  * structurally identical graph (positions included). */
344
- function coreStarterGraph(id, independent = false, configurable = false) {
344
+ function coreStarterGraph(id, independent = false, configurable = false, capabilities) {
345
345
  const shape = CORE_STARTER_SHAPES[id];
346
346
  if (shape === 'ship' && configurable)
347
- return (0, loop_core_factory_1.coreFactoryGraph)('implement', true, true);
347
+ return (0, loop_core_factory_1.coreFactoryGraph)('implement', true, true, capabilities);
348
348
  const { prompt, goal, maxIterations } = STARTER_TEXT[id];
349
349
  const nodes = [{ id: 'start', type: 'start', position: { x: COL_X, y: 0 } }];
350
350
  const edges = [];
@@ -438,7 +438,7 @@ function loopTemplatesForCapabilities(capabilities) {
438
438
  ...template,
439
439
  description: CORE_STARTER_DESCRIPTIONS[template.id],
440
440
  ...(readOnly ? { readOnly: true } : {}),
441
- graph: coreStarterGraph(template.id, capabilities?.implementationSteps === 1, capabilities?.workflowAgentSteps === 1),
441
+ graph: coreStarterGraph(template.id, capabilities?.implementationSteps === 1, capabilities?.workflowAgentSteps === 1, capabilities),
442
442
  };
443
443
  });
444
444
  }
@@ -134,13 +134,8 @@
134
134
  "maxAttempts": { "$ref": "#/definitions/positiveInteger" }, "maxTokens": { "$ref": "#/definitions/positiveInteger" },
135
135
  "maxCostUsd": { "type": "number", "exclusiveMinimum": 0 }, "timeoutMs": { "$ref": "#/definitions/positiveInteger" }
136
136
  } },
137
- "verification": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["repositoryId", "command", "args"], "properties": {
138
- "key": { "$ref": "#/definitions/id" }, "label": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "\\S" }, "policy": { "$ref": "#/definitions/checkPolicy" },
139
- "repositoryId": { "$ref": "#/definitions/id" }, "command": { "type": "string", "minLength": 1 },
140
- "args": { "type": "array", "items": { "type": "string" } }, "cwd": { "type": "string", "minLength": 1 },
141
- "env": { "type": "object", "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "additionalProperties": { "type": "string" } },
142
- "timeoutMs": { "$ref": "#/definitions/positiveInteger" }
143
- } } },
137
+ "verification": { "type": "array", "items": { "$ref": "#/definitions/checkCommand" } },
138
+ "setup": { "type": "array", "description": "Idempotent commands the verify piece runs sequentially in the admitted workspace before each verification plan; a failing setup command blocks the run instead of starting a correction round.", "items": { "$ref": "#/definitions/checkCommand" } },
144
139
  "approvalBeforeArchive": { "type": "boolean" },
145
140
  "review": { "type": "object", "additionalProperties": false, "description": "Review gate thresholds; unset fields keep Core's defaults (score 70, security 75, other aspects 60).", "properties": {
146
141
  "minScore": { "$ref": "#/definitions/score" },
@@ -154,6 +149,13 @@
154
149
  } }
155
150
  },
156
151
  "definitions": {
152
+ "checkCommand": { "type": "object", "additionalProperties": false, "required": ["repositoryId", "command", "args"], "properties": {
153
+ "key": { "$ref": "#/definitions/id" }, "label": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "\\S" }, "policy": { "$ref": "#/definitions/checkPolicy" },
154
+ "repositoryId": { "$ref": "#/definitions/id" }, "command": { "type": "string", "minLength": 1 },
155
+ "args": { "type": "array", "items": { "type": "string" } }, "cwd": { "type": "string", "minLength": 1 },
156
+ "env": { "type": "object", "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "additionalProperties": { "type": "string" } },
157
+ "timeoutMs": { "$ref": "#/definitions/positiveInteger" }
158
+ } },
157
159
  "effort": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,31}$" },
158
160
  "inputList": { "type": "array", "maxItems": 256, "items": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "\\S" } },
159
161
  "checkPolicy": { "type": "object", "additionalProperties": false, "properties": {