specrails-desktop 2.57.0 → 2.58.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 (336) hide show
  1. package/README.md +1 -1
  2. package/cli/dist/args.js +10 -4
  3. package/cli/dist/help.js +1 -1
  4. package/client/dist/assets/{ActivityFeedPage-DdMNmVBk.js → ActivityFeedPage-BQGppIR0.js} +1 -1
  5. package/client/dist/assets/{AgentBrowserCapture-CeerUf--.js → AgentBrowserCapture-PI-cxyxi.js} +1 -1
  6. package/client/dist/assets/{AgentModeAnalyticsPane-BjGb-8rh.js → AgentModeAnalyticsPane-mQsOqdXx.js} +2 -2
  7. package/client/dist/assets/{AgentModeCodePane-8XRkQEo_.js → AgentModeCodePane-BL-vpmlY.js} +2 -2
  8. package/client/dist/assets/{AgentModeJobsPane-BN3dgJOA.js → AgentModeJobsPane-DR_06oqH.js} +1 -1
  9. package/client/dist/assets/AgentsPage-BKIKjm_1.js +118 -0
  10. package/client/dist/assets/{AnalyticsPage-CyfhYiBn.js → AnalyticsPage-069npRVo.js} +1 -1
  11. package/client/dist/assets/{CodePage-C2Nf-Q5W.js → CodePage-Alitorrv.js} +2 -2
  12. package/client/dist/assets/DesktopAnalyticsPage-qtWAPM47.js +1 -0
  13. package/client/dist/assets/{DocsDialog-Br3bczCp.js → DocsDialog-DwL6rgcT.js} +1 -1
  14. package/client/dist/assets/{DocsPage-BcDr2cBm.js → DocsPage-BA1hiJZ-.js} +1 -1
  15. package/client/dist/assets/{ExportDropdown-BVGKEgkd.js → ExportDropdown-BcMpvV89.js} +1 -1
  16. package/client/dist/assets/InteractiveJobComposer-B3H1YHr6.js +19 -0
  17. package/client/dist/assets/JobDetailModal-XZQ9eAAy.js +1 -0
  18. package/client/dist/assets/{JobDetailPage-vBbNeRuz.js → JobDetailPage-B-SIaq_q.js} +1 -1
  19. package/client/dist/assets/{JobsPage-CJCtPhIz.js → JobsPage-iQXAv2mx.js} +1 -1
  20. package/client/dist/assets/LoopBuilderPage-B_gbFAHn.js +1 -0
  21. package/client/dist/assets/LoopPreviewModal-je5pSCf-.js +1 -0
  22. package/client/dist/assets/LoopsPage-BZwcY5cu.js +2 -0
  23. package/client/dist/assets/{MinimizedChatsContext-D3QzgV0B.js → MinimizedChatsContext-CNx_0bZu.js} +1 -1
  24. package/client/dist/assets/{PluginsPage-DOpTV75p.js → PluginsPage-DFocnoHX.js} +2 -2
  25. package/client/dist/assets/{ProjectSettingsDialog-Dkwrn0z1.js → ProjectSettingsDialog-Bh3S6EBB.js} +1 -1
  26. package/client/dist/assets/{RepositoryDeliveries-Du2UCf1a.js → RepositoryDeliveries-BiwJuS8r.js} +1 -1
  27. package/client/dist/assets/{RepositoryScopeSelector-hPGZNRBm.js → RepositoryScopeSelector-C2mvWBQ6.js} +1 -1
  28. package/client/dist/assets/{ReviewPacketPage-DcOq-UTA.js → ReviewPacketPage-CrAf-WDy.js} +2 -2
  29. package/client/dist/assets/TemplatePreviewModal-C7DordpN.js +1 -0
  30. package/client/dist/assets/{TicketDetailModalContext-DAK-NoK6.js → TicketDetailModalContext-B-DySpKL.js} +1 -1
  31. package/client/dist/assets/{Trans-DEbgkuAY.js → Trans-D3AfDylf.js} +1 -1
  32. package/client/dist/assets/agentRuntime-BeUb3g9g.js +1 -0
  33. package/client/dist/assets/agentRuntime-Bg9tbmqm.js +1 -0
  34. package/client/dist/assets/agentRuntime-D-GJVGkj.js +1 -0
  35. package/client/dist/assets/agentRuntime-D2__CVI_.js +1 -0
  36. package/client/dist/assets/agentRuntime-D35dGVOP.js +1 -0
  37. package/client/dist/assets/agentRuntime-DGz_IqKW.js +1 -0
  38. package/client/dist/assets/agentRuntime-O1liDt6G.js +1 -0
  39. package/client/dist/assets/agentRuntime-hInH9Fzj.js +1 -0
  40. package/client/dist/assets/agentstudio-B2CTtQgZ.js +1 -0
  41. package/client/dist/assets/agentstudio-BWAKeGgB.js +1 -0
  42. package/client/dist/assets/{agentstudio-DR7doNBV.js → agentstudio-Cja8Sjcp.js} +1 -1
  43. package/client/dist/assets/agentstudio-DtfPcr99.js +1 -0
  44. package/client/dist/assets/agentstudio-QdAJIDef.js +1 -0
  45. package/client/dist/assets/agentstudio-X70BKf6u.js +1 -0
  46. package/client/dist/assets/agentstudio-fLXpRGIi.js +1 -0
  47. package/client/dist/assets/agentstudio-qXKqVnWS.js +1 -0
  48. package/client/dist/assets/{analytics-jzCkOec3.js → analytics-B1DIQd4m.js} +1 -1
  49. package/client/dist/assets/{analytics-CoEzWPx4.js → analytics-BAIZ9vE_.js} +1 -1
  50. package/client/dist/assets/{analytics-DjLlE7jS.js → analytics-BTqgmgBd.js} +1 -1
  51. package/client/dist/assets/analytics-BWynXPEm.js +1 -0
  52. package/client/dist/assets/{analytics-nZIjjtCc.js → analytics-DGcUsQ44.js} +1 -1
  53. package/client/dist/assets/{analytics-BNgZ1Nj7.js → analytics-GnSnpJ4W.js} +1 -1
  54. package/client/dist/assets/{analytics-DFh5lSOl.js → analytics-TWgYaoO0.js} +1 -1
  55. package/client/dist/assets/{analytics-DZp-DS2U.js → analytics-kJy6Tuko.js} +1 -1
  56. package/client/dist/assets/builder-C0FyvM83.js +1 -0
  57. package/client/dist/assets/builder-C1neIYGR.js +1 -0
  58. package/client/dist/assets/builder-CvnFm0im.js +1 -0
  59. package/client/dist/assets/builder-CzW5ovyI.js +1 -0
  60. package/client/dist/assets/builder-D06L7iPy.js +1 -0
  61. package/client/dist/assets/builder-DGETbtZj.js +1 -0
  62. package/client/dist/assets/builder-DXEJUlC0.js +1 -0
  63. package/client/dist/assets/builder-DyQperXU.js +1 -0
  64. package/client/dist/assets/common-BCK7c9Pv.js +1 -0
  65. package/client/dist/assets/common-C0BOdeNi.js +1 -0
  66. package/client/dist/assets/common-C4Yvvhp6.js +1 -0
  67. package/client/dist/assets/{common-BrlwEnL7.js → common-DmsclwjP.js} +1 -1
  68. package/client/dist/assets/common-DpLWwgI1.js +1 -0
  69. package/client/dist/assets/common-OCvl3c8t.js +1 -0
  70. package/client/dist/assets/common-c0aujEZC.js +1 -0
  71. package/client/dist/assets/common-od8WThlf.js +1 -0
  72. package/client/dist/assets/dashboard-B1ndRHuC.js +1 -0
  73. package/client/dist/assets/dashboard-BX7KXiqA.js +1 -0
  74. package/client/dist/assets/dashboard-B_BSNxcN.js +1 -0
  75. package/client/dist/assets/dashboard-CCeFN7-V.js +1 -0
  76. package/client/dist/assets/dashboard-CRl0WtFJ.js +1 -0
  77. package/client/dist/assets/dashboard-XGdQ5mjw.js +1 -0
  78. package/client/dist/assets/dashboard-_oltA6Jd.js +1 -0
  79. package/client/dist/assets/dashboard-bAuf5KVk.js +1 -0
  80. package/client/dist/assets/{formatDistanceToNow-Cc2u5FwQ.js → formatDistanceToNow-aBss1Nai.js} +1 -1
  81. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BHm2U-w1.js → getTimezoneOffsetInMilliseconds-DOGj1Mfz.js} +1 -1
  82. package/client/dist/assets/index-BS7KCWkV.css +2 -0
  83. package/client/dist/assets/{index-Cxb288Rj.js → index-C4Mv6tV6.js} +32 -32
  84. package/client/dist/assets/{jira-api-DrGxiD9p.js → jira-api-BYbEPlkr.js} +1 -1
  85. package/client/dist/assets/{jobs-DSp9DoD_.js → jobs-6_-r75yC.js} +1 -1
  86. package/client/dist/assets/{jobs-C6qmvUAi.js → jobs-B1yqyId5.js} +1 -1
  87. package/client/dist/assets/{jobs-DoFaEl9g.js → jobs-BFuV7ttH.js} +1 -1
  88. package/client/dist/assets/{jobs-DTQO08nJ.js → jobs-BKa8LuwA.js} +1 -1
  89. package/client/dist/assets/{jobs-DrF8Do8s.js → jobs-BKiK-ZWh.js} +1 -1
  90. package/client/dist/assets/{jobs-DLHdDkHc.js → jobs-DmKV7b7d.js} +1 -1
  91. package/client/dist/assets/{jobs-C9j2PuwW.js → jobs-KG4Sn12X.js} +1 -1
  92. package/client/dist/assets/{jobs-Dpw6AEOP.js → jobs-pPv4KWjb.js} +1 -1
  93. package/client/dist/assets/loop-layout-BF8xMiNv.js +7 -0
  94. package/client/dist/assets/loops-B4_D-nbg.js +1 -0
  95. package/client/dist/assets/loops-BJDY9FTh.js +1 -0
  96. package/client/dist/assets/loops-BO3V8244.js +1 -0
  97. package/client/dist/assets/loops-C-hZK8pf.js +1 -0
  98. package/client/dist/assets/loops-CDOWZ0ci.js +1 -0
  99. package/client/dist/assets/loops-Dv849ckF.js +1 -0
  100. package/client/dist/assets/loops-Q9LIgGkq.js +1 -0
  101. package/client/dist/assets/loops-eUnCfLjC.js +1 -0
  102. package/client/dist/assets/{project-repositories-9Wzs0coQ.js → project-repositories-CSnfzUpg.js} +1 -1
  103. package/client/dist/assets/settings-3o4Ntzi5.js +1 -0
  104. package/client/dist/assets/settings-B97jlkwD.js +1 -0
  105. package/client/dist/assets/settings-BTaT_bsS.js +1 -0
  106. package/client/dist/assets/settings-CT8w7WiC.js +1 -0
  107. package/client/dist/assets/settings-Cvr8_tvh.js +1 -0
  108. package/client/dist/assets/settings-Dn1qgAC8.js +1 -0
  109. package/client/dist/assets/settings-EShDdg4J.js +1 -0
  110. package/client/dist/assets/settings-tfLhQZRN.js +1 -0
  111. package/client/dist/assets/{setup-Dwzt5nO1.js → setup-CBMu3tqH.js} +1 -1
  112. package/client/dist/assets/{setup-B5FHuxvB.js → setup-CPh-e6J2.js} +1 -1
  113. package/client/dist/assets/{setup-DpqzaEEA.js → setup-CRKLp_ke.js} +1 -1
  114. package/client/dist/assets/{setup-CROyqD5N.js → setup-Cy302v4J.js} +1 -1
  115. package/client/dist/assets/{setup-C9Fy9PKQ.js → setup-D681vTPf.js} +1 -1
  116. package/client/dist/assets/{setup-BihzXadx.js → setup-DYK0VBkE.js} +1 -1
  117. package/client/dist/assets/{setup-BtC6Hg4K.js → setup-KAuiIiVs.js} +1 -1
  118. package/client/dist/assets/{setup-zLEDnG7N.js → setup-OGX-oCfI.js} +1 -1
  119. package/client/dist/assets/{spending-T5GM7wpn.js → spending-HBSk-6jv.js} +1 -1
  120. package/client/dist/assets/{useDesktop-QogiN0zR.js → useDesktop-0unHDBJJ.js} +1 -1
  121. package/client/dist/assets/useRuntimeRuns-BN1rhyEm.js +1 -0
  122. package/client/dist/assets/{useSharedWebSocket-Dz-HI6De.js → useSharedWebSocket-BZMrX4jr.js} +2 -2
  123. package/client/dist/index.html +25 -25
  124. package/docs/agent-live-steering.md +16 -0
  125. package/docs/cli.md +7 -7
  126. package/docs/codex.md +2 -2
  127. package/docs/customizing.md +1 -1
  128. package/docs/gemini.md +1 -1
  129. package/docs/getting-started.md +1 -1
  130. package/docs/guide/de/integrations/7-local-engines.md +1 -1
  131. package/docs/guide/de/pipeline/1-rails-and-jobs.md +8 -6
  132. package/docs/guide/de/pipeline/2-the-job-detail-view.md +11 -2
  133. package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +2 -2
  134. package/docs/guide/de/pipeline/5-the-loop-builder.md +24 -5
  135. package/docs/guide/en/integrations/7-local-engines.md +1 -1
  136. package/docs/guide/en/pipeline/1-rails-and-jobs.md +8 -6
  137. package/docs/guide/en/pipeline/2-the-job-detail-view.md +11 -2
  138. package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +2 -2
  139. package/docs/guide/en/pipeline/5-the-loop-builder.md +34 -5
  140. package/docs/guide/en/settings/3-pipeline-telemetry-and-diagnostics.md +1 -1
  141. package/docs/guide/es/integrations/7-local-engines.md +1 -1
  142. package/docs/guide/es/pipeline/1-rails-and-jobs.md +8 -6
  143. package/docs/guide/es/pipeline/2-the-job-detail-view.md +11 -2
  144. package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +2 -2
  145. package/docs/guide/es/pipeline/5-the-loop-builder.md +30 -5
  146. package/docs/guide/es/settings/3-pipeline-telemetry-and-diagnostics.md +1 -2
  147. package/docs/guide/fr/integrations/7-local-engines.md +1 -1
  148. package/docs/guide/fr/pipeline/1-rails-and-jobs.md +8 -6
  149. package/docs/guide/fr/pipeline/2-the-job-detail-view.md +11 -2
  150. package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +2 -2
  151. package/docs/guide/fr/pipeline/5-the-loop-builder.md +24 -5
  152. package/docs/guide/it/integrations/7-local-engines.md +1 -1
  153. package/docs/guide/it/pipeline/1-rails-and-jobs.md +8 -6
  154. package/docs/guide/it/pipeline/2-the-job-detail-view.md +11 -2
  155. package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +2 -2
  156. package/docs/guide/it/pipeline/5-the-loop-builder.md +24 -5
  157. package/docs/guide/ja/integrations/7-local-engines.md +1 -1
  158. package/docs/guide/ja/pipeline/1-rails-and-jobs.md +8 -6
  159. package/docs/guide/ja/pipeline/2-the-job-detail-view.md +11 -2
  160. package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +2 -2
  161. package/docs/guide/ja/pipeline/5-the-loop-builder.md +24 -5
  162. package/docs/guide/pt/integrations/7-local-engines.md +1 -1
  163. package/docs/guide/pt/pipeline/1-rails-and-jobs.md +8 -6
  164. package/docs/guide/pt/pipeline/2-the-job-detail-view.md +11 -2
  165. package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +2 -2
  166. package/docs/guide/pt/pipeline/5-the-loop-builder.md +24 -5
  167. package/docs/guide/zh/integrations/7-local-engines.md +1 -1
  168. package/docs/guide/zh/pipeline/1-rails-and-jobs.md +8 -6
  169. package/docs/guide/zh/pipeline/2-the-job-detail-view.md +11 -2
  170. package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +2 -2
  171. package/docs/guide/zh/pipeline/5-the-loop-builder.md +24 -5
  172. package/docs/internals/api-reference.md +2 -2
  173. package/docs/internals/ci-performance.md +71 -0
  174. package/docs/internals/companion-rails-as-loops-contract.md +4 -3
  175. package/docs/internals/configuration.md +3 -3
  176. package/docs/internals/core-runtime-updates.md +3 -2
  177. package/docs/internals/interactive-jobs.md +2 -2
  178. package/docs/internals/loop-step-log-explorer.md +42 -0
  179. package/docs/internals/mission-rail-cards.md +1 -1
  180. package/docs/internals/operations-runbook.md +22 -0
  181. package/docs/internals/programmatic-agent-runtime.md +130 -7
  182. package/docs/internals/project-builder.md +35 -16
  183. package/docs/internals/source-map.md +48 -2
  184. package/docs/kimi.md +2 -2
  185. package/docs/local-providers.md +1 -1
  186. package/docs/running-pipelines.md +26 -18
  187. package/docs/tracking-cost.md +10 -1
  188. package/package.json +2 -1
  189. package/server/dist/core-compat.js +8 -3
  190. package/server/dist/core-execution.js +49 -3
  191. package/server/dist/db/migrations.js +101 -0
  192. package/server/dist/desktop-db.js +28 -0
  193. package/server/dist/desktop-router.js +2 -0
  194. package/server/dist/mcp/guide.js +6 -3
  195. package/server/dist/mcp/tools/jobs.js +14 -1
  196. package/server/dist/mcp/tools/loops.js +17 -8
  197. package/server/dist/mcp/tools/rails.js +3 -3
  198. package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +313 -9
  199. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls-router.js +34 -1
  200. package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +203 -21
  201. package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +46 -1
  202. package/server/dist/modules/agent-runtime/runtime/agent-runtime-engines.js +10 -0
  203. package/server/dist/modules/agent-runtime/runtime/agent-runtime-loader.js +68 -3
  204. package/server/dist/modules/agent-runtime/runtime/agent-runtime-metrics.js +16 -8
  205. package/server/dist/modules/agent-runtime/runtime/agent-runtime-package-gc.js +81 -0
  206. package/server/dist/modules/agent-runtime/runtime/agent-runtime-package-lock.js +37 -0
  207. package/server/dist/modules/agent-runtime/runtime/agent-runtime-package.js +30 -24
  208. package/server/dist/modules/agent-runtime/runtime/agent-runtime-retention-host.js +87 -0
  209. package/server/dist/modules/agent-runtime/runtime/agent-runtime-retention-quarantine.js +231 -0
  210. package/server/dist/modules/agent-runtime/runtime/agent-runtime-retention-records.js +32 -0
  211. package/server/dist/modules/agent-runtime/runtime/agent-runtime-retention.js +102 -0
  212. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +12 -7
  213. package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +57 -13
  214. package/server/dist/modules/agents/runtime/agent-catalog.js +32 -0
  215. package/server/dist/modules/agents/runtime/agent-refine-manager.js +12 -0
  216. package/server/dist/modules/agents/runtime/agent-role-descriptor.js +64 -0
  217. package/server/dist/modules/agents/runtime/profiles-router.js +26 -34
  218. package/server/dist/modules/builder/runtime/milestone-chain.js +121 -73
  219. package/server/dist/modules/delivery/runtime/definition-fork.js +158 -0
  220. package/server/dist/modules/delivery/runtime/delivery-evidence.js +56 -10
  221. package/server/dist/modules/delivery/runtime/isolated-settlement-reconstruction.js +99 -0
  222. package/server/dist/modules/delivery/runtime/isolated-settlement-store.js +53 -0
  223. package/server/dist/modules/delivery/runtime/multi-repo-execution-store.js +3 -1
  224. package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +388 -203
  225. package/server/dist/modules/delivery/runtime/rail-launch-parser.js +3 -2
  226. package/server/dist/modules/delivery/runtime/rail-pr-store.js +6 -1
  227. package/server/dist/modules/delivery/runtime/rails-router.js +66 -10
  228. package/server/dist/modules/execution/runtime/queue-manager.js +11 -2
  229. package/server/dist/modules/loops/runtime/builtin-loops.js +70 -0
  230. package/server/dist/modules/loops/runtime/definition-cancellation.js +31 -0
  231. package/server/dist/modules/loops/runtime/legacy-launch-telemetry.js +78 -0
  232. package/server/dist/modules/loops/runtime/loop-command-catalog.js +13 -12
  233. package/server/dist/modules/loops/runtime/loop-compat.js +219 -0
  234. package/server/dist/modules/loops/runtime/loop-core-factory.js +42 -0
  235. package/server/dist/modules/loops/runtime/loop-definition-controls.js +48 -0
  236. package/server/dist/modules/loops/runtime/loop-definition-events.js +145 -0
  237. package/server/dist/modules/loops/runtime/loop-definition-recovery.js +106 -0
  238. package/server/dist/modules/loops/runtime/loop-definition-run.js +36 -0
  239. package/server/dist/modules/loops/runtime/loop-definition.js +246 -0
  240. package/server/dist/modules/loops/runtime/loop-effect.js +10 -1
  241. package/server/dist/modules/loops/runtime/loop-executors.js +63 -3
  242. package/server/dist/modules/loops/runtime/loop-factory.js +35 -15
  243. package/server/dist/modules/loops/runtime/loop-graph.js +84 -8
  244. package/server/dist/modules/loops/runtime/loop-migration.js +36 -0
  245. package/server/dist/modules/loops/runtime/loop-preview.js +10 -0
  246. package/server/dist/modules/loops/runtime/loop-run-manager.js +206 -13
  247. package/server/dist/modules/loops/runtime/loop-runs-store.js +271 -7
  248. package/server/dist/modules/loops/runtime/loop-templates.js +205 -45
  249. package/server/dist/modules/loops/runtime/loops-router.js +246 -29
  250. package/server/dist/modules/loops/runtime/loops-store.js +140 -20
  251. package/server/dist/modules/missions/runtime/agent-operator-prompt.js +14 -11
  252. package/server/dist/modules/specs/runtime/spec-addenda.js +28 -0
  253. package/server/dist/project-registry.js +87 -51
  254. package/server/dist/project-router-jobs.js +17 -6
  255. package/server/dist/project-router-loop-runs.js +200 -0
  256. package/server/dist/project-router-spending.js +17 -0
  257. package/server/dist/project-router.js +8 -6
  258. package/server/dist/schemas/agent-runtime.schema.json +93 -1
  259. package/server/dist/schemas/workflow-definition.schema.json +312 -0
  260. package/server/dist/vitest-setup.js +22 -0
  261. package/server/dist/worktree-overlay.js +1 -1
  262. package/client/dist/assets/AgentsPage-DfHWgP_x.js +0 -87
  263. package/client/dist/assets/DesktopAnalyticsPage-tc0p4jJW.js +0 -1
  264. package/client/dist/assets/InteractiveJobComposer-DFPpdg2D.js +0 -19
  265. package/client/dist/assets/JobDetailModal-BdOv-l-M.js +0 -1
  266. package/client/dist/assets/LoopBuilderPage-BemvUOxy.js +0 -7
  267. package/client/dist/assets/LoopPreviewModal-C6hNAeYN.js +0 -1
  268. package/client/dist/assets/LoopsPage-BZr0quvh.js +0 -1
  269. package/client/dist/assets/TemplatePreviewModal-Cejcquah.js +0 -1
  270. package/client/dist/assets/agentRuntime-BUimiX__.js +0 -1
  271. package/client/dist/assets/agentRuntime-BcN97sAB.js +0 -1
  272. package/client/dist/assets/agentRuntime-Ckyg-ahT.js +0 -1
  273. package/client/dist/assets/agentRuntime-DJCwyX4t.js +0 -1
  274. package/client/dist/assets/agentRuntime-DVzPJOl0.js +0 -1
  275. package/client/dist/assets/agentRuntime-V9AZTG3c.js +0 -1
  276. package/client/dist/assets/agentRuntime-bAAjw1vw.js +0 -1
  277. package/client/dist/assets/agentRuntime-v4qKAYH-.js +0 -1
  278. package/client/dist/assets/agentstudio-BNUth0uj.js +0 -1
  279. package/client/dist/assets/agentstudio-BNdB5hkr.js +0 -1
  280. package/client/dist/assets/agentstudio-C7-bt8FO.js +0 -1
  281. package/client/dist/assets/agentstudio-C8uNchpV.js +0 -1
  282. package/client/dist/assets/agentstudio-CKabyMlg.js +0 -1
  283. package/client/dist/assets/agentstudio-CN2pokQu.js +0 -1
  284. package/client/dist/assets/agentstudio-y2dVDXeM.js +0 -1
  285. package/client/dist/assets/analytics-BlypLLR6.js +0 -1
  286. package/client/dist/assets/builder-765NeYTp.js +0 -1
  287. package/client/dist/assets/builder-8MG3V6ea.js +0 -1
  288. package/client/dist/assets/builder-B-1UBs5K.js +0 -1
  289. package/client/dist/assets/builder-BTiGnPT0.js +0 -1
  290. package/client/dist/assets/builder-Cx9Rz-7u.js +0 -1
  291. package/client/dist/assets/builder-DKFMELBl.js +0 -1
  292. package/client/dist/assets/builder-OcmKrC0h.js +0 -1
  293. package/client/dist/assets/builder-P9qVBXFZ.js +0 -1
  294. package/client/dist/assets/common-B5AZNYMH.js +0 -1
  295. package/client/dist/assets/common-BFZ0c_vN.js +0 -1
  296. package/client/dist/assets/common-BttQx3z4.js +0 -1
  297. package/client/dist/assets/common-CM65k_-e.js +0 -1
  298. package/client/dist/assets/common-D4mvFFlu.js +0 -1
  299. package/client/dist/assets/common-TZ9Ny96S.js +0 -1
  300. package/client/dist/assets/common-nFpSF4GC.js +0 -1
  301. package/client/dist/assets/dashboard-B9uWNGO2.js +0 -1
  302. package/client/dist/assets/dashboard-BIwmO9wc.js +0 -1
  303. package/client/dist/assets/dashboard-BV4vOZkR.js +0 -1
  304. package/client/dist/assets/dashboard-Bf6MPVsM.js +0 -1
  305. package/client/dist/assets/dashboard-C8klWVvZ.js +0 -1
  306. package/client/dist/assets/dashboard-D1ORhPF0.js +0 -1
  307. package/client/dist/assets/dashboard-DSubvs9x.js +0 -1
  308. package/client/dist/assets/dashboard-px_vlRx_.js +0 -1
  309. package/client/dist/assets/index-C51x61Bz.css +0 -2
  310. package/client/dist/assets/loops-B7YpGizO.js +0 -1
  311. package/client/dist/assets/loops-BomD2J-h.js +0 -1
  312. package/client/dist/assets/loops-BrQHSQw3.js +0 -1
  313. package/client/dist/assets/loops-BvBwvTPA.js +0 -1
  314. package/client/dist/assets/loops-CODk6Slz.js +0 -1
  315. package/client/dist/assets/loops-DC6k-Ym-.js +0 -1
  316. package/client/dist/assets/loops-DSEg8-uY.js +0 -1
  317. package/client/dist/assets/loops-DhCoXD_v.js +0 -1
  318. package/client/dist/assets/settings-BsehKpty.js +0 -1
  319. package/client/dist/assets/settings-CQWob3SB.js +0 -1
  320. package/client/dist/assets/settings-CgCl0hzy.js +0 -1
  321. package/client/dist/assets/settings-DLRLd22L.js +0 -1
  322. package/client/dist/assets/settings-Dn5prAFl.js +0 -1
  323. package/client/dist/assets/settings-HZSu-K4l.js +0 -1
  324. package/client/dist/assets/settings-p4OVIyRh.js +0 -1
  325. package/client/dist/assets/settings-rddaldUp.js +0 -1
  326. package/client/dist/assets/useRuntimeRuns-Dqmz9fS9.js +0 -1
  327. package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  328. package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +0 -126
  329. package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +0 -114
  330. package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  331. package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  332. package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  333. package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  334. package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +0 -84
  335. package/server/dist/modules/loops/runtime/loop-templates-ported.js +0 -675
  336. /package/client/dist/assets/{LoopBuilderPage-DtxX27Jz.css → loop-layout-DtxX27Jz.css} +0 -0
@@ -3,10 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AgentRuntimeControls = exports.RuntimeControlError = void 0;
6
+ exports.AgentRuntimeControls = exports.RuntimeControlError = exports.STEERING_TEXT_LIMIT = void 0;
7
+ exports.readRuntimeSteering = readRuntimeSteering;
7
8
  exports.pinsRailCard = pinsRailCard;
9
+ exports.stepIdsFor = stepIdsFor;
8
10
  exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
9
11
  exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
12
+ const agent_runtime_retention_1 = require("./agent-runtime-retention");
13
+ const agent_runtime_retention_host_1 = require("./agent-runtime-retention-host");
14
+ const agent_runtime_retention_records_1 = require("./agent-runtime-retention-records");
10
15
  const agent_runtime_events_1 = require("./agent-runtime-events");
11
16
  const agent_runtime_history_1 = require("./agent-runtime-history");
12
17
  const agent_runtime_settlement_1 = require("./agent-runtime-settlement");
@@ -27,8 +32,32 @@ const db_1 = require("../../../db");
27
32
  const loop_run_manager_1 = require("../../loops/runtime/loop-run-manager");
28
33
  const agent_runtime_recovery_1 = require("./agent-runtime-recovery");
29
34
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
30
- const STEP_IDS = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
35
+ const LEGACY_STEP_IDS = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
36
+ const NODE_PATH = /^[A-Za-z0-9][A-Za-z0-9_-]{0,119}(\/[A-Za-z0-9][A-Za-z0-9_-]{0,119}){0,3}$/;
37
+ function isNodePath(value) {
38
+ return NODE_PATH.test(value) && value.split('/').every(id => !['START', 'END', '__start__', '__end__', 'next'].includes(id));
39
+ }
31
40
  const ANSWER_LIMIT = 20_000;
41
+ /** Core contract (engine/steering/inbox.ts): 1–20,000 UTF-16 code units per message. Its 80,000-byte cap cannot be reached below that length. */
42
+ exports.STEERING_TEXT_LIMIT = 20_000;
43
+ const STEERING_PREVIEW_LENGTH = 240;
44
+ const STEERING_RECEIPT_LIMIT = 512;
45
+ const STEERING_REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
46
+ /** Core owns receipt durability; Desktop never infers consumption from process state. */
47
+ function readRuntimeSteering(value) {
48
+ if (value === undefined)
49
+ return undefined;
50
+ if (!value || typeof value !== 'object' || Array.isArray(value))
51
+ throw new Error('Core returned invalid steering receipts');
52
+ const state = value;
53
+ const timestamp = (value) => typeof value === 'string' && Number.isFinite(Date.parse(value));
54
+ if (state.consumptionReported !== true || !Number.isSafeInteger(state.pending) || state.pending < 0 || !Number.isSafeInteger(state.consumed) || state.consumed < 0 || typeof state.truncated !== 'boolean' ||
55
+ !Array.isArray(state.receipts) || state.receipts.length > STEERING_RECEIPT_LIMIT || state.receipts.some(item => !item || typeof item.id !== 'string' || !item.id || item.id.length > 256 || !timestamp(item.acceptedAt) || typeof item.preview !== 'string' || item.preview.length > STEERING_PREVIEW_LENGTH || !Number.isSafeInteger(item.length) || item.length < item.preview.length || item.length > exports.STEERING_TEXT_LIMIT || !['pending', 'consumed'].includes(item.status) ||
56
+ (item.status === 'consumed' && (typeof item.consumedAttemptId !== 'string' || !item.consumedAttemptId || !timestamp(item.consumedAt))) ||
57
+ (item.status === 'pending' && (item.consumedAt !== undefined || item.consumedAttemptId !== undefined))))
58
+ throw new Error('Core returned invalid steering receipts');
59
+ return { receipts: state.receipts.map(({ id, acceptedAt, preview, length, status, consumedAttemptId, consumedAt }) => ({ id, acceptedAt, preview, length, status, ...(consumedAttemptId ? { consumedAttemptId, consumedAt } : {}) })), pending: state.pending, consumed: state.consumed, truncated: state.truncated, consumptionReported: true };
60
+ }
32
61
  /**
33
62
  * Whether a continuation still deserves the rail card. Anything that needs a
34
63
  * human (blocked, failed, interrupted, a pending question or approval, a
@@ -38,7 +67,7 @@ const ANSWER_LIMIT = 20_000;
38
67
  * kept a "Completed" card with only Dismiss on it after every green run.
39
68
  */
40
69
  function pinsRailCard(summary) {
41
- if (summary.dismissed)
70
+ if (summary.dismissed || summary.status === 'expired')
42
71
  return false;
43
72
  if (summary.active || summary.canResume || summary.canSettle || summary.canCancel || summary.pendingQuestion || summary.pendingApproval || summary.recoverableSteps.length)
44
73
  return true;
@@ -54,18 +83,41 @@ class RuntimeControlError extends Error {
54
83
  }
55
84
  }
56
85
  exports.RuntimeControlError = RuntimeControlError;
57
- function validateRuntimeResumeInput(input) {
86
+ function stepIdsFor(state) {
87
+ return state.engineVersion === 2 ? Object.keys(state.steps) : [...LEGACY_STEP_IDS];
88
+ }
89
+ function metricsCatalogFor(state) {
90
+ return state.engineVersion === 2 ? { stepIds: stepIdsFor(state), roleIds: state.roleIds } : undefined;
91
+ }
92
+ /** Open roles belong to the original run, even after project settings change. */
93
+ function frozenRoleIds(contextPath) {
94
+ try {
95
+ const config = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-config.json'), 'utf8'));
96
+ const catalogs = [config.agents, config.roles].filter(value => value !== undefined);
97
+ if (!catalogs.length || catalogs.some(value => !value || typeof value !== 'object' || Array.isArray(value)))
98
+ return undefined;
99
+ const ids = [...new Set(catalogs.flatMap(value => Object.keys(value)))];
100
+ return ids.every(id => /^[a-z][a-z0-9-]{0,63}$/.test(id)) ? ids : undefined;
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ }
106
+ function validateRuntimeResumeInput(input, state) {
58
107
  if (!input || typeof input !== 'object' || Array.isArray(input))
59
108
  throw new RuntimeControlError(400, 'invalid_resume_request', 'Resume request must be an object');
60
109
  const body = input;
110
+ const stepIds = state ? new Set(stepIdsFor(state)) : undefined;
61
111
  for (const [key, value] of Object.entries(body)) {
62
112
  if (key === 'answer') {
63
113
  if (typeof value !== 'string' || !value.trim() || value.length > ANSWER_LIMIT)
64
114
  throw new RuntimeControlError(400, 'invalid_resume_request', `An answer must be a nonempty string of at most ${ANSWER_LIMIT} characters`);
65
115
  continue;
66
116
  }
67
- if (!['approve', 'recover', 'invalidate'].includes(key) || !Array.isArray(value) || value.length > STEP_IDS.length || !value.every((id) => typeof id === 'string' && STEP_IDS.includes(id)) || new Set(value).size !== value.length)
68
- throw new RuntimeControlError(400, 'invalid_resume_request', 'Only approve, recover and invalidate arrays of known phase IDs are allowed');
117
+ if (!['approve', 'recover', 'invalidate'].includes(key) || !Array.isArray(value) || value.length > 10_000 || !value.every((id) => typeof id === 'string' && isNodePath(id)) || new Set(value).size !== value.length)
118
+ throw new RuntimeControlError(400, 'invalid_resume_request', 'Only approve, recover and invalidate arrays of safe node paths are allowed');
119
+ if (stepIds && value.some(id => !stepIds.has(id)))
120
+ throw new RuntimeControlError(400, 'invalid_resume_request', 'Resume node paths must belong to the saved workflow');
69
121
  }
70
122
  return body;
71
123
  }
@@ -90,12 +142,46 @@ async function readAgentRuntimeStatus(contextPath, cwd, env) {
90
142
  const result = JSON.parse(stdout);
91
143
  if (result.type !== 'runtime-status' || result.state === undefined)
92
144
  throw new Error('Core returned an invalid runtime status');
145
+ if (result.engineVersion !== undefined && (!Number.isSafeInteger(result.engineVersion) || result.engineVersion < 1))
146
+ throw new Error('Core returned an invalid runtime engine version');
147
+ if (!result.state)
148
+ return null;
149
+ const state = { ...result.state, ...(result.engineVersion === undefined ? {} : { engineVersion: result.engineVersion }) };
150
+ if (state.engineVersion === 2) {
151
+ if (!state.steps || typeof state.steps !== 'object' || Array.isArray(state.steps) || !Object.entries(state.steps).every(([id, step]) => isNodePath(id) && step && typeof step.status === 'string'))
152
+ throw new Error('Core returned an invalid runtime step catalog');
153
+ if (state.nextNodePath !== null && (typeof state.nextNodePath !== 'string' || !Object.hasOwn(state.steps, state.nextNodePath)))
154
+ throw new Error('Core returned an invalid next node path');
155
+ if (state.recoverableSteps !== undefined && (!Array.isArray(state.recoverableSteps) || state.recoverableSteps.some(step => !step || typeof step.attemptId !== 'string' || !step.attemptId || typeof step.nodePath !== 'string' || typeof step.scopeId !== 'string')))
156
+ throw new Error('Core returned invalid recovery attempt identities');
157
+ if (state.lease !== undefined && state.lease !== null && (typeof state.lease !== 'object' || typeof state.lease.active !== 'boolean'))
158
+ throw new Error('Core returned an invalid execution lease');
159
+ if (state.pendingInterrupts !== undefined && (!Array.isArray(state.pendingInterrupts) || state.pendingInterrupts.some(item => !item || typeof item.id !== 'string' || !item.id || typeof item.nodePath !== 'string' || !['question', 'approval', 'gate'].includes(item.kind))))
160
+ throw new Error('Core returned invalid pending interrupts');
161
+ // Legacy display fields use node paths. Controls must use an exact pending
162
+ // interrupt, including when parallel branches pause at the same node.
163
+ if (state.pendingApproval) {
164
+ const approval = state.pendingInterrupts?.find(item => item.nodePath === state.pendingApproval.stepId && item.kind !== 'question');
165
+ state.pendingApproval = approval ? { ...state.pendingApproval, stepId: approval.id } : undefined;
166
+ }
167
+ if (state.pendingQuestion) {
168
+ const question = state.pendingInterrupts?.find(item => item.nodePath === state.pendingQuestion.stepId && item.kind === 'question');
169
+ state.pendingQuestion = question ? { ...state.pendingQuestion, stepId: question.id } : undefined;
170
+ }
171
+ state.nextStep = state.nextNodePath;
172
+ state.roleIds = frozenRoleIds(contextPath);
173
+ state.steering = readRuntimeSteering(state.steering);
174
+ if (result.completion !== undefined && result.completion !== null && (typeof result.completion !== 'object' || typeof result.completion.ok !== 'boolean' || typeof result.completion.verified !== 'boolean' || !Array.isArray(result.completion.reasons) || result.completion.reasons.some(reason => typeof reason !== 'string')))
175
+ throw new Error('Core returned invalid completion evidence');
176
+ state.completion = result.completion;
177
+ }
93
178
  let selection;
94
179
  try {
95
180
  selection = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(node_path_1.default.dirname(contextPath), 'desktop-runtime-selection.json'), 'utf8'));
96
181
  }
97
182
  catch { /* Original hosts may not record selection origins. */ }
98
- return result.state ? { ...result.state, inspection: result.pipeline, efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary), selection, result.state.runId) } : null;
183
+ const catalog = metricsCatalogFor(state);
184
+ return { ...state, inspection: result.pipeline, metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(result.metrics ?? state.metrics, catalog), efficiencySummary: (0, agent_runtime_metrics_1.applyRuntimeSelectionOrigins)((0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(result.efficiencySummary, catalog), selection, state.runId) };
99
185
  }
100
186
  /** One controller per ProjectContext; Core's durable lease remains the final
101
187
  * cross-process guard. Resume never constructs a new context or worktree. */
@@ -108,13 +194,91 @@ class AgentRuntimeControls {
108
194
  errors = new Map();
109
195
  statusCache = new Map();
110
196
  disposed = false;
197
+ collectingRetention = false;
111
198
  constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe, settle: agent_runtime_settlement_1.settleRuntimeContinuation }) {
112
199
  this.ctx = ctx;
113
200
  this.dependencies = dependencies;
201
+ // Startup recovery never blocks runtime controls; unrecovered entries stay in
202
+ // quarantine, where they also block package collection until reviewed.
203
+ try {
204
+ const recovered = (0, agent_runtime_retention_host_1.recoverHostRuntimeRetention)({ db: ctx.db, pipelineRoot: node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(ctx.project).specrailsDir, 'pipeline'), active: runId => this.active.has(runId) });
205
+ for (const error of recovered.errors)
206
+ console.warn('[agent-runtime] retention recovery kept quarantine:', error);
207
+ }
208
+ catch (error) {
209
+ console.warn('[agent-runtime] retention recovery unavailable:', error instanceof Error ? error.message : error);
210
+ }
211
+ }
212
+ retentionPolicy() { return (0, agent_runtime_retention_records_1.readRuntimeRetentionPolicy)(this.ctx.db); }
213
+ configureRetention(input) {
214
+ try {
215
+ return (0, agent_runtime_retention_records_1.saveRuntimeRetentionPolicy)(this.ctx.db, input);
216
+ }
217
+ catch {
218
+ throw new RuntimeControlError(400, 'invalid_retention_policy', 'Retention days must be null or an integer from 1 to 3650');
219
+ }
220
+ }
221
+ async collectRetention(input) {
222
+ const body = input;
223
+ if (!body || typeof body !== 'object' || Array.isArray(body) || Object.keys(body).some(key => key !== 'dryRun') || body.dryRun !== undefined && typeof body.dryRun !== 'boolean')
224
+ throw new RuntimeControlError(400, 'invalid_retention_request', 'Retention accepts only a boolean dryRun');
225
+ if (this.disposed || this.collectingRetention)
226
+ throw new RuntimeControlError(409, 'runtime_retention_busy', 'Runtime retention is already active or shutting down');
227
+ this.collectingRetention = true;
228
+ try {
229
+ const policy = this.retentionPolicy(), now = Date.now();
230
+ const host = { db: this.ctx.db, projectId: this.ctx.project.id,
231
+ pipelineRoot: node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline'), active: (runId) => this.active.has(runId),
232
+ scope: (runId) => {
233
+ const context = this.context(runId, true);
234
+ return { repositoryMounts: context.frozen.repositories.map(repository => repository.path),
235
+ inspect: () => this.dependencies.status(context.file, node_fs_1.default.existsSync(context.cwd) ? context.cwd : context.frozen.backlogRoot, context.env) };
236
+ },
237
+ };
238
+ const recovery = body.dryRun === false ? (0, agent_runtime_retention_host_1.recoverHostRuntimeRetention)(host).errors : [];
239
+ const report = await (0, agent_runtime_retention_1.collectRuntimeRetention)((0, agent_runtime_retention_host_1.runtimeRetentionHostPorts)(host, policy, now), policy, { dryRun: body.dryRun !== false, now });
240
+ return recovery.length ? { ...report, errors: [...recovery, ...report.errors] } : report;
241
+ }
242
+ finally {
243
+ this.collectingRetention = false;
244
+ this.statusCache.clear();
245
+ }
246
+ }
247
+ async signal(runId, input) {
248
+ if (this.disposed)
249
+ throw new RuntimeControlError(503, 'runtime_shutting_down', 'Project runtime is shutting down');
250
+ const body = input;
251
+ if (!body || typeof body !== 'object' || Array.isArray(body) || Object.keys(body).some(key => !['text', 'requestId'].includes(key)) || typeof body.text !== 'string' || !body.text.trim() || body.text.length > exports.STEERING_TEXT_LIMIT || typeof body.requestId !== 'string' || !STEERING_REQUEST_ID.test(body.requestId))
252
+ throw new RuntimeControlError(400, 'invalid_steering_request', 'Steering requires text of 1–20,000 characters and a stable requestId');
253
+ const { file, cwd, env } = this.context(runId);
254
+ this.assertSteeringOwner(runId);
255
+ const state = await this.dependencies.status(file, cwd, env);
256
+ this.assertSteeringOwner(runId);
257
+ if (!state || state.runId !== runId || state.engineVersion !== 2)
258
+ throw new RuntimeControlError(409, 'steering_unsupported', 'This retained run does not support engine v2 steering');
259
+ try {
260
+ const accepted = await (this.dependencies.control ?? agent_runtime_bridge_1.runAgentRuntimeControl)({ kind: 'signal', contextPath: file, cwd, env, runId, text: body.text, requestId: body.requestId });
261
+ this.statusCache.delete(runId);
262
+ return { id: accepted.id, acceptedAt: accepted.acceptedAt };
263
+ }
264
+ catch (error) {
265
+ const message = error instanceof Error ? error.message : 'Could not send operator steering';
266
+ const code = /^(control_conflict|run_terminal|inbox_full|invalid_arguments):/.exec(message)?.[1];
267
+ throw new RuntimeControlError(code === 'invalid_arguments' ? 400 : code ? 409 : 503, code ?? 'runtime_steering_failed', message);
268
+ }
269
+ }
270
+ assertSteeringOwner(runId) {
271
+ if ((0, loop_runs_store_1.readDefinitionForkTarget)(this.ctx.db, runId)) {
272
+ throw new RuntimeControlError(409, 'runtime_fork_pending', 'This run has a pending or adopted fork; send steering to the child after adoption');
273
+ }
114
274
  }
115
275
  context(runId, historical = false) {
116
276
  if (!SAFE_ID.test(runId))
117
277
  throw new RuntimeControlError(400, 'invalid_run_id', 'Invalid runtime run ID');
278
+ if ((0, agent_runtime_retention_records_1.readRuntimeExpiration)(this.ctx.db, runId))
279
+ throw new RuntimeControlError(410, 'runtime_history_expired', 'Runtime history has expired under the project retention policy');
280
+ if (!historical && (0, loop_runs_store_1.readDefinitionExecutionClaim)(this.ctx.db, runId)?.owner.startsWith('retention'))
281
+ throw new RuntimeControlError(409, 'runtime_retention_busy', 'Runtime history is reserved for retention');
118
282
  const execution = (0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project);
119
283
  const backlogRoot = node_fs_1.default.realpathSync(node_path_1.default.dirname(execution.specrailsDir));
120
284
  const directory = node_path_1.default.join(execution.specrailsDir, 'pipeline', runId);
@@ -259,23 +423,30 @@ class AgentRuntimeControls {
259
423
  }
260
424
  async list() {
261
425
  const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
262
- if (!node_fs_1.default.existsSync(directory))
263
- return [];
264
- const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name) && node_fs_1.default.existsSync(node_path_1.default.join(directory, entry.name, 'agent-runtime-request.json')))
426
+ const entries = (node_fs_1.default.existsSync(directory) ? node_fs_1.default.readdirSync(directory, { withFileTypes: true }) : []).filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name) && node_fs_1.default.existsSync(node_path_1.default.join(directory, entry.name, 'agent-runtime-request.json')))
265
427
  .sort((a, b) => node_fs_1.default.statSync(node_path_1.default.join(directory, b.name)).mtimeMs - node_fs_1.default.statSync(node_path_1.default.join(directory, a.name)).mtimeMs).slice(0, 20);
266
428
  const runs = [];
267
429
  // Bounded sequential reads avoid spawning one Node process per historical run at once.
268
430
  for (const entry of entries)
269
431
  runs.push(await this.summary(entry.name));
270
- return runs;
432
+ const expired = this.ctx.db.prepare('SELECT run_id FROM runtime_retention_records ORDER BY expired_at DESC LIMIT 20').all();
433
+ for (const row of expired)
434
+ if (!runs.some(run => run.runId === row.run_id))
435
+ runs.push(await this.summary(row.run_id));
436
+ return runs.sort((a, b) => Number(b.active) - Number(a.active) || (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')).slice(0, 20);
271
437
  }
272
438
  async summary(runId) {
439
+ const expired = (0, agent_runtime_retention_records_1.readRuntimeExpiration)(this.ctx.db, runId);
440
+ if (expired)
441
+ return { runId, status: 'expired', expiredAt: expired.expiredAt, updatedAt: expired.expiredAt, nextStep: null, historical: true, active: false, canResume: false, canCancel: false, canSettle: false, recoverableSteps: [] };
273
442
  try {
274
443
  const { file, cwd, env } = this.context(runId);
275
444
  // Core owns checkpoint parsing. Its atomic file revision is only a cache
276
445
  // key, keeping idle settings panes from spawning CLI processes repeatedly.
277
446
  const checkpoint = node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', runId, 'checkpoint.json');
278
- const stat = node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
447
+ // A v2 run can also contain a legacy implementation journal; that file
448
+ // cannot invalidate the SQLite/WAL status, so leave v2 inspection uncached.
449
+ const stat = !node_fs_1.default.existsSync(node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', 'run.sqlite')) && node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
279
450
  const fingerprint = stat ? (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(checkpoint)).digest('hex') : null;
280
451
  const cached = this.statusCache.get(runId);
281
452
  const state = fingerprint && cached?.fingerprint === fingerprint ? cached.state : await this.dependencies.status(file, cwd, env);
@@ -284,20 +455,26 @@ class AgentRuntimeControls {
284
455
  if (fingerprint)
285
456
  this.statusCache.set(runId, { fingerprint, state });
286
457
  const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
287
- const active = this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
458
+ const definition = state.engineVersion === 2 && parent?.engine_version === 2;
459
+ const forkSuccessor = definition ? (0, loop_runs_store_1.readDefinitionSuccessor)(this.ctx.db, runId) : undefined;
460
+ const forkReserved = definition && !!(0, loop_runs_store_1.readDefinitionForkTarget)(this.ctx.db, runId);
461
+ const active = definition ? state.lease?.active === true : this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
288
462
  let projection = null;
289
463
  try {
290
464
  projection = (0, agent_runtime_history_1.readRuntimeHistory)(file);
291
465
  }
292
466
  catch { /* Invalid advisory history cannot replace live state. */ }
293
467
  const superseding = !active && projection && ['failed', 'cancelled', 'running'].includes(projection.status) && Date.parse(projection.updatedAt) > Date.parse(state.updatedAt ?? '1970-01-01') ? projection : null;
294
- const recoverableSteps = Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
295
- return { runId, status: superseding ? (superseding.status === 'running' ? 'interrupted' : superseding.status) : state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
296
- updatedAt: superseding?.updatedAt ?? state.updatedAt, error: this.errors.get(runId) ?? superseding?.error ?? state.error, pendingApproval: state.pendingApproval,
297
- traceId: state.traceId, pendingQuestion: openQuestion(state),
298
- metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(superseding ? superseding.metrics : state.metrics), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(superseding ? superseding.efficiencySummary : state.efficiencySummary),
299
- canSettle: !superseding && !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
300
- recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded',
468
+ const recoverableSteps = state.engineVersion === 2 ? (state.recoverableSteps ?? []).map(step => step.attemptId) : Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
469
+ const catalog = metricsCatalogFor(state);
470
+ return { runId, engineVersion: state.engineVersion, status: superseding ? (superseding.status === 'running' ? 'interrupted' : superseding.status) : state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
471
+ updatedAt: superseding?.updatedAt ?? state.updatedAt, error: this.errors.get(runId) ?? superseding?.error ?? state.error, pendingApproval: forkReserved ? undefined : state.pendingApproval,
472
+ traceId: state.traceId, pendingQuestion: forkReserved ? undefined : openQuestion(state), steering: state.steering, completion: state.completion, ...(forkSuccessor ? { forkSuccessor } : {}),
473
+ metrics: (0, agent_runtime_metrics_1.readRuntimeEfficiency)(superseding ? superseding.metrics : state.metrics, catalog), efficiencySummary: (0, agent_runtime_metrics_1.readRuntimeEfficiencySummary)(superseding ? superseding.efficiencySummary : state.efficiencySummary, catalog),
474
+ canSettle: forkReserved ? false : definition
475
+ ? !active && parent.status === 'completed' && state.status === 'succeeded' && !!this.ctx.db.prepare(`SELECT 1 FROM definition_delivery_settlements s JOIN rail_pr_deliveries d ON d.id=s.delivery_id WHERE s.project_id=? AND s.run_id=? AND d.decision IN ('building','pr_failed','implementation_failed') LIMIT 1`).get(this.ctx.project.id, runId)
476
+ : !superseding && !active && state.status === 'succeeded' && this.ctx.db.prepare('SELECT status FROM jobs WHERE id = ?').get(runId)?.status !== 'completed',
477
+ recoverableSteps: forkReserved ? [] : recoverableSteps, ...(state.engineVersion === 2 ? { recoveryAttempts: forkReserved ? [] : state.recoverableSteps ?? [] } : {}), active, canCancel: !forkReserved && (definition ? !state.completion && !['succeeded', 'cancelled'].includes(state.status) : this.active.has(runId)), canResume: !forkReserved && (state.engineVersion === 2 ? definition && !active && parent.status !== 'completed' && state.status !== 'cancelled' : !active && parent?.status === 'completed' && state.status !== 'succeeded'),
301
478
  canDismiss: !active, dismissed: this.isDismissed(runId) };
302
479
  }
303
480
  catch (error) {
@@ -315,6 +492,8 @@ class AgentRuntimeControls {
315
492
  if (this.disposed)
316
493
  throw new RuntimeControlError(503, 'runtime_shutting_down', 'Project runtime is shutting down');
317
494
  const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
495
+ if (parent?.engine_version === 2)
496
+ throw new RuntimeControlError(409, 'definition_lifecycle_required', 'Resume this workflow through its definition lifecycle');
318
497
  if (!parent || parent.status !== 'completed' || this.active.has(runId))
319
498
  throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the original Desktop execution to settle before resuming');
320
499
  if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
@@ -326,10 +505,13 @@ class AgentRuntimeControls {
326
505
  try {
327
506
  // Accounting from a prior continuation must settle even when Core now
328
507
  // reports success and correctly refuses any further execution.
329
- (0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
330
508
  const state = await this.dependencies.status(context.file, context.cwd, context.env);
509
+ if (state?.engineVersion === 2)
510
+ throw new RuntimeControlError(409, 'definition_lifecycle_required', 'Resume this workflow through its definition lifecycle');
511
+ (0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
331
512
  if (!state || state.runId !== runId || state.status === 'succeeded')
332
513
  throw new RuntimeControlError(409, 'runtime_not_resumable', 'This execution has no resumable workflow');
514
+ validateRuntimeResumeInput(input, state);
333
515
  if (parent.rail_index != null && [...(this.ctx.railLoopRuns?.values() ?? []), ...(this.ctx.railJobs?.values() ?? [])].some(meta => meta.railIndex === parent.rail_index))
334
516
  throw new RuntimeControlError(409, 'runtime_rail_active', 'The implementation card became active while checking the saved execution');
335
517
  // A pending question resumes the architect only with the operator's answer.
@@ -1,8 +1,48 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workflowRoleDefaults = workflowRoleDefaults;
4
+ exports.bindWorkflowRoleSelections = bindWorkflowRoleSelections;
5
+ exports.bindWorkflowRoleDefaults = bindWorkflowRoleDefaults;
3
6
  exports.resolveEffectiveRuntimeConfig = resolveEffectiveRuntimeConfig;
4
7
  exports.fillDefaultRoleModels = fillDefaultRoleModels;
5
8
  const providers_1 = require("../../../providers");
9
+ /** The loop decision role uses the selected review engine, but has no OpenSpec
10
+ * workflow obligation. Native implementation's reviewer keeps its own policy. */
11
+ function workflowRoleDefaults(config) {
12
+ return { 'loop-decider': { ...config.agents.reviewer, access: 'read', artifacts: 'none' } };
13
+ }
14
+ /** Explicit launch-owned roles, used when a converted workflow has a different
15
+ * decision engine from the project's reviewer. Validate the complete selection
16
+ * before applying it; project settings and caller descriptors remain untouched. */
17
+ function bindWorkflowRoleSelections(config, definition, selections) {
18
+ if (!selections || !Object.keys(selections).length)
19
+ return {};
20
+ const declared = definition?.roles;
21
+ const names = Object.keys(selections);
22
+ if (!Array.isArray(declared) || names.some(id => !declared.includes(id) ||
23
+ !/^[a-z][a-z0-9-]{0,63}$/.test(id) ||
24
+ ['architect', 'developer', 'reviewer', 'fixer', 'constructor', 'prototype'].includes(id))) {
25
+ throw new Error('Workflow role selections must name declared custom roles');
26
+ }
27
+ config.roles = { ...config.roles, ...structuredClone(selections) };
28
+ fillDefaultRoleModels(config);
29
+ return Object.fromEntries(names.map(id => [id, 'explicit-workflow-selection']));
30
+ }
31
+ /** Bind only roles referenced by the frozen definition; preserve explicit
32
+ * project assignments and never persist generated defaults into project files. */
33
+ function bindWorkflowRoleDefaults(config, definition) {
34
+ const roles = definition?.roles;
35
+ if (!Array.isArray(roles) || !roles.includes('loop-decider'))
36
+ return {};
37
+ const existing = config.roles?.['loop-decider'];
38
+ if (existing) {
39
+ if (existing.access !== 'read' || existing.artifacts !== 'none' || existing.openspecSkill)
40
+ throw new Error('loop-decider requires read access, no artifact writes and no OpenSpec skill');
41
+ return { 'loop-decider': 'project-role' };
42
+ }
43
+ config.roles = { ...config.roles, ...workflowRoleDefaults(config) };
44
+ return { 'loop-decider': 'inherited-reviewer-engine' };
45
+ }
6
46
  /** A pure admission resolver; only deliberate launch intent overrides a project role. */
7
47
  function resolveEffectiveRuntimeConfig(input, options) {
8
48
  const config = structuredClone(input);
@@ -29,6 +69,11 @@ function resolveEffectiveRuntimeConfig(input, options) {
29
69
  config.agents[role] = next;
30
70
  }
31
71
  }
72
+ if (override && config.roles)
73
+ for (const [id, current] of Object.entries(config.roles)) {
74
+ const changed = current.provider !== override.provider || (override.model !== undefined && override.model !== current.model);
75
+ config.roles[id] = { ...current, provider: override.provider, model: override.model ?? (current.provider !== override.provider ? undefined : current.model), effort: override.effort ?? (changed ? undefined : current.effort), escalation: changed ? undefined : current.escalation };
76
+ }
32
77
  fillDefaultRoleModels(config);
33
78
  config.verification = config.verification.filter(check => options.repositoryIds.includes(check.repositoryId));
34
79
  return { config, origins: { architect: override ? 'explicit-launch-override' : options.source, developer: override ? 'explicit-launch-override' : options.source, reviewer: override ? 'explicit-launch-override' : options.source } };
@@ -42,7 +87,7 @@ function resolveEffectiveRuntimeConfig(input, options) {
42
87
  * "Role architect requires a model for its API provider".
43
88
  */
44
89
  function fillDefaultRoleModels(config, options = {}) {
45
- for (const agent of [...Object.values(config.agents), ...(config.fixer ? [config.fixer] : [])]) {
90
+ for (const agent of [...Object.values(config.agents), ...Object.values(config.roles ?? {}), ...(config.fixer ? [config.fixer] : [])]) {
46
91
  const provider = config.providers.find(entry => entry.id === agent.provider);
47
92
  if (!agent.model && provider?.kind === 'cli' && !options.localOnly)
48
93
  agent.model = (0, providers_1.getAdapter)(provider.cli).defaultModel();
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supportedEngines = supportedEngines;
4
+ /** Engines the active Core can launch. Cores released before the `engines`
5
+ * field still run the legacy engine, plus definitions when they advertise engine v2. */
6
+ function supportedEngines(api) {
7
+ if (api?.engines)
8
+ return [...api.engines];
9
+ return api?.capabilities?.engineV2 === 1 ? [1, 2] : [1];
10
+ }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readWorkflowCatalog = readWorkflowCatalog;
3
4
  exports.findCoreAgentRuntimeEntry = findCoreAgentRuntimeEntry;
4
5
  exports.resetCoreAgentRuntimeApiCache = resetCoreAgentRuntimeApiCache;
5
6
  exports.loadCoreAgentRuntime = loadCoreAgentRuntime;
@@ -15,6 +16,35 @@ const core_runtime_1 = require("../../../core-runtime");
15
16
  const core_node_runtime_1 = require("../../../core-node-runtime");
16
17
  const win_spawn_1 = require("../../../util/win-spawn");
17
18
  const agent_runtime_package_1 = require("./agent-runtime-package");
19
+ function readWorkflowCatalog(value) {
20
+ if (!object(value) || value.type !== 'runtime-workflows' || !object(value.definitionSchema) || !Number.isSafeInteger(value.nodeKindsVersion) || Number(value.nodeKindsVersion) < 1 ||
21
+ !Array.isArray(value.nodeKinds) || value.nodeKinds.length > 128 || !Array.isArray(value.builtins) ||
22
+ !value.nodeKinds.every(piece => object(piece) && typeof piece.kind === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(piece.kind) && object(piece.paramsSchema) &&
23
+ Array.isArray(piece.outcomes) && piece.outcomes.every(label => typeof label === 'string' && /^[a-z][a-z0-9-]{0,31}$/.test(label)) &&
24
+ new Set(piece.outcomes).size === piece.outcomes.length && ['read', 'write', 'derived'].includes(String(piece.effect)) && typeof piece.requiresAI === 'boolean') ||
25
+ new Set(value.nodeKinds.map(piece => piece.kind)).size !== value.nodeKinds.length ||
26
+ !value.builtins.every(item => object(item) && typeof item.id === 'string' && typeof item.version === 'string' && typeof item.deprecated === 'boolean')) {
27
+ throw new Error('Core returned malformed workflow catalog');
28
+ }
29
+ return value;
30
+ }
31
+ function object(value) {
32
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+ /** Validation is a protocol result even when Core exits 1 for an invalid definition. */
35
+ function readDefinitionValidation(value) {
36
+ const invalid = () => { throw new Error('Core returned malformed workflow definition validation'); };
37
+ if (!object(value) || value.type !== 'runtime-definition-validated')
38
+ return invalid();
39
+ if (value.ok === true) {
40
+ if (typeof value.version !== 'string' || !/^[a-f0-9]{64}$/.test(value.version) || !object(value.definition) || value.definition.version !== value.version || !object(value.graph) || !Array.isArray(value.graph.nodes) || !Array.isArray(value.graph.edges))
41
+ return invalid();
42
+ return { ok: true, version: value.version, definition: value.definition, graph: { nodes: value.graph.nodes, edges: value.graph.edges } };
43
+ }
44
+ if (value.ok !== false || !Array.isArray(value.errors) || !value.errors.length || !value.errors.every(error => object(error) && typeof error.code === 'string' && error.code.length > 0 && typeof error.message === 'string' && ['nodeId', 'path'].every(key => error[key] === undefined || typeof error[key] === 'string')))
45
+ return invalid();
46
+ return { ok: false, errors: value.errors };
47
+ }
18
48
  /** An explicit or bundled installation is authoritative: never silently replace
19
49
  * a broken production bundle with a developer's checkout. */
20
50
  function findCoreAgentRuntimeEntry() {
@@ -73,6 +103,17 @@ async function loadCoreAgentRuntime() {
73
103
  throw new Error('Core returned malformed runtime capabilities');
74
104
  if (api.workflowVersions !== undefined && (!Array.isArray(api.workflowVersions) || !api.workflowVersions.every(value => typeof value === 'string' && /^\d+$/.test(value))))
75
105
  throw new Error('Core returned malformed workflow versions');
106
+ for (const key of ['engineVersion', 'nodeKindsVersion']) {
107
+ const value = api[key];
108
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < (key === 'engineVersion' ? 1 : 0)))
109
+ throw new Error(`Core returned malformed ${key}`);
110
+ }
111
+ if (api.engines !== undefined && (!Array.isArray(api.engines) || !api.engines.length || !api.engines.every(value => Number.isSafeInteger(value) && value >= 1) || new Set(api.engines).size !== api.engines.length))
112
+ throw new Error('Core returned malformed engines');
113
+ if (api.nodeKinds !== undefined && (!Array.isArray(api.nodeKinds) || !api.nodeKinds.every(value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(value)) || new Set(api.nodeKinds).size !== api.nodeKinds.length))
114
+ throw new Error('Core returned malformed node kinds');
115
+ if (api.builtins !== undefined && (!Array.isArray(api.builtins) || !api.builtins.every(value => object(value) && typeof value.id === 'string' && /^[a-z0-9][a-z0-9-]{0,63}$/.test(value.id) && typeof value.version === 'string' && value.version.length > 0 && value.version.length <= 128 && typeof value.deprecated === 'boolean') || new Set(api.builtins.map(value => value.id)).size !== api.builtins.length))
116
+ throw new Error('Core returned malformed builtins');
76
117
  if (api.runtimeIdentity !== undefined) {
77
118
  const identity = api.runtimeIdentity;
78
119
  if (!identity || identity.apiVersion !== 1 || !['packageVersion', 'workflowVersion', 'instructionsVersion', 'packageIntegrity'].every(key => typeof identity[key] === 'string') || !/^sha256:[a-f0-9]{64}$/.test(identity.packageIntegrity))
@@ -89,6 +130,11 @@ async function loadCoreAgentRuntime() {
89
130
  const result = JSON.parse(invoke(['capabilities', '--stdin'], JSON.stringify(input)));
90
131
  return { ...validateRoleCapabilities(result, input), runtimeIdentity: api.runtimeIdentity };
91
132
  },
133
+ listWorkflows() {
134
+ if (api.capabilities?.workflowDefinitions !== 1 || api.capabilities?.engineV2 !== 1)
135
+ throw new Error('Installed Core does not support workflow definitions. Update the paired Core package.');
136
+ return readWorkflowCatalog(JSON.parse(invoke(['workflows', 'list'])));
137
+ },
92
138
  rolePromptDefaults() {
93
139
  const result = JSON.parse(invoke(['prompts']));
94
140
  if (result.type !== 'runtime-role-prompts' || !['architect', 'developer', 'reviewer'].every(role => typeof result.defaults?.[role] === 'string' && result.defaults[role].trim()))
@@ -102,6 +148,25 @@ async function loadCoreAgentRuntime() {
102
148
  throw new Error('Core did not validate the runtime configuration');
103
149
  return input;
104
150
  },
151
+ validateWorkflowDefinition(input, options) {
152
+ if (api.capabilities?.workflowDefinitions !== 1)
153
+ throw new Error('Installed Core does not support workflow definitions. Update the paired Core package.');
154
+ const args = ['workflows', 'validate', '--stdin', ...(options?.configPath ? ['--config', options.configPath] : []), ...(options?.structural !== false && !options?.configPath ? ['--structural'] : [])];
155
+ let output;
156
+ try {
157
+ output = invoke(args, JSON.stringify(input));
158
+ }
159
+ catch (error) {
160
+ const failure = error;
161
+ if (failure.status !== 1 || typeof failure.stdout !== 'string')
162
+ throw error;
163
+ const result = readDefinitionValidation(JSON.parse(failure.stdout));
164
+ if (result.ok)
165
+ throw error;
166
+ return result;
167
+ }
168
+ return readDefinitionValidation(JSON.parse(output));
169
+ },
105
170
  };
106
171
  }
107
172
  function requireRuntimeCapabilities(api, input) {
@@ -118,8 +183,8 @@ function validateRoleCapabilities(value, input) {
118
183
  const invalid = () => { throw new Error('Core returned malformed role capabilities'); };
119
184
  const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : invalid();
120
185
  const result = object(value), config = object(input);
121
- const choices = Object.entries(object(config.agents));
122
- if (result.type !== 'runtime-capabilities' || result.schemaVersion !== 1 || !Array.isArray(result.roles) || result.roles.length > 6)
186
+ const choices = Object.entries({ ...object(config.agents), ...(config.roles === undefined ? {} : object(config.roles)) });
187
+ if (result.type !== 'runtime-capabilities' || result.schemaVersion !== 1 || !Array.isArray(result.roles))
123
188
  return invalid();
124
189
  const expected = choices.flatMap(([role, value]) => {
125
190
  const selected = object(value);
@@ -152,7 +217,7 @@ function validateRoleCapabilities(value, input) {
152
217
  return { type: 'runtime-capabilities', schemaVersion: 1, roles };
153
218
  }
154
219
  function validateRequestedRoleEfforts(runtime, config) {
155
- if (!Object.values(config.agents).some(role => role.effort !== undefined || role.escalation?.effort !== undefined))
220
+ if (![...Object.values(config.agents), ...Object.values(config.roles ?? {})].some(role => role.effort !== undefined || role.escalation?.effort !== undefined))
156
221
  return;
157
222
  if (!runtime.capabilities)
158
223
  throw new Error('Installed Core cannot validate requested role effort');
@@ -6,6 +6,7 @@ exports.applyRuntimeSelectionOrigins = applyRuntimeSelectionOrigins;
6
6
  const COUNTS = ['attempts', 'measuredAttempts', 'providerCalls', 'toolCalls', 'inputTokens', 'outputTokens', 'uncachedInputTokens', 'cacheReadInputTokens', 'cacheWriteInputTokens'];
7
7
  const KEYS = [...COUNTS, 'durationMs', 'agentDurationMs', 'costUsd'];
8
8
  const PHASES = ['architect', 'developer', 'fixer', 'verify', 'reviewer', 'archive'];
9
+ const ROLES = ['architect', 'developer', 'reviewer'];
9
10
  function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null; }
10
11
  function counters(value) {
11
12
  const input = object(value);
@@ -27,28 +28,32 @@ function counters(value) {
27
28
  return output;
28
29
  }
29
30
  /** Older Core versions omit metrics; malformed/unknown versions do not break run controls. */
30
- function readRuntimeEfficiency(value) {
31
+ function readRuntimeEfficiency(value, catalog) {
31
32
  const input = object(value);
32
- if (input?.schemaVersion !== 1 || !Array.isArray(input.phases) || input.phases.length > PHASES.length)
33
+ const stepIds = catalog?.stepIds ?? PHASES;
34
+ if (input?.schemaVersion !== 1 || !Array.isArray(input.phases) || input.phases.length > stepIds.length)
33
35
  return undefined;
34
36
  const total = counters(input.total);
35
37
  if (!total)
36
38
  return undefined;
39
+ const allowedSteps = new Set(stepIds), seenSteps = new Set();
37
40
  const phases = [];
38
41
  for (const raw of input.phases) {
39
42
  const phase = object(raw), count = counters(phase);
40
- if (!phase || !count || typeof phase.stepId !== 'string' || !PHASES.includes(phase.stepId) || phases.some(item => item.stepId === phase.stepId))
43
+ if (!phase || !count || typeof phase.stepId !== 'string' || !allowedSteps.has(phase.stepId) || seenSteps.has(phase.stepId))
41
44
  return undefined;
42
45
  for (const key of ['providers', 'models'])
43
46
  if (!Array.isArray(phase[key]) || phase[key].length > 50 || !phase[key].every((item) => typeof item === 'string' && item.length > 0 && item.length <= 256))
44
47
  return undefined;
45
48
  phases.push({ ...count, stepId: phase.stepId, providers: [...phase.providers], models: [...phase.models] });
49
+ seenSteps.add(phase.stepId);
46
50
  }
47
51
  return { schemaVersion: 1, total, phases };
48
52
  }
49
- function readRuntimeEfficiencySummary(value) {
53
+ function readRuntimeEfficiencySummary(value, catalog) {
50
54
  const input = object(value), invocations = object(input?.invocations), checks = object(input?.checks);
51
- if (!input || input.schemaVersion !== 1 || typeof input.runId !== 'string' || input.runId.length > 128 || typeof input.workflowVersion !== 'string' || input.workflowVersion.length > 16 || !['pending', 'validated', 'with-exceptions', 'blocked'].includes(String(input.technicalAcceptance)) || !invocations || !checks || !Array.isArray(input.roles) || input.roles.length > 3)
55
+ const roleIds = catalog?.roleIds ?? ROLES;
56
+ if (!input || input.schemaVersion !== 1 || typeof input.runId !== 'string' || input.runId.length > 128 || typeof input.workflowVersion !== 'string' || input.workflowVersion.length > (catalog ? 64 : 16) || !['pending', 'validated', 'with-exceptions', 'blocked'].includes(String(input.technicalAcceptance)) || !invocations || !checks || !Array.isArray(input.roles) || input.roles.length > roleIds.length)
52
57
  return undefined;
53
58
  if (typeof input.archive !== 'string' || input.archive.length > 32 || typeof input.delivery !== 'string' || input.delivery.length > 32 || typeof invocations.complete !== 'boolean' || typeof checks.available !== 'boolean')
54
59
  return undefined;
@@ -77,7 +82,7 @@ function readRuntimeEfficiencySummary(value) {
77
82
  const roles = [];
78
83
  for (const raw of input.roles) {
79
84
  const role = object(raw);
80
- if (!role || !['architect', 'developer', 'reviewer'].includes(String(role.role)) || roles.some(item => item.role === role.role))
85
+ if (!role || !roleIds.includes(String(role.role)) || roles.some(item => item.role === role.role))
81
86
  return undefined;
82
87
  const row = { role: String(role.role) };
83
88
  for (const key of ['provider', 'model', 'effort', 'observedModel', 'observedEffort', 'origin', 'tier']) {
@@ -96,7 +101,7 @@ function readRuntimeEfficiencySummary(value) {
96
101
  escalations = [];
97
102
  for (const item of input.escalations) {
98
103
  const raw = object(item);
99
- if (!raw || !['architect', 'developer', 'reviewer'].includes(String(raw.role)))
104
+ if (!raw || !roleIds.includes(String(raw.role)))
100
105
  return undefined;
101
106
  const row = { role: String(raw.role) };
102
107
  for (const key of ['attemptId', 'provider', 'model', 'effort', 'reason']) {
@@ -118,5 +123,8 @@ function applyRuntimeSelectionOrigins(summary, selection, runId) {
118
123
  return summary;
119
124
  if (!['architect', 'developer', 'reviewer'].every(role => ['project-role', 'default', ...(role === 'developer' ? ['explicit-launch-override'] : [])].includes(String(origins[role]))))
120
125
  return summary;
121
- return { ...summary, roles: summary.roles.map(role => ({ ...role, origin: String(origins[role.role]) })) };
126
+ return { ...summary, roles: summary.roles.map(role => {
127
+ const origin = origins[role.role];
128
+ return { ...role, origin: typeof origin === 'string' ? origin : role.origin };
129
+ }) };
122
130
  }