@yemi33/minions 0.1.2447 → 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 +66 -7
  7. package/dashboard/js/detail-panel.js +36 -0
  8. package/dashboard/js/memory-panel.js +59 -12
  9. package/dashboard/js/qa.js +186 -45
  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 +52 -6
  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
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const shared = require('./shared');
5
+ const shared = require('../core/shared');
6
6
  const apiValidation = require('./api-validation');
7
7
 
8
8
  const CC_API_LIMITS = Object.freeze({
@@ -0,0 +1,560 @@
1
+ /**
2
+ * engine/api/companion.js — Constellation companion detection (P-e93c85a1).
3
+ *
4
+ * Makes Minions HONEST about whether a compatible Constellation companion is
5
+ * present on this host. It reports; it never repairs.
6
+ *
7
+ * NON-GOALS, deliberately and permanently:
8
+ * - Minions NEVER downloads Constellation.
9
+ * - Minions NEVER executes a Constellation binary or installer.
10
+ * - There is no recursion marker, reciprocal bootstrap, or ping-pong
11
+ * handshake here. The single optional bootstrap path is Constellation-owned
12
+ * and runs in one direction only.
13
+ * Minions installs and operates INDEPENDENTLY: companion absence is a visible
14
+ * degraded state, never a takeover trigger and never fatal.
15
+ *
16
+ * Detection signal, in order:
17
+ * 1. The existing cross-repo marker `~/.minions/engine/constellation-bridge.json`
18
+ * (written by the Constellation agent — see docs/constellation-bridge.md).
19
+ * Its presence IS the presence signal; there is no PATH lookup and no
20
+ * directory-name heuristic anywhere in this module.
21
+ * 2. An OPTIONAL bounded health probe against the companion's declared
22
+ * loopback health endpoint, used to resolve a version the marker omitted.
23
+ * Loopback-only by construction, so the probe can never reach the network.
24
+ *
25
+ * Every path is bounded: the marker read is a single stat+read, and the health
26
+ * probe always carries an explicit timeout, so a hung companion can never stall
27
+ * `minions doctor`, `minions bridge health`, `minions init`, `minions update`,
28
+ * or engine start.
29
+ *
30
+ * Zero runtime dependencies: Node built-ins only.
31
+ */
32
+
33
+ const fs = require('fs');
34
+ const http = require('http');
35
+ const https = require('https');
36
+
37
+ const shared = require('../core/shared');
38
+
39
+ const {
40
+ CONSTELLATION_BRIDGE_MARKER_PATH,
41
+ CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
42
+ ENGINE_DEFAULTS,
43
+ } = shared;
44
+
45
+ /** The complete status vocabulary reported by the probe. */
46
+ const COMPANION_STATUS = Object.freeze({
47
+ OK: 'companion-ok',
48
+ INCOMPATIBLE: 'companion-incompatible',
49
+ MISSING: 'companion-missing',
50
+ UNKNOWN: 'companion-unknown',
51
+ });
52
+
53
+ // Declared compatibility window for the Constellation companion agent. The
54
+ // marker contract (`schemaVersion: 1`, documented `agentVersion: "0.2.3"`) is
55
+ // the floor; the next major is the ceiling because a major bump is exactly the
56
+ // signal that the shared contract changed.
57
+ const COMPANION_MIN_VERSION = '0.2.0';
58
+ const COMPANION_MAX_VERSION_EXCLUSIVE = '1.0.0';
59
+ const COMPANION_COMPATIBLE_RANGE = `>=${COMPANION_MIN_VERSION} <${COMPANION_MAX_VERSION_EXCLUSIVE}`;
60
+
61
+ // Documented Constellation distribution surface. These strings are PRINTED for
62
+ // the operator to run — this module never invokes them.
63
+ const COMPANION_DOCS_URL = 'https://icy-water-0224cc51e.2.azurestaticapps.net/';
64
+ const COMPANION_INSTALL_PS1 = 'https://icy-water-0224cc51e.2.azurestaticapps.net/install.ps1';
65
+ const COMPANION_INSTALL_SH = 'https://icy-water-0224cc51e.2.azurestaticapps.net/install.sh';
66
+
67
+ const HEALTH_PROBE_TIMEOUT_MIN_MS = 250;
68
+ const HEALTH_PROBE_TIMEOUT_MAX_MS = 15000;
69
+ // A health endpoint answers with a tiny JSON body. Cap the read so a
70
+ // misbehaving (or wrong) local endpoint cannot stream unbounded bytes at us.
71
+ const HEALTH_PROBE_MAX_BYTES = 64 * 1024;
72
+
73
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
74
+
75
+ // ── Config resolution ────────────────────────────────────────────────────────
76
+
77
+ function _companionConfig(config) {
78
+ const block = config && config.engine && config.engine.constellationCompanion;
79
+ return block && typeof block === 'object' && !Array.isArray(block) ? block : {};
80
+ }
81
+
82
+ /**
83
+ * Documented opt-out for managed deployments. Either seam suppresses the probe
84
+ * ENTIRELY (no marker read, no health request):
85
+ * - `engine.constellationCompanion.probeEnabled: false` in config.json
86
+ * - `MINIONS_DISABLE_COMPANION_PROBE=1` in the environment
87
+ * The env var wins so a fleet image can suppress it without editing config.
88
+ */
89
+ function isCompanionProbeEnabled(config, env = process.env) {
90
+ const flag = env && env.MINIONS_DISABLE_COMPANION_PROBE;
91
+ if (flag === '1' || flag === 'true') return false;
92
+ const configured = _companionConfig(config).probeEnabled;
93
+ if (configured === false) return false;
94
+ if (configured === true) return true;
95
+ return ENGINE_DEFAULTS.constellationCompanion.probeEnabled !== false;
96
+ }
97
+
98
+ function resolveCompanionProbeTimeoutMs(config) {
99
+ const raw = _companionConfig(config).probeTimeoutMs;
100
+ const fallback = ENGINE_DEFAULTS.constellationCompanion.probeTimeoutMs;
101
+ const value = Number.isFinite(raw) ? Number(raw) : fallback;
102
+ return Math.min(HEALTH_PROBE_TIMEOUT_MAX_MS, Math.max(HEALTH_PROBE_TIMEOUT_MIN_MS, value));
103
+ }
104
+
105
+ /**
106
+ * Resolve the companion's health endpoint. Config wins over the optional
107
+ * `healthUrl` the companion declares in its own marker, so an operator can
108
+ * always override what a third-party file claims.
109
+ *
110
+ * Loopback-only by construction. A remote host, a non-HTTP scheme, or an
111
+ * unparseable URL is REFUSED without being requested — that is what keeps this
112
+ * probe incapable of any outbound network traffic.
113
+ */
114
+ function resolveCompanionHealthUrl(config, marker) {
115
+ const configured = _companionConfig(config).healthUrl;
116
+ const declared = marker && typeof marker.healthUrl === 'string' ? marker.healthUrl : null;
117
+ const candidate = (typeof configured === 'string' && configured.trim())
118
+ ? configured.trim()
119
+ : (declared && declared.trim() ? declared.trim() : null);
120
+ if (!candidate) return { url: null, error: null };
121
+
122
+ let parsed;
123
+ try { parsed = new URL(candidate); }
124
+ catch { return { url: null, error: `unsupported companion health URL: ${candidate}` }; }
125
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
126
+ return { url: null, error: `unsupported companion health URL scheme: ${parsed.protocol}` };
127
+ }
128
+ const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
129
+ if (!LOOPBACK_HOSTS.has(host)) {
130
+ return { url: null, error: `refused non-loopback companion health URL: ${parsed.host}` };
131
+ }
132
+ return { url: parsed.toString(), error: null };
133
+ }
134
+
135
+ // ── Version handling ─────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Parse a `major.minor.patch[-prerelease][+build]` string. Returns null for
139
+ * anything else — an unparseable version is "unknown", never "compatible".
140
+ */
141
+ function parseCompanionVersion(input) {
142
+ if (typeof input !== 'string') return null;
143
+ const m = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\s*$/.exec(input);
144
+ if (!m) return null;
145
+ return {
146
+ major: Number(m[1]),
147
+ minor: Number(m[2]),
148
+ patch: Number(m[3]),
149
+ prerelease: m[4] || null,
150
+ raw: input.trim(),
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Compare on the numeric core only. A prerelease of X is treated as X, so
156
+ * `1.0.0-beta.1` is judged against the `<1.0.0` ceiling as the next major —
157
+ * the conservative reading, and the one an operator expects from a
158
+ * "compatible range" report.
159
+ */
160
+ function compareCompanionVersions(a, b) {
161
+ const left = typeof a === 'string' ? parseCompanionVersion(a) : a;
162
+ const right = typeof b === 'string' ? parseCompanionVersion(b) : b;
163
+ if (!left || !right) return null;
164
+ for (const key of ['major', 'minor', 'patch']) {
165
+ if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
166
+ }
167
+ return 0;
168
+ }
169
+
170
+ function isCompatibleCompanionVersion(version) {
171
+ const parsed = parseCompanionVersion(version);
172
+ if (!parsed) return false;
173
+ const aboveFloor = compareCompanionVersions(parsed, COMPANION_MIN_VERSION);
174
+ const belowCeiling = compareCompanionVersions(parsed, COMPANION_MAX_VERSION_EXCLUSIVE);
175
+ if (aboveFloor === null || belowCeiling === null) return false;
176
+ return aboveFloor >= 0 && belowCeiling < 0;
177
+ }
178
+
179
+ // ── Marker read ──────────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Detailed marker read. Deliberately NOT `bridge.readBridgeMarker`, which
183
+ * collapses "absent" and "malformed" into a single `null` — this probe has to
184
+ * tell those apart to avoid reporting a broken companion as an absent one.
185
+ */
186
+ function readCompanionMarker(markerPath = CONSTELLATION_BRIDGE_MARKER_PATH) {
187
+ let raw;
188
+ try {
189
+ raw = fs.readFileSync(markerPath, 'utf8');
190
+ } catch (err) {
191
+ if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
192
+ return { present: false, marker: null, reason: 'marker-absent', error: null };
193
+ }
194
+ return { present: true, marker: null, reason: 'marker-unreadable', error: err.message };
195
+ }
196
+
197
+ let parsed;
198
+ try { parsed = JSON.parse(raw); }
199
+ catch (err) { return { present: true, marker: null, reason: 'marker-unreadable', error: err.message }; }
200
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
201
+ return { present: true, marker: null, reason: 'marker-unreadable', error: 'marker is not a JSON object' };
202
+ }
203
+
204
+ const schemaVersion = typeof parsed.schemaVersion === 'number' ? parsed.schemaVersion : null;
205
+ if (schemaVersion !== CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION) {
206
+ return {
207
+ present: true, marker: null, schemaVersion,
208
+ reason: 'marker-schema-unsupported',
209
+ error: `marker schemaVersion ${schemaVersion === null ? '(absent)' : schemaVersion} — this Minions build reads ${CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION}`,
210
+ };
211
+ }
212
+ if (typeof parsed.lastSeenAt !== 'string' || !parsed.lastSeenAt) {
213
+ return {
214
+ present: true, marker: null, schemaVersion,
215
+ reason: 'marker-unreadable', error: 'marker is missing lastSeenAt',
216
+ };
217
+ }
218
+
219
+ return {
220
+ present: true,
221
+ schemaVersion,
222
+ reason: null,
223
+ error: null,
224
+ marker: {
225
+ schemaVersion,
226
+ lastSeenAt: parsed.lastSeenAt,
227
+ agentVersion: typeof parsed.agentVersion === 'string' ? parsed.agentVersion : null,
228
+ source: typeof parsed.source === 'string' ? parsed.source : null,
229
+ healthUrl: typeof parsed.healthUrl === 'string' ? parsed.healthUrl : null,
230
+ },
231
+ };
232
+ }
233
+
234
+ // ── Bounded loopback health probe ────────────────────────────────────────────
235
+
236
+ /**
237
+ * GET a small JSON document from a loopback URL under an explicit timeout.
238
+ * No redirects are followed and the body is byte-capped, so the worst case is
239
+ * a bounded read from localhost.
240
+ */
241
+ function fetchCompanionHealthJson(url, { timeoutMs } = {}) {
242
+ return new Promise((resolve, reject) => {
243
+ let settled = false;
244
+ const finish = (err, value) => {
245
+ if (settled) return;
246
+ settled = true;
247
+ if (err) reject(err); else resolve(value);
248
+ };
249
+
250
+ let req;
251
+ const timer = setTimeout(() => {
252
+ try { if (req) req.destroy(); } catch { /* already gone */ }
253
+ finish(Object.assign(new Error(`companion health probe timed out after ${timeoutMs}ms`), { code: 'ETIMEDOUT' }));
254
+ }, timeoutMs);
255
+ if (typeof timer.unref === 'function') timer.unref();
256
+
257
+ const done = (err, value) => { clearTimeout(timer); finish(err, value); };
258
+
259
+ try {
260
+ const client = url.startsWith('https:') ? https : http;
261
+ req = client.get(url, { timeout: timeoutMs }, (res) => {
262
+ if (res.statusCode !== 200) {
263
+ res.resume();
264
+ done(new Error(`companion health endpoint returned HTTP ${res.statusCode}`));
265
+ return;
266
+ }
267
+ let body = '';
268
+ res.setEncoding('utf8');
269
+ res.on('data', (chunk) => {
270
+ body += chunk;
271
+ if (body.length > HEALTH_PROBE_MAX_BYTES) {
272
+ try { res.destroy(); } catch { /* already gone */ }
273
+ done(new Error('companion health response exceeded the size cap'));
274
+ }
275
+ });
276
+ res.on('end', () => {
277
+ try { done(null, JSON.parse(body)); }
278
+ catch (err) { done(new Error(`companion health response was not JSON: ${err.message}`)); }
279
+ });
280
+ res.on('error', (err) => done(err));
281
+ });
282
+ req.on('timeout', () => { try { req.destroy(); } catch { /* already gone */ } });
283
+ req.on('error', (err) => done(err));
284
+ } catch (err) {
285
+ done(err);
286
+ }
287
+ });
288
+ }
289
+
290
+ function _versionFromHealthPayload(payload) {
291
+ if (!payload || typeof payload !== 'object') return null;
292
+ for (const key of ['agentVersion', 'version', 'companionVersion']) {
293
+ if (typeof payload[key] === 'string' && payload[key].trim()) return payload[key].trim();
294
+ }
295
+ return null;
296
+ }
297
+
298
+ // ── Probe ────────────────────────────────────────────────────────────────────
299
+
300
+ function _skippedResult(markerPath) {
301
+ return {
302
+ skipped: true,
303
+ status: null,
304
+ reason: 'probe-disabled',
305
+ fatal: false,
306
+ version: null,
307
+ versionSource: null,
308
+ compatibleRange: COMPANION_COMPATIBLE_RANGE,
309
+ markerPath,
310
+ markerPresent: null,
311
+ markerSchemaVersion: null,
312
+ lastSeenAt: null,
313
+ source: null,
314
+ health: null,
315
+ };
316
+ }
317
+
318
+ function _baseResult(markerPath, read) {
319
+ return {
320
+ skipped: false,
321
+ status: null,
322
+ reason: null,
323
+ detail: null,
324
+ fatal: false,
325
+ version: null,
326
+ versionSource: null,
327
+ compatibleRange: COMPANION_COMPATIBLE_RANGE,
328
+ markerPath,
329
+ markerPresent: read.present,
330
+ markerSchemaVersion: read.schemaVersion === undefined ? null : read.schemaVersion,
331
+ lastSeenAt: read.marker ? read.marker.lastSeenAt : null,
332
+ source: read.marker ? read.marker.source : null,
333
+ health: null,
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Classify everything that can be decided from the marker alone. Shared by the
339
+ * sync and async entry points so both report identically when no health
340
+ * endpoint is declared (the default).
341
+ */
342
+ function _classifyFromMarker(markerPath, read) {
343
+ const result = _baseResult(markerPath, read);
344
+ if (!read.present) {
345
+ result.status = COMPANION_STATUS.MISSING;
346
+ result.reason = 'marker-absent';
347
+ return result;
348
+ }
349
+ if (read.reason === 'marker-schema-unsupported') {
350
+ // The companion IS here; it just speaks a marker contract this build does
351
+ // not read. That is an incompatibility, not an absence.
352
+ result.status = COMPANION_STATUS.INCOMPATIBLE;
353
+ result.reason = 'marker-schema-unsupported';
354
+ result.detail = read.error;
355
+ return result;
356
+ }
357
+ if (!read.marker) {
358
+ result.status = COMPANION_STATUS.UNKNOWN;
359
+ result.reason = 'marker-unreadable';
360
+ result.detail = read.error;
361
+ return result;
362
+ }
363
+ if (read.marker.agentVersion) {
364
+ result.version = read.marker.agentVersion;
365
+ result.versionSource = 'marker';
366
+ result.status = isCompatibleCompanionVersion(result.version)
367
+ ? COMPANION_STATUS.OK
368
+ : COMPANION_STATUS.INCOMPATIBLE;
369
+ result.reason = result.status === COMPANION_STATUS.OK ? 'version-in-range' : 'version-out-of-range';
370
+ return result;
371
+ }
372
+ result.status = COMPANION_STATUS.UNKNOWN;
373
+ result.reason = 'version-unresolved';
374
+ return result;
375
+ }
376
+
377
+ /**
378
+ * Synchronous probe: marker only, no health request. Used where the caller
379
+ * cannot await (and where a network handle would be inappropriate anyway).
380
+ * Identical to the async probe whenever no health endpoint is declared.
381
+ */
382
+ function probeCompanionSync({ config = null, markerPath = CONSTELLATION_BRIDGE_MARKER_PATH, env = process.env } = {}) {
383
+ if (!isCompanionProbeEnabled(config, env)) return _skippedResult(markerPath);
384
+ return _classifyFromMarker(markerPath, readCompanionMarker(markerPath));
385
+ }
386
+
387
+ /**
388
+ * Full probe. Marker first; then, only when the companion declares (or the
389
+ * operator configures) a LOOPBACK health endpoint, one bounded GET to resolve a
390
+ * version the marker omitted. A refused URL, a failed request, or a timeout is
391
+ * reported as `companion-unknown` — never as `companion-missing`.
392
+ *
393
+ * `fetchJson` is injectable for tests; production always uses the built-in
394
+ * bounded loopback fetch above.
395
+ */
396
+ async function probeCompanion({
397
+ config = null,
398
+ markerPath = CONSTELLATION_BRIDGE_MARKER_PATH,
399
+ env = process.env,
400
+ timeoutMs = null,
401
+ fetchJson = fetchCompanionHealthJson,
402
+ } = {}) {
403
+ if (!isCompanionProbeEnabled(config, env)) return _skippedResult(markerPath);
404
+
405
+ const read = readCompanionMarker(markerPath);
406
+ const result = _classifyFromMarker(markerPath, read);
407
+
408
+ // A health probe only ever ADDS information. Skip it when a version is
409
+ // already resolved, and when there is nothing to probe: an absent marker with
410
+ // no operator-configured endpoint means the only detection channel we have
411
+ // already answered "not here".
412
+ const operatorHealthUrl = _companionConfig(config).healthUrl;
413
+ const canProbeHealth = Boolean(read.marker) || Boolean(operatorHealthUrl);
414
+ if (result.version || !canProbeHealth) return result;
415
+
416
+ const resolved = resolveCompanionHealthUrl(config, read.marker);
417
+ if (!resolved.url) {
418
+ if (resolved.error) {
419
+ result.health = { ok: false, url: null, error: resolved.error };
420
+ result.status = COMPANION_STATUS.UNKNOWN;
421
+ result.reason = 'health-url-refused';
422
+ }
423
+ return result;
424
+ }
425
+
426
+ const budgetMs = Number.isFinite(timeoutMs) && timeoutMs > 0
427
+ ? timeoutMs
428
+ : resolveCompanionProbeTimeoutMs(config);
429
+ try {
430
+ const payload = await fetchJson(resolved.url, { timeoutMs: budgetMs });
431
+ const version = _versionFromHealthPayload(payload);
432
+ result.health = { ok: true, url: resolved.url, error: null };
433
+ if (version) {
434
+ result.version = version;
435
+ result.versionSource = 'health';
436
+ result.status = isCompatibleCompanionVersion(version)
437
+ ? COMPANION_STATUS.OK
438
+ : COMPANION_STATUS.INCOMPATIBLE;
439
+ result.reason = result.status === COMPANION_STATUS.OK ? 'version-in-range' : 'version-out-of-range';
440
+ }
441
+ } catch (err) {
442
+ result.health = { ok: false, url: resolved.url, error: (err && err.message) || String(err) };
443
+ result.status = COMPANION_STATUS.UNKNOWN;
444
+ result.reason = 'health-probe-failed';
445
+ }
446
+ return result;
447
+ }
448
+
449
+ // ── Setup guidance (printed as text — NEVER executed) ────────────────────────
450
+
451
+ /**
452
+ * The documented Constellation install command for a platform. Returns data:
453
+ * callers print it so the OPERATOR can run it. Nothing in this module executes
454
+ * a shell, and nothing downloads these URLs.
455
+ */
456
+ function getCompanionInstallCommand(platform = process.platform) {
457
+ if (platform === 'win32') {
458
+ return {
459
+ platform,
460
+ shell: 'PowerShell',
461
+ command: `irm ${COMPANION_INSTALL_PS1} | iex`,
462
+ docsUrl: COMPANION_DOCS_URL,
463
+ };
464
+ }
465
+ return {
466
+ platform,
467
+ shell: platform === 'darwin' ? 'Terminal (macOS)' : 'shell',
468
+ command: `curl -fsSL ${COMPANION_INSTALL_SH} | sh`,
469
+ docsUrl: COMPANION_DOCS_URL,
470
+ };
471
+ }
472
+
473
+ const _REASON_TEXT = {
474
+ 'marker-absent': 'no Constellation companion has registered with this Minions instance',
475
+ 'marker-unreadable': 'the companion marker exists but could not be read',
476
+ 'marker-schema-unsupported': 'the companion marker uses a schema this Minions build does not read',
477
+ 'version-in-range': 'companion version is inside the compatible range',
478
+ 'version-out-of-range': 'companion version is outside the compatible range',
479
+ 'version-unresolved': 'the companion registered but did not declare a version',
480
+ 'health-url-refused': 'the declared companion health endpoint was refused',
481
+ 'health-probe-failed': 'the companion health probe did not answer',
482
+ };
483
+
484
+ /**
485
+ * Render the probe result as plain report lines. Returns `[]` for a suppressed
486
+ * probe so an opted-out deployment prints nothing at all.
487
+ */
488
+ function formatCompanionLines(result, { platform = process.platform, indent = ' ' } = {}) {
489
+ if (!result || result.skipped || !result.status) return [];
490
+ const lines = [];
491
+ const version = result.version ? result.version : '(unknown)';
492
+ lines.push(`Constellation companion: ${result.status}`);
493
+ lines.push(`${indent}version: ${version} compatible range: ${result.compatibleRange}`);
494
+ const why = _REASON_TEXT[result.reason];
495
+ if (why) lines.push(`${indent}${why}`);
496
+ if (result.detail) lines.push(`${indent}${result.detail}`);
497
+ if (result.lastSeenAt) lines.push(`${indent}last seen: ${result.lastSeenAt}`);
498
+ if (result.health && result.health.ok === false) {
499
+ lines.push(`${indent}health probe: ${result.health.error}`);
500
+ }
501
+ if (result.status === COMPANION_STATUS.OK) return lines;
502
+
503
+ const install = getCompanionInstallCommand(platform);
504
+ lines.push(`${indent}Minions runs fine without it — Constellation only mirrors state into its HUD.`);
505
+ lines.push(`${indent}To install or update the companion, run it yourself (${install.shell}):`);
506
+ lines.push(`${indent} ${install.command}`);
507
+ lines.push(`${indent}Docs: ${install.docsUrl}`);
508
+ lines.push(`${indent}Minions never runs it for you — this is guidance, not an action.`);
509
+ return lines;
510
+ }
511
+
512
+ /**
513
+ * Render the probe result as a `minions doctor` row. Returns `null` when the
514
+ * probe was suppressed. NEVER `ok: false`: companion absence or incompatibility
515
+ * is a degraded state Minions works around, not a critical Minions failure, so
516
+ * `minions doctor` must not start exiting 1 on hosts without Constellation.
517
+ */
518
+ function companionDoctorResult(result, { platform = process.platform } = {}) {
519
+ if (!result || result.skipped || !result.status) return null;
520
+ const version = result.version ? result.version : 'unknown version';
521
+ if (result.status === COMPANION_STATUS.OK) {
522
+ return {
523
+ name: 'Constellation companion',
524
+ ok: true,
525
+ message: `${result.status} — ${version} (compatible range ${result.compatibleRange})`,
526
+ };
527
+ }
528
+ const install = getCompanionInstallCommand(platform);
529
+ const why = _REASON_TEXT[result.reason] || result.reason || 'unrecognized companion state';
530
+ return {
531
+ name: 'Constellation companion',
532
+ ok: 'warn',
533
+ message: `${result.status} — ${why} (detected ${version}; compatible range ${result.compatibleRange}). `
534
+ + `Optional — install it yourself: \`${install.command}\`. `
535
+ + `Minions never runs it for you (see docs/constellation-bridge.md).`,
536
+ };
537
+ }
538
+
539
+ module.exports = {
540
+ COMPANION_STATUS,
541
+ COMPANION_COMPATIBLE_RANGE,
542
+ COMPANION_MIN_VERSION,
543
+ COMPANION_MAX_VERSION_EXCLUSIVE,
544
+ COMPANION_DOCS_URL,
545
+ isCompanionProbeEnabled,
546
+ resolveCompanionProbeTimeoutMs,
547
+ resolveCompanionHealthUrl,
548
+ parseCompanionVersion,
549
+ compareCompanionVersions,
550
+ isCompatibleCompanionVersion,
551
+ readCompanionMarker,
552
+ probeCompanion,
553
+ probeCompanionSync,
554
+ getCompanionInstallCommand,
555
+ formatCompanionLines,
556
+ companionDoctorResult,
557
+ CONSTELLATION_BRIDGE_MARKER_PATH,
558
+ // exported for testing
559
+ _fetchCompanionHealthJson: fetchCompanionHealthJson,
560
+ };
@@ -2,8 +2,8 @@
2
2
 
3
3
  const path = require('path');
4
4
  const api = require('./api-validation');
5
- const shared = require('./shared');
6
- const memoryStore = require('./memory-store');
5
+ const shared = require('../core/shared');
6
+ const memoryStore = require('../memory/memory-store');
7
7
 
8
8
  const LIMITS = Object.freeze({
9
9
  title: 200,
@@ -2,11 +2,11 @@
2
2
 
3
3
  const path = require('path');
4
4
  const apiValidation = require('./api-validation');
5
- const issues = require('./issues');
6
- const prAction = require('./pr-action');
7
- const prFixTarget = require('./pr-fix-target');
8
- const prResolve = require('./pr-resolve');
9
- const shared = require('./shared');
5
+ const issues = require('../planning/issues');
6
+ const prAction = require('../providers/pr-action');
7
+ const prFixTarget = require('../providers/pr-fix-target');
8
+ const prResolve = require('../providers/pr-resolve');
9
+ const shared = require('../core/shared');
10
10
 
11
11
  const MAX_PR_REFERENCE_LENGTH = 2048;
12
12
  const MAX_CONTEXT_LENGTH = 16 * 1024;
@@ -455,7 +455,7 @@ function validateCreatePrRequest(body, options = {}) {
455
455
  return result;
456
456
  }
457
457
 
458
- function validatePrObserveRequest(body) {
458
+ function validatePrMutationTarget(body) {
459
459
  validateBody(body);
460
460
  const host = requiredString(body.host, 'host', { maxLength: 16 }).toLowerCase();
461
461
  apiValidation.validateEnum(host, {
@@ -473,6 +473,11 @@ function validatePrObserveRequest(body) {
473
473
  rejectedValue: slug,
474
474
  });
475
475
  }
476
+ return ref;
477
+ }
478
+
479
+ function validatePrObserveRequest(body) {
480
+ const ref = validatePrMutationTarget(body);
476
481
  const hasContextOnly = hasOwn(body, 'contextOnly') && body.contextOnly !== undefined;
477
482
  const hasObserve = hasOwn(body, 'observe') && body.observe !== undefined;
478
483
  const contextOnly = hasContextOnly
@@ -501,6 +506,27 @@ function validatePrObserveRequest(body) {
501
506
  };
502
507
  }
503
508
 
509
+ function validatePrAutomationRequest(body) {
510
+ const ref = validatePrMutationTarget(body);
511
+ const automation = requiredString(body.automation, 'automation', { maxLength: 16 }).toLowerCase();
512
+ apiValidation.validateEnum(automation, {
513
+ field: 'automation',
514
+ values: ['fix', 'review'],
515
+ message: 'automation must be "fix" or "review"',
516
+ });
517
+ const enabled = apiValidation.validateBoolean(body.enabled, {
518
+ field: 'enabled',
519
+ message: 'enabled must be a boolean',
520
+ });
521
+ return {
522
+ host: ref.host,
523
+ slug: ref.slug,
524
+ number: ref.number,
525
+ automation,
526
+ enabled,
527
+ };
528
+ }
529
+
504
530
  function validatePrDeleteRequest(body, options = {}) {
505
531
  validateBody(body);
506
532
  const rawId = requiredString(body.id, 'id', { maxLength: MAX_PR_REFERENCE_LENGTH });
@@ -645,6 +671,7 @@ module.exports = {
645
671
  validatePrLinkRequest,
646
672
  validateCreatePrRequest,
647
673
  validatePrObserveRequest,
674
+ validatePrAutomationRequest,
648
675
  validatePrDeleteRequest,
649
676
  validatePauseClearRequest,
650
677
  validateNoInputRequest,