@revisium/orchestrator 0.2.0-alpha.2 → 0.2.0-alpha.4

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 (268) hide show
  1. package/README.md +3 -3
  2. package/control-plane/bootstrap.config.json +104 -550
  3. package/control-plane/default-playbook/catalog/pipelines.json +405 -1065
  4. package/control-plane/default-playbook/catalog/run-profiles.json +208 -0
  5. package/control-plane/default-playbook/package.json +1 -1
  6. package/control-plane/default-playbook/playbook.json +2 -1
  7. package/control-plane/default-playbook/prompts/developer.md +6 -6
  8. package/dist/__generated__/client/browser.js +18 -0
  9. package/dist/__generated__/client/browser.js.map +1 -0
  10. package/dist/__generated__/client/client.js +32 -0
  11. package/dist/__generated__/client/client.js.map +1 -0
  12. package/dist/__generated__/client/commonInputTypes.js +11 -0
  13. package/dist/__generated__/client/commonInputTypes.js.map +1 -0
  14. package/dist/__generated__/client/enums.js +19 -0
  15. package/dist/__generated__/client/enums.js.map +1 -0
  16. package/dist/__generated__/client/internal/class.js +53 -0
  17. package/dist/__generated__/client/internal/class.js.map +1 -0
  18. package/dist/__generated__/client/internal/prismaNamespace.js +451 -0
  19. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
  20. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +422 -0
  21. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
  22. package/dist/__generated__/client/models/Branch.js +2 -0
  23. package/dist/__generated__/client/models/Branch.js.map +1 -0
  24. package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
  25. package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
  26. package/dist/__generated__/client/models/FileBlob.js +2 -0
  27. package/dist/__generated__/client/models/FileBlob.js.map +1 -0
  28. package/dist/__generated__/client/models/InboxItem.js +2 -0
  29. package/dist/__generated__/client/models/InboxItem.js.map +1 -0
  30. package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
  31. package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
  32. package/dist/__generated__/client/models/Revision.js +2 -0
  33. package/dist/__generated__/client/models/Revision.js.map +1 -0
  34. package/dist/__generated__/client/models/RevoProject.js +2 -0
  35. package/dist/__generated__/client/models/RevoProject.js.map +1 -0
  36. package/dist/__generated__/client/models/Row.js +2 -0
  37. package/dist/__generated__/client/models/Row.js.map +1 -0
  38. package/dist/__generated__/client/models/RunAttempt.js +2 -0
  39. package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
  40. package/dist/__generated__/client/models/RunEvent.js +2 -0
  41. package/dist/__generated__/client/models/RunEvent.js.map +1 -0
  42. package/dist/__generated__/client/models/RunOutput.js +2 -0
  43. package/dist/__generated__/client/models/RunOutput.js.map +1 -0
  44. package/dist/__generated__/client/models/RunTask.js +2 -0
  45. package/dist/__generated__/client/models/RunTask.js.map +1 -0
  46. package/dist/__generated__/client/models/Table.js +2 -0
  47. package/dist/__generated__/client/models/Table.js.map +1 -0
  48. package/dist/__generated__/client/models/TableMigration.js +2 -0
  49. package/dist/__generated__/client/models/TableMigration.js.map +1 -0
  50. package/dist/__generated__/client/models/TaskRun.js +2 -0
  51. package/dist/__generated__/client/models/TaskRun.js.map +1 -0
  52. package/dist/__generated__/client/models.js +2 -0
  53. package/dist/__generated__/client/models.js.map +1 -0
  54. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
  55. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  56. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
  57. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  58. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
  59. package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
  60. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +46 -0
  61. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  62. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
  63. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  64. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  65. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  67. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  68. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  69. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  70. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  71. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  72. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  73. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  74. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  75. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  76. package/dist/cli/commands/doctor-report.js +1 -13
  77. package/dist/cli/commands/doctor-report.js.map +1 -1
  78. package/dist/cli/commands/lifecycle.js +50 -65
  79. package/dist/cli/commands/lifecycle.js.map +1 -1
  80. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  81. package/dist/cli/commands/process-helpers.js.map +1 -0
  82. package/dist/config.js +17 -55
  83. package/dist/config.js.map +1 -1
  84. package/dist/control-plane/audit-validation-helpers.js +31 -0
  85. package/dist/control-plane/audit-validation-helpers.js.map +1 -0
  86. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  87. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  88. package/dist/control-plane/bootstrap.js +25 -38
  89. package/dist/control-plane/bootstrap.js.map +1 -1
  90. package/dist/control-plane/change-notifications.js +45 -14
  91. package/dist/control-plane/change-notifications.js.map +1 -1
  92. package/dist/control-plane/data-access.js +1 -106
  93. package/dist/control-plane/data-access.js.map +1 -1
  94. package/dist/control-plane/default-playbook-policy.js +499 -78
  95. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  96. package/dist/control-plane/definitions.js +4 -5
  97. package/dist/control-plane/definitions.js.map +1 -1
  98. package/dist/control-plane/engine-transport.js +381 -0
  99. package/dist/control-plane/engine-transport.js.map +1 -0
  100. package/dist/control-plane/inbox.js +15 -7
  101. package/dist/control-plane/inbox.js.map +1 -1
  102. package/dist/control-plane/index.js +0 -1
  103. package/dist/control-plane/index.js.map +1 -1
  104. package/dist/control-plane/manual-adoption-audit.js +3 -24
  105. package/dist/control-plane/manual-adoption-audit.js.map +1 -1
  106. package/dist/control-plane/merge-override-audit.js +44 -0
  107. package/dist/control-plane/merge-override-audit.js.map +1 -0
  108. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  109. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  110. package/dist/control-plane/query-types.js +2 -0
  111. package/dist/control-plane/query-types.js.map +1 -0
  112. package/dist/control-plane/run-profiles.js +89 -0
  113. package/dist/control-plane/run-profiles.js.map +1 -0
  114. package/dist/control-plane/schema-migration.js +0 -35
  115. package/dist/control-plane/schema-migration.js.map +1 -1
  116. package/dist/control-plane/seed-default-playbook.js +2 -31
  117. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  118. package/dist/control-plane/tables.js +8 -0
  119. package/dist/control-plane/tables.js.map +1 -1
  120. package/dist/control-plane/transport.js +16 -0
  121. package/dist/control-plane/transport.js.map +1 -0
  122. package/dist/control-plane/versioned-meaning.js +56 -14
  123. package/dist/control-plane/versioned-meaning.js.map +1 -1
  124. package/dist/e2e/kit/agents.js +60 -6
  125. package/dist/e2e/kit/agents.js.map +1 -1
  126. package/dist/e2e/kit/assertions.js +13 -1
  127. package/dist/e2e/kit/assertions.js.map +1 -1
  128. package/dist/e2e/kit/drive.js +46 -11
  129. package/dist/e2e/kit/drive.js.map +1 -1
  130. package/dist/e2e/kit/env.js +17 -1
  131. package/dist/e2e/kit/env.js.map +1 -1
  132. package/dist/e2e/kit/fake-integrator.js +25 -5
  133. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  134. package/dist/e2e/kit/gh-emulator.js +172 -40
  135. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  136. package/dist/e2e/kit/harness.js +44 -10
  137. package/dist/e2e/kit/harness.js.map +1 -1
  138. package/dist/e2e/kit/index.js +1 -0
  139. package/dist/e2e/kit/index.js.map +1 -1
  140. package/dist/e2e/kit/scenario.js +165 -0
  141. package/dist/e2e/kit/scenario.js.map +1 -0
  142. package/dist/e2e/kit/scenarios.js +12 -18
  143. package/dist/e2e/kit/scenarios.js.map +1 -1
  144. package/dist/engine/dbos.service.js +8 -1
  145. package/dist/engine/dbos.service.js.map +1 -1
  146. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  147. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  148. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  149. package/dist/host/daemon.js +32 -8
  150. package/dist/host/daemon.js.map +1 -1
  151. package/dist/host/ensure-host.js +3 -3
  152. package/dist/host/ensure-host.js.map +1 -1
  153. package/dist/host/host-runtime.js.map +1 -1
  154. package/dist/host/host.lifecycle.js +3 -16
  155. package/dist/host/host.lifecycle.js.map +1 -1
  156. package/dist/http/graphql-host.js +17 -8
  157. package/dist/http/graphql-host.js.map +1 -1
  158. package/dist/mcp/mcp-capabilities.js +12 -11
  159. package/dist/mcp/mcp-capabilities.js.map +1 -1
  160. package/dist/mcp/mcp-facade.service.js +45 -5
  161. package/dist/mcp/mcp-facade.service.js.map +1 -1
  162. package/dist/mcp/mcp-tools.js +59 -5
  163. package/dist/mcp/mcp-tools.js.map +1 -1
  164. package/dist/mcp/monitoring-directive.js +43 -0
  165. package/dist/mcp/monitoring-directive.js.map +1 -0
  166. package/dist/pipeline/data-driven-task.workflow.js +393 -182
  167. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  168. package/dist/pipeline/pipeline.service.js +27 -5
  169. package/dist/pipeline/pipeline.service.js.map +1 -1
  170. package/dist/pipeline/route-contract.js +123 -3
  171. package/dist/pipeline/route-contract.js.map +1 -1
  172. package/dist/pipeline-core/condition-scopes.js +15 -0
  173. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  174. package/dist/pipeline-core/index.js +2 -0
  175. package/dist/pipeline-core/index.js.map +1 -1
  176. package/dist/pipeline-core/kit/fixtures.js +81 -44
  177. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  178. package/dist/pipeline-core/materialize.js +221 -0
  179. package/dist/pipeline-core/materialize.js.map +1 -0
  180. package/dist/pipeline-core/types.js +5 -0
  181. package/dist/pipeline-core/types.js.map +1 -1
  182. package/dist/pipeline-core/validate-loops.js +2 -15
  183. package/dist/pipeline-core/validate-loops.js.map +1 -1
  184. package/dist/pipeline-core/validate-resilience.js +257 -0
  185. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  186. package/dist/pipeline-core/validate.js +2 -0
  187. package/dist/pipeline-core/validate.js.map +1 -1
  188. package/dist/playbook/catalog-loader.js +39 -2
  189. package/dist/playbook/catalog-loader.js.map +1 -1
  190. package/dist/playbook/catalog-schema-validator.js +438 -0
  191. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  192. package/dist/playbook/import-mapper.js +41 -0
  193. package/dist/playbook/import-mapper.js.map +1 -1
  194. package/dist/playbook/manifest.js +5 -0
  195. package/dist/playbook/manifest.js.map +1 -1
  196. package/dist/playbook/playbook-installer.js +17 -1
  197. package/dist/playbook/playbook-installer.js.map +1 -1
  198. package/dist/poller/pr-readiness-core.js +280 -94
  199. package/dist/poller/pr-readiness-core.js.map +1 -1
  200. package/dist/poller/pr-readiness.js +2 -2
  201. package/dist/poller/pr-readiness.js.map +1 -1
  202. package/dist/revisium/inbox.service.js +8 -8
  203. package/dist/revisium/inbox.service.js.map +1 -1
  204. package/dist/revisium/playbooks.service.js +116 -21
  205. package/dist/revisium/playbooks.service.js.map +1 -1
  206. package/dist/revisium/revisium.module.js +8 -8
  207. package/dist/revisium/revisium.module.js.map +1 -1
  208. package/dist/revisium/roles.service.js +3 -1
  209. package/dist/revisium/roles.service.js.map +1 -1
  210. package/dist/revisium/run.service.js +9 -8
  211. package/dist/revisium/run.service.js.map +1 -1
  212. package/dist/revisium/tokens.js +0 -1
  213. package/dist/revisium/tokens.js.map +1 -1
  214. package/dist/run/create-run.js +1 -1
  215. package/dist/run/create-run.js.map +1 -1
  216. package/dist/run/inspect-run.js +32 -28
  217. package/dist/run/inspect-run.js.map +1 -1
  218. package/dist/run/issue-ref.js +97 -2
  219. package/dist/run/issue-ref.js.map +1 -1
  220. package/dist/run/prisma-runtime-data-access.js +619 -0
  221. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  222. package/dist/run/run-outputs.js +39 -14
  223. package/dist/run/run-outputs.js.map +1 -1
  224. package/dist/run/terminal-run-status.js.map +1 -1
  225. package/dist/runners/integrator.js +657 -56
  226. package/dist/runners/integrator.js.map +1 -1
  227. package/dist/runners/worktree.service.js +10 -8
  228. package/dist/runners/worktree.service.js.map +1 -1
  229. package/dist/schema/ajv-errors.js +15 -0
  230. package/dist/schema/ajv-errors.js.map +1 -0
  231. package/dist/smoke/isolation.js +7 -16
  232. package/dist/smoke/isolation.js.map +1 -1
  233. package/dist/storage/ensure-storage.js +117 -0
  234. package/dist/storage/ensure-storage.js.map +1 -0
  235. package/dist/storage/revo-database.js +39 -0
  236. package/dist/storage/revo-database.js.map +1 -0
  237. package/dist/storage/revo-prisma.service.js +31 -0
  238. package/dist/storage/revo-prisma.service.js.map +1 -0
  239. package/dist/storage/revo-storage.module.js +18 -0
  240. package/dist/storage/revo-storage.module.js.map +1 -0
  241. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  242. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  243. package/dist/task-control-plane/run-watch.service.js +3 -6
  244. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  245. package/dist/task-control-plane/task-control-plane-api.service.js +234 -43
  246. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  247. package/dist/testing/runtime-data-access.js +175 -0
  248. package/dist/testing/runtime-data-access.js.map +1 -0
  249. package/dist/worker/build-context.js +178 -2
  250. package/dist/worker/build-context.js.map +1 -1
  251. package/dist/worker/claude-code-runner.js +22 -3
  252. package/dist/worker/claude-code-runner.js.map +1 -1
  253. package/dist/worker/codex-runner.js +6 -1
  254. package/dist/worker/codex-runner.js.map +1 -1
  255. package/package.json +20 -7
  256. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  257. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  258. package/prisma/migrations/migration_lock.toml +3 -0
  259. package/prisma/schema.prisma +339 -0
  260. package/prisma.config.ts +11 -0
  261. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  262. package/dist/control-plane/client-transport.js +0 -168
  263. package/dist/control-plane/client-transport.js.map +0 -1
  264. package/dist/engine/ensure-postgres.js +0 -46
  265. package/dist/engine/ensure-postgres.js.map +0 -1
  266. package/dist/host/ensure-revisium.js +0 -135
  267. package/dist/host/ensure-revisium.js.map +0 -1
  268. package/revisium.config.json +0 -10
@@ -8,7 +8,19 @@ export const DEFAULT_PLAYBOOK_POLICY_DIAGNOSTIC_CODES = [
8
8
  'DEFAULT_POLICY_BLOCKED_TERMINAL_MISSING',
9
9
  'DEFAULT_POLICY_CANCELLED_TERMINAL_MISSING',
10
10
  'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
11
+ 'DEFAULT_POLICY_RECOVERABLE_CATCH_TERMINAL',
12
+ 'DEFAULT_POLICY_CAP_EXHAUSTION_OFFRAMP_MISSING',
13
+ 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
14
+ 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
15
+ 'DEFAULT_POLICY_MERGE_READINESS_FRESHNESS_MISSING',
16
+ 'DEFAULT_POLICY_CONFIRM_MERGE_FAILURE_TERMINAL',
17
+ 'DEFAULT_POLICY_POST_MERGE_CLEANUP_MISSING',
18
+ 'DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT',
11
19
  ];
20
+ export const POLICY_VERSION = '1';
21
+ const SUPPORTED_PIPELINE_IDS = ['feature-development'];
22
+ const POLL_LOOP_SCOPE = 'pollLoop';
23
+ const POLL_LOOP_CAP = 8;
12
24
  class PolicySink {
13
25
  pipelineId;
14
26
  diagnostics = [];
@@ -25,19 +37,28 @@ class PolicySink {
25
37
  });
26
38
  }
27
39
  }
28
- export function validateDefaultFeatureDevelopmentPolicy(template) {
40
+ export function validateDefaultPlaybookPolicy(template) {
29
41
  const sink = new PolicySink(template.pipelineId);
30
- if (template.pipelineId !== 'feature-development') {
31
- sink.error('DEFAULT_POLICY_WRONG_PIPELINE', 'default playbook policy verifier only applies to feature-development', { expected: 'feature-development', actual: template.pipelineId });
42
+ if (!SUPPORTED_PIPELINE_IDS.includes(template.pipelineId)) {
43
+ sink.error('DEFAULT_POLICY_WRONG_PIPELINE', 'default playbook policy verifier only applies to feature-development variants', { expected: SUPPORTED_PIPELINE_IDS.join('|'), actual: template.pipelineId });
32
44
  return sink.diagnostics;
33
45
  }
34
46
  checkBlockedTerminal(template, sink);
35
47
  checkCancelledTerminal(template, sink);
36
48
  checkProducedChangeHandoff(template, sink);
37
49
  checkPrFreshnessWiring(template, sink);
50
+ checkApproveReverifyBeforeMerge(template, sink);
51
+ checkOverrideMergeRouting(template, sink);
52
+ checkMergeConsumesFreshReadiness(template, sink);
38
53
  checkMergeGateRecheckRouting(template, sink);
54
+ checkRecoveryGateRouting(template, sink);
39
55
  checkReviewFeedbackLoop(template, sink);
40
56
  checkLoopExhaustionEscalation(template, sink);
57
+ checkRecoverableCatches(template, sink);
58
+ checkCapExhaustionOffRamp(template, sink);
59
+ checkConfirmMergeFailureRecoverable(template, sink);
60
+ checkPostMergeCleanup(template, sink);
61
+ checkGateOutcomesExplicit(template, sink);
41
62
  return sink.diagnostics;
42
63
  }
43
64
  function checkBlockedTerminal(template, sink) {
@@ -61,23 +82,43 @@ function checkCancelledTerminal(template, sink) {
61
82
  });
62
83
  }
63
84
  function checkProducedChangeHandoff(template, sink) {
64
- for (const nodeId of ['developer', 'reworkDeveloper', 'stuckReworkDeveloper', 'ciRework', 'reviewRework']) {
85
+ for (const nodeId of ['developer', 'reworkDeveloper', 'stuckReworkDeveloper', 'ciRework', 'reviewRework', 'questionReviewRework']) {
65
86
  expectChangeProducer(template, sink, nodeId);
66
87
  }
67
- expectConsume(template, sink, {
68
- code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
69
- consumerId: 'codeReview',
70
- producerId: 'developer',
71
- as: 'developerChange',
72
- staleOk: true,
73
- });
74
- expectConsume(template, sink, {
75
- code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
76
- consumerId: 'codeReview',
77
- producerId: 'reworkDeveloper',
78
- as: 'reworkChange',
79
- optional: true,
80
- });
88
+ if (template.nodes['codeReviewFanout']) {
89
+ for (const branchId of ['codeReviewPrimary', 'codeReviewSecondary']) {
90
+ expectConsume(template, sink, {
91
+ code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
92
+ consumerId: branchId,
93
+ producerId: 'developer',
94
+ as: 'developerChange',
95
+ staleOk: true,
96
+ });
97
+ expectConsume(template, sink, {
98
+ code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
99
+ consumerId: branchId,
100
+ producerId: 'reworkDeveloper',
101
+ as: 'reworkChange',
102
+ optional: true,
103
+ });
104
+ }
105
+ }
106
+ else {
107
+ expectConsume(template, sink, {
108
+ code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
109
+ consumerId: 'codeReview',
110
+ producerId: 'developer',
111
+ as: 'developerChange',
112
+ staleOk: true,
113
+ });
114
+ expectConsume(template, sink, {
115
+ code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
116
+ consumerId: 'codeReview',
117
+ producerId: 'reworkDeveloper',
118
+ as: 'reworkChange',
119
+ optional: true,
120
+ });
121
+ }
81
122
  expectConsume(template, sink, {
82
123
  code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
83
124
  consumerId: 'integrator',
@@ -113,52 +154,129 @@ function checkProducedChangeHandoff(template, sink) {
113
154
  producerId: 'reviewRework',
114
155
  as: 'reviewChange',
115
156
  });
157
+ expectConsume(template, sink, {
158
+ code: 'DEFAULT_POLICY_CHANGE_HANDOFF_MISSING',
159
+ consumerId: 'questionReviewIntegrator',
160
+ producerId: 'questionReviewRework',
161
+ as: 'reviewChange',
162
+ });
116
163
  }
117
164
  function checkPrFreshnessWiring(template, sink) {
118
- expectScript(template, sink, {
165
+ expectScopeConfig(template, sink, {
166
+ scopeId: POLL_LOOP_SCOPE,
167
+ cap: POLL_LOOP_CAP,
168
+ parent: null,
169
+ });
170
+ for (const hop of [
171
+ { scriptId: 'pollPr', routerId: 'prRouter', cleanTarget: 'mergeReadiness', recheckTarget: 'pollPr' },
172
+ {
173
+ scriptId: 'mergeReadiness',
174
+ routerId: 'mergeReadinessRouter',
175
+ cleanTarget: 'mergeGate',
176
+ recheckTarget: 'mergeReadiness',
177
+ },
178
+ ]) {
179
+ expectPollPrFeedbackHop(template, sink, hop);
180
+ }
181
+ expectGateArtifact(template, sink, {
119
182
  code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
120
- nodeId: 'pollPr',
121
- scriptRef: 'script:pollPr',
122
- next: 'prRouter',
123
- resultSchema: 'schema:prFeedback',
124
- produces: 'prFeedback',
183
+ nodeId: 'mergeGate',
184
+ producerId: 'mergeReadiness',
185
+ as: 'prFeedback',
125
186
  });
187
+ }
188
+ function checkApproveReverifyBeforeMerge(template, sink) {
126
189
  expectRoute(template, sink, {
127
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
128
- nodeId: 'prRouter',
129
- verdict: 'clean',
130
- target: 'mergeReadiness',
190
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
191
+ nodeId: 'mergeGate',
192
+ verdict: 'approved',
193
+ target: 'mergeApproveReverify',
131
194
  });
132
195
  expectScript(template, sink, {
133
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
134
- nodeId: 'mergeReadiness',
196
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
197
+ nodeId: 'mergeApproveReverify',
135
198
  scriptRef: 'script:pollPr',
136
- next: 'mergeReadinessRouter',
199
+ next: 'mergeApproveReverifyRouter',
137
200
  resultSchema: 'schema:prFeedback',
138
201
  produces: 'prFeedback',
139
202
  });
140
203
  expectRoute(template, sink, {
141
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
142
- nodeId: 'mergeReadinessRouter',
204
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
205
+ nodeId: 'mergeApproveReverifyRouter',
143
206
  verdict: 'clean',
144
- target: 'mergeGate',
207
+ target: 'confirmMerge',
145
208
  });
146
- expectGateArtifact(template, sink, {
147
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
148
- nodeId: 'mergeGate',
149
- producerId: 'mergeReadiness',
150
- as: 'prFeedback',
209
+ expectRoute(template, sink, {
210
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
211
+ nodeId: 'mergeApproveReverifyRouter',
212
+ verdict: 'merged',
213
+ target: 'cleanupWorktree',
151
214
  });
152
215
  expectRoute(template, sink, {
153
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
216
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
217
+ nodeId: 'mergeApproveReverifyRouter',
218
+ verdict: 'closed',
219
+ target: 'recoveryGate',
220
+ });
221
+ expectDefaultRoute(template, sink, {
222
+ code: 'DEFAULT_POLICY_APPROVE_REVERIFY_MISSING',
223
+ nodeId: 'mergeApproveReverifyRouter',
224
+ target: 'classifyRecovery',
225
+ });
226
+ }
227
+ function checkOverrideMergeRouting(template, sink) {
228
+ expectRoute(template, sink, {
229
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
154
230
  nodeId: 'mergeGate',
155
- verdict: 'approved',
156
- target: 'confirmMerge',
231
+ verdict: 'override_merge',
232
+ target: 'overrideMerge',
233
+ });
234
+ expectScript(template, sink, {
235
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
236
+ nodeId: 'overrideMerge',
237
+ scriptRef: 'script:overrideMerge',
238
+ next: 'overrideMergeRouter',
239
+ resultSchema: 'schema:prFeedback',
240
+ produces: 'prFeedback',
157
241
  });
158
242
  expectConsume(template, sink, {
159
- code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
243
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
244
+ consumerId: 'overrideMerge',
245
+ producerId: 'mergeGate',
246
+ as: 'gateResolution',
247
+ });
248
+ expectRoutesWithDefault(template, sink, 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING', 'overrideMergeRouter', [
249
+ ['clean', 'overrideConfirmMerge'],
250
+ ['merged', 'cleanupWorktree'],
251
+ ['closed', 'recoveryGate'],
252
+ ], 'classifyRecovery');
253
+ expectScript(template, sink, {
254
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
255
+ nodeId: 'overrideConfirmMerge',
256
+ scriptRef: 'script:confirmMerge',
257
+ next: 'cleanupWorktree',
258
+ resultSchema: 'schema:integration',
259
+ });
260
+ expectConsume(template, sink, {
261
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
262
+ consumerId: 'overrideConfirmMerge',
263
+ producerId: 'overrideMerge',
264
+ as: 'mergeReadiness',
265
+ });
266
+ expectConsume(template, sink, {
267
+ code: 'DEFAULT_POLICY_OVERRIDE_MERGE_ROUTE_MISSING',
268
+ consumerId: 'classifyRecovery',
269
+ producerId: 'overrideMerge',
270
+ as: 'overrideFeedback',
271
+ optional: true,
272
+ staleOk: true,
273
+ });
274
+ }
275
+ function checkMergeConsumesFreshReadiness(template, sink) {
276
+ expectConsume(template, sink, {
277
+ code: 'DEFAULT_POLICY_MERGE_READINESS_FRESHNESS_MISSING',
160
278
  consumerId: 'confirmMerge',
161
- producerId: 'mergeReadiness',
279
+ producerId: 'mergeApproveReverify',
162
280
  as: 'mergeReadiness',
163
281
  });
164
282
  }
@@ -166,20 +284,15 @@ function checkMergeGateRecheckRouting(template, sink) {
166
284
  expectGateOutcomes(template, sink, {
167
285
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
168
286
  nodeId: 'mergeGate',
169
- outcomes: ['approved', 'recheck', 'cancel'],
170
- });
171
- expectRoute(template, sink, {
172
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
173
- nodeId: 'mergeGate',
174
- verdict: 'recheck',
175
- target: 'mergeRecheck',
176
- });
177
- expectRoute(template, sink, {
178
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
179
- nodeId: 'mergeGate',
180
- verdict: 'cancel',
181
- target: 'cancelledEnd',
182
- });
287
+ outcomes: ['approved', 'recheck', 'address_review_threads', 'return_to_development', 'override_merge', 'cancel'],
288
+ });
289
+ expectRoutes(template, sink, 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING', 'mergeGate', [
290
+ ['recheck', 'mergeRecheck'],
291
+ ['address_review_threads', 'triage'],
292
+ ['return_to_development', 'triage'],
293
+ ['override_merge', 'overrideMerge'],
294
+ ['cancel', 'cancelledEnd'],
295
+ ]);
183
296
  expectScript(template, sink, {
184
297
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
185
298
  nodeId: 'mergeRecheck',
@@ -188,18 +301,13 @@ function checkMergeGateRecheckRouting(template, sink) {
188
301
  resultSchema: 'schema:prFeedback',
189
302
  produces: 'prFeedback',
190
303
  });
191
- expectRoute(template, sink, {
192
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
193
- nodeId: 'mergeRecheckRouter',
194
- verdict: 'clean',
195
- target: 'blockedEnd',
196
- });
197
- expectRoute(template, sink, {
198
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
199
- nodeId: 'mergeRecheckRouter',
200
- verdict: 'review_changes',
201
- target: 'triage',
202
- });
304
+ expectRoutes(template, sink, 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING', 'mergeRecheckRouter', [
305
+ ['clean', 'mergeGate'],
306
+ ['merged', 'cleanupWorktree'],
307
+ ['closed', 'recoveryGate'],
308
+ ['review_changes', 'triage'],
309
+ ['recheck', 'mergeReadiness'],
310
+ ]);
203
311
  expectBoundedRoute(template, sink, {
204
312
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
205
313
  nodeId: 'mergeRecheckRouter',
@@ -208,16 +316,10 @@ function checkMergeGateRecheckRouting(template, sink) {
208
316
  scope: 'ciLoop',
209
317
  value: 3,
210
318
  });
211
- expectRoute(template, sink, {
212
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
213
- nodeId: 'mergeRecheckRouter',
214
- verdict: 'recheck',
215
- target: 'mergeReadiness',
216
- });
217
319
  expectDefaultRoute(template, sink, {
218
320
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
219
321
  nodeId: 'mergeRecheckRouter',
220
- target: 'blockedEnd',
322
+ target: 'recoveryGate',
221
323
  });
222
324
  expectConsume(template, sink, {
223
325
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
@@ -236,6 +338,22 @@ function checkMergeGateRecheckRouting(template, sink) {
236
338
  staleOk: true,
237
339
  });
238
340
  }
341
+ function checkRecoveryGateRouting(template, sink) {
342
+ expectHumanGateOutcomes(template, sink, {
343
+ code: 'DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT',
344
+ nodeId: 'recoveryGate',
345
+ outcomes: ['recheck', 'cancel'],
346
+ });
347
+ expectRoutes(template, sink, 'DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT', 'recoveryGate', [
348
+ ['recheck', 'pollPr'],
349
+ ['cancel', 'cancelledEnd'],
350
+ ]);
351
+ expectDefaultRoute(template, sink, {
352
+ code: 'DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT',
353
+ nodeId: 'recoveryGate',
354
+ target: 'blockedEnd',
355
+ });
356
+ }
239
357
  function checkReviewFeedbackLoop(template, sink) {
240
358
  expectRoute(template, sink, {
241
359
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
@@ -298,6 +416,16 @@ function checkReviewFeedbackLoop(template, sink) {
298
416
  verdict: 'question',
299
417
  target: 'questionGate',
300
418
  });
419
+ expectHumanGateOutcomes(template, sink, {
420
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
421
+ nodeId: 'questionGate',
422
+ outcomes: ['fix', 'wontfix', 'cancel'],
423
+ });
424
+ expectRoutesWithDefault(template, sink, 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING', 'questionGate', [
425
+ ['fix', 'questionReviewRework'],
426
+ ['wontfix', 'respondThreads'],
427
+ ['cancel', 'cancelledEnd'],
428
+ ], 'recoveryGate');
301
429
  expectRoute(template, sink, {
302
430
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
303
431
  nodeId: 'triageRouter',
@@ -310,6 +438,30 @@ function checkReviewFeedbackLoop(template, sink) {
310
438
  verdict: 'wontfix',
311
439
  target: 'respondThreads',
312
440
  });
441
+ expectNodeNext(template, sink, {
442
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
443
+ nodeId: 'questionReviewRework',
444
+ target: 'questionReviewIntegrator',
445
+ });
446
+ expectConsume(template, sink, {
447
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
448
+ consumerId: 'questionReviewRework',
449
+ producerId: 'triage',
450
+ as: 'triage',
451
+ });
452
+ expectConsume(template, sink, {
453
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
454
+ consumerId: 'questionReviewRework',
455
+ producerId: 'questionGate',
456
+ as: 'gateResolution',
457
+ });
458
+ expectScript(template, sink, {
459
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
460
+ nodeId: 'questionReviewIntegrator',
461
+ scriptRef: 'script:integrator',
462
+ next: 'respondThreads',
463
+ resultSchema: 'schema:integration',
464
+ });
313
465
  expectNodeNext(template, sink, {
314
466
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
315
467
  nodeId: 'reviewRework',
@@ -335,13 +487,47 @@ function checkReviewFeedbackLoop(template, sink) {
335
487
  producerId: 'triage',
336
488
  as: 'triage',
337
489
  });
490
+ expectConsume(template, sink, {
491
+ code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
492
+ consumerId: 'respondThreads',
493
+ producerId: 'questionGate',
494
+ as: 'gateResolution',
495
+ optional: true,
496
+ staleOk: true,
497
+ });
338
498
  }
339
499
  function checkLoopExhaustionEscalation(template, sink) {
500
+ expectRoutes(template, sink, 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING', 'planReviewRouter', [
501
+ ['approved', 'planGate'],
502
+ ['clean', 'planGate'],
503
+ ]);
504
+ expectBoundedRoute(template, sink, {
505
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
506
+ nodeId: 'planReviewRouter',
507
+ verdict: 'blocker',
508
+ target: 'analyst',
509
+ scope: 'planReviewLoop',
510
+ value: 4,
511
+ });
512
+ expectBoundedRoute(template, sink, {
513
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
514
+ nodeId: 'planReviewRouter',
515
+ verdict: 'changes_requested',
516
+ target: 'analyst',
517
+ scope: 'planReviewLoop',
518
+ value: 4,
519
+ });
340
520
  expectHumanGateOutcomes(template, sink, {
341
521
  code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
342
522
  nodeId: 'planGate',
343
523
  outcomes: ['approved', 'rework', 'cancel'],
344
524
  });
525
+ expectRoute(template, sink, {
526
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
527
+ nodeId: 'planGate',
528
+ verdict: 'approved',
529
+ target: 'developer',
530
+ });
345
531
  expectRoute(template, sink, {
346
532
  code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
347
533
  nodeId: 'planGate',
@@ -354,6 +540,11 @@ function checkLoopExhaustionEscalation(template, sink) {
354
540
  verdict: 'cancel',
355
541
  target: 'cancelledEnd',
356
542
  });
543
+ expectDefaultRoute(template, sink, {
544
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
545
+ nodeId: 'planGate',
546
+ target: 'blockedEnd',
547
+ });
357
548
  expectRouterDefaultGate(template, sink, {
358
549
  routerId: 'planReviewRouter',
359
550
  gateId: 'planStuckGate',
@@ -376,6 +567,26 @@ function checkLoopExhaustionEscalation(template, sink) {
376
567
  verdict: 'cancel',
377
568
  target: 'cancelledEnd',
378
569
  });
570
+ expectRoutes(template, sink, 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING', 'codeReviewRouter', [
571
+ ['approved', 'integrator'],
572
+ ['clean', 'integrator'],
573
+ ]);
574
+ expectBoundedRoute(template, sink, {
575
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
576
+ nodeId: 'codeReviewRouter',
577
+ verdict: 'blocker',
578
+ target: 'reworkDeveloper',
579
+ scope: 'codeReviewLoop',
580
+ value: 3,
581
+ });
582
+ expectBoundedRoute(template, sink, {
583
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
584
+ nodeId: 'codeReviewRouter',
585
+ verdict: 'changes_requested',
586
+ target: 'reworkDeveloper',
587
+ scope: 'codeReviewLoop',
588
+ value: 3,
589
+ });
379
590
  expectRouterDefaultGate(template, sink, {
380
591
  routerId: 'codeReviewRouter',
381
592
  gateId: 'codeStuckGate',
@@ -417,6 +628,178 @@ function checkLoopExhaustionEscalation(template, sink) {
417
628
  parent: null,
418
629
  });
419
630
  }
631
+ function checkRecoverableCatches(template, sink) {
632
+ const recoverableNodes = [
633
+ 'pollPr',
634
+ 'mergeReadiness',
635
+ 'mergeRecheck',
636
+ 'mergeApproveReverify',
637
+ 'overrideMerge',
638
+ 'integrator',
639
+ 'reviewIntegrator',
640
+ 'questionReviewIntegrator',
641
+ 'respondThreads',
642
+ ];
643
+ for (const nodeId of recoverableNodes) {
644
+ const node = effectNode(template, nodeId);
645
+ if (!node)
646
+ continue;
647
+ for (const entry of node.catch ?? []) {
648
+ if (isTerminalNode(template, entry.goto)) {
649
+ sink.error('DEFAULT_POLICY_RECOVERABLE_CATCH_TERMINAL', `node ${nodeId} catch for ${entry.onError} must not route to a terminal`, {
650
+ nodeId,
651
+ expected: `${entry.onError} -> non-terminal`,
652
+ actual: `${entry.onError} -> ${entry.goto}`,
653
+ });
654
+ }
655
+ }
656
+ }
657
+ }
658
+ function checkCapExhaustionOffRamp(template, sink) {
659
+ const capRouters = [
660
+ 'prRouter',
661
+ 'mergeReadinessRouter',
662
+ 'mergeRecheckRouter',
663
+ 'overrideMergeRouter',
664
+ 'triageRouter',
665
+ 'recoveryRouter',
666
+ 'planReviewRouter',
667
+ 'codeReviewRouter',
668
+ ];
669
+ for (const nodeId of capRouters) {
670
+ const node = routingNode(template, nodeId);
671
+ if (!node)
672
+ continue;
673
+ const target = defaultBranchTarget(node);
674
+ const targetNode = target ? template.nodes[target] : undefined;
675
+ const isHumanGate = targetNode?.kind === 'humanGate';
676
+ const isClassifier = target === 'classifyRecovery';
677
+ if (!isHumanGate && !isClassifier) {
678
+ sink.error('DEFAULT_POLICY_CAP_EXHAUSTION_OFFRAMP_MISSING', `cap-router ${nodeId} default must route to a humanGate or classifyRecovery, not a terminal`, {
679
+ nodeId,
680
+ expected: 'default -> humanGate or classifyRecovery',
681
+ actual: target ? `default -> ${target} (${targetNode?.kind ?? 'missing'})` : 'missing default branch',
682
+ });
683
+ }
684
+ }
685
+ }
686
+ function checkConfirmMergeFailureRecoverable(template, sink) {
687
+ for (const nodeId of ['confirmMerge', 'overrideConfirmMerge']) {
688
+ const node = effectNode(template, nodeId);
689
+ if (!node)
690
+ continue;
691
+ for (const entry of node.catch ?? []) {
692
+ if (isTerminalNode(template, entry.goto)) {
693
+ sink.error('DEFAULT_POLICY_CONFIRM_MERGE_FAILURE_TERMINAL', `${nodeId} catch for ${entry.onError} must not route to a terminal`, {
694
+ nodeId,
695
+ expected: `${entry.onError} -> non-terminal`,
696
+ actual: `${entry.onError} -> ${entry.goto}`,
697
+ });
698
+ }
699
+ }
700
+ }
701
+ }
702
+ function checkPostMergeCleanup(template, sink) {
703
+ for (const nodeId of ['confirmMerge', 'overrideConfirmMerge']) {
704
+ expectNodeNext(template, sink, {
705
+ code: 'DEFAULT_POLICY_POST_MERGE_CLEANUP_MISSING',
706
+ nodeId,
707
+ target: 'cleanupWorktree',
708
+ });
709
+ }
710
+ expectScript(template, sink, {
711
+ code: 'DEFAULT_POLICY_POST_MERGE_CLEANUP_MISSING',
712
+ nodeId: 'cleanupWorktree',
713
+ scriptRef: 'script:cleanupWorktree',
714
+ next: 'mergedEnd',
715
+ resultSchema: 'schema:integration',
716
+ });
717
+ for (const onError of ['revo.ScriptBlocked', 'revo.ScriptFailed']) {
718
+ expectCatch(template, sink, {
719
+ code: 'DEFAULT_POLICY_POST_MERGE_CLEANUP_MISSING',
720
+ nodeId: 'cleanupWorktree',
721
+ onError,
722
+ target: 'mergedEnd',
723
+ });
724
+ }
725
+ }
726
+ function checkGateOutcomesExplicit(template, sink) {
727
+ const expectedDefaults = {
728
+ mergeGate: 'blockedEnd',
729
+ };
730
+ for (const [nodeId, node] of Object.entries(template.nodes)) {
731
+ if (node.kind !== 'humanGate')
732
+ continue;
733
+ for (const outcome of node.outcomes) {
734
+ const branch = guardedBranchForVerdict(node, outcome);
735
+ if (!branch) {
736
+ sink.error('DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT', `gate ${nodeId} declares ${outcome} but has no guarded branch for it`, {
737
+ nodeId,
738
+ expected: `guarded branch for ${outcome}`,
739
+ actual: 'missing guarded branch',
740
+ });
741
+ }
742
+ }
743
+ const defaultTarget = expectedDefaults[nodeId];
744
+ if (defaultTarget) {
745
+ expectDefaultRoute(template, sink, {
746
+ code: 'DEFAULT_POLICY_GATE_OUTCOMES_IMPLICIT',
747
+ nodeId,
748
+ target: defaultTarget,
749
+ });
750
+ }
751
+ }
752
+ }
753
+ function expectPollPrFeedbackHop(template, sink, hop) {
754
+ expectScript(template, sink, {
755
+ code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
756
+ nodeId: hop.scriptId,
757
+ scriptRef: 'script:pollPr',
758
+ next: hop.routerId,
759
+ resultSchema: 'schema:prFeedback',
760
+ produces: 'prFeedback',
761
+ });
762
+ expectIncrementCounters(template, sink, {
763
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
764
+ nodeId: hop.scriptId,
765
+ scopes: [POLL_LOOP_SCOPE],
766
+ });
767
+ expectRoute(template, sink, {
768
+ code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
769
+ nodeId: hop.routerId,
770
+ verdict: 'clean',
771
+ target: hop.cleanTarget,
772
+ });
773
+ expectRoute(template, sink, {
774
+ code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
775
+ nodeId: hop.routerId,
776
+ verdict: 'merged',
777
+ target: 'cleanupWorktree',
778
+ });
779
+ expectRoute(template, sink, {
780
+ code: 'DEFAULT_POLICY_PR_FRESHNESS_WIRING_MISSING',
781
+ nodeId: hop.routerId,
782
+ verdict: 'closed',
783
+ target: 'recoveryGate',
784
+ });
785
+ expectBoundedRoute(template, sink, {
786
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
787
+ nodeId: hop.routerId,
788
+ verdict: 'recheck',
789
+ target: hop.recheckTarget,
790
+ scope: POLL_LOOP_SCOPE,
791
+ value: POLL_LOOP_CAP,
792
+ });
793
+ }
794
+ function expectRoutes(template, sink, code, nodeId, routes) {
795
+ for (const [verdict, target] of routes) {
796
+ expectRoute(template, sink, { code, nodeId, verdict, target });
797
+ }
798
+ }
799
+ function expectRoutesWithDefault(template, sink, code, nodeId, routes, defaultTarget) {
800
+ expectRoutes(template, sink, code, nodeId, routes);
801
+ expectDefaultRoute(template, sink, { code, nodeId, target: defaultTarget });
802
+ }
420
803
  function expectChangeProducer(template, sink, nodeId) {
421
804
  const node = effectNode(template, nodeId);
422
805
  if (node?.resultSchema === 'schema:change' && node.produces?.name === 'change')
@@ -457,6 +840,41 @@ function expectScript(template, sink, rule) {
457
840
  actual: describeNode(node),
458
841
  });
459
842
  }
843
+ function expectCatch(template, sink, rule) {
844
+ const node = effectNode(template, rule.nodeId);
845
+ const actual = node?.catch?.find((entry) => entry.onError === rule.onError)?.goto;
846
+ if (actual === rule.target)
847
+ return;
848
+ let actualDescription;
849
+ if (actual) {
850
+ actualDescription = `${rule.onError} -> ${actual}`;
851
+ }
852
+ else if (node) {
853
+ actualDescription = `missing catch for ${rule.onError}`;
854
+ }
855
+ else {
856
+ actualDescription = describeNode(template.nodes[rule.nodeId]);
857
+ }
858
+ sink.error(rule.code, `node ${rule.nodeId} must route ${rule.onError} to ${rule.target}`, {
859
+ nodeId: rule.nodeId,
860
+ path: 'catch',
861
+ expected: `${rule.onError} -> ${rule.target}`,
862
+ actual: actualDescription,
863
+ });
864
+ }
865
+ function expectIncrementCounters(template, sink, rule) {
866
+ const node = effectNode(template, rule.nodeId);
867
+ const actual = node?.incrementCounters ?? [];
868
+ if (node && actual.length === rule.scopes.length && actual.every((scope, index) => scope === rule.scopes[index])) {
869
+ return;
870
+ }
871
+ sink.error(rule.code, `node ${rule.nodeId} must increment ${rule.scopes.join(',')}`, {
872
+ nodeId: rule.nodeId,
873
+ path: 'incrementCounters',
874
+ expected: `incrementCounters=${rule.scopes.join(',')}`,
875
+ actual: node ? `incrementCounters=${actual.join(',')}` : describeNode(template.nodes[rule.nodeId]),
876
+ });
877
+ }
460
878
  function expectGateOutcomes(template, sink, rule) {
461
879
  const node = template.nodes[rule.nodeId];
462
880
  if (node?.kind === 'humanGate' &&
@@ -464,7 +882,7 @@ function expectGateOutcomes(template, sink, rule) {
464
882
  node.outcomes.every((outcome, index) => outcome === rule.outcomes[index])) {
465
883
  return;
466
884
  }
467
- sink.error(rule.code, `gate ${rule.nodeId} must map approve to ${rule.outcomes[0]} and reject to ${rule.outcomes.at(-1)}`, {
885
+ sink.error(rule.code, `gate ${rule.nodeId} must declare explicit outcomes in order: ${rule.outcomes.join(',')}`, {
468
886
  nodeId: rule.nodeId,
469
887
  path: 'outcomes',
470
888
  expected: rule.outcomes.join(','),
@@ -603,6 +1021,9 @@ function expectRouterDefaultGate(template, sink, rule) {
603
1021
  actual: gate ? `kind=${gate.kind} ${approveVerdict}=${approveTarget ?? 'missing'} default=${gateDefault ?? 'missing'}` : 'missing gate',
604
1022
  });
605
1023
  }
1024
+ function isTerminalNode(template, nodeId) {
1025
+ return template.nodes[nodeId]?.kind === 'terminal';
1026
+ }
606
1027
  function effectNode(template, nodeId) {
607
1028
  const node = template.nodes[nodeId];
608
1029
  if (node?.kind === 'agent' || node?.kind === 'script')