@revisium/orchestrator 0.2.0-alpha.3 → 0.2.0-alpha.5

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 (323) hide show
  1. package/README.md +4 -4
  2. package/control-plane/bootstrap.config.json +108 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +446 -1123
  4. package/control-plane/default-playbook/catalog/roles.json +1 -1
  5. package/control-plane/default-playbook/catalog/run-profiles.json +338 -0
  6. package/control-plane/default-playbook/package.json +1 -1
  7. package/control-plane/default-playbook/playbook.json +2 -1
  8. package/control-plane/default-playbook/prompts/developer.md +6 -6
  9. package/control-plane/default-playbook/prompts/integrator.md +1 -1
  10. package/dist/__generated__/client/browser.js +18 -0
  11. package/dist/__generated__/client/browser.js.map +1 -0
  12. package/dist/__generated__/client/client.js +32 -0
  13. package/dist/__generated__/client/client.js.map +1 -0
  14. package/dist/__generated__/client/commonInputTypes.js +11 -0
  15. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  16. package/dist/__generated__/client/enums.js +19 -0
  17. package/dist/__generated__/client/enums.js.map +1 -0
  18. package/dist/__generated__/client/internal/class.js +53 -0
  19. package/dist/__generated__/client/internal/class.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespace.js +450 -0
  21. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  22. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +421 -0
  23. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  24. package/dist/__generated__/client/models/Branch.js +2 -0
  25. package/dist/__generated__/client/models/Branch.js.map +1 -0
  26. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  27. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  28. package/dist/__generated__/client/models/FileBlob.js +2 -0
  29. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  30. package/dist/__generated__/client/models/InboxItem.js +2 -0
  31. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  32. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  33. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  34. package/dist/__generated__/client/models/Revision.js +2 -0
  35. package/dist/__generated__/client/models/Revision.js.map +1 -0
  36. package/dist/__generated__/client/models/RevoProject.js +2 -0
  37. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  38. package/dist/__generated__/client/models/Row.js +2 -0
  39. package/dist/__generated__/client/models/Row.js.map +1 -0
  40. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  41. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  42. package/dist/__generated__/client/models/RunEvent.js +2 -0
  43. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  44. package/dist/__generated__/client/models/RunOutput.js +2 -0
  45. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  46. package/dist/__generated__/client/models/RunTask.js +2 -0
  47. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  48. package/dist/__generated__/client/models/Table.js +2 -0
  49. package/dist/__generated__/client/models/Table.js.map +1 -0
  50. package/dist/__generated__/client/models/TableMigration.js +2 -0
  51. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  52. package/dist/__generated__/client/models/TaskRun.js +2 -0
  53. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  54. package/dist/__generated__/client/models.js +2 -0
  55. package/dist/__generated__/client/models.js.map +1 -0
  56. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  57. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  59. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  60. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  61. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  62. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +9 -0
  63. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  64. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js +54 -0
  65. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js.map +1 -0
  66. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js +37 -0
  67. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js.map +1 -0
  68. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js +32 -0
  69. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js.map +1 -0
  70. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js +33 -0
  71. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js.map +1 -0
  72. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js +59 -0
  73. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js.map +1 -0
  74. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js +33 -0
  75. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js.map +1 -0
  76. package/dist/api/graphql-api/method/method.resolver.js +76 -1
  77. package/dist/api/graphql-api/method/method.resolver.js.map +1 -1
  78. package/dist/api/graphql-api/method/model/run-profile-connection.model.js +16 -0
  79. package/dist/api/graphql-api/method/model/run-profile-connection.model.js.map +1 -0
  80. package/dist/api/graphql-api/method/model/run-profile-status.model.js +6 -0
  81. package/dist/api/graphql-api/method/model/run-profile-status.model.js.map +1 -0
  82. package/dist/api/graphql-api/method/model/run-profile.model.js +79 -0
  83. package/dist/api/graphql-api/method/model/run-profile.model.js.map +1 -0
  84. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  85. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  86. package/dist/api/graphql-api/registerGraphqlEnums.js +6 -0
  87. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  88. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -1
  89. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  90. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -1
  91. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  92. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  93. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  94. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  95. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  96. package/dist/cli/commands/doctor-report.js +1 -13
  97. package/dist/cli/commands/doctor-report.js.map +1 -1
  98. package/dist/cli/commands/lifecycle.js +50 -65
  99. package/dist/cli/commands/lifecycle.js.map +1 -1
  100. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  101. package/dist/cli/commands/process-helpers.js.map +1 -0
  102. package/dist/config.js +17 -55
  103. package/dist/config.js.map +1 -1
  104. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  105. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  106. package/dist/control-plane/bootstrap.js +25 -38
  107. package/dist/control-plane/bootstrap.js.map +1 -1
  108. package/dist/control-plane/change-notifications.js +45 -14
  109. package/dist/control-plane/change-notifications.js.map +1 -1
  110. package/dist/control-plane/data-access.js +1 -106
  111. package/dist/control-plane/data-access.js.map +1 -1
  112. package/dist/control-plane/default-playbook-policy.js +488 -94
  113. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  114. package/dist/control-plane/definitions.js +4 -5
  115. package/dist/control-plane/definitions.js.map +1 -1
  116. package/dist/control-plane/engine-transport.js +383 -0
  117. package/dist/control-plane/engine-transport.js.map +1 -0
  118. package/dist/control-plane/inbox.js +15 -7
  119. package/dist/control-plane/inbox.js.map +1 -1
  120. package/dist/control-plane/index.js +0 -1
  121. package/dist/control-plane/index.js.map +1 -1
  122. package/dist/control-plane/json-fields.js +1 -1
  123. package/dist/control-plane/json-fields.js.map +1 -1
  124. package/dist/control-plane/merge-override-audit.js +2 -2
  125. package/dist/control-plane/merge-override-audit.js.map +1 -1
  126. package/dist/control-plane/pipeline-coverage-registry.js +486 -0
  127. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  128. package/dist/control-plane/query-types.js +2 -0
  129. package/dist/control-plane/query-types.js.map +1 -0
  130. package/dist/control-plane/run-profiles.js +175 -0
  131. package/dist/control-plane/run-profiles.js.map +1 -0
  132. package/dist/control-plane/schema-migration.js +0 -35
  133. package/dist/control-plane/schema-migration.js.map +1 -1
  134. package/dist/control-plane/seed-default-playbook.js +2 -31
  135. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  136. package/dist/control-plane/tables.js +8 -0
  137. package/dist/control-plane/tables.js.map +1 -1
  138. package/dist/control-plane/transport.js +16 -0
  139. package/dist/control-plane/transport.js.map +1 -0
  140. package/dist/control-plane/versioned-meaning.js +72 -15
  141. package/dist/control-plane/versioned-meaning.js.map +1 -1
  142. package/dist/e2e/kit/agents.js +63 -8
  143. package/dist/e2e/kit/agents.js.map +1 -1
  144. package/dist/e2e/kit/assertions.js +15 -3
  145. package/dist/e2e/kit/assertions.js.map +1 -1
  146. package/dist/e2e/kit/crash.js +8 -4
  147. package/dist/e2e/kit/crash.js.map +1 -1
  148. package/dist/e2e/kit/drive.js +46 -11
  149. package/dist/e2e/kit/drive.js.map +1 -1
  150. package/dist/e2e/kit/env.js +17 -1
  151. package/dist/e2e/kit/env.js.map +1 -1
  152. package/dist/e2e/kit/fake-integrator.js +9 -22
  153. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  154. package/dist/e2e/kit/gh-emulator.js +166 -26
  155. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  156. package/dist/e2e/kit/harness.js +44 -10
  157. package/dist/e2e/kit/harness.js.map +1 -1
  158. package/dist/e2e/kit/index.js +2 -0
  159. package/dist/e2e/kit/index.js.map +1 -1
  160. package/dist/e2e/kit/run-profiles.js +50 -0
  161. package/dist/e2e/kit/run-profiles.js.map +1 -0
  162. package/dist/e2e/kit/scenario.js +207 -0
  163. package/dist/e2e/kit/scenario.js.map +1 -0
  164. package/dist/e2e/kit/scenarios.js +38 -45
  165. package/dist/e2e/kit/scenarios.js.map +1 -1
  166. package/dist/e2e/recovery-crash-child.js +2 -2
  167. package/dist/e2e/recovery-crash-child.js.map +1 -1
  168. package/dist/e2e/recovery-dd-crash-child.js +8 -6
  169. package/dist/e2e/recovery-dd-crash-child.js.map +1 -1
  170. package/dist/engine/dbos.service.js +8 -1
  171. package/dist/engine/dbos.service.js.map +1 -1
  172. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  173. package/dist/features/method/commands/handlers/method-command.handlers.js +64 -0
  174. package/dist/features/method/commands/handlers/method-command.handlers.js.map +1 -0
  175. package/dist/features/method/commands/impl/create-run-profile.command.js +7 -0
  176. package/dist/features/method/commands/impl/create-run-profile.command.js.map +1 -0
  177. package/dist/features/method/commands/impl/deprecate-run-profile.command.js +7 -0
  178. package/dist/features/method/commands/impl/deprecate-run-profile.command.js.map +1 -0
  179. package/dist/features/method/commands/impl/update-run-profile.command.js +7 -0
  180. package/dist/features/method/commands/impl/update-run-profile.command.js.map +1 -0
  181. package/dist/features/method/commands/index.js +6 -1
  182. package/dist/features/method/commands/index.js.map +1 -1
  183. package/dist/features/method/method-api.service.js +31 -3
  184. package/dist/features/method/method-api.service.js.map +1 -1
  185. package/dist/features/method/queries/handlers/method-query.handlers.js +58 -1
  186. package/dist/features/method/queries/handlers/method-query.handlers.js.map +1 -1
  187. package/dist/features/method/queries/impl/get-run-profile.query.js +7 -0
  188. package/dist/features/method/queries/impl/get-run-profile.query.js.map +1 -0
  189. package/dist/features/method/queries/impl/list-run-profiles.query.js +7 -0
  190. package/dist/features/method/queries/impl/list-run-profiles.query.js.map +1 -0
  191. package/dist/features/method/queries/impl/validate-run-profile.query.js +7 -0
  192. package/dist/features/method/queries/impl/validate-run-profile.query.js.map +1 -0
  193. package/dist/features/method/queries/index.js +4 -1
  194. package/dist/features/method/queries/index.js.map +1 -1
  195. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  196. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  197. package/dist/features/runs/queries/impl/simulate-route.query.js.map +1 -1
  198. package/dist/features/shared/connection.js +6 -3
  199. package/dist/features/shared/connection.js.map +1 -1
  200. package/dist/host/daemon.js +32 -8
  201. package/dist/host/daemon.js.map +1 -1
  202. package/dist/host/ensure-host.js +3 -3
  203. package/dist/host/ensure-host.js.map +1 -1
  204. package/dist/host/host-runtime.js.map +1 -1
  205. package/dist/host/host.lifecycle.js +3 -16
  206. package/dist/host/host.lifecycle.js.map +1 -1
  207. package/dist/http/graphql-host.js +17 -8
  208. package/dist/http/graphql-host.js.map +1 -1
  209. package/dist/mcp/mcp-capabilities.js +8 -2
  210. package/dist/mcp/mcp-capabilities.js.map +1 -1
  211. package/dist/mcp/mcp-facade.service.js +53 -10
  212. package/dist/mcp/mcp-facade.service.js.map +1 -1
  213. package/dist/mcp/mcp-tool-result.js +22 -0
  214. package/dist/mcp/mcp-tool-result.js.map +1 -0
  215. package/dist/mcp/mcp-tools.js +108 -1
  216. package/dist/mcp/mcp-tools.js.map +1 -1
  217. package/dist/pipeline/await-human.js +27 -8
  218. package/dist/pipeline/await-human.js.map +1 -1
  219. package/dist/pipeline/data-driven-task.workflow.js +573 -105
  220. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  221. package/dist/pipeline/pipeline.service.js +28 -12
  222. package/dist/pipeline/pipeline.service.js.map +1 -1
  223. package/dist/pipeline/route-contract.js +80 -39
  224. package/dist/pipeline/route-contract.js.map +1 -1
  225. package/dist/pipeline-core/condition-scopes.js +15 -0
  226. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  227. package/dist/pipeline-core/index.js +2 -0
  228. package/dist/pipeline-core/index.js.map +1 -1
  229. package/dist/pipeline-core/kit/fixtures.js +81 -44
  230. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  231. package/dist/pipeline-core/materialize.js +221 -0
  232. package/dist/pipeline-core/materialize.js.map +1 -0
  233. package/dist/pipeline-core/types.js +5 -0
  234. package/dist/pipeline-core/types.js.map +1 -1
  235. package/dist/pipeline-core/validate-loops.js +2 -15
  236. package/dist/pipeline-core/validate-loops.js.map +1 -1
  237. package/dist/pipeline-core/validate-resilience.js +257 -0
  238. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  239. package/dist/pipeline-core/validate.js +2 -0
  240. package/dist/pipeline-core/validate.js.map +1 -1
  241. package/dist/playbook/catalog-loader.js +40 -3
  242. package/dist/playbook/catalog-loader.js.map +1 -1
  243. package/dist/playbook/catalog-schema-validator.js +468 -0
  244. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  245. package/dist/playbook/import-mapper.js +55 -3
  246. package/dist/playbook/import-mapper.js.map +1 -1
  247. package/dist/playbook/manifest.js +5 -0
  248. package/dist/playbook/manifest.js.map +1 -1
  249. package/dist/playbook/playbook-installer.js +17 -1
  250. package/dist/playbook/playbook-installer.js.map +1 -1
  251. package/dist/poller/pr-readiness-core.js +257 -93
  252. package/dist/poller/pr-readiness-core.js.map +1 -1
  253. package/dist/poller/pr-readiness.js +2 -2
  254. package/dist/poller/pr-readiness.js.map +1 -1
  255. package/dist/revisium/inbox.service.js +8 -8
  256. package/dist/revisium/inbox.service.js.map +1 -1
  257. package/dist/revisium/playbooks.service.js +311 -21
  258. package/dist/revisium/playbooks.service.js.map +1 -1
  259. package/dist/revisium/revisium.module.js +8 -8
  260. package/dist/revisium/revisium.module.js.map +1 -1
  261. package/dist/revisium/roles.service.js +3 -1
  262. package/dist/revisium/roles.service.js.map +1 -1
  263. package/dist/revisium/run.service.js +9 -8
  264. package/dist/revisium/run.service.js.map +1 -1
  265. package/dist/revisium/tokens.js +0 -1
  266. package/dist/revisium/tokens.js.map +1 -1
  267. package/dist/run/create-run.js +1 -4
  268. package/dist/run/create-run.js.map +1 -1
  269. package/dist/run/inspect-run.js +33 -29
  270. package/dist/run/inspect-run.js.map +1 -1
  271. package/dist/run/issue-ref.js +97 -2
  272. package/dist/run/issue-ref.js.map +1 -1
  273. package/dist/run/prisma-runtime-data-access.js +616 -0
  274. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  275. package/dist/run/run-outputs.js +39 -14
  276. package/dist/run/run-outputs.js.map +1 -1
  277. package/dist/run/terminal-run-status.js.map +1 -1
  278. package/dist/runners/gh-identity.js +29 -7
  279. package/dist/runners/gh-identity.js.map +1 -1
  280. package/dist/runners/integrator.js +692 -89
  281. package/dist/runners/integrator.js.map +1 -1
  282. package/dist/runners/worktree.service.js +10 -8
  283. package/dist/runners/worktree.service.js.map +1 -1
  284. package/dist/schema/ajv-errors.js +15 -0
  285. package/dist/schema/ajv-errors.js.map +1 -0
  286. package/dist/smoke/isolation.js +7 -16
  287. package/dist/smoke/isolation.js.map +1 -1
  288. package/dist/storage/ensure-storage.js +118 -0
  289. package/dist/storage/ensure-storage.js.map +1 -0
  290. package/dist/storage/revo-database.js +39 -0
  291. package/dist/storage/revo-database.js.map +1 -0
  292. package/dist/storage/revo-prisma.service.js +31 -0
  293. package/dist/storage/revo-prisma.service.js.map +1 -0
  294. package/dist/storage/revo-storage.module.js +18 -0
  295. package/dist/storage/revo-storage.module.js.map +1 -0
  296. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  297. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  298. package/dist/task-control-plane/run-watch.service.js +3 -6
  299. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  300. package/dist/task-control-plane/task-control-plane-api.service.js +518 -85
  301. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  302. package/dist/testing/runtime-data-access.js +175 -0
  303. package/dist/testing/runtime-data-access.js.map +1 -0
  304. package/dist/worker/build-context.js +178 -2
  305. package/dist/worker/build-context.js.map +1 -1
  306. package/dist/worker/claude-code-runner.js +23 -6
  307. package/dist/worker/claude-code-runner.js.map +1 -1
  308. package/dist/worker/codex-runner.js +6 -1
  309. package/dist/worker/codex-runner.js.map +1 -1
  310. package/package.json +20 -7
  311. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  312. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +234 -0
  313. package/prisma/migrations/migration_lock.toml +3 -0
  314. package/prisma/schema.prisma +338 -0
  315. package/prisma.config.ts +11 -0
  316. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  317. package/dist/control-plane/client-transport.js +0 -168
  318. package/dist/control-plane/client-transport.js.map +0 -1
  319. package/dist/engine/ensure-postgres.js +0 -46
  320. package/dist/engine/ensure-postgres.js.map +0 -1
  321. package/dist/host/ensure-revisium.js +0 -135
  322. package/dist/host/ensure-revisium.js.map +0 -1
  323. package/revisium.config.json +0 -10
@@ -15,17 +15,24 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
15
15
  import { basename, join, resolve } from 'node:path';
16
16
  import { promisify } from 'node:util';
17
17
  import { Inject, Injectable } from '@nestjs/common';
18
- import { baseUrl, getConfig, isAlive, isHealthy, readRuntime } from '../cli/config.js';
18
+ import { baseUrl, getConfig, isAlive } from '../cli/config.js';
19
+ import { isGraphqlHealthy } from '../host/ensure-host.js';
20
+ import { readHostRuntime } from '../host/host-runtime.js';
19
21
  import { AgentObservabilityService } from '../observability/agent-observability.service.js';
20
22
  import { ControlPlaneError } from '../control-plane/errors.js';
23
+ import { VALID_MODEL_LEVELS } from '../control-plane/definitions.js';
21
24
  import { validateManualAdoptionAudit } from '../control-plane/manual-adoption-audit.js';
22
25
  import { validateMergeOverrideAudit } from '../control-plane/merge-override-audit.js';
23
26
  import { fnv1a64Hex } from '../control-plane/steps.js';
27
+ import { POLICY_VERSION } from '../control-plane/default-playbook-policy.js';
28
+ import { launchBindingsFromRunProfile, runProfileHash, topologyProfileFromRunProfile, topologyStageTargetsFromRunProfile, } from '../control-plane/run-profiles.js';
29
+ import { materializeTemplate, MATERIALIZER_VERSION } from '../pipeline-core/materialize.js';
24
30
  import { DbosService } from '../engine/dbos.service.js';
25
31
  import { PipelineService } from '../pipeline/pipeline.service.js';
26
- import { RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
32
+ import { INTEGRATOR_PROGRESS_EVENT_TYPES, RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
27
33
  import { templateFromExecutionPolicy } from '../pipeline/data-driven-template.js';
28
- import { normalizeExecutionProfile, normalizeParams, normalizeRouteGates, resolveRunnerForProfile, } from '../pipeline/route-contract.js';
34
+ import { normalizeParams, normalizeRouteGates, resolveBindingForRole, resolveRunnerForRole, RUNNER_PERMISSION_MODES, } from '../pipeline/route-contract.js';
35
+ import { assertValidInlineRunProfile } from '../playbook/catalog-schema-validator.js';
29
36
  import { InboxService } from '../revisium/inbox.service.js';
30
37
  import { PlaybooksService } from '../revisium/playbooks.service.js';
31
38
  import { RolesService } from '../revisium/roles.service.js';
@@ -33,10 +40,17 @@ import { RunService } from '../revisium/run.service.js';
33
40
  import { CreateRunWorkflowError, previewCreateRunIds, } from '../run/create-run.js';
34
41
  import { PrReadinessService } from './pr-readiness.service.js';
35
42
  const execFileAsync = promisify(execFile);
36
- const GATE_TOPICS = new Set(['plan', 'merge', 'question']);
37
- const WORKFLOW_SUCCESS_EVENT_TYPES = new Set(['step_succeeded', 'gate_signaled']);
43
+ const GATE_TOPICS = new Set(['plan', 'merge', 'question', 'retry']);
44
+ const WORKFLOW_SUCCESS_EVENT_TYPES = new Set(['step_succeeded', 'gate_signaled', 'question_signaled']);
38
45
  const WORKFLOW_FAILURE_EVENT_TYPES = new Set(['step_failed', 'attempt_failed']);
39
- const BUILTIN_RUNNERS = new Set(['claude-code', 'codex', 'script', 'stub-agent', 'revo-integrator', 'revo-merger', 'revo-deterministic']);
46
+ export const WORKFLOW_PROGRESS_EVENT_TYPES = new Set([
47
+ 'pipeline_blocked',
48
+ 'question_signal_pending',
49
+ 'question_signaled',
50
+ 'pr_polled',
51
+ ...INTEGRATOR_PROGRESS_EVENT_TYPES,
52
+ ]);
53
+ const BUILTIN_RUNNERS = new Set(['claude-code', 'codex', 'script', 'stub-agent']);
40
54
  function asErrorMessage(error) {
41
55
  if (error instanceof Error)
42
56
  return error.message;
@@ -47,6 +61,9 @@ function asRecord(value) {
47
61
  return null;
48
62
  return value;
49
63
  }
64
+ function nonEmptyString(value) {
65
+ return typeof value === 'string' && value.length > 0 ? value : null;
66
+ }
50
67
  function dateOrEpoch(value) {
51
68
  if (value instanceof Date)
52
69
  return Number.isNaN(value.getTime()) ? new Date(0) : value;
@@ -64,6 +81,39 @@ function gateTopic(item) {
64
81
  return null;
65
82
  return topic;
66
83
  }
84
+ function gateSignalTopic(item, topic) {
85
+ const context = asRecord(item.context);
86
+ const signalTopic = context?.signalTopic;
87
+ return typeof signalTopic === 'string' && signalTopic.length > 0 ? signalTopic : topic;
88
+ }
89
+ function questionSignalTopic(item) {
90
+ if (item.kind !== 'question' || !item.runId)
91
+ return null;
92
+ const context = asRecord(item.context);
93
+ if (context?.topic !== 'question')
94
+ return null;
95
+ const signalTopic = context.signalTopic;
96
+ return typeof signalTopic === 'string' && signalTopic.length > 0 ? signalTopic : null;
97
+ }
98
+ function questionSummary(item) {
99
+ const context = asRecord(item.context);
100
+ return asRecord(context?.summary);
101
+ }
102
+ function questionSignalTaskId(item) {
103
+ if (item.taskId)
104
+ return item.taskId;
105
+ const taskId = questionSummary(item)?.taskId;
106
+ return typeof taskId === 'string' ? taskId : '';
107
+ }
108
+ function questionSignalStepKey(item, signalTopic) {
109
+ const step = questionSummary(item)?.step;
110
+ if (typeof step === 'string' && step.length > 0)
111
+ return step;
112
+ return signalTopic.startsWith('question:') ? signalTopic : `question:${signalTopic}`;
113
+ }
114
+ function questionSignalResolvedBy(item, answer, fallbackResolvedBy) {
115
+ return nonEmptyString(item.resolvedBy) ?? nonEmptyString(asRecord(answer)?.resolvedBy) ?? fallbackResolvedBy;
116
+ }
67
117
  function gateDeclaredOutcomes(item) {
68
118
  const context = asRecord(item.context);
69
119
  const summary = asRecord(context?.summary);
@@ -76,6 +126,38 @@ function gateDeclaredOutcomes(item) {
76
126
  function gateOutcomes(item) {
77
127
  return gateDeclaredOutcomes(item);
78
128
  }
129
+ function gateSummaryNodeId(item) {
130
+ const context = asRecord(item.context);
131
+ const summary = asRecord(context?.summary);
132
+ const nodeId = summary?.nodeId;
133
+ return typeof nodeId === 'string' ? nodeId : undefined;
134
+ }
135
+ function isQuestionGateReasonOutcome(item, outcome) {
136
+ if (outcome !== 'fix' && outcome !== 'wontfix')
137
+ return false;
138
+ if (gateSummaryNodeId(item) === 'questionGate')
139
+ return true;
140
+ const outcomes = gateOutcomes(item);
141
+ return gateTopic(item) === 'question' && outcomes.includes('fix') && outcomes.includes('wontfix');
142
+ }
143
+ function assertRequiredGateNote(item, outcome, note) {
144
+ if (outcome === 'approve_anyway' && !note) {
145
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'approve_anyway requires a non-empty note');
146
+ }
147
+ if (outcome === 'override_merge' && !note) {
148
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'override_merge requires a non-empty note');
149
+ }
150
+ if (isQuestionGateReasonOutcome(item, outcome) && !note) {
151
+ throw new ControlPlaneError('VALIDATION_FAILURE', `questionGate ${outcome} requires a non-empty note`);
152
+ }
153
+ }
154
+ function normalizeGateReconcile(value) {
155
+ if (value === undefined)
156
+ return undefined;
157
+ if (value === 'keep')
158
+ return value;
159
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'gate reconcile must be keep');
160
+ }
79
161
  async function git(cwd, args) {
80
162
  try {
81
163
  const result = await execFileAsync('git', args, {
@@ -136,13 +218,11 @@ function summarizeAttempts(attempts) {
136
218
  function normalizedRunStatus(status) {
137
219
  return status === 'paused' ? 'blocked' : status;
138
220
  }
139
- function hasWorkflowProgress(events) {
221
+ export function hasWorkflowProgress(events) {
140
222
  return events.some((event) => (event.type.startsWith('step_')
141
223
  || event.type.startsWith('attempt_')
142
224
  || event.type.startsWith('gate_')
143
- || event.type === 'pipeline_blocked'
144
- || event.type === 'pr_polled'
145
- || event.type === 'integrate_succeeded'));
225
+ || WORKFLOW_PROGRESS_EVENT_TYPES.has(event.type)));
146
226
  }
147
227
  function observedRunStatus(rowStatus, workflowStatus, events) {
148
228
  if (rowStatus !== 'ready')
@@ -215,12 +295,6 @@ function optionalString(value) {
215
295
  function optionalInteger(value) {
216
296
  return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
217
297
  }
218
- function requiredRecord(value, field, runId) {
219
- const record = asRecord(value);
220
- if (record)
221
- return record;
222
- throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent ${field} is not a record`);
223
- }
224
298
  function optionalRecord(value) {
225
299
  return asRecord(value) ?? {};
226
300
  }
@@ -385,19 +459,15 @@ function isRouteDecision(value) {
385
459
  const record = asRecord(value);
386
460
  return Boolean(record?.playbookId && record?.pipelineRowId && Array.isArray(record.roleBindings));
387
461
  }
388
- function assertRunnerAvailable(runnerId, profile, roleId) {
389
- const available = profile.availableRunners;
390
- if (available && !available.includes(runnerId)) {
391
- throw new ControlPlaneError('VALIDATION_FAILURE', `runner unavailable for role ${roleId}: ${runnerId}`);
392
- }
393
- if (!available && !BUILTIN_RUNNERS.has(runnerId)) {
462
+ function assertRunnerAvailable(runnerId, roleId) {
463
+ if (!BUILTIN_RUNNERS.has(runnerId)) {
394
464
  throw new ControlPlaneError('VALIDATION_FAILURE', `runner implementation is not registered for role ${roleId}: ${runnerId}`);
395
465
  }
396
466
  }
397
467
  function assertProductionRunnerBinding(runnerId, runnerSource, roleId) {
398
- if (runnerId !== 'stub-agent' || runnerSource === 'execution-profile')
468
+ if (runnerId !== 'stub-agent' || runnerSource === 'profile')
399
469
  return;
400
- throw new ControlPlaneError('VALIDATION_FAILURE', `role ${roleId} binds production runner stub-agent; use an execution profile override for test stubs`);
470
+ throw new ControlPlaneError('VALIDATION_FAILURE', `role ${roleId} binds production runner stub-agent; use a run profile binding for test stubs`);
401
471
  }
402
472
  let TaskControlPlaneApiService = class TaskControlPlaneApiService {
403
473
  runs;
@@ -419,17 +489,17 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
419
489
  this.prReadiness = prReadiness;
420
490
  }
421
491
  async getStatus() {
422
- const runtime = readRuntime();
423
- const alive = runtime ? isAlive(runtime.pid) : false;
424
- const healthy = runtime && alive ? await isHealthy(runtime.httpPort) : false;
492
+ const host = readHostRuntime();
493
+ const hostAlive = host ? isAlive(host.pid) : false;
494
+ const hostHealthy = host && hostAlive ? await isGraphqlHealthy(host.graphqlPort) : false;
425
495
  return {
426
496
  daemon: {
427
- running: Boolean(runtime && alive),
428
- healthy,
429
- pid: runtime?.pid ?? null,
430
- baseUrl: runtime ? baseUrl(runtime.httpPort) : null,
431
- httpPort: runtime?.httpPort ?? null,
432
- pgPort: runtime?.pgPort ?? null,
497
+ running: Boolean(host && hostAlive),
498
+ healthy: hostHealthy,
499
+ pid: host?.pid ?? null,
500
+ baseUrl: host ? `${baseUrl(host.graphqlPort)}/graphql` : null,
501
+ graphqlPort: host?.graphqlPort ?? null,
502
+ mcpPort: host?.mcpPort ?? null,
433
503
  },
434
504
  project: this.getProject(),
435
505
  };
@@ -438,9 +508,9 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
438
508
  const status = await this.getStatus();
439
509
  const issues = [];
440
510
  if (!status.daemon.running)
441
- issues.push('Local Revisium daemon is not running.');
511
+ issues.push('Revo host daemon is not running.');
442
512
  if (status.daemon.running && !status.daemon.healthy)
443
- issues.push('Local Revisium daemon is running but unhealthy.');
513
+ issues.push('Revo host daemon is running but unhealthy.');
444
514
  try {
445
515
  await this.roles.loadPipelinePolicy();
446
516
  }
@@ -523,10 +593,12 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
523
593
  scope: input.scope,
524
594
  playbookId: input.playbookId,
525
595
  pipelineId: input.pipelineId,
596
+ profileId: input.profileId,
597
+ profile: input.profile,
526
598
  params: input.params,
527
599
  issueRef: input.issueRef,
528
- executionProfile: input.executionProfile,
529
- source: input.pipelineId ? 'explicit' : 'deterministic-installed-playbook',
600
+ issueAction: input.issueAction,
601
+ source: 'explicit',
530
602
  });
531
603
  const result = await this.runs.createRun({
532
604
  title: input.title,
@@ -540,7 +612,6 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
540
612
  params: route.params,
541
613
  issueRef: route.params.issueRef,
542
614
  routeDecision: route,
543
- executionProfile: route.executionProfile,
544
615
  });
545
616
  if (!input.start)
546
617
  return { ...result, started: false, route };
@@ -655,9 +726,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
655
726
  if (!isRouteDecision(routeDecision)) {
656
727
  throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${parentRunId}: parent route_decision is invalid`);
657
728
  }
658
- const executionProfile = requiredRecord(parentData.execution_profile, 'execution_profile', parentRunId);
659
729
  const idSuffix = fnv1a64Hex(`${parentRunId}|${blockedEvent.eventId}`).slice(0, 8);
660
- return { title, repo, description, scope, priority, role: recoveryRole, playbookId, pipelineId, params, routeDecision, executionProfile, now, idSuffix };
730
+ return { title, repo, description, scope, priority, role: recoveryRole, playbookId, pipelineId, params, routeDecision, now, idSuffix };
661
731
  }
662
732
  async createRecoveryRunOrReuseExpected(input, expected) {
663
733
  try {
@@ -815,6 +885,16 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
815
885
  routeGates: route.routeGates,
816
886
  activeNodeIds: activeIds,
817
887
  status: cursorStatus(progress),
888
+ provenance: Object.fromEntries([
889
+ ['requestedPipelineId', route.requestedPipelineId],
890
+ ['basePipelineId', route.basePipelineId],
891
+ ['profileId', route.profileId],
892
+ ['profileVersion', route.profileVersion],
893
+ ['profileHash', route.profileHash],
894
+ ['materializedTemplateHash', route.materializedTemplateHash],
895
+ ['materializerVersion', route.materializerVersion],
896
+ ['policyVersion', route.policyVersion],
897
+ ].filter(([, v]) => v !== undefined)),
818
898
  },
819
899
  nodes,
820
900
  edges,
@@ -987,7 +1067,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
987
1067
  async approveGate(input) {
988
1068
  const item = await this.getInboxItem(input.inboxId);
989
1069
  const outcomes = gateDeclaredOutcomes(item);
990
- this.assertLegacyGateWrapperAllowed(input.inboxId, outcomes);
1070
+ this.assertLegacyGateWrapperAllowed(item, input.inboxId, outcomes);
991
1071
  if (outcomes.length === 0 || (outcomes.length === 1 && outcomes[0] === 'approved')) {
992
1072
  const resolvedBy = input.resolvedBy ?? 'mcp';
993
1073
  return this.resolveLegacyGate(item, { decision: 'approve', resolvedBy }, resolvedBy, input.inboxId);
@@ -1001,7 +1081,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1001
1081
  async rejectGate(input) {
1002
1082
  const item = await this.getInboxItem(input.inboxId);
1003
1083
  const outcomes = gateDeclaredOutcomes(item);
1004
- this.assertLegacyGateWrapperAllowed(input.inboxId, outcomes);
1084
+ this.assertLegacyGateWrapperAllowed(item, input.inboxId, outcomes);
1005
1085
  if (outcomes.length === 0) {
1006
1086
  const resolvedBy = input.resolvedBy ?? 'mcp';
1007
1087
  return this.resolveLegacyGate(item, { decision: 'reject', resolvedBy }, resolvedBy, input.inboxId);
@@ -1017,7 +1097,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1017
1097
  const item = await this.getInboxItem(input.inboxId);
1018
1098
  const topic = gateTopic(item);
1019
1099
  if (!topic) {
1020
- throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is not a plan or merge gate: ${input.inboxId}`);
1100
+ throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is not a named approval gate: ${input.inboxId}`);
1021
1101
  }
1022
1102
  const outcomes = gateOutcomes(item);
1023
1103
  if (outcomes.length === 0) {
@@ -1028,9 +1108,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1028
1108
  throw new ControlPlaneError('VALIDATION_FAILURE', `invalid gate outcome ${outcome}; expected one of: ${outcomes.join(', ')}`);
1029
1109
  }
1030
1110
  const note = input.note?.trim();
1031
- if (outcome === 'approve_anyway' && !note) {
1032
- throw new ControlPlaneError('VALIDATION_FAILURE', 'approve_anyway requires a non-empty note');
1033
- }
1111
+ const reconcile = normalizeGateReconcile(input.reconcile);
1112
+ assertRequiredGateNote(item, outcome, note);
1034
1113
  const adoptionAudit = outcome === 'adopt_patch_manually'
1035
1114
  ? validateManualAdoptionAudit(input.adoptionAudit, item)
1036
1115
  : undefined;
@@ -1041,6 +1120,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1041
1120
  const answer = {
1042
1121
  outcome,
1043
1122
  ...(note ? { note } : {}),
1123
+ ...(reconcile ? { reconcile } : {}),
1044
1124
  ...(adoptionAudit ? { adoptionAudit } : {}),
1045
1125
  ...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
1046
1126
  resolvedBy,
@@ -1058,8 +1138,12 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1058
1138
  runId: item.runId,
1059
1139
  };
1060
1140
  }
1061
- assertLegacyGateWrapperAllowed(inboxId, outcomes) {
1062
- if (outcomes.length > 2 || outcomes.includes('approve_anyway')) {
1141
+ assertLegacyGateWrapperAllowed(item, inboxId, outcomes) {
1142
+ if (outcomes.length > 2
1143
+ || outcomes.includes('approve_anyway')
1144
+ || outcomes.includes('retry')
1145
+ || outcomes.includes('give_up')
1146
+ || outcomes.some((outcome) => isQuestionGateReasonOutcome(item, outcome))) {
1063
1147
  throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item has named gate outcomes; use resolve_gate with an explicit outcome: ${inboxId}`);
1064
1148
  }
1065
1149
  }
@@ -1090,12 +1174,27 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1090
1174
  if (gateTopic(item)) {
1091
1175
  throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is a gate; use resolve_gate, approve_gate, or reject_gate: ${input.inboxId}`);
1092
1176
  }
1093
- return this.resolveInboxItem({
1177
+ const resolvedBy = input.resolvedBy ?? 'mcp';
1178
+ const answerRecord = asRecord(input.answer);
1179
+ if (typeof answerRecord?.outcome === 'string' && answerRecord.outcome.trim() === 'adopt_patch_manually') {
1180
+ validateManualAdoptionAudit(answerRecord.adoptionAudit, item);
1181
+ }
1182
+ const result = await this.inbox.resolveInbox(input.inboxId, input.answer, resolvedBy);
1183
+ const signalTopic = questionSignalTopic(item);
1184
+ const shouldSignal = signalTopic !== null;
1185
+ if (signalTopic) {
1186
+ const signalItem = result.status === 'pending' ? await this.getInboxItem(input.inboxId) : item;
1187
+ const signalResolvedBy = questionSignalResolvedBy(signalItem, result.answer, resolvedBy);
1188
+ await this.signalQuestion(signalItem, signalTopic, result.answer, input.inboxId, signalResolvedBy);
1189
+ }
1190
+ return {
1094
1191
  inboxId: input.inboxId,
1095
- answer: input.answer,
1096
- resolvedBy: input.resolvedBy ?? 'mcp',
1097
- signalGate: false,
1098
- });
1192
+ previousStatus: result.status,
1193
+ answer: result.answer,
1194
+ signaled: shouldSignal,
1195
+ topic: shouldSignal ? 'question' : null,
1196
+ runId: item.runId,
1197
+ };
1099
1198
  }
1100
1199
  async resolveInboxItem(input) {
1101
1200
  const item = await this.getInboxItem(input.inboxId);
@@ -1116,9 +1215,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1116
1215
  }
1117
1216
  this.assertGateOutcome(item, outcome);
1118
1217
  const note = typeof answer?.note === 'string' ? answer.note.trim() : '';
1119
- if (outcome === 'approve_anyway' && !note) {
1120
- throw new ControlPlaneError('VALIDATION_FAILURE', 'approve_anyway requires a non-empty note');
1121
- }
1218
+ const reconcile = normalizeGateReconcile(answer.reconcile);
1219
+ assertRequiredGateNote(item, outcome, note);
1122
1220
  const adoptionAudit = outcome === 'adopt_patch_manually'
1123
1221
  ? validateManualAdoptionAudit(answer.adoptionAudit, item)
1124
1222
  : undefined;
@@ -1129,6 +1227,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1129
1227
  ...answer,
1130
1228
  outcome,
1131
1229
  ...(typeof answer.note === 'string' ? { note } : {}),
1230
+ ...(reconcile ? { reconcile } : {}),
1132
1231
  ...(adoptionAudit ? { adoptionAudit } : {}),
1133
1232
  ...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
1134
1233
  };
@@ -1149,18 +1248,35 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1149
1248
  };
1150
1249
  }
1151
1250
  async signalGate(item, topic, answer, inboxId) {
1251
+ const signalTopic = gateSignalTopic(item, topic);
1152
1252
  const eventBase = {
1153
1253
  runId: item.runId,
1154
1254
  taskId: item.taskId,
1155
1255
  stepId: item.stepId,
1156
1256
  stepKey: `gate:${topic}`,
1157
1257
  actor: 'mcp',
1158
- payload: { inboxId, topic },
1258
+ payload: { inboxId, topic, ...(signalTopic !== topic ? { signalTopic } : {}) },
1159
1259
  };
1160
1260
  await this.runs.appendEvent({ ...eventBase, type: 'gate_signal_pending' });
1161
- await this.dbos.signal(item.runId, topic, answer, inboxId);
1261
+ await this.dbos.signal(item.runId, signalTopic, answer, inboxId);
1162
1262
  await this.runs.appendEvent({ ...eventBase, type: 'gate_signaled' });
1163
1263
  }
1264
+ async signalQuestion(item, signalTopic, answer, inboxId, resolvedBy) {
1265
+ const stepKey = questionSignalStepKey(item, signalTopic);
1266
+ const eventBase = {
1267
+ runId: item.runId,
1268
+ taskId: questionSignalTaskId(item),
1269
+ stepId: item.stepId,
1270
+ stepKey,
1271
+ actor: 'mcp',
1272
+ idempotencyKey: inboxId,
1273
+ payload: { inboxId, topic: 'question', signalTopic, stepKey },
1274
+ };
1275
+ const signalPayload = { answer, resolvedBy, inboxId };
1276
+ await this.runs.appendEvent({ ...eventBase, type: 'question_signal_pending' });
1277
+ await this.dbos.signal(item.runId, signalTopic, signalPayload, inboxId);
1278
+ await this.runs.appendEvent({ ...eventBase, type: 'question_signaled' });
1279
+ }
1164
1280
  async summarizeGateRisk(inboxId) {
1165
1281
  const item = await this.getInboxItem(inboxId);
1166
1282
  return {
@@ -1188,6 +1304,83 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1188
1304
  listPipelines() {
1189
1305
  return this.playbooks.listPipelines();
1190
1306
  }
1307
+ listProfiles(input = {}) {
1308
+ return this.playbooks.listRunProfiles(input);
1309
+ }
1310
+ listProfilesPage(input) {
1311
+ return this.playbooks.listRunProfilesPage(input);
1312
+ }
1313
+ async getProfile(input) {
1314
+ const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
1315
+ const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
1316
+ return this.playbooks.resolveRunProfile({
1317
+ playbookId: playbook.id,
1318
+ pipelineId: pipeline.pipelineId,
1319
+ profileId: input.profileId,
1320
+ includeDeprecated: true,
1321
+ });
1322
+ }
1323
+ validateProfile(input) {
1324
+ return this.validateRunProfileForPipeline(input);
1325
+ }
1326
+ async createProfile(input) {
1327
+ const route = await this.validateRunProfileForPipeline(input);
1328
+ const profile = asRecord(route.profileSnapshot);
1329
+ if (!profile)
1330
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'validated profile snapshot is missing');
1331
+ return this.playbooks.createRunProfile({
1332
+ playbookId: route.playbookId,
1333
+ pipelineId: route.basePipelineId ?? route.pipelineId,
1334
+ profileId: input.profileId,
1335
+ displayName: input.displayName,
1336
+ summary: input.summary,
1337
+ profile,
1338
+ status: input.status,
1339
+ });
1340
+ }
1341
+ async updateProfile(input) {
1342
+ let playbookId;
1343
+ let pipelineId = input.pipelineId;
1344
+ let profile;
1345
+ if (input.profile !== undefined) {
1346
+ const route = await this.validateRunProfileForPipeline({
1347
+ playbookId: input.playbookId,
1348
+ pipelineId: input.pipelineId,
1349
+ profile: input.profile,
1350
+ });
1351
+ playbookId = route.playbookId;
1352
+ pipelineId = route.basePipelineId ?? route.pipelineId;
1353
+ profile = asRecord(route.profileSnapshot) ?? undefined;
1354
+ if (!profile)
1355
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'validated profile snapshot is missing');
1356
+ }
1357
+ else {
1358
+ const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
1359
+ const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
1360
+ playbookId = playbook.id;
1361
+ pipelineId = pipeline.pipelineId;
1362
+ }
1363
+ return this.playbooks.updateRunProfile({
1364
+ playbookId,
1365
+ pipelineId,
1366
+ profileId: input.profileId,
1367
+ expectedProfileRevisionHash: input.expectedProfileRevisionHash,
1368
+ displayName: input.displayName,
1369
+ summary: input.summary,
1370
+ profile,
1371
+ status: input.status,
1372
+ });
1373
+ }
1374
+ async deprecateProfile(input) {
1375
+ const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
1376
+ const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
1377
+ return this.playbooks.deprecateRunProfile({
1378
+ playbookId: playbook.id,
1379
+ pipelineId: pipeline.pipelineId,
1380
+ profileId: input.profileId,
1381
+ expectedProfileRevisionHash: input.expectedProfileRevisionHash,
1382
+ });
1383
+ }
1191
1384
  async getPipeline(pipelineId) {
1192
1385
  const result = await this.playbooks.getPipeline(pipelineId);
1193
1386
  if (!result)
@@ -1200,8 +1393,10 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1200
1393
  repo: input.repo ?? '',
1201
1394
  playbookId: input.playbookId,
1202
1395
  pipelineId: input.pipeline,
1396
+ profileId: input.profileId,
1397
+ profile: input.profile,
1203
1398
  params: input.params,
1204
- source: input.pipeline ? 'explicit' : 'deterministic-installed-playbook',
1399
+ source: 'explicit',
1205
1400
  });
1206
1401
  }
1207
1402
  async previewPipelineSelection(input) {
@@ -1221,40 +1416,269 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1221
1416
  async routeForRun(run) {
1222
1417
  if (isRouteDecision(run.data.route_decision))
1223
1418
  return run.data.route_decision;
1224
- return this.resolveRouteDecision({
1225
- title: typeof run.data.title === 'string' ? run.data.title : 'Run',
1226
- repo: Array.isArray(run.data.repos) && typeof run.data.repos[0] === 'string' ? run.data.repos[0] : '',
1227
- description: typeof run.data.description === 'string' ? run.data.description : '',
1228
- scope: typeof run.data.scope === 'string' ? run.data.scope : '',
1229
- playbookId: typeof run.data.playbook_id === 'string' ? run.data.playbook_id : undefined,
1230
- pipelineId: typeof run.data.pipeline_id === 'string' ? run.data.pipeline_id : undefined,
1231
- params: run.data.params,
1232
- source: run.data.pipeline_id ? 'explicit' : 'deterministic-installed-playbook',
1233
- });
1419
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'run route_decision is missing or invalid');
1234
1420
  }
1235
1421
  async resolveRouteDecision(input) {
1236
- const params = normalizeParams(input.params, input.issueRef);
1237
- const executionProfile = normalizeExecutionProfile(input.executionProfile);
1422
+ const params = normalizeParams(input.params, input.issueRef, input.issueAction);
1238
1423
  const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
1239
- const pipeline = input.pipelineId
1240
- ? await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId })
1241
- : await this.resolveAutoPipeline(playbook.id, [input.title, input.description, input.scope].join(' '));
1242
- const roleBindings = await this.resolveRouteRoles(playbook.id, pipeline, executionProfile);
1424
+ const requestedPipelineId = input.pipelineId?.trim();
1425
+ if (!requestedPipelineId) {
1426
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'pipelineId is required');
1427
+ }
1428
+ const hasProfileId = input.profileId !== undefined;
1429
+ const hasInlineProfile = input.profile !== undefined;
1430
+ if (hasProfileId === hasInlineProfile) {
1431
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'exactly one of profileId or profile is required');
1432
+ }
1433
+ if (input.profileId?.trim() === '') {
1434
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'profileId must be a non-empty string');
1435
+ }
1436
+ const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: requestedPipelineId });
1437
+ let profileSnapshot;
1438
+ let profileHash;
1439
+ let provenanceFields;
1440
+ if (input.profileId !== undefined) {
1441
+ const storedProfile = await this.playbooks.resolveRunProfile({
1442
+ playbookId: playbook.id,
1443
+ pipelineId: pipeline.pipelineId,
1444
+ profileId: input.profileId,
1445
+ });
1446
+ profileSnapshot = storedProfile.profile;
1447
+ this.assertStoredRunProfileValid(storedProfile);
1448
+ profileHash = runProfileHash(profileSnapshot, {
1449
+ pipelineId: storedProfile.pipelineId,
1450
+ schemaVersion: storedProfile.schemaVersion,
1451
+ });
1452
+ if (storedProfile.profileHash !== profileHash) {
1453
+ throw new ControlPlaneError('VALIDATION_FAILURE', `stored run profile ${storedProfile.profileId} hash mismatch: expected ${storedProfile.profileHash}, got ${profileHash}`);
1454
+ }
1455
+ provenanceFields = {
1456
+ profileSource: 'stored',
1457
+ profileId: storedProfile.profileId,
1458
+ profileVersion: storedProfile.version,
1459
+ };
1460
+ }
1461
+ else {
1462
+ const inlineProfile = asRecord(input.profile);
1463
+ if (!inlineProfile) {
1464
+ throw new ControlPlaneError('VALIDATION_FAILURE', 'profile must be an object');
1465
+ }
1466
+ try {
1467
+ assertValidInlineRunProfile(inlineProfile, 'profile');
1468
+ }
1469
+ catch (error) {
1470
+ const message = error instanceof Error ? error.message : String(error);
1471
+ throw new ControlPlaneError('VALIDATION_FAILURE', message);
1472
+ }
1473
+ profileSnapshot = inlineProfile;
1474
+ profileHash = runProfileHash(profileSnapshot, { pipelineId: pipeline.pipelineId });
1475
+ provenanceFields = { profileSource: 'inline' };
1476
+ }
1477
+ const topologyProfile = topologyProfileFromRunProfile(profileSnapshot, {
1478
+ pipelineId: pipeline.pipelineId,
1479
+ profileId: provenanceFields.profileId ?? 'inline',
1480
+ });
1481
+ const baseTemplate = pipeline.executionPolicy.template_json;
1482
+ if (!baseTemplate) {
1483
+ throw new ControlPlaneError('VALIDATION_FAILURE', `pipeline "${pipeline.pipelineId}" carries no template_json`);
1484
+ }
1485
+ this.assertRunProfileStagesClosed(profileSnapshot, baseTemplate);
1486
+ const { template: materializedTemplate, materializedTemplateHash, diagnostics } = materializeTemplate(baseTemplate, topologyProfile, { allowlist: topologyProfile.toggles.map((toggle) => toggle.target) });
1487
+ if (diagnostics.length > 0) {
1488
+ throw new ControlPlaneError('VALIDATION_FAILURE', `run profile cannot be materialized: ${diagnostics.map((item) => item.message).join('; ')}`, { details: { diagnostics } });
1489
+ }
1490
+ const executionPolicy = { ...asRecord(pipeline.executionPolicy), template_json: materializedTemplate };
1491
+ const launchBindings = launchBindingsFromRunProfile(profileSnapshot, {
1492
+ lifecycleNodeIds: Object.entries(materializedTemplate.nodes)
1493
+ .filter(([, node]) => node.kind === 'script')
1494
+ .map(([nodeId]) => nodeId),
1495
+ });
1496
+ const allRoles = (await this.roles.listRoles()).filter((role) => role.playbookId === playbook.id);
1497
+ await this.assertLaunchBindingsClosed(launchBindings, allRoles, materializedTemplate);
1498
+ const roleBindings = await this.resolveRouteRoles(playbook.id, pipeline, launchBindings, allRoles);
1243
1499
  return {
1244
1500
  playbookId: playbook.id,
1245
- pipelineId: pipeline.pipelineId,
1501
+ pipelineId: requestedPipelineId,
1246
1502
  pipelineRowId: pipeline.id,
1247
1503
  source: input.source,
1248
1504
  roles: roleBindings.map((binding) => binding.roleId),
1249
1505
  requiredRoles: pipeline.requiredRoles,
1250
1506
  optionalRoles: pipeline.optionalRoles,
1251
1507
  routeGates: normalizeRouteGates(pipeline.routeGates),
1252
- executionPolicy: pipeline.executionPolicy,
1253
- executionProfile,
1508
+ executionPolicy,
1509
+ launchBindings,
1254
1510
  roleBindings,
1255
1511
  params,
1512
+ requestedPipelineId,
1513
+ basePipelineId: pipeline.pipelineId,
1514
+ profileHash,
1515
+ profileSnapshot,
1516
+ materializedTemplateHash,
1517
+ materializedTemplate,
1518
+ materializerVersion: MATERIALIZER_VERSION,
1519
+ policyVersion: POLICY_VERSION,
1520
+ ...provenanceFields,
1256
1521
  };
1257
1522
  }
1523
+ async validateRunProfileForPipeline(input) {
1524
+ return this.resolveRouteDecision({
1525
+ title: 'Run profile validation',
1526
+ repo: '',
1527
+ playbookId: input.playbookId,
1528
+ pipelineId: input.pipelineId,
1529
+ profile: input.profile,
1530
+ source: 'explicit',
1531
+ });
1532
+ }
1533
+ assertStoredRunProfileValid(storedProfile) {
1534
+ try {
1535
+ assertValidInlineRunProfile(storedProfile.profile, `run_profiles.${storedProfile.profileId}.profile_json`);
1536
+ }
1537
+ catch (error) {
1538
+ throw new ControlPlaneError('VALIDATION_FAILURE', asErrorMessage(error));
1539
+ }
1540
+ }
1541
+ assertRunProfileStagesClosed(profileSnapshot, template) {
1542
+ const nodeIds = new Set(Object.keys(template.nodes));
1543
+ for (const stage of topologyStageTargetsFromRunProfile(profileSnapshot)) {
1544
+ if (nodeIds.has(stage))
1545
+ continue;
1546
+ throw this.profileSchemaClosed(`topology stage "${stage}" does not exist in pipeline ${template.pipelineId}`);
1547
+ }
1548
+ }
1549
+ async assertLaunchBindingsClosed(launchBindings, roles, template) {
1550
+ if (launchBindings.length === 0)
1551
+ return;
1552
+ const byPlaybookRole = new Map(roles.map((r) => [r.playbookRoleId || r.name, r]));
1553
+ const nodes = template.nodes;
1554
+ const cachedProfiles = new Map();
1555
+ for (const override of launchBindings) {
1556
+ const matchLabel = JSON.stringify(override.match);
1557
+ this.assertBindingMatchIsClosed(override, matchLabel);
1558
+ this.assertBindingTargetIsKnown(override, matchLabel, byPlaybookRole, nodes);
1559
+ this.assertScriptBindingShape(override, matchLabel, nodes);
1560
+ this.assertBindingAccounts(override, matchLabel, nodes);
1561
+ this.assertRegisteredRunner(override.match.runnerId, matchLabel);
1562
+ this.assertRegisteredRunner(override.runnerId, matchLabel);
1563
+ await this.assertModelLevelAvailable(override.modelLevel, matchLabel, cachedProfiles);
1564
+ this.assertBindingTimeout(override, matchLabel);
1565
+ this.assertBindingPermissionMode(override, matchLabel, byPlaybookRole, launchBindings);
1566
+ }
1567
+ }
1568
+ profileSchemaClosed(message) {
1569
+ return new ControlPlaneError('VALIDATION_FAILURE', `PROFILE_SCHEMA_CLOSED: ${message}`);
1570
+ }
1571
+ assertBindingMatchIsClosed(override, matchLabel) {
1572
+ const { roleId, nodeId, runnerId } = override.match;
1573
+ if (roleId || nodeId || runnerId)
1574
+ return;
1575
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} must specify at least one of roleId, nodeId, runnerId`);
1576
+ }
1577
+ assertBindingTargetIsKnown(override, matchLabel, byPlaybookRole, nodes) {
1578
+ const { roleId, nodeId } = override.match;
1579
+ if (roleId !== undefined && !byPlaybookRole.has(roleId)) {
1580
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} roleId "${roleId}" does not exist in the selected playbook`);
1581
+ }
1582
+ if (nodeId !== undefined && !nodes[nodeId]) {
1583
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} nodeId "${nodeId}" does not exist in the selected pipeline`);
1584
+ }
1585
+ }
1586
+ assertScriptBindingShape(override, matchLabel, nodes) {
1587
+ const nodeId = override.match.nodeId;
1588
+ if (!nodeId || nodes[nodeId]?.kind !== 'script')
1589
+ return;
1590
+ if (override.runnerId || override.modelLevel || override.timeoutMs !== undefined || override.permissionMode) {
1591
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} targets script node "${nodeId}" and must not set runnerId, modelLevel, timeoutMs, or permissionMode`);
1592
+ }
1593
+ }
1594
+ assertBindingAccounts(override, matchLabel, nodes) {
1595
+ const github = override.accounts?.github;
1596
+ if (github === undefined)
1597
+ return;
1598
+ if (!github.trim()) {
1599
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} accounts.github must be a non-empty string`);
1600
+ }
1601
+ const nodeId = override.match.nodeId;
1602
+ if (!nodeId || nodes[nodeId]?.kind !== 'script') {
1603
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} accounts are only supported for script nodes`);
1604
+ }
1605
+ }
1606
+ assertRegisteredRunner(runnerId, matchLabel) {
1607
+ if (runnerId === undefined || BUILTIN_RUNNERS.has(runnerId))
1608
+ return;
1609
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} runnerId "${runnerId}" is not a registered runner`);
1610
+ }
1611
+ async assertModelLevelAvailable(modelLevel, matchLabel, cachedProfiles) {
1612
+ if (modelLevel === undefined)
1613
+ return;
1614
+ if (!VALID_MODEL_LEVELS.includes(modelLevel)) {
1615
+ throw this.unavailableModelLevel(matchLabel, modelLevel);
1616
+ }
1617
+ const cached = cachedProfiles.get(modelLevel);
1618
+ if (cached === true)
1619
+ return;
1620
+ if (cached === false)
1621
+ throw this.unavailableModelLevel(matchLabel, modelLevel);
1622
+ try {
1623
+ await this.roles.loadModelProfile(modelLevel);
1624
+ cachedProfiles.set(modelLevel, true);
1625
+ }
1626
+ catch (err) {
1627
+ const isUnavailable = err instanceof ControlPlaneError && (err.code === 'ROW_NOT_FOUND' || err.code === 'VALIDATION_FAILURE');
1628
+ if (!isUnavailable)
1629
+ throw err;
1630
+ cachedProfiles.set(modelLevel, false);
1631
+ throw this.unavailableModelLevel(matchLabel, modelLevel);
1632
+ }
1633
+ }
1634
+ unavailableModelLevel(matchLabel, modelLevel) {
1635
+ return this.profileSchemaClosed(`bindingOverride match ${matchLabel} modelLevel "${modelLevel}" is unavailable`);
1636
+ }
1637
+ assertBindingTimeout(override, matchLabel) {
1638
+ const ms = override.timeoutMs;
1639
+ if (ms === undefined || (Number.isInteger(ms) && ms > 0 && ms <= 86_400_000))
1640
+ return;
1641
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} timeoutMs must be a positive integer <= 86400000, got ${ms}`);
1642
+ }
1643
+ assertBindingPermissionMode(override, matchLabel, byPlaybookRole, launchBindings) {
1644
+ const permissionMode = override.permissionMode;
1645
+ if (permissionMode === undefined)
1646
+ return;
1647
+ const effectiveRunner = this.effectivePermissionRunner(override, matchLabel, byPlaybookRole, launchBindings);
1648
+ const validModes = RUNNER_PERMISSION_MODES[effectiveRunner];
1649
+ if (!validModes) {
1650
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode not supported for runner ${effectiveRunner}`);
1651
+ }
1652
+ if (!validModes.includes(permissionMode)) {
1653
+ throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode "${permissionMode}" is not valid for runner ${effectiveRunner}; expected one of ${validModes.join(', ')}`);
1654
+ }
1655
+ }
1656
+ effectivePermissionRunner(override, matchLabel, byPlaybookRole, launchBindings) {
1657
+ if (override.runnerId)
1658
+ return override.runnerId;
1659
+ if (override.match.runnerId)
1660
+ return override.match.runnerId;
1661
+ const roleId = override.match.roleId;
1662
+ if (!roleId)
1663
+ throw this.unresolvablePermissionRunner(matchLabel);
1664
+ const role = byPlaybookRole.get(roleId);
1665
+ if (!role)
1666
+ throw this.unresolvablePermissionRunner(matchLabel);
1667
+ const roleOverride = this.findLastRoleRunnerOverride(launchBindings, roleId);
1668
+ return roleOverride?.runnerId ?? role.runner;
1669
+ }
1670
+ findLastRoleRunnerOverride(launchBindings, roleId) {
1671
+ for (let index = launchBindings.length - 1; index >= 0; index -= 1) {
1672
+ const candidate = launchBindings[index];
1673
+ if (candidate.match.roleId === roleId && !candidate.match.nodeId && candidate.runnerId !== undefined) {
1674
+ return candidate;
1675
+ }
1676
+ }
1677
+ return undefined;
1678
+ }
1679
+ unresolvablePermissionRunner(matchLabel) {
1680
+ return this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode override requires a resolvable runner; pin runnerId`);
1681
+ }
1258
1682
  async resolveAutoPipeline(playbookId, text) {
1259
1683
  const pipelines = (await this.playbooks.listPipelines())
1260
1684
  .filter((pipeline) => pipeline.playbookId === playbookId)
@@ -1275,8 +1699,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1275
1699
  }
1276
1700
  return best.pipeline;
1277
1701
  }
1278
- async resolveRouteRoles(playbookId, pipeline, executionProfile) {
1279
- const roles = (await this.roles.listRoles()).filter((role) => role.playbookId === playbookId);
1702
+ async resolveRouteRoles(playbookId, pipeline, launchBindings, preloadedRoles) {
1703
+ const roles = preloadedRoles ?? (await this.roles.listRoles()).filter((role) => role.playbookId === playbookId);
1280
1704
  const byPlaybookRole = new Map(roles.map((role) => [role.playbookRoleId || role.name, role]));
1281
1705
  const selected = [...pipeline.requiredRoles];
1282
1706
  for (const group of pipeline.alternativeRoles) {
@@ -1295,16 +1719,25 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
1295
1719
  return selected.map((roleId) => {
1296
1720
  const role = byPlaybookRole.get(roleId);
1297
1721
  assertProductionRunnerBinding(role.runner, 'playbook', roleId);
1298
- const resolved = resolveRunnerForProfile(role.runner, executionProfile);
1299
- assertProductionRunnerBinding(resolved.runnerId, resolved.source, roleId);
1300
- assertRunnerAvailable(resolved.runnerId, executionProfile, roleId);
1722
+ const runnerResolved = resolveRunnerForRole(role.runner, roleId, launchBindings);
1723
+ assertProductionRunnerBinding(runnerResolved.runnerId, runnerResolved.source, roleId);
1724
+ assertRunnerAvailable(runnerResolved.runnerId, roleId);
1725
+ const bindingResolution = resolveBindingForRole(role, roleId, runnerResolved.runnerId, launchBindings);
1301
1726
  return {
1302
1727
  roleId,
1303
1728
  rowId: role.id,
1304
1729
  modelLevel: role.modelLevel,
1305
1730
  runnerId: role.runner,
1306
- resolvedRunnerId: resolved.runnerId,
1307
- runnerSource: resolved.source,
1731
+ resolvedRunnerId: runnerResolved.runnerId,
1732
+ runnerSource: runnerResolved.source,
1733
+ resolvedModelLevel: bindingResolution.resolvedModelLevel,
1734
+ modelSource: bindingResolution.modelSource,
1735
+ timeoutMs: role.timeoutMs,
1736
+ resolvedTimeoutMs: bindingResolution.resolvedTimeoutMs,
1737
+ timeoutSource: bindingResolution.timeoutSource,
1738
+ permissionMode: role.permissionMode,
1739
+ resolvedPermissionMode: bindingResolution.resolvedPermissionMode,
1740
+ permissionSource: bindingResolution.permissionSource,
1308
1741
  };
1309
1742
  });
1310
1743
  }