@revisium/orchestrator 0.2.0-alpha.3 → 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 (259) 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 +394 -1122
  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/pr/inputs/pr-readiness.input.js +6 -0
  61. package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
  62. package/dist/api/graphql-api/registerGraphqlEnums.js +2 -0
  63. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  64. package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -0
  65. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  66. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +85 -0
  67. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +1 -0
  68. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +11 -0
  69. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  70. package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
  71. package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
  72. package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
  73. package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
  74. package/dist/cli/commands/doctor-report.js +1 -13
  75. package/dist/cli/commands/doctor-report.js.map +1 -1
  76. package/dist/cli/commands/lifecycle.js +50 -65
  77. package/dist/cli/commands/lifecycle.js.map +1 -1
  78. package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
  79. package/dist/cli/commands/process-helpers.js.map +1 -0
  80. package/dist/config.js +17 -55
  81. package/dist/config.js.map +1 -1
  82. package/dist/control-plane/bootstrap-json-validator.js +65 -0
  83. package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
  84. package/dist/control-plane/bootstrap.js +25 -38
  85. package/dist/control-plane/bootstrap.js.map +1 -1
  86. package/dist/control-plane/change-notifications.js +45 -14
  87. package/dist/control-plane/change-notifications.js.map +1 -1
  88. package/dist/control-plane/data-access.js +1 -106
  89. package/dist/control-plane/data-access.js.map +1 -1
  90. package/dist/control-plane/default-playbook-policy.js +497 -94
  91. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  92. package/dist/control-plane/definitions.js +4 -5
  93. package/dist/control-plane/definitions.js.map +1 -1
  94. package/dist/control-plane/engine-transport.js +381 -0
  95. package/dist/control-plane/engine-transport.js.map +1 -0
  96. package/dist/control-plane/inbox.js +15 -7
  97. package/dist/control-plane/inbox.js.map +1 -1
  98. package/dist/control-plane/index.js +0 -1
  99. package/dist/control-plane/index.js.map +1 -1
  100. package/dist/control-plane/merge-override-audit.js +2 -2
  101. package/dist/control-plane/merge-override-audit.js.map +1 -1
  102. package/dist/control-plane/pipeline-coverage-registry.js +478 -0
  103. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
  104. package/dist/control-plane/query-types.js +2 -0
  105. package/dist/control-plane/query-types.js.map +1 -0
  106. package/dist/control-plane/run-profiles.js +89 -0
  107. package/dist/control-plane/run-profiles.js.map +1 -0
  108. package/dist/control-plane/schema-migration.js +0 -35
  109. package/dist/control-plane/schema-migration.js.map +1 -1
  110. package/dist/control-plane/seed-default-playbook.js +2 -31
  111. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  112. package/dist/control-plane/tables.js +8 -0
  113. package/dist/control-plane/tables.js.map +1 -1
  114. package/dist/control-plane/transport.js +16 -0
  115. package/dist/control-plane/transport.js.map +1 -0
  116. package/dist/control-plane/versioned-meaning.js +56 -14
  117. package/dist/control-plane/versioned-meaning.js.map +1 -1
  118. package/dist/e2e/kit/agents.js +60 -6
  119. package/dist/e2e/kit/agents.js.map +1 -1
  120. package/dist/e2e/kit/assertions.js +13 -1
  121. package/dist/e2e/kit/assertions.js.map +1 -1
  122. package/dist/e2e/kit/drive.js +46 -11
  123. package/dist/e2e/kit/drive.js.map +1 -1
  124. package/dist/e2e/kit/env.js +17 -1
  125. package/dist/e2e/kit/env.js.map +1 -1
  126. package/dist/e2e/kit/fake-integrator.js +25 -5
  127. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  128. package/dist/e2e/kit/gh-emulator.js +158 -24
  129. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  130. package/dist/e2e/kit/harness.js +44 -10
  131. package/dist/e2e/kit/harness.js.map +1 -1
  132. package/dist/e2e/kit/index.js +1 -0
  133. package/dist/e2e/kit/index.js.map +1 -1
  134. package/dist/e2e/kit/scenario.js +165 -0
  135. package/dist/e2e/kit/scenario.js.map +1 -0
  136. package/dist/e2e/kit/scenarios.js +12 -18
  137. package/dist/e2e/kit/scenarios.js.map +1 -1
  138. package/dist/engine/dbos.service.js +8 -1
  139. package/dist/engine/dbos.service.js.map +1 -1
  140. package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
  141. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  142. package/dist/host/daemon.js +32 -8
  143. package/dist/host/daemon.js.map +1 -1
  144. package/dist/host/ensure-host.js +3 -3
  145. package/dist/host/ensure-host.js.map +1 -1
  146. package/dist/host/host-runtime.js.map +1 -1
  147. package/dist/host/host.lifecycle.js +3 -16
  148. package/dist/host/host.lifecycle.js.map +1 -1
  149. package/dist/http/graphql-host.js +17 -8
  150. package/dist/http/graphql-host.js.map +1 -1
  151. package/dist/mcp/mcp-capabilities.js +3 -2
  152. package/dist/mcp/mcp-capabilities.js.map +1 -1
  153. package/dist/mcp/mcp-facade.service.js +21 -0
  154. package/dist/mcp/mcp-facade.service.js.map +1 -1
  155. package/dist/mcp/mcp-tools.js +35 -1
  156. package/dist/mcp/mcp-tools.js.map +1 -1
  157. package/dist/pipeline/data-driven-task.workflow.js +276 -57
  158. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  159. package/dist/pipeline/pipeline.service.js +27 -5
  160. package/dist/pipeline/pipeline.service.js.map +1 -1
  161. package/dist/pipeline/route-contract.js +123 -3
  162. package/dist/pipeline/route-contract.js.map +1 -1
  163. package/dist/pipeline-core/condition-scopes.js +15 -0
  164. package/dist/pipeline-core/condition-scopes.js.map +1 -0
  165. package/dist/pipeline-core/index.js +2 -0
  166. package/dist/pipeline-core/index.js.map +1 -1
  167. package/dist/pipeline-core/kit/fixtures.js +81 -44
  168. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  169. package/dist/pipeline-core/materialize.js +221 -0
  170. package/dist/pipeline-core/materialize.js.map +1 -0
  171. package/dist/pipeline-core/types.js +5 -0
  172. package/dist/pipeline-core/types.js.map +1 -1
  173. package/dist/pipeline-core/validate-loops.js +2 -15
  174. package/dist/pipeline-core/validate-loops.js.map +1 -1
  175. package/dist/pipeline-core/validate-resilience.js +257 -0
  176. package/dist/pipeline-core/validate-resilience.js.map +1 -0
  177. package/dist/pipeline-core/validate.js +2 -0
  178. package/dist/pipeline-core/validate.js.map +1 -1
  179. package/dist/playbook/catalog-loader.js +39 -2
  180. package/dist/playbook/catalog-loader.js.map +1 -1
  181. package/dist/playbook/catalog-schema-validator.js +438 -0
  182. package/dist/playbook/catalog-schema-validator.js.map +1 -0
  183. package/dist/playbook/import-mapper.js +41 -0
  184. package/dist/playbook/import-mapper.js.map +1 -1
  185. package/dist/playbook/manifest.js +5 -0
  186. package/dist/playbook/manifest.js.map +1 -1
  187. package/dist/playbook/playbook-installer.js +17 -1
  188. package/dist/playbook/playbook-installer.js.map +1 -1
  189. package/dist/poller/pr-readiness-core.js +257 -93
  190. package/dist/poller/pr-readiness-core.js.map +1 -1
  191. package/dist/poller/pr-readiness.js +2 -2
  192. package/dist/poller/pr-readiness.js.map +1 -1
  193. package/dist/revisium/inbox.service.js +8 -8
  194. package/dist/revisium/inbox.service.js.map +1 -1
  195. package/dist/revisium/playbooks.service.js +116 -21
  196. package/dist/revisium/playbooks.service.js.map +1 -1
  197. package/dist/revisium/revisium.module.js +8 -8
  198. package/dist/revisium/revisium.module.js.map +1 -1
  199. package/dist/revisium/roles.service.js +3 -1
  200. package/dist/revisium/roles.service.js.map +1 -1
  201. package/dist/revisium/run.service.js +9 -8
  202. package/dist/revisium/run.service.js.map +1 -1
  203. package/dist/revisium/tokens.js +0 -1
  204. package/dist/revisium/tokens.js.map +1 -1
  205. package/dist/run/create-run.js +1 -1
  206. package/dist/run/create-run.js.map +1 -1
  207. package/dist/run/inspect-run.js +32 -28
  208. package/dist/run/inspect-run.js.map +1 -1
  209. package/dist/run/issue-ref.js +97 -2
  210. package/dist/run/issue-ref.js.map +1 -1
  211. package/dist/run/prisma-runtime-data-access.js +619 -0
  212. package/dist/run/prisma-runtime-data-access.js.map +1 -0
  213. package/dist/run/run-outputs.js +39 -14
  214. package/dist/run/run-outputs.js.map +1 -1
  215. package/dist/run/terminal-run-status.js.map +1 -1
  216. package/dist/runners/integrator.js +657 -56
  217. package/dist/runners/integrator.js.map +1 -1
  218. package/dist/runners/worktree.service.js +10 -8
  219. package/dist/runners/worktree.service.js.map +1 -1
  220. package/dist/schema/ajv-errors.js +15 -0
  221. package/dist/schema/ajv-errors.js.map +1 -0
  222. package/dist/smoke/isolation.js +7 -16
  223. package/dist/smoke/isolation.js.map +1 -1
  224. package/dist/storage/ensure-storage.js +117 -0
  225. package/dist/storage/ensure-storage.js.map +1 -0
  226. package/dist/storage/revo-database.js +39 -0
  227. package/dist/storage/revo-database.js.map +1 -0
  228. package/dist/storage/revo-prisma.service.js +31 -0
  229. package/dist/storage/revo-prisma.service.js.map +1 -0
  230. package/dist/storage/revo-storage.module.js +18 -0
  231. package/dist/storage/revo-storage.module.js.map +1 -0
  232. package/dist/task-control-plane/pr-readiness.service.js +5 -2
  233. package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
  234. package/dist/task-control-plane/run-watch.service.js +3 -6
  235. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  236. package/dist/task-control-plane/task-control-plane-api.service.js +225 -43
  237. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  238. package/dist/testing/runtime-data-access.js +175 -0
  239. package/dist/testing/runtime-data-access.js.map +1 -0
  240. package/dist/worker/build-context.js +178 -2
  241. package/dist/worker/build-context.js.map +1 -1
  242. package/dist/worker/claude-code-runner.js +22 -3
  243. package/dist/worker/claude-code-runner.js.map +1 -1
  244. package/dist/worker/codex-runner.js +6 -1
  245. package/dist/worker/codex-runner.js.map +1 -1
  246. package/package.json +20 -7
  247. package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
  248. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +235 -0
  249. package/prisma/migrations/migration_lock.toml +3 -0
  250. package/prisma/schema.prisma +339 -0
  251. package/prisma.config.ts +11 -0
  252. package/dist/cli/commands/revisium-helpers.js.map +0 -1
  253. package/dist/control-plane/client-transport.js +0 -168
  254. package/dist/control-plane/client-transport.js.map +0 -1
  255. package/dist/engine/ensure-postgres.js +0 -46
  256. package/dist/engine/ensure-postgres.js.map +0 -1
  257. package/dist/host/ensure-revisium.js +0 -135
  258. package/dist/host/ensure-revisium.js.map +0 -1
  259. 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
  }
@@ -168,36 +286,13 @@ function checkMergeGateRecheckRouting(template, sink) {
168
286
  nodeId: 'mergeGate',
169
287
  outcomes: ['approved', 'recheck', 'address_review_threads', 'return_to_development', 'override_merge', 'cancel'],
170
288
  });
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: 'address_review_threads',
181
- target: 'triage',
182
- });
183
- expectRoute(template, sink, {
184
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
185
- nodeId: 'mergeGate',
186
- verdict: 'return_to_development',
187
- target: 'triage',
188
- });
189
- expectRoute(template, sink, {
190
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
191
- nodeId: 'mergeGate',
192
- verdict: 'override_merge',
193
- target: 'confirmMerge',
194
- });
195
- expectRoute(template, sink, {
196
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
197
- nodeId: 'mergeGate',
198
- verdict: 'cancel',
199
- target: 'cancelledEnd',
200
- });
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
+ ]);
201
296
  expectScript(template, sink, {
202
297
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
203
298
  nodeId: 'mergeRecheck',
@@ -206,18 +301,13 @@ function checkMergeGateRecheckRouting(template, sink) {
206
301
  resultSchema: 'schema:prFeedback',
207
302
  produces: 'prFeedback',
208
303
  });
209
- expectRoute(template, sink, {
210
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
211
- nodeId: 'mergeRecheckRouter',
212
- verdict: 'clean',
213
- target: 'blockedEnd',
214
- });
215
- expectRoute(template, sink, {
216
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
217
- nodeId: 'mergeRecheckRouter',
218
- verdict: 'review_changes',
219
- target: 'triage',
220
- });
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
+ ]);
221
311
  expectBoundedRoute(template, sink, {
222
312
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
223
313
  nodeId: 'mergeRecheckRouter',
@@ -226,16 +316,10 @@ function checkMergeGateRecheckRouting(template, sink) {
226
316
  scope: 'ciLoop',
227
317
  value: 3,
228
318
  });
229
- expectRoute(template, sink, {
230
- code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
231
- nodeId: 'mergeRecheckRouter',
232
- verdict: 'recheck',
233
- target: 'mergeReadiness',
234
- });
235
319
  expectDefaultRoute(template, sink, {
236
320
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
237
321
  nodeId: 'mergeRecheckRouter',
238
- target: 'blockedEnd',
322
+ target: 'recoveryGate',
239
323
  });
240
324
  expectConsume(template, sink, {
241
325
  code: 'DEFAULT_POLICY_MERGE_RECHECK_ROUTE_MISSING',
@@ -254,6 +338,22 @@ function checkMergeGateRecheckRouting(template, sink) {
254
338
  staleOk: true,
255
339
  });
256
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
+ }
257
357
  function checkReviewFeedbackLoop(template, sink) {
258
358
  expectRoute(template, sink, {
259
359
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
@@ -316,6 +416,16 @@ function checkReviewFeedbackLoop(template, sink) {
316
416
  verdict: 'question',
317
417
  target: 'questionGate',
318
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');
319
429
  expectRoute(template, sink, {
320
430
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
321
431
  nodeId: 'triageRouter',
@@ -328,6 +438,30 @@ function checkReviewFeedbackLoop(template, sink) {
328
438
  verdict: 'wontfix',
329
439
  target: 'respondThreads',
330
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
+ });
331
465
  expectNodeNext(template, sink, {
332
466
  code: 'DEFAULT_POLICY_REVIEW_CHANGES_ROUTE_MISSING',
333
467
  nodeId: 'reviewRework',
@@ -353,13 +487,47 @@ function checkReviewFeedbackLoop(template, sink) {
353
487
  producerId: 'triage',
354
488
  as: 'triage',
355
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
+ });
356
498
  }
357
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
+ });
358
520
  expectHumanGateOutcomes(template, sink, {
359
521
  code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
360
522
  nodeId: 'planGate',
361
523
  outcomes: ['approved', 'rework', 'cancel'],
362
524
  });
525
+ expectRoute(template, sink, {
526
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
527
+ nodeId: 'planGate',
528
+ verdict: 'approved',
529
+ target: 'developer',
530
+ });
363
531
  expectRoute(template, sink, {
364
532
  code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
365
533
  nodeId: 'planGate',
@@ -372,6 +540,11 @@ function checkLoopExhaustionEscalation(template, sink) {
372
540
  verdict: 'cancel',
373
541
  target: 'cancelledEnd',
374
542
  });
543
+ expectDefaultRoute(template, sink, {
544
+ code: 'DEFAULT_POLICY_LOOP_EXHAUSTION_ESCALATION_MISSING',
545
+ nodeId: 'planGate',
546
+ target: 'blockedEnd',
547
+ });
375
548
  expectRouterDefaultGate(template, sink, {
376
549
  routerId: 'planReviewRouter',
377
550
  gateId: 'planStuckGate',
@@ -394,6 +567,26 @@ function checkLoopExhaustionEscalation(template, sink) {
394
567
  verdict: 'cancel',
395
568
  target: 'cancelledEnd',
396
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
+ });
397
590
  expectRouterDefaultGate(template, sink, {
398
591
  routerId: 'codeReviewRouter',
399
592
  gateId: 'codeStuckGate',
@@ -435,6 +628,178 @@ function checkLoopExhaustionEscalation(template, sink) {
435
628
  parent: null,
436
629
  });
437
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
+ }
438
803
  function expectChangeProducer(template, sink, nodeId) {
439
804
  const node = effectNode(template, nodeId);
440
805
  if (node?.resultSchema === 'schema:change' && node.produces?.name === 'change')
@@ -475,6 +840,41 @@ function expectScript(template, sink, rule) {
475
840
  actual: describeNode(node),
476
841
  });
477
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
+ }
478
878
  function expectGateOutcomes(template, sink, rule) {
479
879
  const node = template.nodes[rule.nodeId];
480
880
  if (node?.kind === 'humanGate' &&
@@ -482,7 +882,7 @@ function expectGateOutcomes(template, sink, rule) {
482
882
  node.outcomes.every((outcome, index) => outcome === rule.outcomes[index])) {
483
883
  return;
484
884
  }
485
- 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(',')}`, {
486
886
  nodeId: rule.nodeId,
487
887
  path: 'outcomes',
488
888
  expected: rule.outcomes.join(','),
@@ -621,6 +1021,9 @@ function expectRouterDefaultGate(template, sink, rule) {
621
1021
  actual: gate ? `kind=${gate.kind} ${approveVerdict}=${approveTarget ?? 'missing'} default=${gateDefault ?? 'missing'}` : 'missing gate',
622
1022
  });
623
1023
  }
1024
+ function isTerminalNode(template, nodeId) {
1025
+ return template.nodes[nodeId]?.kind === 'terminal';
1026
+ }
624
1027
  function effectNode(template, nodeId) {
625
1028
  const node = template.nodes[nodeId];
626
1029
  if (node?.kind === 'agent' || node?.kind === 'script')