@yemi33/minions 0.1.2448 → 0.1.2449

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 (315) hide show
  1. package/bin/cli-api-client.js +1 -1
  2. package/bin/install-internal-minions.js +1382 -44
  3. package/bin/install-layout.js +150 -0
  4. package/bin/minions.js +460 -167
  5. package/dashboard/docs/typography.md +65 -12
  6. package/dashboard/js/command-center.js +58 -4
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +179 -20
  10. package/dashboard/js/refresh.js +148 -12
  11. package/dashboard/js/render-dispatch.js +3 -4
  12. package/dashboard/js/render-inbox.js +2 -2
  13. package/dashboard/js/render-other.js +3 -3
  14. package/dashboard/js/render-pipelines.js +14 -0
  15. package/dashboard/js/render-plans.js +57 -9
  16. package/dashboard/js/render-prd.js +132 -23
  17. package/dashboard/js/render-prs.js +195 -166
  18. package/dashboard/js/render-schedules.js +63 -3
  19. package/dashboard/js/render-utils.js +3 -3
  20. package/dashboard/js/render-watches.js +19 -3
  21. package/dashboard/js/render-work-items.js +238 -30
  22. package/dashboard/js/settings.js +205 -54
  23. package/dashboard/js/utils.js +51 -1
  24. package/dashboard/pages/home.html +1 -1
  25. package/dashboard/pages/qa.html +1 -16
  26. package/dashboard/pages/work.html +40 -0
  27. package/dashboard/shared/cc-limits.js +79 -0
  28. package/dashboard/shared/pr-filters.js +21 -38
  29. package/dashboard/shared/project-git-summary.js +1 -1
  30. package/dashboard/shared/record-filters.js +169 -0
  31. package/dashboard/shared/watches-source.js +1 -1
  32. package/dashboard/shared/welcome-popup.js +1 -1
  33. package/dashboard/shared/wi-filters.js +302 -0
  34. package/dashboard/slim/body.html +1 -0
  35. package/dashboard/slim/js/command-send.js +26 -0
  36. package/dashboard/slim/js/modals-tiles.js +380 -39
  37. package/dashboard/slim/js/status.js +13 -21
  38. package/dashboard/slim/layout.html +1 -0
  39. package/dashboard/slim/panel-bootstrap.js +6 -2
  40. package/dashboard/slim/styles.css +38 -0
  41. package/dashboard/styles.css +159 -55
  42. package/dashboard-build.js +13 -2
  43. package/dashboard.js +956 -423
  44. package/docs/README.md +11 -6
  45. package/docs/api-errors.md +2 -2
  46. package/docs/architecture-review-2026-07-09.md +1 -1
  47. package/docs/architecture.excalidraw +2 -2
  48. package/docs/auto-discovery.md +18 -9
  49. package/docs/branch-derivation.md +4 -4
  50. package/docs/capture-demos.js +39 -2
  51. package/docs/ci-runner-canary.md +123 -0
  52. package/docs/claude-md-propagation.md +2 -2
  53. package/docs/cloud-agent-dispatch.md +204 -0
  54. package/docs/command-center.md +7 -7
  55. package/docs/completion-reports.md +43 -20
  56. package/docs/constants.md +10 -3
  57. package/docs/constellation-bridge.md +134 -6
  58. package/docs/constellation-style-telemetry.md +4 -4
  59. package/docs/contracts/capability-protocol.v1.json +165 -0
  60. package/docs/cooldown-merge-semantics.md +12 -12
  61. package/docs/copilot-cli-schema.md +7 -7
  62. package/docs/cross-repo-plans.md +10 -10
  63. package/docs/dead-code-audit-retractions.md +5 -5
  64. package/docs/default-branch-ci.md +173 -0
  65. package/docs/deprecated.json +31 -31
  66. package/docs/design-inbox-entries-schema.md +3 -3
  67. package/docs/design-language.md +1051 -0
  68. package/docs/design-state-storage.md +11 -11
  69. package/docs/diagnostics-crash-reports.md +9 -9
  70. package/docs/diagnostics-memory.md +5 -5
  71. package/docs/documentation-audit-2026-07-09.md +7 -7
  72. package/docs/engine-restart.md +90 -5
  73. package/docs/harness-mode.md +1 -1
  74. package/docs/internal-install.md +338 -39
  75. package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
  76. package/docs/kb-pr3223-cascade-archiving.md +1 -1
  77. package/docs/kb-pr696-merge-conflict-docs.md +6 -6
  78. package/docs/kb-sweep.md +35 -35
  79. package/docs/keep-processes.md +1 -1
  80. package/docs/live-checkout-mode.md +30 -30
  81. package/docs/managed-spawn.md +18 -14
  82. package/docs/named-agents.md +7 -7
  83. package/docs/plan-lifecycle.md +69 -2
  84. package/docs/pr-author-identity.md +114 -0
  85. package/docs/pr-auto-fix-dispatch.md +19 -4
  86. package/docs/pr-comment-followup.md +6 -6
  87. package/docs/pr-review-fix-loop.md +59 -10
  88. package/docs/process-termination.md +40 -0
  89. package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
  90. package/docs/qa-runbook-lifecycle.md +367 -17
  91. package/docs/qa-runbooks.md +3 -3
  92. package/docs/rfc-completion-json.md +18 -18
  93. package/docs/runtime-adapters.md +26 -21
  94. package/docs/security.md +6 -6
  95. package/docs/self-improvement.md +4 -4
  96. package/docs/shared-lifecycle-module-map.md +473 -472
  97. package/docs/skills.md +52 -3
  98. package/docs/slim-ux/concepts.md +121 -116
  99. package/docs/specs/agent-configurability.md +18 -18
  100. package/docs/specs/agent-rename.md +18 -18
  101. package/docs/team-memory.md +38 -21
  102. package/docs/timeouts-and-liveness.md +118 -10
  103. package/docs/tutorials/01-install-and-connect.md +1 -1
  104. package/docs/watches.md +40 -39
  105. package/docs/workspace-manifests.md +4 -4
  106. package/docs/worktree-lifecycle.md +293 -14
  107. package/engine/README.md +46 -0
  108. package/engine/{ado-comment.js → ado/comment.js} +8 -8
  109. package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
  110. package/engine/{ado.js → ado/index.js} +417 -63
  111. package/engine/{ado-status.js → ado/status.js} +6 -8
  112. package/engine/{ado-token.js → ado/token.js} +1 -1
  113. package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
  114. package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
  115. package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
  116. package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
  117. package/engine/{harness-context.js → agents/harness-context.js} +5 -5
  118. package/engine/{harness.js → agents/harness.js} +3 -3
  119. package/engine/{llm.js → agents/llm.js} +18 -14
  120. package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
  121. package/engine/{playbook.js → agents/playbook.js} +155 -22
  122. package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
  123. package/engine/{preflight.js → agents/preflight.js} +29 -10
  124. package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
  125. package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
  126. package/engine/{steering.js → agents/steering.js} +5 -5
  127. package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
  128. package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
  129. package/engine/{api-validation.js → api/api-validation.js} +1 -1
  130. package/engine/api/bridge.js +787 -0
  131. package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
  132. package/engine/api/companion.js +560 -0
  133. package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
  134. package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
  135. package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
  136. package/engine/api-contracts/agent-content.js +4 -4
  137. package/engine/api-contracts/capability-manifest.js +236 -0
  138. package/engine/api-contracts/capability-protocol.js +333 -0
  139. package/engine/api-contracts/cc-ops.js +1 -1
  140. package/engine/api-contracts/config-runtime.js +5 -0
  141. package/engine/api-contracts/core.js +28 -1
  142. package/engine/api-contracts/index.js +100 -0
  143. package/engine/api-contracts/orchestration.js +18 -5
  144. package/engine/api-contracts/pull-requests.js +37 -6
  145. package/engine/api-contracts/qa-process.js +29 -6
  146. package/engine/api-contracts/work-plan-prd.js +21 -1
  147. package/engine/cloud/contract.js +212 -0
  148. package/engine/cloud/index.js +159 -0
  149. package/engine/{execution-model.js → core/execution-model.js} +1 -1
  150. package/engine/{features.js → core/features.js} +4 -4
  151. package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
  152. package/engine/{queries.js → core/queries.js} +201 -36
  153. package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
  154. package/engine/{shared.js → core/shared.js} +1637 -175
  155. package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
  156. package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
  157. package/engine/db/index.js +11 -2
  158. package/engine/db/migrations/002-dispatches.js +3 -3
  159. package/engine/db/migrations/003-work-items.js +1 -1
  160. package/engine/db/migrations/004-pull-requests.js +1 -1
  161. package/engine/db/migrations/006-metrics.js +1 -1
  162. package/engine/db/migrations/007-watches.js +2 -2
  163. package/engine/db/migrations/008-small-state.js +1 -1
  164. package/engine/db/migrations/009-qa.js +1 -1
  165. package/engine/db/migrations/010-pr-links.js +1 -1
  166. package/engine/db/migrations/011-remaining-state.js +1 -1
  167. package/engine/db/migrations/012-steering-deliveries.js +2 -2
  168. package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
  169. package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
  170. package/engine/db/migrations/015-plans-prds.js +0 -0
  171. package/engine/db/migrations/018-sql-only-cutover.js +2 -2
  172. package/engine/db/migrations/021-archived-work-items.js +1 -1
  173. package/engine/db/migrations/022-global-cc-session.js +1 -1
  174. package/engine/db/migrations/023-engine-state.js +1 -1
  175. package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
  176. package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
  177. package/engine/db/migrations/029-repair-reused-versions.js +20 -0
  178. package/engine/db/migrations/031-pr-author-identity.js +137 -0
  179. package/engine/{consolidation.js → memory/consolidation.js} +6 -6
  180. package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
  181. package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
  182. package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
  183. package/engine/{memory-store.js → memory/memory-store.js} +3 -3
  184. package/engine/{promotion.js → memory/promotion.js} +3 -3
  185. package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
  186. package/engine/{review-learning.js → memory/review-learning.js} +10 -5
  187. package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
  188. package/engine/{logs-store.js → observability/logs-store.js} +5 -5
  189. package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
  190. package/engine/{check-status.js → operations/check-status.js} +3 -3
  191. package/engine/{cli.js → operations/cli.js} +271 -113
  192. package/engine/{distribution.js → operations/distribution.js} +5 -6
  193. package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
  194. package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
  195. package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
  196. package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
  197. package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
  198. package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
  199. package/engine/{meeting.js → orchestration/meeting.js} +6 -16
  200. package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
  201. package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
  202. package/engine/{routing.js → orchestration/routing.js} +3 -3
  203. package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
  204. package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
  205. package/engine/{timeout.js → orchestration/timeout.js} +158 -109
  206. package/engine/{db-events.js → persistence/db-events.js} +2 -2
  207. package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
  208. package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
  209. package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
  210. package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
  211. package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
  212. package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
  213. package/engine/persistence/state-operations.js +350 -0
  214. package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
  215. package/engine/{issues.js → planning/issues.js} +2 -2
  216. package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
  217. package/engine/planning/prd-result-sidecar.js +190 -0
  218. package/engine/{prd-store.js → planning/prd-store.js} +17 -17
  219. package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
  220. package/engine/{projects.js → planning/projects.js} +10 -10
  221. package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
  222. package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
  223. package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
  224. package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
  225. package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
  226. package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
  227. package/engine/{process-utils.js → processes/process-utils.js} +599 -55
  228. package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
  229. package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
  230. package/engine/{comment-format.js → providers/comment-format.js} +5 -5
  231. package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
  232. package/engine/{gh-token.js → providers/gh-token.js} +4 -4
  233. package/engine/{github.js → providers/github.js} +131 -54
  234. package/engine/{pr-action.js → providers/pr-action.js} +13 -12
  235. package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
  236. package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
  237. package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
  238. package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
  239. package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
  240. package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
  241. package/engine/{pr-track.js → providers/pr-track.js} +11 -13
  242. package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
  243. package/engine/qa/auto-prd-qa.js +313 -0
  244. package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
  245. package/engine/qa/prd-session.js +240 -0
  246. package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
  247. package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
  248. package/engine/{qa-runs.js → qa/runs.js} +286 -15
  249. package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
  250. package/engine/qa/visual-journey.js +654 -0
  251. package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
  252. package/engine/qa-runners/maestro.js +3 -3
  253. package/engine/qa-runners/playwright.js +2 -2
  254. package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
  255. package/engine/recovery/stop-stack.js +607 -0
  256. package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
  257. package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
  258. package/engine/runtimes/claude.js +14 -12
  259. package/engine/runtimes/codex.js +8 -6
  260. package/engine/runtimes/copilot.js +17 -16
  261. package/engine/{watch-actions.js → watches/actions.js} +13 -13
  262. package/engine/{watches.js → watches/index.js} +43 -32
  263. package/engine/{watches-store.js → watches/store.js} +4 -4
  264. package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
  265. package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
  266. package/engine/worktrees/inventory.js +671 -0
  267. package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
  268. package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
  269. package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
  270. package/engine/worktrees/quarantine-refs.js +173 -0
  271. package/engine.js +1137 -208
  272. package/minions.js +147 -77
  273. package/package.json +10 -6
  274. package/playbooks/_pr-description-audit.md +110 -78
  275. package/playbooks/build-fix-complex.md +2 -0
  276. package/playbooks/fix.md +16 -12
  277. package/playbooks/implement-shared.md +2 -0
  278. package/playbooks/implement.md +19 -20
  279. package/playbooks/plan-to-prd.md +18 -3
  280. package/playbooks/qa-session-draft.md +136 -1
  281. package/playbooks/qa-session-execute.md +80 -2
  282. package/playbooks/qa-session-setup.md +17 -1
  283. package/playbooks/qa-validate.md +1 -1
  284. package/playbooks/setup.md +2 -0
  285. package/playbooks/shared-rules.md +25 -32
  286. package/playbooks/templates/followup-dispatch.md +4 -3
  287. package/playbooks/verify.md +1 -1
  288. package/prompts/cc-system.md +19 -27
  289. package/watch-plugins/README.md +92 -0
  290. package/watch-plugins/ado-author-prs.js +336 -0
  291. package/watch-plugins/gh-author-prs.js +375 -0
  292. package/watch-plugins/http.js +474 -0
  293. package/watch-plugins/teams-channel.js +869 -0
  294. package/docs/dev-composite-workflow.md +0 -101
  295. package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
  296. package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
  297. package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
  298. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  299. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  300. package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
  301. package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
  302. package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
  303. package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
  304. package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
  305. package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
  306. package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
  307. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  308. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  309. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  310. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  311. package/docs/visual-evidence-ci.md +0 -103
  312. package/engine/bridge.js +0 -379
  313. package/engine/quarantine-refs.js +0 -103
  314. package/engine/state-operations.js +0 -178
  315. /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { CC_API_LIMITS, PRESENCE_VISIBILITIES } = require('../cc-api-validation');
3
+ const { CC_API_LIMITS, PRESENCE_VISIBILITIES } = require('../api/cc-api-validation');
4
+ const { CAPABILITY_SCOPE, CAPABILITY_CONFIRM } = require('./capability-protocol');
4
5
 
5
6
  module.exports = {
6
7
  owner: 'core',
@@ -10,6 +11,8 @@ module.exports = {
10
11
  'GET /api/status',
11
12
  'GET /api/engine-log',
12
13
  'GET /api/constellation/v1/snapshot',
14
+ 'GET /api/capabilities/protocol',
15
+ 'GET /api/capabilities',
13
16
  'GET /state/<path>',
14
17
  'GET /api/metrics',
15
18
  'GET /api/tools',
@@ -49,6 +52,22 @@ module.exports = {
49
52
  'GET /api/status': {
50
53
  audit: 'audited',
51
54
  noInputReason: 'Returns the current dashboard snapshot; accepts no caller input.',
55
+ // Phase-0 allowlist. Since issue #2949 this snapshot is only the small
56
+ // live envelope — engine health/tick, the ADO+GitHub throttle state,
57
+ // project git status, autoMode, and the version banner. The heavy work,
58
+ // plan, and PR slices moved to their own endpoints, each of which is a
59
+ // separately-declared capability. So the summary projection grants ONE
60
+ // scope, not the union of every read scope: granting the plan and PR
61
+ // read scopes here would be over-declaration for a payload that carries
62
+ // neither.
63
+ capability: {
64
+ id: 'status.summary',
65
+ title: 'Read the engine status summary',
66
+ description: 'Read the live engine health, throttle, and project summary snapshot.',
67
+ scopes: [CAPABILITY_SCOPE.WORK_READ],
68
+ destructive: false,
69
+ confirm: CAPABILITY_CONFIRM.NONE,
70
+ },
52
71
  },
53
72
  'GET /api/engine-log': {
54
73
  audit: 'audited',
@@ -62,6 +81,14 @@ module.exports = {
62
81
  audit: 'audited',
63
82
  noInputReason: 'Returns the versioned bridge snapshot; accepts no caller input.',
64
83
  },
84
+ 'GET /api/capabilities/protocol': {
85
+ audit: 'audited',
86
+ noInputReason: 'Returns the static capability-protocol artifact for live negotiation; accepts no caller input.',
87
+ },
88
+ 'GET /api/capabilities': {
89
+ audit: 'audited',
90
+ noInputReason: 'Returns the derived capability manifest; accepts no caller input.',
91
+ },
65
92
  'GET /state/<path>': {
66
93
  audit: 'audited',
67
94
  path: {
@@ -19,6 +19,100 @@ const OWNERS = Object.freeze(OWNER_NAMES);
19
19
  const LOCATION_POLICIES = new Set(['none', 'optional', 'required', 'pending-audit']);
20
20
  const AUDIT_STATUSES = new Set(['audited', 'pending-audit']);
21
21
 
22
+ // The capability vocabulary is owned by the cross-repo protocol contract, never
23
+ // restated here. This module only decides WHICH routes opt in and enforces that
24
+ // each declaration is complete against that contract.
25
+ const {
26
+ CAPABILITY_SCOPES,
27
+ CAPABILITY_CONFIRM_MODES,
28
+ CAPABILITY_CONFIRM,
29
+ CAPABILITY_ENTRY_FIELDS,
30
+ } = require('./capability-protocol');
31
+
32
+ const CAPABILITY_SCOPE_SET = new Set(CAPABILITY_SCOPES);
33
+ const CAPABILITY_CONFIRM_SET = new Set(CAPABILITY_CONFIRM_MODES);
34
+ // Derived from the route key or the audited request contract — a hand-typed
35
+ // copy could drift from the route it claims to describe, so these entry fields
36
+ // may not be declared in an override.
37
+ const CAPABILITY_DERIVED_FIELDS = new Set(['method', 'path', 'params']);
38
+ const CAPABILITY_DECLARED_FIELDS = CAPABILITY_ENTRY_FIELDS
39
+ .filter(entryField => !CAPABILITY_DERIVED_FIELDS.has(entryField.name));
40
+ // Producer-only key: not published in the manifest, it exists so a
41
+ // non-destructive claim on a state-changing route has to be argued rather than
42
+ // assumed (see normalizeCapability).
43
+ const CAPABILITY_PRODUCER_FIELDS = new Set(['nonDestructiveRationale']);
44
+
45
+ /**
46
+ * Validate the opt-in `capability` block on a route override.
47
+ *
48
+ * Fail-closed in both directions: a route without a block is simply not a
49
+ * capability (it never reaches the manifest), and a route WITH a block must
50
+ * declare every required protocol field or the catalog build throws. In
51
+ * particular `destructive` and `confirm` are always explicit — the remote
52
+ * authorization posture of a capability must never be inferred from a route's
53
+ * method, path, or free-text description, because rewording a description
54
+ * would then silently reclassify it for a remote caller.
55
+ */
56
+ function normalizeCapability(value, key, stateChanging) {
57
+ if (value === undefined) return null;
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
59
+ throw new Error(`${key} capability must be an object`);
60
+ }
61
+ for (const declaredField of Object.keys(value)) {
62
+ if (CAPABILITY_DERIVED_FIELDS.has(declaredField)) {
63
+ throw new Error(`${key} capability must not declare "${declaredField}" — it is derived from the route contract`);
64
+ }
65
+ if (!CAPABILITY_DECLARED_FIELDS.some(entryField => entryField.name === declaredField)
66
+ && !CAPABILITY_PRODUCER_FIELDS.has(declaredField)) {
67
+ throw new Error(`${key} capability declares unknown field "${declaredField}"`);
68
+ }
69
+ }
70
+ for (const entryField of CAPABILITY_DECLARED_FIELDS) {
71
+ if (entryField.required && value[entryField.name] === undefined) {
72
+ throw new Error(`${key} capability must explicitly declare "${entryField.name}"`);
73
+ }
74
+ }
75
+ for (const textField of ['id', 'title', 'description']) {
76
+ if (typeof value[textField] !== 'string' || !value[textField].trim()) {
77
+ throw new Error(`${key} capability ${textField} must be a non-empty string`);
78
+ }
79
+ }
80
+ if (!Array.isArray(value.scopes) || value.scopes.length === 0) {
81
+ throw new Error(`${key} capability must declare at least one scope`);
82
+ }
83
+ for (const scope of value.scopes) {
84
+ if (!CAPABILITY_SCOPE_SET.has(scope)) {
85
+ throw new Error(`${key} capability declares unknown scope "${scope}"; the scope vocabulary is owned by engine/api-contracts/capability-protocol.js`);
86
+ }
87
+ }
88
+ if (typeof value.destructive !== 'boolean') {
89
+ throw new Error(`${key} capability destructive must be an explicit boolean`);
90
+ }
91
+ if (!CAPABILITY_CONFIRM_SET.has(value.confirm)) {
92
+ throw new Error(`${key} capability declares unknown confirm mode "${value.confirm}"; the confirmation enum is owned by engine/api-contracts/capability-protocol.js`);
93
+ }
94
+ if (value.destructive && value.confirm !== CAPABILITY_CONFIRM.PER_CALL) {
95
+ throw new Error(`${key} capability is destructive and must require per-call confirmation`);
96
+ }
97
+ // Corroborating assertion only: `stateChanging` is method-derived, so it can
98
+ // contradict a `destructive: false` claim but never supply one.
99
+ const rationale = value.nonDestructiveRationale;
100
+ const needsRationale = stateChanging && value.destructive === false;
101
+ if (needsRationale && (typeof rationale !== 'string' || !rationale.trim())) {
102
+ throw new Error(`${key} capability claims destructive: false on a state-changing route and must justify it with nonDestructiveRationale`);
103
+ }
104
+ if (!needsRationale && rationale !== undefined) {
105
+ throw new Error(`${key} capability declares nonDestructiveRationale where it does not apply`);
106
+ }
107
+ const normalized = {};
108
+ for (const entryField of CAPABILITY_DECLARED_FIELDS) {
109
+ const declared = value[entryField.name];
110
+ if (declared === undefined) continue;
111
+ normalized[entryField.name] = typeof declared === 'string' ? declared.trim() : cloneSerializable(declared);
112
+ }
113
+ return normalized;
114
+ }
115
+
22
116
  function canonicalPathTemplate(value) {
23
117
  const pathTemplate = String(value || '').trim();
24
118
  if (!pathTemplate.startsWith('/')) {
@@ -212,6 +306,8 @@ function buildRouteContract(owner, key, bodyless, override = {}) {
212
306
  request,
213
307
  negativeTests,
214
308
  };
309
+ const capability = normalizeCapability(override.capability, parsed.key, stateChanging);
310
+ if (capability) contract.capability = capability;
215
311
  if (noInput) {
216
312
  contract.noInputReason = override.noInputReason
217
313
  || (stateChanging
@@ -301,6 +397,10 @@ function routeContractMetadata(route) {
301
397
  ...(contract.noInputReason ? { noInputReason: contract.noInputReason } : {}),
302
398
  request: contract.request,
303
399
  negativeTests: contract.negativeTests,
400
+ // Whitelisted explicitly: this clone is the only metadata a consumer sees,
401
+ // so an un-listed key is silently dropped and the capability manifest would
402
+ // come back empty.
403
+ ...(contract.capability ? { capability: contract.capability } : {}),
304
404
  });
305
405
  }
306
406
 
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const keepProcesses = require('../keep-process-sweep');
4
- const managedSpawn = require('../managed-spawn');
3
+ const keepProcesses = require('../processes/keep-process-sweep');
4
+ const managedSpawn = require('../processes/managed-spawn');
5
5
  const managedSpecNameMax = managedSpawn.validateManagedSpecName('x').maxLength;
6
- const agentApiValidation = require('../agent-api-validation');
6
+ const agentApiValidation = require('../api/agent-api-validation');
7
7
  const paging = require('./paging');
8
8
 
9
9
  // The optional progressive-load paging window is the ONLY caller-supplied input
@@ -48,6 +48,7 @@ module.exports = {
48
48
  'GET /api/pipelines',
49
49
  'GET /api/keep-processes',
50
50
  'GET /api/worktree-quarantine-refs',
51
+ 'GET /api/worktree-inventory',
51
52
  'POST /api/keep-processes/kill',
52
53
  'GET /api/managed-processes',
53
54
  'GET /api/managed-processes/by-name/<name>',
@@ -159,6 +160,16 @@ module.exports = {
159
160
  },
160
161
  ],
161
162
  },
163
+ 'GET /api/worktree-inventory': {
164
+ audit: 'audited',
165
+ noInputReason: 'Read-only classification of configured project worktree roots and git registrations; accepts no caller input.',
166
+ constraints: [
167
+ {
168
+ kind: 'configured-project-membership',
169
+ source: 'config.projects[].localPath',
170
+ },
171
+ ],
172
+ },
162
173
  'GET /api/managed-processes': {
163
174
  audit: 'audited',
164
175
  query: {
@@ -347,6 +358,7 @@ module.exports = {
347
358
  { name: 'participants', type: 'array', required: false, allowedValuesSource: 'config.agents keys' },
348
359
  { name: 'priority', type: 'string', required: false, default: 'medium', allowedValuesSource: 'shared.WORK_ITEM_PRIORITY_VALUES' },
349
360
  { name: 'enabled', type: 'boolean', required: false, default: true },
361
+ { name: 'executionSurface', type: 'string', required: false, default: 'repo', allowedValuesSource: 'shared.EXECUTION_SURFACE_VALUES' },
350
362
  ],
351
363
  },
352
364
  constraints: [
@@ -373,6 +385,7 @@ module.exports = {
373
385
  { name: 'participants', type: 'array', required: false, allowedValuesSource: 'config.agents keys' },
374
386
  { name: 'priority', type: 'string', required: false, allowedValuesSource: 'shared.WORK_ITEM_PRIORITY_VALUES' },
375
387
  { name: 'enabled', type: 'boolean', required: false },
388
+ { name: 'executionSurface', type: 'string', required: false, allowedValuesSource: 'shared.EXECUTION_SURFACE_VALUES' },
376
389
  ],
377
390
  },
378
391
  constraints: [
@@ -562,7 +575,7 @@ module.exports = {
562
575
  { name: 'notify', type: 'string', required: false, default: 'inbox', allowedValues: ['inbox', 'none'] },
563
576
  { name: 'stopAfter', type: 'integer', required: false, default: 0, min: 0, max: Number.MAX_SAFE_INTEGER },
564
577
  { name: 'onNotMet', type: 'string|null', required: false, allowedValues: ['notify'] },
565
- { name: 'action', type: 'object|array|null', required: false, allowedValuesSource: 'watch-actions registry' },
578
+ { name: 'action', type: 'object|array|null', required: false, allowedValuesSource: 'watch action registry' },
566
579
  { name: 'requires', type: 'array|null', required: false, allowedValuesSource: 'watches target-type registry requirement conditions' },
567
580
  ],
568
581
  },
@@ -591,7 +604,7 @@ module.exports = {
591
604
  { name: 'notify', type: 'string', required: false, allowedValues: ['inbox', 'none'] },
592
605
  { name: 'stopAfter', type: 'integer', required: false, min: 0, max: Number.MAX_SAFE_INTEGER },
593
606
  { name: 'onNotMet', type: 'string|null', required: false, allowedValues: ['notify'] },
594
- { name: 'action', type: 'object|array|null', required: false, allowedValuesSource: 'watch-actions registry' },
607
+ { name: 'action', type: 'object|array|null', required: false, allowedValuesSource: 'watch action registry' },
595
608
  { name: 'requires', type: 'array|null', required: false, allowedValuesSource: 'watches target-type registry requirement conditions' },
596
609
  ],
597
610
  },
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const { PR_ACTIONS } = require('../pr-action');
4
- const { EXECUTION_TARGET_IDS } = require('../pr-fix-target');
5
- const { PR_FIX_CAUSE } = require('../shared');
3
+ const { PR_ACTIONS } = require('../providers/pr-action');
4
+ const { EXECUTION_TARGET_IDS } = require('../providers/pr-fix-target');
5
+ const { PR_FIX_CAUSE } = require('../core/shared');
6
6
  const paging = require('./paging');
7
+ const { CAPABILITY_SCOPE, CAPABILITY_CONFIRM } = require('./capability-protocol');
7
8
 
8
9
  const PR_REFERENCE_FORMATS = Object.freeze([
9
10
  'GitHub PR URL',
@@ -44,6 +45,7 @@ module.exports = {
44
45
  'POST /api/pr-action/prepare-create-pr-worktree',
45
46
  'POST /api/pr-action/cleanup-create-pr-worktree',
46
47
  'POST /api/pull-requests/observe',
48
+ 'POST /api/pull-requests/automation',
47
49
  'POST /api/pull-requests/delete',
48
50
  'POST /api/pull-requests/clear-paused-cause',
49
51
  'POST /api/pull-requests/clear-build-fix-ineffective',
@@ -62,6 +64,14 @@ module.exports = {
62
64
  ...paging.listPagingClampConstraints(),
63
65
  { kind: 'ignores-unknown-query-params', location: 'query' },
64
66
  ],
67
+ capability: {
68
+ id: 'pr.list',
69
+ title: 'List tracked pull requests',
70
+ description: 'Read the tracked pull-request records with build, review, and merge state.',
71
+ scopes: [CAPABILITY_SCOPE.PR_READ],
72
+ destructive: false,
73
+ confirm: CAPABILITY_CONFIRM.NONE,
74
+ },
65
75
  },
66
76
  'GET /api/prs/<id>': {
67
77
  audit: 'audited',
@@ -129,7 +139,7 @@ module.exports = {
129
139
  type: 'string',
130
140
  required: true,
131
141
  allowedValues: PR_ACTIONS,
132
- allowedValuesSource: 'engine/pr-action.PR_ACTIONS',
142
+ allowedValuesSource: 'engine/providers/pr-action.PR_ACTIONS',
133
143
  },
134
144
  { name: 'execute', type: 'boolean', required: false, default: false },
135
145
  { name: 'draftOnly', type: 'boolean', required: false, default: false },
@@ -154,7 +164,7 @@ module.exports = {
154
164
  type: 'string',
155
165
  required: false,
156
166
  allowedValues: EXECUTION_TARGET_IDS,
157
- allowedValuesSource: 'engine/pr-fix-target.EXECUTION_TARGET_IDS',
167
+ allowedValuesSource: 'engine/providers/pr-fix-target.EXECUTION_TARGET_IDS',
158
168
  },
159
169
  { name: 'remember', type: 'boolean', required: false, default: false },
160
170
  { name: 'reprompt', type: 'boolean', required: false, default: false },
@@ -249,6 +259,27 @@ module.exports = {
249
259
  precedence: ['contextOnly', 'observe'],
250
260
  }],
251
261
  }),
262
+ 'POST /api/pull-requests/automation': auditedBody([
263
+ { name: 'host', type: 'string', required: true, allowedValues: ['github', 'ado'] },
264
+ {
265
+ name: 'slug',
266
+ type: 'string',
267
+ required: true,
268
+ shapeByHost: {
269
+ github: 'owner/repo',
270
+ ado: 'org/project/repo',
271
+ },
272
+ },
273
+ {
274
+ name: 'number',
275
+ type: 'integer',
276
+ required: true,
277
+ min: 1,
278
+ max: Number.MAX_SAFE_INTEGER,
279
+ },
280
+ { name: 'automation', type: 'string', required: true, allowedValues: ['fix', 'review'] },
281
+ { name: 'enabled', type: 'boolean', required: true },
282
+ ], { seam: 'tracked PR automation' }),
252
283
  'POST /api/pull-requests/delete': auditedBody([
253
284
  {
254
285
  name: 'id',
@@ -280,7 +311,7 @@ module.exports = {
280
311
  type: 'string',
281
312
  required: true,
282
313
  allowedValues: Object.values(PR_FIX_CAUSE),
283
- allowedValuesSource: 'engine/shared.PR_FIX_CAUSE',
314
+ allowedValuesSource: 'engine/core/shared.PR_FIX_CAUSE',
284
315
  },
285
316
  ], { seam: 'tracked PR pause store' }),
286
317
  'POST /api/pull-requests/clear-build-fix-ineffective': auditedBody([
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const qaFromPrd = require('../qa-from-prd');
4
- const keepProcesses = require('../keep-process-sweep');
5
- const qaRunbooks = require('../qa-runbooks');
6
- const qaRuns = require('../qa-runs');
7
- const qaSessions = require('../qa-sessions');
8
- const shared = require('../shared');
3
+ const qaFromPrd = require('../qa/from-prd');
4
+ const keepProcesses = require('../processes/keep-process-sweep');
5
+ const qaRunbooks = require('../qa/runbooks');
6
+ const qaRuns = require('../qa/runs');
7
+ const qaSessions = require('../qa/sessions');
8
+ const shared = require('../core/shared');
9
9
 
10
10
  const sessionIdPath = {
11
11
  policy: 'required',
@@ -65,6 +65,7 @@ module.exports = {
65
65
  'POST /api/qa/sessions/<id>/dismiss',
66
66
  'GET /api/qa/sessions/<id>/test',
67
67
  'GET /api/qa/sessions/<id>',
68
+ 'DELETE /api/qa/sessions/<id>',
68
69
  'GET /api/qa/runners',
69
70
  'POST /api/qa/runners/reload',
70
71
  'POST /api/meetings',
@@ -80,6 +81,7 @@ module.exports = {
80
81
  bodyless: [
81
82
  'DELETE /api/qa/runs/<id>',
82
83
  'DELETE /api/qa/runbooks/<id>',
84
+ 'DELETE /api/qa/sessions/<id>',
83
85
  'POST /api/qa/sessions/<id>/approve',
84
86
  'POST /api/qa/runners/reload',
85
87
  ],
@@ -531,6 +533,27 @@ module.exports = {
531
533
  { strategy: 'missing-session-or-test-file', expectedStatus: 404 },
532
534
  ],
533
535
  },
536
+ 'DELETE /api/qa/sessions/<id>': {
537
+ audit: 'audited',
538
+ path: sessionIdPath,
539
+ constraints: [
540
+ { kind: 'body-forbidden' },
541
+ { kind: 'terminal-state-required', source: 'qa-sessions.TERMINAL_STATES' },
542
+ {
543
+ kind: 'owned-linked-resource-cleanup',
544
+ source: 'qa-sessions.deleteSession',
545
+ resources: ['qa-tests/<id>/', 'qa-runs record with runbookId qa-session-<id>'],
546
+ excludes: ['managed-spawn processes'],
547
+ },
548
+ ],
549
+ negativeTests: [
550
+ { strategy: 'non-empty-body-before-delete', expectedStatus: 400 },
551
+ { strategy: 'unsafe-id', expectedStatus: 400 },
552
+ { strategy: 'well-formed-missing-id', expectedStatus: 404 },
553
+ { strategy: 'non-terminal-session', expectedStatus: 409 },
554
+ { strategy: 'snapshot-session-run-and-test-directory-on-rejection', expectedStatus: '4xx' },
555
+ ],
556
+ },
534
557
  'POST /api/qa/sessions/<id>/approve': {
535
558
  audit: 'audited',
536
559
  path: sessionIdPath,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const paging = require('./paging');
4
+ const { CAPABILITY_SCOPE, CAPABILITY_CONFIRM } = require('./capability-protocol');
4
5
 
5
6
  const AUDITED_NEGATIVE_TESTS = Object.freeze([{
6
7
  strategy: 'isolated-http-invalid-input-state-snapshot',
@@ -77,6 +78,14 @@ module.exports = {
77
78
  negativeTests: [
78
79
  { strategy: 'reject-unknown-query-param-blaming-the-rejected-name', expectedStatus: 400 },
79
80
  ],
81
+ capability: {
82
+ id: 'plan.list',
83
+ title: 'List plans',
84
+ description: 'Read the plan inventory with each plan\'s lifecycle status.',
85
+ scopes: [CAPABILITY_SCOPE.PLAN_READ],
86
+ destructive: false,
87
+ confirm: CAPABILITY_CONFIRM.NONE,
88
+ },
80
89
  },
81
90
  'POST /api/work-items': {
82
91
  audit: 'audited',
@@ -279,6 +288,14 @@ module.exports = {
279
288
  ...paging.listPagingClampConstraints(),
280
289
  { kind: 'ignores-unknown-query-params', location: 'query' },
281
290
  ],
291
+ capability: {
292
+ id: 'work.list',
293
+ title: 'List work items',
294
+ description: 'Read the current work-item queue with its status, agent, and project.',
295
+ scopes: [CAPABILITY_SCOPE.WORK_READ],
296
+ destructive: false,
297
+ confirm: CAPABILITY_CONFIRM.NONE,
298
+ },
282
299
  },
283
300
  'GET /api/work-items/archive': {
284
301
  // Archived-item listing: reads no query/body. Affirmed input-less.
@@ -412,7 +429,10 @@ module.exports = {
412
429
  { name: 'requestedBy', type: 'string', required: false },
413
430
  ],
414
431
  },
415
- constraints: ['feedback must contain non-whitespace text and PRD status must allow revision'],
432
+ constraints: [
433
+ 'feedback must contain non-whitespace text and PRD status must allow revision',
434
+ 'a live revision operation is rejected with 409 revision-in-progress and the existing work item id',
435
+ ],
416
436
  },
417
437
  'POST /api/plans/discuss': {
418
438
  audit: 'audited',
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * engine/cloud/contract.js — versioned contract for CLOUD DISPATCH PROVIDERS.
5
+ *
6
+ * A cloud provider hands a work item to a REMOTE coding agent (Agency, the
7
+ * GitHub Copilot coding agent, an ADO work-item assignment picked up by a bot)
8
+ * and lets Minions track that remote session until a pull request exists.
9
+ *
10
+ * ── Why this is NOT a runtime adapter ────────────────────────────────────
11
+ * `engine/runtimes/contract.js` describes a LOCAL CLI PROCESS: argv building,
12
+ * prompt-on-stdin, PID tracking, stdout stream parsing, session resume, cost
13
+ * accounting per turn. A cloud agent has none of those. You submit a task, get
14
+ * an opaque session handle, poll it, and eventually a PR appears in the repo.
15
+ * Modelling that as a runtime would force every adapter method to lie
16
+ * (`buildArgs` for a process that never spawns, `parseStreamChunk` for a stream
17
+ * that does not exist) and would drag cloud dispatch through the local
18
+ * spawn/timeout/worktree machinery it must specifically avoid.
19
+ *
20
+ * So cloud is its own small contract, reached through its own registry, and
21
+ * selected by the `cloud` EXECUTION SURFACE rather than by a runtime name.
22
+ * Orchestration must never import a concrete provider or branch on a provider
23
+ * id — resolve through `engine/cloud/index.js` and use `capabilities`.
24
+ *
25
+ * ── Contract (apiVersion 1) ──────────────────────────────────────────────
26
+ * name : string — registry key; must match the registration key.
27
+ * apiVersion : 1
28
+ * capabilities : {
29
+ * cancel?: boolean — cancelSession() is meaningful.
30
+ * steering?: boolean — mid-session follow-up messages.
31
+ * liveLog?: boolean — a human-viewable live session URL.
32
+ * }
33
+ *
34
+ * isConfigured({ config, project }) -> boolean
35
+ * Cheap, synchronous, NO network. Answers "could a dispatch to this
36
+ * provider possibly work right now for this project?" — credentials
37
+ * present, repo binding resolvable, not disabled in config. Used as a
38
+ * fail-closed pre-dispatch gate so a misconfigured provider surfaces as
39
+ * CLOUD_PROVIDER_UNAVAILABLE at dispatch time instead of a mystery
40
+ * session that never starts.
41
+ *
42
+ * createSession({ project, repo, baseBranch, prompt, workItem, workdir })
43
+ * -> Promise<{ sessionId, url?, providerRef? }>
44
+ * Submits the task. `prompt` is the SAME rendered playbook prompt a
45
+ * local agent would have received (minus local-only sections) — that
46
+ * rendered instruction block is the entire reason this surface exists.
47
+ * `sessionId` must be a stable, non-empty string usable by pollSession.
48
+ *
49
+ * pollSession(sessionId, { project }) -> Promise<CloudSessionStatus>
50
+ * Read-only. Must be safe to call every poll tick, and must never throw
51
+ * for an ordinary "still running" answer.
52
+ *
53
+ * cancelSession(sessionId, { reason, project }) -> Promise<void>
54
+ * Best-effort. Only required when capabilities.cancel is true.
55
+ *
56
+ * CloudSessionStatus:
57
+ * { state, prUrl?, summary?, error?, raw? }
58
+ * state ∈ CLOUD_SESSION_STATE. `prUrl` is what the engine adopts into its PR
59
+ * record so the existing ADO/GitHub pollers and review/fix loops take over
60
+ * unchanged; a SUCCEEDED status with no `prUrl` is a contract violation and
61
+ * is rejected by `normalizeSessionStatus`.
62
+ */
63
+
64
+ const CLOUD_PROVIDER_API_VERSION = 1;
65
+
66
+ const REQUIRED_METHODS = Object.freeze([
67
+ 'isConfigured',
68
+ 'createSession',
69
+ 'pollSession',
70
+ ]);
71
+
72
+ /**
73
+ * Remote session lifecycle. Deliberately smaller than any one provider's
74
+ * native state machine — providers map their vocabulary onto these five so
75
+ * orchestration never learns provider-specific state names.
76
+ */
77
+ const CLOUD_SESSION_STATE = Object.freeze({
78
+ QUEUED: 'queued',
79
+ RUNNING: 'running',
80
+ SUCCEEDED: 'succeeded',
81
+ FAILED: 'failed',
82
+ CANCELLED: 'cancelled',
83
+ });
84
+
85
+ const CLOUD_SESSION_STATE_VALUES = Object.freeze(Object.values(CLOUD_SESSION_STATE));
86
+
87
+ const TERMINAL_CLOUD_SESSION_STATES = Object.freeze(new Set([
88
+ CLOUD_SESSION_STATE.SUCCEEDED,
89
+ CLOUD_SESSION_STATE.FAILED,
90
+ CLOUD_SESSION_STATE.CANCELLED,
91
+ ]));
92
+
93
+ function isTerminalCloudSessionState(state) {
94
+ return TERMINAL_CLOUD_SESSION_STATES.has(String(state || '').trim().toLowerCase());
95
+ }
96
+
97
+ /**
98
+ * Validate + normalize a provider at registration time. Strict by design: a
99
+ * provider that registers successfully is one orchestration can call without
100
+ * defensive checks at every call site.
101
+ */
102
+ function normalizeCloudProvider(name, provider) {
103
+ if (!name || typeof name !== 'string') {
104
+ throw new Error('registerCloudProvider: name must be a non-empty string');
105
+ }
106
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider)) {
107
+ throw new Error('registerCloudProvider: provider must be an object');
108
+ }
109
+ if (provider.name != null && provider.name !== name) {
110
+ throw new Error(
111
+ `registerCloudProvider: provider name "${provider.name}" does not match registry key "${name}"`
112
+ );
113
+ }
114
+ if (provider.apiVersion == null) {
115
+ throw new Error(`Cloud provider "${name}" must declare apiVersion ${CLOUD_PROVIDER_API_VERSION}`);
116
+ }
117
+ if (provider.apiVersion !== CLOUD_PROVIDER_API_VERSION) {
118
+ throw new Error(
119
+ `Cloud provider "${name}" API version ${provider.apiVersion} is unsupported; `
120
+ + `expected ${CLOUD_PROVIDER_API_VERSION}`
121
+ );
122
+ }
123
+
124
+ if (provider.capabilities == null) {
125
+ provider.capabilities = {};
126
+ } else if (typeof provider.capabilities !== 'object' || Array.isArray(provider.capabilities)) {
127
+ throw new Error(`Cloud provider "${name}" member capabilities must be an object`);
128
+ }
129
+
130
+ for (const method of REQUIRED_METHODS) {
131
+ if (typeof provider[method] !== 'function') {
132
+ throw new Error(`Cloud provider "${name}" is missing required method ${method}()`);
133
+ }
134
+ }
135
+ if (provider.cancelSession != null && typeof provider.cancelSession !== 'function') {
136
+ throw new Error(`Cloud provider "${name}" member cancelSession must be a function`);
137
+ }
138
+ // A provider that advertises cancellation must actually implement it —
139
+ // otherwise the session-timeout path would silently leave remote work
140
+ // running with no way to stop it.
141
+ if (provider.capabilities.cancel === true && typeof provider.cancelSession !== 'function') {
142
+ throw new Error(`Cloud provider "${name}" declares capabilities.cancel but does not implement cancelSession()`);
143
+ }
144
+
145
+ provider.name = name;
146
+ return provider;
147
+ }
148
+
149
+ /**
150
+ * Normalize what a provider returned from pollSession into the shape
151
+ * orchestration is allowed to trust. Throws on a contract violation rather
152
+ * than guessing, so a broken provider fails loudly at the poll site instead of
153
+ * silently completing a work item with no PR.
154
+ */
155
+ function normalizeSessionStatus(providerName, status) {
156
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
157
+ throw new Error(`Cloud provider "${providerName}" pollSession() must return an object`);
158
+ }
159
+ const state = String(status.state || '').trim().toLowerCase();
160
+ if (!CLOUD_SESSION_STATE_VALUES.includes(state)) {
161
+ throw new Error(
162
+ `Cloud provider "${providerName}" pollSession() returned unknown state `
163
+ + `"${status.state}" (expected one of ${CLOUD_SESSION_STATE_VALUES.join(', ')})`
164
+ );
165
+ }
166
+ const prUrl = typeof status.prUrl === 'string' && status.prUrl.trim() ? status.prUrl.trim() : null;
167
+ if (state === CLOUD_SESSION_STATE.SUCCEEDED && !prUrl) {
168
+ throw new Error(
169
+ `Cloud provider "${providerName}" reported state "succeeded" without a prUrl — `
170
+ + `a cloud dispatch succeeds only when it produced a pull request Minions can adopt`
171
+ );
172
+ }
173
+ return {
174
+ state,
175
+ prUrl,
176
+ summary: typeof status.summary === 'string' ? status.summary : '',
177
+ error: typeof status.error === 'string' ? status.error : '',
178
+ raw: status.raw !== undefined ? status.raw : null,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Normalize a createSession() result. `sessionId` is the handle every later
184
+ * poll/cancel depends on, so an empty one is a hard error at submit time
185
+ * rather than an untrackable dispatch discovered minutes later.
186
+ */
187
+ function normalizeCreatedSession(providerName, created) {
188
+ if (!created || typeof created !== 'object' || Array.isArray(created)) {
189
+ throw new Error(`Cloud provider "${providerName}" createSession() must return an object`);
190
+ }
191
+ const sessionId = typeof created.sessionId === 'string' ? created.sessionId.trim() : '';
192
+ if (!sessionId) {
193
+ throw new Error(`Cloud provider "${providerName}" createSession() must return a non-empty sessionId`);
194
+ }
195
+ return {
196
+ sessionId,
197
+ url: typeof created.url === 'string' && created.url.trim() ? created.url.trim() : null,
198
+ providerRef: created.providerRef !== undefined ? created.providerRef : null,
199
+ };
200
+ }
201
+
202
+ module.exports = {
203
+ CLOUD_PROVIDER_API_VERSION,
204
+ CLOUD_SESSION_STATE,
205
+ CLOUD_SESSION_STATE_VALUES,
206
+ TERMINAL_CLOUD_SESSION_STATES,
207
+ REQUIRED_METHODS,
208
+ isTerminalCloudSessionState,
209
+ normalizeCloudProvider,
210
+ normalizeSessionStatus,
211
+ normalizeCreatedSession,
212
+ };