@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,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const apiValidation = require('./api-validation');
4
- const features = require('./features');
5
- const modelDiscovery = require('./model-discovery');
6
- const runtimes = require('./runtimes');
7
- const shared = require('./shared');
4
+ const features = require('../core/features');
5
+ const modelDiscovery = require('../agents/model-discovery');
6
+ const runtimes = require('../runtimes');
7
+ const cloudProviders = require('../cloud');
8
+ const shared = require('../core/shared');
8
9
 
9
10
  const MAX_SETTINGS_ERRORS = 25;
10
11
  const SETTINGS_TOP_LEVEL_FIELDS = new Set(['engine', 'claude', 'agents', 'projects', 'features']);
@@ -57,6 +58,9 @@ const ENGINE_NUMERIC_RANGES = Object.freeze({
57
58
  worktreeCreateTimeout: { min: 60000 },
58
59
  worktreeCreateRetries: { min: 0, max: 3 },
59
60
  worktreeMinFreeSpaceBytes: { min: 0, max: 10 * 1024 * 1024 * 1024 * 1024 },
61
+ // W-ms5tb6ha016fda10 — 0 disables (keep protected quarantine dirs forever);
62
+ // cap at 365 days so a typo cannot silently mean "never".
63
+ worktreeProtectedQuarantineRetentionMs: { min: 0, max: 365 * 24 * 60 * 60 * 1000 },
60
64
  assertCleanStatusTimeoutMs: { min: 1000, max: 120000 },
61
65
  restartHealthTimeoutMs: { min: 15000, max: 300000 },
62
66
  orphanHolderScanTimeoutMs: { min: 1000, max: 30000 },
@@ -72,9 +76,17 @@ const ENGINE_NUMERIC_RANGES = Object.freeze({
72
76
  versionCheckInterval: { min: 60000 },
73
77
  prPollStatusEvery: { min: 1 },
74
78
  prPollCommentsEvery: { min: 1 },
79
+ // W-msav5edk00g81710 — bounded re-probe window for `abandoned` PR records so a
80
+ // reopened PR can leave the terminal stamp. 0 disables re-probing entirely;
81
+ // cap at 90 days so a typo cannot turn a bounded window into a permanent one.
82
+ abandonedPrRepollWindowMs: { min: 0, max: 90 * 24 * 60 * 60 * 1000 },
75
83
  agentBusyReassignMs: { min: 0 },
76
84
  maxRetriesPerAgent: { min: 1, max: 20 },
77
85
  ccTurnTimeoutMs: { min: 10000, max: 3600000 },
86
+ // Issue #1105 — cold ACP worker handshake budget. Bounds are single-sourced
87
+ // from engine/shared.js so the validator and resolveAcpHandshakeTimeoutMs()
88
+ // can never disagree about the legal range.
89
+ acpHandshakeTimeoutMs: { min: shared.ACP_HANDSHAKE_TIMEOUT_MIN_MS, max: shared.ACP_HANDSHAKE_TIMEOUT_MAX_MS },
78
90
  preDispatchEvalConcurrency: { min: 1, max: 20 },
79
91
  memoryRetrievalTopK: { min: 1, max: 30 },
80
92
  memoryRetrievalMaxBytes: { min: 1024, max: 65536 },
@@ -110,6 +122,7 @@ const ENGINE_SPECIAL_FIELDS = new Set([
110
122
  'maxTurnsByType',
111
123
  'evalMaxCost',
112
124
  'ignoredCommentAuthors',
125
+ 'cloudProvider',
113
126
  ]);
114
127
  const ENGINE_ALLOWED_FIELDS = new Set([
115
128
  ...Object.keys(ENGINE_NUMERIC_RANGES),
@@ -337,6 +350,21 @@ function applyEngineSettings(candidate, bodyEngine, errors) {
337
350
  if (!CLEAR_SENTINELS.has(bodyEngine[key]) && value === undefined) continue;
338
351
  setOrDelete(engine, key, value);
339
352
  }
353
+ // Cloud dispatch default provider. Validated against the REGISTERED provider
354
+ // list rather than accepted as free text: an id that resolves to nothing
355
+ // would produce a work item that dispatches nowhere, discovered only when
356
+ // the engine refuses it. Fail-closed here instead. (While no providers are
357
+ // registered, the only accepted value is the clear sentinel — which is the
358
+ // honest answer to "configure cloud dispatch" on a build that has none.)
359
+ if (hasOwn(bodyEngine, 'cloudProvider')) {
360
+ if (CLEAR_SENTINELS.has(bodyEngine.cloudProvider)) {
361
+ delete engine.cloudProvider;
362
+ } else {
363
+ const registered = cloudProviders.listCloudProviders();
364
+ const value = parseEnum(bodyEngine.cloudProvider, 'engine.cloudProvider', registered, errors);
365
+ if (value !== undefined) engine.cloudProvider = value;
366
+ }
367
+ }
340
368
  if (hasOwn(bodyEngine, 'copilotStreamMode')) {
341
369
  if (CLEAR_SENTINELS.has(bodyEngine.copilotStreamMode)) {
342
370
  delete engine.copilotStreamMode;
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const agentApiValidation = require('../agent-api-validation');
4
- const steeringConstraints = require('../steering-constraints');
5
- const steeringStore = require('../steering-store');
6
- const { LIMITS, PIN_LEVELS } = require('../content-api-validation');
3
+ const agentApiValidation = require('../api/agent-api-validation');
4
+ const steeringConstraints = require('../agents/steering-constraints');
5
+ const steeringStore = require('../persistence/steering-store');
6
+ const { LIMITS, PIN_LEVELS } = require('../api/content-api-validation');
7
7
 
8
8
  function rejectsInvalid(...fields) {
9
9
  return [{
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * engine/api-contracts/capability-manifest.js — the PRODUCER half of the
5
+ * cross-repo capability protocol (P-c4a1f7b2).
6
+ *
7
+ * Minions publishes a typed capability manifest; Constellation consumes it.
8
+ * This module owns the BUILDER and nothing else: every piece of vocabulary
9
+ * (scope list, confirmation enum, parameter types, capability-entry field list,
10
+ * envelope field list, protocol name/version) is imported from
11
+ * ./capability-protocol, which is the single owner of all of it.
12
+ *
13
+ * Two properties make the manifest safe to hand to a remote caller:
14
+ *
15
+ * 1. OPT-IN, FAIL-CLOSED. A route reaches the manifest only when its owning
16
+ * contract module declares an explicit `capability` block. A route that
17
+ * declares nothing is invisible here — there is no heuristic, no
18
+ * derived-by-default entry, and no partial entry.
19
+ * 2. NO INFERRED AUTHORIZATION. `destructive` and `confirm` are read straight
20
+ * from that declaration. They are deliberately NOT derived from the route
21
+ * registry's `_routeDestructive()` classification in dashboard.js, which
22
+ * is a fail-open regex over the route path AND its free-text description:
23
+ * rewording a description would otherwise silently reclassify a
24
+ * capability's safety posture for a remote caller.
25
+ *
26
+ * Parameters are projected from the audited route contract (the same
27
+ * `request.{path,query,body,headers}.fields` that GET /api/routes exposes
28
+ * through `_routeContractParameters`), so a published parameter type cannot
29
+ * drift from the contract the route actually validates against.
30
+ */
31
+
32
+ const {
33
+ CAPABILITY_PROTOCOL_NAME,
34
+ CAPABILITY_PROTOCOL_VERSION,
35
+ CAPABILITY_PARAM_TYPES,
36
+ CAPABILITY_ENTRY_FIELDS,
37
+ CAPABILITY_PARAM_FIELDS,
38
+ CAPABILITY_MANIFEST_FIELDS,
39
+ validateCapabilityManifest,
40
+ } = require('./capability-protocol');
41
+
42
+ const { listRouteContracts } = require('./index');
43
+
44
+ // Route contracts speak a slightly wider type vocabulary than the protocol
45
+ // (`identifier`, `basename` and `path` are all string refinements). This is a
46
+ // TRANSLATION table, not a second copy of the protocol's type set: every target
47
+ // below is verified against CAPABILITY_PARAM_TYPES at module load, and an
48
+ // unmapped contract type throws rather than degrading to a guessed type.
49
+ const PARAM_TYPE_FROM_CONTRACT = Object.freeze({
50
+ string: 'string',
51
+ identifier: 'string',
52
+ basename: 'string',
53
+ path: 'string',
54
+ number: 'number',
55
+ integer: 'integer',
56
+ boolean: 'boolean',
57
+ array: 'array',
58
+ object: 'object',
59
+ });
60
+
61
+ for (const [contractType, protocolType] of Object.entries(PARAM_TYPE_FROM_CONTRACT)) {
62
+ if (!CAPABILITY_PARAM_TYPES.includes(protocolType)) {
63
+ throw new Error(`capability-manifest: contract type "${contractType}" maps to "${protocolType}", which is not in the protocol parameter-type set`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Resolve a route-contract field type to a protocol parameter type.
69
+ *
70
+ * `array<string>` / `array<object>` normalize to `array`: the protocol has no
71
+ * way to express an item type, so dropping the parameterization loses nothing a
72
+ * consumer could have used. A UNION (`string|array<string>`) is a different
73
+ * matter — there is no honest single type for it, so it throws. That is a real
74
+ * signal, not an obstacle: a capability published to a remote caller has to
75
+ * have an unambiguous parameter type, and the route contract is where the
76
+ * ambiguity should be resolved.
77
+ */
78
+ function protocolParamType(contractType) {
79
+ const raw = String(contractType || '').trim();
80
+ if (!raw) return null;
81
+ if (raw.includes('|')) return null;
82
+ const normalized = raw.replace(/<.*>$/, '');
83
+ return PARAM_TYPE_FROM_CONTRACT[normalized] || null;
84
+ }
85
+
86
+ // Same locations, in the same order, that dashboard.js#_routeContractParameters
87
+ // walks when it projects contract metadata onto GET /api/routes.
88
+ const PARAM_LOCATIONS = Object.freeze(['path', 'query', 'body', 'headers']);
89
+
90
+ const PARAM_FIELD_NAMES = Object.freeze(CAPABILITY_PARAM_FIELDS.map(entryField => entryField.name));
91
+
92
+ function fail(message) {
93
+ throw new Error(`capability-manifest: ${message}`);
94
+ }
95
+
96
+ /**
97
+ * Project one contract request field onto a protocol parameter descriptor.
98
+ * Only fields the protocol declares are copied — contract-only annotations such
99
+ * as `allowedValuesSource` are producer bookkeeping and would be rejected by
100
+ * the protocol's strict unknown-field check.
101
+ */
102
+ function capabilityParam(contractField, contract, location) {
103
+ const name = String(contractField?.name || '').trim();
104
+ if (!name) fail(`${contract.key} declares an unnamed ${location} field`);
105
+ const type = protocolParamType(contractField.type);
106
+ if (!type) {
107
+ fail(`${contract.key} ${location} field "${name}" has contract type "${contractField.type}", which has no unambiguous protocol parameter type`);
108
+ }
109
+ const param = { name, type, required: contractField.required === true };
110
+ if (typeof contractField.description === 'string' && contractField.description.trim()) {
111
+ param.description = contractField.description;
112
+ }
113
+ if (Array.isArray(contractField.allowedValues)) {
114
+ param.allowedValues = [...contractField.allowedValues];
115
+ }
116
+ if (Number.isInteger(contractField.maxLength)) {
117
+ param.maxLength = contractField.maxLength;
118
+ }
119
+ for (const key of Object.keys(param)) {
120
+ if (!PARAM_FIELD_NAMES.includes(key)) fail(`${contract.key} produced an out-of-contract parameter field "${key}"`);
121
+ }
122
+ return param;
123
+ }
124
+
125
+ /**
126
+ * Typed parameters for one capability, sourced from the merged route contract.
127
+ * The protocol's parameter descriptor has no location discriminator, so a name
128
+ * declared in two locations collapses to its first occurrence — path first,
129
+ * which is the canonical selector for every route that has one.
130
+ */
131
+ function capabilityParams(contract) {
132
+ const params = [];
133
+ const seen = new Set();
134
+ for (const location of PARAM_LOCATIONS) {
135
+ const fields = contract?.request?.[location]?.fields;
136
+ if (!Array.isArray(fields)) continue;
137
+ for (const contractField of fields) {
138
+ const param = capabilityParam(contractField, contract, location);
139
+ if (seen.has(param.name)) continue;
140
+ seen.add(param.name);
141
+ params.push(param);
142
+ }
143
+ }
144
+ return params;
145
+ }
146
+
147
+ /**
148
+ * Assemble one capability entry. The emitted key set is generated by walking
149
+ * CAPABILITY_ENTRY_FIELDS, so it is the protocol's field list by construction
150
+ * and can never become a hand-maintained copy that drifts from it.
151
+ */
152
+ function buildCapabilityEntry(contract) {
153
+ const declared = contract.capability;
154
+ const derived = {
155
+ method: contract.method,
156
+ path: contract.path,
157
+ params: capabilityParams(contract),
158
+ };
159
+ const entry = {};
160
+ for (const entryField of CAPABILITY_ENTRY_FIELDS) {
161
+ const value = Object.hasOwn(derived, entryField.name) ? derived[entryField.name] : declared[entryField.name];
162
+ if (value === undefined) {
163
+ if (entryField.required) fail(`${contract.key} capability is missing required field "${entryField.name}"`);
164
+ continue;
165
+ }
166
+ entry[entryField.name] = value;
167
+ }
168
+ return entry;
169
+ }
170
+
171
+ function resolveMinionsVersion() {
172
+ try {
173
+ const version = require('../../package.json').version;
174
+ return typeof version === 'string' && version.trim() ? version : null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Build the capability manifest.
182
+ *
183
+ * THROWS on any contract violation — an out-of-vocabulary scope, an unknown
184
+ * confirmation mode, an unmapped parameter type or a duplicate capability id is
185
+ * a producer bug, never a warning that ships a manifest the consumer cannot
186
+ * generate against.
187
+ *
188
+ * @param {object} [options]
189
+ * @param {function} [options.listContracts] contract source (tests inject one)
190
+ * @param {string} [options.generatedAt] ISO timestamp override
191
+ * @param {string} [options.minionsVersion] producer version override
192
+ */
193
+ function buildCapabilityManifest(options = {}) {
194
+ const contracts = typeof options.listContracts === 'function'
195
+ ? options.listContracts()
196
+ : listRouteContracts();
197
+ if (!Array.isArray(contracts)) fail('contract source must return an array');
198
+
199
+ const capabilities = contracts
200
+ .filter(contract => contract && contract.capability)
201
+ .map(buildCapabilityEntry)
202
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
203
+
204
+ const minionsVersion = options.minionsVersion === undefined
205
+ ? resolveMinionsVersion()
206
+ : options.minionsVersion;
207
+
208
+ const values = {
209
+ protocol: CAPABILITY_PROTOCOL_NAME,
210
+ protocolVersion: CAPABILITY_PROTOCOL_VERSION,
211
+ generatedAt: options.generatedAt || new Date().toISOString(),
212
+ minionsVersion,
213
+ capabilities,
214
+ };
215
+
216
+ const manifest = {};
217
+ for (const envelopeField of CAPABILITY_MANIFEST_FIELDS) {
218
+ const value = values[envelopeField.name];
219
+ if (value === undefined || value === null) {
220
+ if (envelopeField.required) fail(`manifest is missing required envelope field "${envelopeField.name}"`);
221
+ continue;
222
+ }
223
+ manifest[envelopeField.name] = value;
224
+ }
225
+
226
+ validateCapabilityManifest(manifest);
227
+ return manifest;
228
+ }
229
+
230
+ module.exports = {
231
+ buildCapabilityManifest,
232
+ // exported for testing
233
+ PARAM_TYPE_FROM_CONTRACT,
234
+ PARAM_LOCATIONS,
235
+ protocolParamType,
236
+ };
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * engine/api-contracts/capability-protocol.js — the canonical, versioned
5
+ * cross-repo capability protocol contract.
6
+ *
7
+ * Minions is the PRODUCER of the capability manifest and Constellation is the
8
+ * CONSUMER. Without a single owned vocabulary the two repos independently
9
+ * invent the scope strings, the confirmation enum, the parameter types and the
10
+ * version rules, and drift silently. This module is that single owned
11
+ * vocabulary: the scope list, confirmation enum, parameter-type set,
12
+ * capability-entry/envelope field lists and the version-compatibility rule are
13
+ * defined HERE and nowhere else in this repo.
14
+ *
15
+ * Constraints that shape the design:
16
+ *
17
+ * 1. Minions has zero runtime dependencies (package.json declares none; the
18
+ * engine is built entirely on Node built-ins). So the contract is a plain
19
+ * CommonJS module — no schema library, no build step, no transpile.
20
+ * 2. Constellation cannot import CommonJS from this repo, so the contract is
21
+ * also emitted as a machine-readable artifact:
22
+ *
23
+ * node engine/api-contracts/capability-protocol.js --emit
24
+ *
25
+ * writes docs/contracts/capability-protocol.v1.json, which is checked in
26
+ * and guarded by a drift test. Consumers GENERATE code from that artifact
27
+ * (pinning `protocolVersion` + `checksum`); they never transcribe it.
28
+ * 3. The artifact is content-addressed, not time-stamped: regenerating it
29
+ * from unchanged sources must reproduce the same bytes, so the drift test
30
+ * can compare byte-for-byte.
31
+ *
32
+ * The module intentionally requires only Node built-ins so a generator in
33
+ * another repo can load it standalone without dragging in the engine.
34
+ */
35
+
36
+ const crypto = require('node:crypto');
37
+ const fs = require('node:fs');
38
+ const path = require('node:path');
39
+
40
+ const CAPABILITY_PROTOCOL_NAME = 'minions.capability.manifest';
41
+ const CAPABILITY_PROTOCOL_VERSION = 1;
42
+
43
+ /**
44
+ * Stated ONCE, and deliberately byte-identical to the rule already published
45
+ * for the bridge snapshot in docs/constellation-bridge.md. Restating it in
46
+ * different words is exactly the drift this contract exists to prevent, so a
47
+ * unit test asserts this string appears verbatim in that document.
48
+ */
49
+ const CAPABILITY_VERSION_RULE = '`protocolVersion` changes only for a breaking schema change. Additive optional fields do not require a version bump.';
50
+
51
+ /**
52
+ * The complete scope vocabulary. A capability declares the scopes it consumes;
53
+ * a consumer grants or withholds them. Adding a scope is additive (no version
54
+ * bump); removing or repurposing one is breaking (version bump).
55
+ */
56
+ const CAPABILITY_SCOPES = Object.freeze([
57
+ 'work:read',
58
+ 'work:write',
59
+ 'plan:read',
60
+ 'plan:write',
61
+ 'pr:read',
62
+ 'notes:write',
63
+ 'watch:write',
64
+ 'engine:control',
65
+ 'cc:converse',
66
+ 'docchat:write',
67
+ 'qa:read',
68
+ ]);
69
+
70
+ /**
71
+ * Confirmation policy for invoking a capability.
72
+ * none — the consumer may invoke without asking the operator.
73
+ * per-call — the consumer must obtain operator confirmation for every call.
74
+ */
75
+ const CAPABILITY_CONFIRM_MODES = Object.freeze(['none', 'per-call']);
76
+
77
+ /**
78
+ * Parameter types. Deliberately a small JSON-native set drawn from the
79
+ * vocabulary already used by the route contracts in this directory, so a
80
+ * generated consumer type maps onto it without a translation table. Value
81
+ * constraints are expressed with `allowedValues` / `maxLength`, not with more
82
+ * types.
83
+ */
84
+ const CAPABILITY_PARAM_TYPES = Object.freeze([
85
+ 'string',
86
+ 'number',
87
+ 'integer',
88
+ 'boolean',
89
+ 'array',
90
+ 'object',
91
+ ]);
92
+
93
+ /**
94
+ * Symbolic accessors DERIVED from the vocabularies above — never a second copy.
95
+ * A producer that has to name one scope or confirmation mode writes
96
+ * `CAPABILITY_SCOPE.WORK_READ` rather than transcribing `'work:read'`, so the
97
+ * "defined exactly once" property survives contact with the route declarations
98
+ * that consume it. The repo-wide scan in test/unit/capability-protocol.test.js
99
+ * treats a transcribed scope literal anywhere else as drift, and these
100
+ * accessors are what make declaring a capability possible without tripping it.
101
+ */
102
+ function symbolicIndex(values) {
103
+ return Object.freeze(Object.fromEntries(
104
+ values.map(value => [value.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase(), value]),
105
+ ));
106
+ }
107
+
108
+ const CAPABILITY_SCOPE = symbolicIndex(CAPABILITY_SCOPES);
109
+ const CAPABILITY_CONFIRM = symbolicIndex(CAPABILITY_CONFIRM_MODES);
110
+
111
+ function field(name, type, required, options) {
112
+ return Object.freeze({ name, type, required, ...(options || {}) });
113
+ }
114
+
115
+ /** Field list for one capability entry. */
116
+ const CAPABILITY_ENTRY_FIELDS = Object.freeze([
117
+ field('id', 'string', true, { description: 'Stable capability identifier, unique within a manifest.' }),
118
+ field('title', 'string', true, { description: 'Short human-readable label.' }),
119
+ field('description', 'string', true, { description: 'One-line description of the effect.' }),
120
+ field('method', 'string', true, { description: 'HTTP method of the backing Minions route.' }),
121
+ field('path', 'string', true, { description: 'Canonical route path template of the backing Minions route.' }),
122
+ field('scopes', 'array', true, { itemsOf: 'scopes', description: 'Scopes consumed, drawn from the scope vocabulary.' }),
123
+ field('destructive', 'boolean', true, { description: 'Whether invoking the capability destroys or irreversibly mutates state. A consumer authorization input, so it is always declared explicitly and never inferred from the route name, method, or description.' }),
124
+ field('confirm', 'string', true, { allowedValuesOf: 'confirmModes', description: 'Confirmation policy for invocation.' }),
125
+ field('params', 'array', true, { itemsOf: 'capabilityParamFields', description: 'Parameter descriptors; may be empty.' }),
126
+ ]);
127
+
128
+ /** Field list for one parameter descriptor inside a capability entry. */
129
+ const CAPABILITY_PARAM_FIELDS = Object.freeze([
130
+ field('name', 'string', true, { description: 'Parameter name as sent to the route.' }),
131
+ field('type', 'string', true, { allowedValuesOf: 'paramTypes', description: 'Parameter type from the parameter-type set.' }),
132
+ field('required', 'boolean', true, { description: 'Whether the caller must supply the parameter.' }),
133
+ field('description', 'string', false, { description: 'Optional one-line parameter description.' }),
134
+ field('allowedValues', 'array', false, { description: 'Optional closed value set.' }),
135
+ field('maxLength', 'integer', false, { description: 'Optional maximum length for string parameters.' }),
136
+ ]);
137
+
138
+ /** Field list for the manifest envelope that wraps the capability entries. */
139
+ const CAPABILITY_MANIFEST_FIELDS = Object.freeze([
140
+ field('protocol', 'string', true, { constant: CAPABILITY_PROTOCOL_NAME, description: 'Protocol identifier.' }),
141
+ field('protocolVersion', 'integer', true, { constant: CAPABILITY_PROTOCOL_VERSION, description: 'Protocol version; see versionRule.' }),
142
+ field('generatedAt', 'string', true, { description: 'ISO-8601 UTC timestamp of manifest generation.' }),
143
+ field('minionsVersion', 'string', false, { description: 'Optional Minions package version of the producer.' }),
144
+ field('capabilities', 'array', true, { itemsOf: 'capabilityEntryFields', description: 'Capability entries.' }),
145
+ ]);
146
+
147
+ const SCOPE_SET = new Set(CAPABILITY_SCOPES);
148
+ const CONFIRM_SET = new Set(CAPABILITY_CONFIRM_MODES);
149
+ const PARAM_TYPE_SET = new Set(CAPABILITY_PARAM_TYPES);
150
+
151
+ const ARTIFACT_RELATIVE_PATH = `docs/contracts/capability-protocol.v${CAPABILITY_PROTOCOL_VERSION}.json`;
152
+ const GENERATOR_RELATIVE_PATH = 'engine/api-contracts/capability-protocol.js';
153
+
154
+ /**
155
+ * The version-compatibility rule, executable. A manifest is compatible only
156
+ * when its `protocolVersion` equals ours: per CAPABILITY_VERSION_RULE the
157
+ * number moves solely on a breaking schema change, so an exact match is the
158
+ * whole rule — and additive optional fields keep the number (and therefore
159
+ * compatibility) unchanged.
160
+ */
161
+ function isCompatibleProtocolVersion(version) {
162
+ return version === CAPABILITY_PROTOCOL_VERSION;
163
+ }
164
+
165
+ function fail(message) {
166
+ throw new Error(`capability-protocol: ${message}`);
167
+ }
168
+
169
+ function typeMatches(type, value) {
170
+ switch (type) {
171
+ case 'string': return typeof value === 'string';
172
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
173
+ case 'integer': return Number.isInteger(value);
174
+ case 'boolean': return typeof value === 'boolean';
175
+ case 'array': return Array.isArray(value);
176
+ case 'object': return !!value && typeof value === 'object' && !Array.isArray(value);
177
+ default: return false;
178
+ }
179
+ }
180
+
181
+ function assertFields(target, fields, label, itemLabel) {
182
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
183
+ fail(`${label} must be an object`);
184
+ }
185
+ const known = new Set(fields.map(entry => entry.name));
186
+ for (const key of Object.keys(target)) {
187
+ if (!known.has(key)) fail(`unknown ${itemLabel} field: ${key}`);
188
+ }
189
+ for (const entry of fields) {
190
+ const value = target[entry.name];
191
+ if (value === undefined) {
192
+ if (entry.required) fail(`missing required ${itemLabel} field: ${entry.name}`);
193
+ continue;
194
+ }
195
+ if (!typeMatches(entry.type, value)) {
196
+ fail(`${itemLabel} field ${entry.name} must be of type ${entry.type}`);
197
+ }
198
+ }
199
+ }
200
+
201
+ function validateCapabilityParam(param, capabilityId) {
202
+ assertFields(param, CAPABILITY_PARAM_FIELDS, `capability ${capabilityId} parameter`, 'parameter');
203
+ if (!PARAM_TYPE_SET.has(param.type)) {
204
+ fail(`unknown parameter type "${param.type}" on capability ${capabilityId}; allowed: ${CAPABILITY_PARAM_TYPES.join(', ')}`);
205
+ }
206
+ if (!param.name.trim()) fail(`capability ${capabilityId} has a parameter with an empty name`);
207
+ }
208
+
209
+ function validateCapabilityEntry(entry, index) {
210
+ assertFields(entry, CAPABILITY_ENTRY_FIELDS, `capability at index ${index}`, 'capability');
211
+ const id = entry.id;
212
+ if (!id.trim()) fail(`capability at index ${index} has an empty id`);
213
+ if (!entry.scopes.length) fail(`capability ${id} must declare at least one scope`);
214
+ for (const scope of entry.scopes) {
215
+ if (!SCOPE_SET.has(scope)) {
216
+ fail(`unknown capability scope "${scope}" on capability ${id}; allowed: ${CAPABILITY_SCOPES.join(', ')}`);
217
+ }
218
+ }
219
+ if (!CONFIRM_SET.has(entry.confirm)) {
220
+ fail(`unknown confirm mode "${entry.confirm}" on capability ${id}; allowed: ${CAPABILITY_CONFIRM_MODES.join(', ')}`);
221
+ }
222
+ for (const param of entry.params) validateCapabilityParam(param, id);
223
+ const paramNames = entry.params.map(param => param.name);
224
+ if (new Set(paramNames).size !== paramNames.length) {
225
+ fail(`capability ${id} declares duplicate parameter names`);
226
+ }
227
+ return id;
228
+ }
229
+
230
+ /**
231
+ * Validate a capability manifest against this contract. THROWS on any
232
+ * violation — an out-of-contract scope, confirmation mode or parameter type is
233
+ * a build-time failure for the producer, never a warning that ships a manifest
234
+ * the consumer cannot generate against. Returns `true` on success so callers
235
+ * can assert on it.
236
+ */
237
+ function validateCapabilityManifest(manifest) {
238
+ assertFields(manifest, CAPABILITY_MANIFEST_FIELDS, 'manifest', 'manifest');
239
+ if (manifest.protocol !== CAPABILITY_PROTOCOL_NAME) {
240
+ fail(`manifest protocol must be "${CAPABILITY_PROTOCOL_NAME}", got "${manifest.protocol}"`);
241
+ }
242
+ if (!isCompatibleProtocolVersion(manifest.protocolVersion)) {
243
+ fail(`incompatible protocolVersion ${manifest.protocolVersion}; this contract emits ${CAPABILITY_PROTOCOL_VERSION}`);
244
+ }
245
+ const ids = [];
246
+ manifest.capabilities.forEach((entry, index) => { ids.push(validateCapabilityEntry(entry, index)); });
247
+ if (new Set(ids).size !== ids.length) fail('manifest declares duplicate capability ids');
248
+ return true;
249
+ }
250
+
251
+ // Recursively sort object keys so the checksum is independent of key order and
252
+ // reproducible from any language that can sort strings.
253
+ function canonicalize(value) {
254
+ if (Array.isArray(value)) return value.map(canonicalize);
255
+ if (value && typeof value === 'object') {
256
+ const out = {};
257
+ for (const key of Object.keys(value).sort()) out[key] = canonicalize(value[key]);
258
+ return out;
259
+ }
260
+ return value;
261
+ }
262
+
263
+ /**
264
+ * Content checksum over every field except `checksum` itself, so a consumer can
265
+ * pin the artifact it generated from and detect a swapped or edited copy.
266
+ */
267
+ function computeArtifactChecksum(artifact) {
268
+ const { checksum, ...rest } = artifact || {};
269
+ void checksum;
270
+ const digest = crypto.createHash('sha256').update(JSON.stringify(canonicalize(rest))).digest('hex');
271
+ return `sha256:${digest}`;
272
+ }
273
+
274
+ /** Build the machine-readable artifact from this module. Deterministic. */
275
+ function buildProtocolArtifact() {
276
+ const body = {
277
+ protocol: CAPABILITY_PROTOCOL_NAME,
278
+ protocolVersion: CAPABILITY_PROTOCOL_VERSION,
279
+ generator: GENERATOR_RELATIVE_PATH,
280
+ versionRule: CAPABILITY_VERSION_RULE,
281
+ scopes: [...CAPABILITY_SCOPES],
282
+ confirmModes: [...CAPABILITY_CONFIRM_MODES],
283
+ paramTypes: [...CAPABILITY_PARAM_TYPES],
284
+ manifestEnvelopeFields: CAPABILITY_MANIFEST_FIELDS.map(entry => ({ ...entry })),
285
+ capabilityEntryFields: CAPABILITY_ENTRY_FIELDS.map(entry => ({ ...entry })),
286
+ capabilityParamFields: CAPABILITY_PARAM_FIELDS.map(entry => ({ ...entry })),
287
+ };
288
+ return { ...body, checksum: computeArtifactChecksum(body) };
289
+ }
290
+
291
+ /** Exact bytes of the checked-in artifact (LF, trailing newline). */
292
+ function serializeProtocolArtifact() {
293
+ return `${JSON.stringify(buildProtocolArtifact(), null, 2)}\n`;
294
+ }
295
+
296
+ /** Write the artifact next to this module's repo root. Returns the path written. */
297
+ function emitProtocolArtifact(repoRoot) {
298
+ const root = repoRoot || path.resolve(__dirname, '..', '..');
299
+ const target = path.join(root, ...ARTIFACT_RELATIVE_PATH.split('/'));
300
+ fs.mkdirSync(path.dirname(target), { recursive: true });
301
+ fs.writeFileSync(target, serializeProtocolArtifact());
302
+ return target;
303
+ }
304
+
305
+ if (require.main === module) {
306
+ if (process.argv.includes('--emit')) {
307
+ console.log(emitProtocolArtifact());
308
+ } else {
309
+ process.stdout.write(serializeProtocolArtifact());
310
+ }
311
+ }
312
+
313
+ module.exports = Object.freeze({
314
+ CAPABILITY_PROTOCOL_NAME,
315
+ CAPABILITY_PROTOCOL_VERSION,
316
+ CAPABILITY_VERSION_RULE,
317
+ CAPABILITY_SCOPES,
318
+ CAPABILITY_CONFIRM_MODES,
319
+ CAPABILITY_PARAM_TYPES,
320
+ CAPABILITY_SCOPE,
321
+ CAPABILITY_CONFIRM,
322
+ CAPABILITY_ENTRY_FIELDS,
323
+ CAPABILITY_PARAM_FIELDS,
324
+ CAPABILITY_MANIFEST_FIELDS,
325
+ ARTIFACT_RELATIVE_PATH,
326
+ GENERATOR_RELATIVE_PATH,
327
+ isCompatibleProtocolVersion,
328
+ validateCapabilityManifest,
329
+ computeArtifactChecksum,
330
+ buildProtocolArtifact,
331
+ serializeProtocolArtifact,
332
+ emitProtocolArtifact,
333
+ });
@@ -5,7 +5,7 @@ const {
5
5
  CC_IMAGE_MIME_TYPES,
6
6
  TRANSCRIPT_ROLES,
7
7
  TRIAGE_EFFORTS,
8
- } = require('../cc-api-validation');
8
+ } = require('../api/cc-api-validation');
9
9
 
10
10
  const transcriptField = () => ({
11
11
  name: 'transcript',
@@ -10,6 +10,7 @@ module.exports = {
10
10
  'POST /api/projects/add',
11
11
  'POST /api/projects/remove',
12
12
  'GET /api/runtimes',
13
+ 'GET /api/cloud-providers',
13
14
  'POST /api/runtimes/<runtime>/models/refresh',
14
15
  'GET /api/runtimes/<runtime>/models',
15
16
  'GET /api/settings',
@@ -136,6 +137,10 @@ module.exports = {
136
137
  audit: 'audited',
137
138
  noInputReason: 'Lists the canonical runtime registry and accepts no caller input.',
138
139
  },
140
+ 'GET /api/cloud-providers': {
141
+ audit: 'audited',
142
+ noInputReason: 'Lists the canonical cloud dispatch provider registry and accepts no caller input.',
143
+ },
139
144
  'POST /api/runtimes/<runtime>/models/refresh': {
140
145
  audit: 'audited',
141
146
  path: {