@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
@@ -0,0 +1,474 @@
1
+ /**
2
+ * watch-plugins/http.js — Generic HTTP target type (W-mp7i22mu00191b07)
3
+ *
4
+ * The canonical watch target plugin — demonstrates the extension contract
5
+ * landed in W-mp7hg58e000b5212. Unlocks "watch any REST endpoint / GH Actions
6
+ * workflow_run / Jenkins job / Slack thread" without writing more bespoke
7
+ * target types.
8
+ *
9
+ * ── Target syntax ────────────────────────────────────────────────────────────
10
+ * target: 'https://api.github.com/zen'
11
+ * → bare URL, GET, JSON parsed automatically when content-type is JSON.
12
+ *
13
+ * target: { url, method, headers, body, extract, expected, regex, timeoutMs }
14
+ * → full config. `extract` is a JSONPath-style accessor (`$.foo[0].bar`)
15
+ * or a `/regex/flags` string; the result becomes `state.extracted` and
16
+ * is what `value-*` conditions compare. `expected` (for value-equals)
17
+ * and `regex` (for value-matches) live alongside on the target.
18
+ *
19
+ * Auth: `headers.Authorization: 'Bearer {{env.GH_TOKEN}}'` substitutes from
20
+ * `process.env` at fetch time. Missing env var = WARN log + header dropped.
21
+ * Secrets are never persisted in watch records.
22
+ *
23
+ * ── Conditions ──────────────────────────────────────────────────────────────
24
+ * status-change — HTTP status code differs from previous poll
25
+ * value-change — `state.extracted` differs (deep equal)
26
+ * value-equals — `state.extracted === target.expected`
27
+ * value-matches — `state.extracted` matches `new RegExp(target.regex)`
28
+ * http-error — HTTP status >= 400 OR fetch threw
29
+ *
30
+ * All conditions are change-based — `stopAfter:0` truly runs forever
31
+ * (absoluteConditions: []).
32
+ *
33
+ * ── Templating vars exposed to actions ───────────────────────────────────────
34
+ * {{httpStatus}}, {{url}}, {{extracted}}, {{prevExtracted}},
35
+ * {{fetchedAt}}, {{durationMs}}, {{rawBody}} (truncated to 4KB).
36
+ * Plus standard watch vars (target, condition, watchId, message, ...).
37
+ *
38
+ * ── Fetch model ─────────────────────────────────────────────────────────────
39
+ * The engine's checkWatches loop is synchronous (it runs inside a
40
+ * mutateJsonFileLocked callback), so this plugin maintains its own background
41
+ * fetch cache. `fetchEntity` is sync — it returns the cached entity and, when
42
+ * the cached value is older than 30s, kicks off a background refresh. First
43
+ * tick after a watch is created therefore returns null (no entity yet);
44
+ * second tick captures the baseline; subsequent ticks detect changes.
45
+ *
46
+ * Tests can short-circuit this entirely by passing a pre-populated map at
47
+ * `state.httpResponses[targetKey]` (see `_targetKey`) — the plugin uses that
48
+ * shape directly, no async required. Tests can also override the network
49
+ * fetcher via `setFetcher(fn)` for unit-testing the fetch-and-extract path.
50
+ *
51
+ * Out of scope: webhooks, auth schemes beyond `headers`, streaming / chunked
52
+ * downloads.
53
+ */
54
+
55
+ const http = require('http');
56
+ const https = require('https');
57
+ const { URL } = require('url');
58
+
59
+ // Plugins live in `<MINIONS_DIR>/watch-plugins/` which may not be a sibling of
60
+ // the engine source on installed layouts, so this plugin avoids requiring
61
+ // `engine/core/shared.js`. The two helpers it needs (timestamp + warn-logger) are
62
+ // inlined: `ts()` matches shared.ts(), and `_warn()` falls back to
63
+ // `console.warn` (no dependency on the structured-log file).
64
+ function ts() { return new Date().toISOString(); }
65
+ function _warn(msg) { try { console.warn(`[${ts()}] [warn] ${msg}`); } catch {} }
66
+
67
+ const DEFAULT_TIMEOUT_MS = 10000;
68
+ const MAX_BODY_BYTES = 4096;
69
+ const CACHE_FRESHNESS_MS = 30000;
70
+
71
+ // Module-level fetch cache. fetchEntity returns from here synchronously while
72
+ // background refreshes update it as needed. Keyed by _targetKey(target).
73
+ const _cache = new Map();
74
+ const _inFlight = new Set();
75
+
76
+ // Override-able fetcher — default uses node:http(s). Tests inject a mock.
77
+ let _fetcher = _defaultHttpFetch;
78
+ function setFetcher(fn) { _fetcher = typeof fn === 'function' ? fn : _defaultHttpFetch; }
79
+ function _resetCache() { _cache.clear(); _inFlight.clear(); }
80
+
81
+ function _targetKey(target) {
82
+ if (typeof target === 'string') return target;
83
+ // Stable key — ordered serialization via Object.keys().sort() so cache
84
+ // hits don't depend on caller's property ordering.
85
+ if (target && typeof target === 'object') {
86
+ const ordered = {};
87
+ for (const k of Object.keys(target).sort()) ordered[k] = target[k];
88
+ return JSON.stringify(ordered);
89
+ }
90
+ return String(target);
91
+ }
92
+
93
+ function _resolveCfg(target) {
94
+ if (typeof target === 'string' && target) {
95
+ return {
96
+ url: target, method: 'GET', headers: {}, body: null, extract: null,
97
+ timeoutMs: DEFAULT_TIMEOUT_MS,
98
+ };
99
+ }
100
+ if (target && typeof target === 'object' && typeof target.url === 'string' && target.url) {
101
+ return {
102
+ url: target.url,
103
+ method: String(target.method || 'GET').toUpperCase(),
104
+ headers: (target.headers && typeof target.headers === 'object') ? target.headers : {},
105
+ body: target.body ?? null,
106
+ extract: target.extract || null,
107
+ timeoutMs: Number.isFinite(Number(target.timeoutMs)) ? Number(target.timeoutMs) : DEFAULT_TIMEOUT_MS,
108
+ };
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function _validateTarget(target, context = {}) {
114
+ const cfg = _resolveCfg(target);
115
+ if (!cfg) return 'target must be an HTTP(S) URL string or an object with a url';
116
+ let parsed;
117
+ try {
118
+ parsed = new URL(cfg.url);
119
+ } catch {
120
+ return 'target.url must be a valid HTTP(S) URL';
121
+ }
122
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
123
+ return 'target.url must use http: or https:';
124
+ }
125
+ const targetObject = target && typeof target === 'object' ? target : null;
126
+ if (targetObject) {
127
+ if (target.method !== undefined) {
128
+ if (typeof target.method !== 'string') return 'target.method must be a valid HTTP token';
129
+ try {
130
+ http.validateHeaderName(target.method);
131
+ } catch {
132
+ return 'target.method must be a valid HTTP token';
133
+ }
134
+ }
135
+ if (target.headers !== undefined) {
136
+ if (!target.headers || typeof target.headers !== 'object' || Array.isArray(target.headers)) {
137
+ return 'target.headers must be an object';
138
+ }
139
+ for (const [name, value] of Object.entries(target.headers)) {
140
+ if (typeof value !== 'string') return 'target.headers values must be strings';
141
+ try {
142
+ http.validateHeaderName(name);
143
+ http.validateHeaderValue(name, value);
144
+ } catch {
145
+ return 'target.headers contains an invalid HTTP header';
146
+ }
147
+ }
148
+ }
149
+ if (target.extract !== undefined && target.extract !== null
150
+ && (typeof target.extract !== 'string' || !target.extract.trim())) {
151
+ return 'target.extract must be a non-empty string';
152
+ }
153
+ if (target.regex !== undefined && target.regex !== null) {
154
+ if (typeof target.regex !== 'string' || !target.regex) {
155
+ return 'target.regex must be a non-empty string';
156
+ }
157
+ try {
158
+ new RegExp(target.regex);
159
+ } catch {
160
+ return 'target.regex must be a valid regular expression';
161
+ }
162
+ }
163
+ if (target.timeoutMs !== undefined
164
+ && (typeof target.timeoutMs !== 'number'
165
+ || !Number.isFinite(target.timeoutMs)
166
+ || target.timeoutMs <= 0)) {
167
+ return 'target.timeoutMs must be a positive finite number';
168
+ }
169
+ }
170
+
171
+ if (['value-change', 'value-equals', 'value-matches'].includes(context.condition)) {
172
+ if (!targetObject || typeof target.extract !== 'string' || !target.extract.trim()) {
173
+ return `target.extract is required for ${context.condition}`;
174
+ }
175
+ }
176
+ if (context.condition === 'value-equals'
177
+ && (!Object.prototype.hasOwnProperty.call(target, 'expected')
178
+ || target.expected === undefined)) {
179
+ return 'target.expected is required for value-equals';
180
+ }
181
+ if (context.condition === 'value-matches'
182
+ && (typeof target.regex !== 'string' || !target.regex)) {
183
+ return 'target.regex is required for value-matches';
184
+ }
185
+ return null;
186
+ }
187
+
188
+ // Substitute {{env.VAR}} in a single header value. Returns the substituted
189
+ // string when every referenced env var resolved, or { _missingEnv } when any
190
+ // referenced var is unset — caller drops the header and emits a WARN.
191
+ function _substituteEnvHeader(value) {
192
+ const re = /\{\{\s*env\.([A-Z_][A-Z0-9_]*)\s*\}\}/g;
193
+ let missing = null;
194
+ const out = String(value).replace(re, (m, k) => {
195
+ const v = process.env[k];
196
+ if (v === undefined || v === '') {
197
+ if (missing === null) missing = k;
198
+ return m;
199
+ }
200
+ return v;
201
+ });
202
+ return missing === null ? out : { _missingEnv: missing };
203
+ }
204
+
205
+ // JSONPath-lite accessor: `$.foo[0].bar` or `$.foo['weird key']`. Tokens are
206
+ // `.field`, `[N]`, or `['key']`. Returns null on parse-failure or any miss.
207
+ function _extractJsonPath(jpath, body) {
208
+ if (typeof body === 'string') {
209
+ try { body = JSON.parse(body); } catch { return null; }
210
+ }
211
+ if (!jpath || jpath[0] !== '$') return null;
212
+ const tokens = jpath.slice(1).match(/\.[a-zA-Z_$][\w$]*|\[\d+\]|\['[^']*'\]|\["[^"]*"\]/g) || [];
213
+ let cursor = body;
214
+ for (const tok of tokens) {
215
+ if (cursor === null || cursor === undefined) return null;
216
+ if (tok[0] === '.') cursor = cursor[tok.slice(1)];
217
+ else if (tok[1] === "'" || tok[1] === '"') cursor = cursor[tok.slice(2, -2)];
218
+ else cursor = cursor[parseInt(tok.slice(1, -1), 10)];
219
+ }
220
+ return cursor === undefined ? null : cursor;
221
+ }
222
+
223
+ function _parseRegexLiteral(s) {
224
+ const m = s.match(/^\/(.+)\/([gimsuy]*)$/);
225
+ if (!m) return null;
226
+ return { pattern: m[1], flags: m[2] || '' };
227
+ }
228
+
229
+ function _extractRegex(rePattern, flags, rawBody) {
230
+ let re;
231
+ try { re = new RegExp(rePattern, flags || ''); } catch { return null; }
232
+ const m = String(rawBody).match(re);
233
+ if (!m) return null;
234
+ return m.length > 1 ? m[1] : m[0];
235
+ }
236
+
237
+ function _extract(extract, rawBody) {
238
+ if (!extract) return null;
239
+ if (typeof extract === 'string') {
240
+ if (extract.startsWith('$')) return _extractJsonPath(extract, rawBody);
241
+ const lit = _parseRegexLiteral(extract);
242
+ if (lit) return _extractRegex(lit.pattern, lit.flags, rawBody);
243
+ // Bare property name → treat as $.name for ergonomics.
244
+ return _extractJsonPath('$.' + extract, rawBody);
245
+ }
246
+ return null;
247
+ }
248
+
249
+ // Default Node http(s) fetcher. Returns { status, headers, body }.
250
+ function _defaultHttpFetch(cfg) {
251
+ return new Promise((resolve, reject) => {
252
+ let parsed;
253
+ try { parsed = new URL(cfg.url); } catch (err) { reject(err); return; }
254
+ const lib = parsed.protocol === 'https:' ? https : http;
255
+ const opts = {
256
+ method: cfg.method,
257
+ hostname: parsed.hostname,
258
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
259
+ path: (parsed.pathname || '/') + (parsed.search || ''),
260
+ headers: { 'User-Agent': 'minions-watch', ...cfg.headers },
261
+ timeout: cfg.timeoutMs,
262
+ };
263
+ const req = lib.request(opts, (res) => {
264
+ const chunks = [];
265
+ let bytes = 0;
266
+ // Cap captured bytes at 4× truncation budget so we can still parse the
267
+ // first 4KB of valid JSON without buffering megabyte payloads.
268
+ const cap = MAX_BODY_BYTES * 4;
269
+ res.on('data', (c) => {
270
+ if (bytes >= cap) return;
271
+ chunks.push(c);
272
+ bytes += c.length;
273
+ });
274
+ res.on('end', () => {
275
+ resolve({
276
+ status: res.statusCode,
277
+ headers: res.headers || {},
278
+ body: Buffer.concat(chunks).toString('utf8'),
279
+ });
280
+ });
281
+ });
282
+ req.on('error', reject);
283
+ req.on('timeout', () => { req.destroy(new Error(`fetch timeout after ${cfg.timeoutMs}ms`)); });
284
+ if (cfg.body !== null && cfg.body !== undefined) {
285
+ req.write(typeof cfg.body === 'string' ? cfg.body : JSON.stringify(cfg.body));
286
+ }
287
+ req.end();
288
+ });
289
+ }
290
+
291
+ async function _doFetch(target) {
292
+ const cfg = _resolveCfg(target);
293
+ if (!cfg) {
294
+ return {
295
+ url: '', status: 0, body: '', rawBody: '', extracted: null,
296
+ fetchedAt: ts(), durationMs: 0, error: 'invalid target',
297
+ };
298
+ }
299
+ const headers = {};
300
+ for (const [k, v] of Object.entries(cfg.headers)) {
301
+ const subbed = _substituteEnvHeader(v);
302
+ if (subbed && typeof subbed === 'object' && subbed._missingEnv) {
303
+ _warn(`watch-plugins/http: header '${k}' dropped — env var ${subbed._missingEnv} not set in process.env`);
304
+ continue;
305
+ }
306
+ headers[k] = subbed;
307
+ }
308
+ const start = Date.now();
309
+ let result, err;
310
+ try {
311
+ result = await _fetcher({ ...cfg, headers });
312
+ } catch (e) {
313
+ err = e;
314
+ }
315
+ const durationMs = Date.now() - start;
316
+ if (err) {
317
+ return {
318
+ url: cfg.url, status: 0, body: '', rawBody: '', extracted: null,
319
+ fetchedAt: ts(), durationMs, error: err.message || String(err),
320
+ };
321
+ }
322
+ const fullBody = String(result.body || '');
323
+ const rawBody = fullBody.length > MAX_BODY_BYTES ? fullBody.slice(0, MAX_BODY_BYTES) : fullBody;
324
+ const extracted = cfg.extract ? _extract(cfg.extract, fullBody) : null;
325
+ return {
326
+ url: cfg.url,
327
+ status: typeof result.status === 'number' ? result.status : 0,
328
+ body: rawBody,
329
+ rawBody,
330
+ extracted,
331
+ fetchedAt: ts(),
332
+ durationMs,
333
+ };
334
+ }
335
+
336
+ function _scheduleRefresh(target, key) {
337
+ if (_inFlight.has(key)) return;
338
+ _inFlight.add(key);
339
+ Promise.resolve()
340
+ .then(() => _doFetch(target))
341
+ .then((entity) => { _cache.set(key, entity); })
342
+ .catch((e) => {
343
+ _cache.set(key, {
344
+ url: '', status: 0, body: '', rawBody: '', extracted: null,
345
+ fetchedAt: ts(), durationMs: 0, error: e && e.message ? e.message : String(e),
346
+ });
347
+ })
348
+ .then(() => { _inFlight.delete(key); });
349
+ }
350
+
351
+ function fetchEntity(target, state) {
352
+ const key = _targetKey(target);
353
+ // Test/external override path — state.httpResponses[key] short-circuits the
354
+ // background-fetch cache so unit tests can drive the plugin deterministically.
355
+ if (state && state.httpResponses && Object.prototype.hasOwnProperty.call(state.httpResponses, key)) {
356
+ return { ...state.httpResponses[key], _target: target };
357
+ }
358
+ const cached = _cache.get(key);
359
+ const cachedAge = cached && cached.fetchedAt ? Date.now() - new Date(cached.fetchedAt).getTime() : Infinity;
360
+ if (!cached || cachedAge > CACHE_FRESHNESS_MS) {
361
+ _scheduleRefresh(target, key);
362
+ }
363
+ return cached ? { ...cached, _target: target } : null;
364
+ }
365
+
366
+ function captureState(entity) {
367
+ if (!entity) return {};
368
+ return {
369
+ httpStatus: entity.status,
370
+ url: entity.url,
371
+ extracted: entity.extracted,
372
+ fetchedAt: entity.fetchedAt,
373
+ durationMs: entity.durationMs,
374
+ rawBody: entity.rawBody,
375
+ error: entity.error || null,
376
+ };
377
+ }
378
+
379
+ function _deepEqual(a, b) {
380
+ if (a === b) return true;
381
+ if (a === null || b === null || a === undefined || b === undefined) return false;
382
+ if (typeof a !== typeof b) return false;
383
+ if (typeof a !== 'object') return a === b;
384
+ try { return JSON.stringify(a) === JSON.stringify(b); }
385
+ catch { return false; }
386
+ }
387
+
388
+ function evaluate(condition, entity, prevState, target) {
389
+ const cur = captureState(entity);
390
+ switch (condition) {
391
+ case 'status-change': {
392
+ if (prevState.httpStatus === undefined) return { triggered: false, message: '' };
393
+ const changed = prevState.httpStatus !== cur.httpStatus;
394
+ return { triggered: changed, message: changed ? `HTTP ${cur.url}: status ${prevState.httpStatus} → ${cur.httpStatus}` : '' };
395
+ }
396
+ case 'value-change': {
397
+ if (!('extracted' in prevState)) return { triggered: false, message: '' };
398
+ const changed = !_deepEqual(prevState.extracted, cur.extracted);
399
+ const fmt = (v) => { try { return JSON.stringify(v); } catch { return String(v); } };
400
+ return { triggered: changed, message: changed ? `HTTP ${cur.url}: extracted changed: ${fmt(prevState.extracted)} → ${fmt(cur.extracted)}` : '' };
401
+ }
402
+ case 'value-equals': {
403
+ const expected = (target && typeof target === 'object' && Object.prototype.hasOwnProperty.call(target, 'expected'))
404
+ ? target.expected : undefined;
405
+ if (expected === undefined) return { triggered: false, message: '' };
406
+ const matched = cur.extracted === expected;
407
+ return { triggered: matched, message: matched ? `HTTP ${cur.url}: extracted equals expected (${expected})` : '' };
408
+ }
409
+ case 'value-matches': {
410
+ const pattern = (target && typeof target === 'object' && typeof target.regex === 'string') ? target.regex : null;
411
+ if (!pattern || cur.extracted === null || cur.extracted === undefined) return { triggered: false, message: '' };
412
+ let re;
413
+ try { re = new RegExp(pattern); } catch { return { triggered: false, message: '' }; }
414
+ const matched = re.test(String(cur.extracted));
415
+ return { triggered: matched, message: matched ? `HTTP ${cur.url}: extracted matches /${pattern}/` : '' };
416
+ }
417
+ case 'http-error': {
418
+ const errored = !!cur.error || (typeof cur.httpStatus === 'number' && cur.httpStatus >= 400);
419
+ const detail = cur.error ? cur.error : `status ${cur.httpStatus}`;
420
+ return { triggered: errored, message: errored ? `HTTP ${cur.url}: ${detail}` : '' };
421
+ }
422
+ default:
423
+ return { triggered: false, message: `Unknown condition: ${condition}` };
424
+ }
425
+ }
426
+
427
+ // contextVars supplements the action template ctx with plugin-specific aliases.
428
+ // httpStatus/url/extracted/fetchedAt/durationMs/rawBody auto-promote from
429
+ // newState scalars in buildTriggerContext, but prevExtracted is on prevState
430
+ // and an object-shaped extracted value would otherwise be unreachable as a
431
+ // scalar — covering both here keeps the templating contract uniform.
432
+ function contextVars(entity, prevState, newState) {
433
+ const out = {};
434
+ if (newState) {
435
+ if (newState.httpStatus !== undefined) out.httpStatus = newState.httpStatus;
436
+ if (newState.url !== undefined) out.url = newState.url;
437
+ if (newState.extracted !== undefined && newState.extracted !== null) out.extracted = newState.extracted;
438
+ if (newState.fetchedAt !== undefined) out.fetchedAt = newState.fetchedAt;
439
+ if (newState.durationMs !== undefined) out.durationMs = newState.durationMs;
440
+ if (newState.rawBody !== undefined) out.rawBody = newState.rawBody;
441
+ }
442
+ if (prevState && prevState.extracted !== undefined && prevState.extracted !== null) {
443
+ out.prevExtracted = prevState.extracted;
444
+ }
445
+ return out;
446
+ }
447
+
448
+ module.exports = {
449
+ name: 'http',
450
+ spec: {
451
+ label: 'HTTP Endpoint',
452
+ description: 'Poll any URL, extract a value, compare against previous state.',
453
+ conditions: ['status-change', 'value-change', 'value-equals', 'value-matches', 'http-error'],
454
+ absoluteConditions: [], // all change-based — stopAfter:0 truly runs forever
455
+ validateTarget: _validateTarget,
456
+ fetchEntity,
457
+ captureState,
458
+ evaluate,
459
+ contextVars,
460
+ },
461
+ // Test hooks (unit tests only — not part of the plugin contract).
462
+ setFetcher,
463
+ _resetCache,
464
+ _resolveCfg,
465
+ _targetKey,
466
+ _extract,
467
+ _extractJsonPath,
468
+ _extractRegex,
469
+ _parseRegexLiteral,
470
+ _substituteEnvHeader,
471
+ _doFetch,
472
+ DEFAULT_TIMEOUT_MS,
473
+ MAX_BODY_BYTES,
474
+ };