@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,375 @@
1
+ /**
2
+ * watch-plugins/gh-author-prs.js — Watch a GitHub repo for open PRs by author
3
+ *
4
+ * Use case: surface an external collaborator's PRs for context-only tracking
5
+ * and review dispatch without adding them to the engine's PR sync loops.
6
+ *
7
+ * ── Target syntax ───────────────────────────────────────────────────────────
8
+ * target: 'owner/repo:author'
9
+ * → bare colon-delimited string. Authentication resolves from
10
+ * config.engine.ghAccounts for the repository owner.
11
+ *
12
+ * target: { slug: 'owner/repo', author: 'login', ghAccount?: 'account' }
13
+ * → explicit form; ghAccount optionally overrides configured account
14
+ * resolution for this watch.
15
+ *
16
+ * ── Conditions ──────────────────────────────────────────────────────────────
17
+ * new-pr — fires once per newly-observed open PR number. Drafts are
18
+ * ignored (they aren't "new-PR-ready"). The watch state retains
19
+ * an all-time-seen set, so closing + reopening the same PR does
20
+ * NOT re-fire — manually re-review reopens.
21
+ *
22
+ * Burst handling: when N new PRs land between ticks, the plugin advances
23
+ * its seen-set by exactly ONE per evaluation. The watch fires once per
24
+ * tick until the backlog drains — zero PRs are lost when several arrive in
25
+ * the same interval. Costs one watch tick per PR to drain.
26
+ *
27
+ * ── Templating vars exposed to actions ──────────────────────────────────────
28
+ * {{prNumber}}, {{prTitle}}, {{prUrl}}, {{prBranch}}, {{prAuthor}},
29
+ * {{prHeadSha}}, {{prCreatedAt}}, {{prIsDraft}}, {{repoSlug}},
30
+ * {{ghAccount}}
31
+ * Plus standard watch vars (target, condition, watchId, message, …).
32
+ *
33
+ * ── Fetch model ─────────────────────────────────────────────────────────────
34
+ * The engine's checkWatches loop is synchronous and runs inside a
35
+ * mutateJsonFileLocked callback, so this plugin maintains its own
36
+ * background-fetch cache. `fetchEntity` is sync: it returns the cached
37
+ * entity and, when the cached value is older than 60s (or absent), kicks
38
+ * off a background `gh pr list` refresh. First tick after a watch is
39
+ * created therefore returns null (no entity yet); second tick captures
40
+ * the baseline; subsequent ticks detect new PRs.
41
+ *
42
+ * Tests can short-circuit this entirely by passing a pre-populated map at
43
+ * `state.ghAuthorPrs[targetKey]` (see `_targetKey`) — the plugin uses
44
+ * that shape directly, no shell-out required. Tests can also override
45
+ * the underlying lister via `setLister(fn)` to drive the cache path.
46
+ *
47
+ * Auth: engine/providers/gh-token.js resolves the configured account. Token acquisition
48
+ * stays asynchronous and is cached in-process. NEVER `gh auth switch` — that
49
+ * races with engine pollers.
50
+ *
51
+ * Security: token is passed to `gh pr list` via the child process `env`
52
+ * map only — it never appears on argv, never in logs, never written to
53
+ * disk. Plugin trust level matches playbooks (user-trusted).
54
+ */
55
+
56
+ const { spawn } = require('child_process');
57
+ const ghToken = require('../engine/providers/gh-token');
58
+
59
+ function ts() { return new Date().toISOString(); }
60
+ function _warn(msg) { try { console.warn(`[${ts()}] [warn] ${msg}`); } catch {} }
61
+ function _info(msg) { try { console.log(`[${ts()}] [info] ${msg}`); } catch {} }
62
+
63
+ const CACHE_FRESHNESS_MS = 60 * 1000;
64
+ const TOKEN_TTL_MS = 25 * 60 * 1000;
65
+ const GH_TIMEOUT_MS = 15 * 1000;
66
+ const SEEN_NUMBERS_CAP = 500; // bound state growth per watch
67
+
68
+ // Module-level caches. Survives across ticks; reset on engine restart.
69
+ const _entityCache = new Map(); // key -> { prs, fetchedAt, error? }
70
+ const _inFlight = new Set();
71
+ const _tokenCache = new Map(); // account -> { token, expiresAt }
72
+
73
+ // Override-able lister — default uses real `gh` CLI. Tests inject mocks.
74
+ let _lister = _defaultGhPrList;
75
+ function setLister(fn) { _lister = typeof fn === 'function' ? fn : _defaultGhPrList; }
76
+ function _resetCache() { _entityCache.clear(); _inFlight.clear(); _tokenCache.clear(); }
77
+
78
+ function _resolveCfg(target) {
79
+ if (typeof target === 'string' && target.includes(':')) {
80
+ const idx = target.lastIndexOf(':');
81
+ const slug = target.slice(0, idx).trim();
82
+ const author = target.slice(idx + 1).trim();
83
+ if (!slug || !author || !slug.includes('/')) return null;
84
+ return { slug, author, ghAccount: null };
85
+ }
86
+ if (target && typeof target === 'object'
87
+ && typeof target.slug === 'string' && target.slug.includes('/')
88
+ && typeof target.author === 'string' && target.author) {
89
+ return {
90
+ slug: target.slug.trim(),
91
+ author: target.author.trim(),
92
+ ghAccount: typeof target.ghAccount === 'string' && target.ghAccount
93
+ ? target.ghAccount.trim()
94
+ : null,
95
+ };
96
+ }
97
+ return null;
98
+ }
99
+
100
+ function _validateTarget(target) {
101
+ const cfg = _resolveCfg(target);
102
+ if (!cfg
103
+ || !/^[^/\s]+\/[^/\s]+$/.test(cfg.slug)
104
+ || !cfg.author) {
105
+ return 'target must be "owner/repo:author" or an object with slug and author';
106
+ }
107
+ if (target && typeof target === 'object'
108
+ && target.ghAccount !== undefined
109
+ && (typeof target.ghAccount !== 'string' || !target.ghAccount.trim())) {
110
+ return 'target.ghAccount must be a non-empty string';
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function _targetKey(target) {
116
+ const cfg = _resolveCfg(target);
117
+ if (!cfg) return typeof target === 'string' ? target : JSON.stringify(target);
118
+ return `${cfg.slug}::${cfg.author}::${cfg.ghAccount || 'configured'}`;
119
+ }
120
+
121
+ /**
122
+ * Run a gh subcommand with a per-invocation env, no shell, captured stdout.
123
+ * Always returns { code, stdout, stderr } — never throws on non-zero exit.
124
+ */
125
+ function _runGh(args, env, timeoutMs) {
126
+ return new Promise((resolve) => {
127
+ const child = spawn('gh', args, {
128
+ env: { ...process.env, ...env },
129
+ windowsHide: true,
130
+ shell: false,
131
+ });
132
+ let stdout = '';
133
+ let stderr = '';
134
+ let settled = false;
135
+ const timer = setTimeout(() => {
136
+ if (settled) return;
137
+ settled = true;
138
+ try { child.kill('SIGTERM'); } catch {}
139
+ resolve({ code: -1, stdout, stderr: stderr + `\n[gh-author-prs] timeout after ${timeoutMs}ms` });
140
+ }, timeoutMs);
141
+ child.stdout.on('data', (b) => { stdout += b.toString('utf8'); });
142
+ child.stderr.on('data', (b) => { stderr += b.toString('utf8'); });
143
+ child.on('error', (err) => {
144
+ if (settled) return;
145
+ settled = true;
146
+ clearTimeout(timer);
147
+ resolve({ code: -1, stdout, stderr: stderr + `\n[gh-author-prs] spawn error: ${err.message}` });
148
+ });
149
+ child.on('close', (code) => {
150
+ if (settled) return;
151
+ settled = true;
152
+ clearTimeout(timer);
153
+ resolve({ code: code == null ? -1 : code, stdout, stderr });
154
+ });
155
+ });
156
+ }
157
+
158
+ async function _resolveToken(cfg) {
159
+ const account = cfg.ghAccount || ghToken.resolveAccountForSlug(cfg.slug);
160
+ if (!account) return { account: null, token: null };
161
+ const cached = _tokenCache.get(account);
162
+ if (cached && cached.expiresAt > Date.now()) return { account, token: cached.token };
163
+ const res = await _runGh(
164
+ ['auth', 'token', '--user', account, '--hostname', 'github.com'],
165
+ {},
166
+ GH_TIMEOUT_MS,
167
+ );
168
+ if (res.code !== 0) {
169
+ _warn(`gh-author-prs: token fetch failed for configured account "${account}": ${res.stderr.trim() || `exit ${res.code}`}`);
170
+ return { account, token: null };
171
+ }
172
+ const token = res.stdout.trim();
173
+ if (!token) {
174
+ _warn(`gh-author-prs: empty token for configured account "${account}"`);
175
+ return { account, token: null };
176
+ }
177
+ _tokenCache.set(account, { token, expiresAt: Date.now() + TOKEN_TTL_MS });
178
+ return { account, token };
179
+ }
180
+
181
+ async function _defaultGhPrList(cfg) {
182
+ const { account, token } = await _resolveToken(cfg);
183
+ if (!token) {
184
+ return {
185
+ prs: [],
186
+ error: account
187
+ ? `no gh token for configured account "${account}"`
188
+ : `no configured gh account/token for repository "${cfg.slug}"`,
189
+ fetchedAt: ts(),
190
+ };
191
+ }
192
+ const args = [
193
+ 'pr', 'list',
194
+ '--repo', cfg.slug,
195
+ '--author', cfg.author,
196
+ '--state', 'open',
197
+ '--limit', '100',
198
+ '--json', 'number,title,headRefName,url,createdAt,headRefOid,isDraft',
199
+ ];
200
+ const res = await _runGh(args, { GH_TOKEN: token }, GH_TIMEOUT_MS);
201
+ if (res.code !== 0) {
202
+ return {
203
+ prs: [],
204
+ error: `gh pr list exit ${res.code}: ${res.stderr.trim().slice(0, 500)}`,
205
+ fetchedAt: ts(),
206
+ };
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = JSON.parse(res.stdout || '[]');
211
+ } catch (e) {
212
+ return {
213
+ prs: [],
214
+ error: `gh pr list JSON parse failed: ${e.message}`,
215
+ fetchedAt: ts(),
216
+ };
217
+ }
218
+ if (!Array.isArray(parsed)) {
219
+ return { prs: [], error: 'gh pr list returned non-array JSON', fetchedAt: ts() };
220
+ }
221
+ const prs = parsed
222
+ .filter((p) => p && typeof p.number === 'number')
223
+ .map((p) => ({
224
+ number: p.number,
225
+ title: String(p.title || ''),
226
+ branch: String(p.headRefName || ''),
227
+ url: String(p.url || ''),
228
+ createdAt: String(p.createdAt || ''),
229
+ headSha: String(p.headRefOid || ''),
230
+ isDraft: Boolean(p.isDraft),
231
+ }));
232
+ return { prs, fetchedAt: ts() };
233
+ }
234
+
235
+ function _scheduleRefresh(target, key) {
236
+ if (_inFlight.has(key)) return;
237
+ const cfg = _resolveCfg(target);
238
+ if (!cfg) return;
239
+ _inFlight.add(key);
240
+ Promise.resolve()
241
+ .then(() => _lister(cfg))
242
+ .then((entity) => {
243
+ _entityCache.set(key, entity);
244
+ })
245
+ .catch((e) => {
246
+ _entityCache.set(key, {
247
+ prs: [],
248
+ error: e && e.message ? e.message : String(e),
249
+ fetchedAt: ts(),
250
+ });
251
+ })
252
+ .then(() => { _inFlight.delete(key); });
253
+ }
254
+
255
+ function fetchEntity(target, state) {
256
+ const key = _targetKey(target);
257
+ // Test/external override path.
258
+ if (state && state.ghAuthorPrs && Object.prototype.hasOwnProperty.call(state.ghAuthorPrs, key)) {
259
+ const inj = state.ghAuthorPrs[key];
260
+ return inj ? { ...inj, _target: target } : null;
261
+ }
262
+ const cached = _entityCache.get(key);
263
+ const cachedAge = cached && cached.fetchedAt
264
+ ? Date.now() - new Date(cached.fetchedAt).getTime()
265
+ : Infinity;
266
+ if (!cached || cachedAge > CACHE_FRESHNESS_MS) {
267
+ _scheduleRefresh(target, key);
268
+ }
269
+ return cached ? { ...cached, _target: target } : null;
270
+ }
271
+
272
+ /**
273
+ * State shape: { numbers: number[], lastFetchedAt?: string, lastError?: string }
274
+ * `numbers` is the all-time-seen set (sorted, capped at SEEN_NUMBERS_CAP).
275
+ *
276
+ * captureState advances by AT MOST ONE new number per tick so bursts drain
277
+ * lossnessly across consecutive checks (in concert with evaluate).
278
+ */
279
+ function captureState(entity, prevState) {
280
+ const prevNumbers = Array.isArray(prevState && prevState.numbers) ? prevState.numbers : [];
281
+ if (!entity) return { numbers: prevNumbers };
282
+ const seen = new Set(prevNumbers);
283
+ const currentReady = (entity.prs || [])
284
+ .filter((p) => !p.isDraft)
285
+ .map((p) => p.number)
286
+ .sort((a, b) => a - b);
287
+ const newOnes = currentReady.filter((n) => !seen.has(n));
288
+ let next;
289
+ if (newOnes.length === 0) {
290
+ next = prevNumbers.slice();
291
+ } else {
292
+ next = [...seen, newOnes[0]].sort((a, b) => a - b);
293
+ }
294
+ if (next.length > SEEN_NUMBERS_CAP) {
295
+ next = next.slice(next.length - SEEN_NUMBERS_CAP);
296
+ }
297
+ return {
298
+ numbers: next,
299
+ lastFetchedAt: entity.fetchedAt || null,
300
+ lastError: entity.error || null,
301
+ };
302
+ }
303
+
304
+ function _firstUnseenReadyPr(entity, prevState) {
305
+ if (!entity) return null;
306
+ const prevNumbers = Array.isArray(prevState && prevState.numbers) ? prevState.numbers : [];
307
+ const seen = new Set(prevNumbers);
308
+ const candidates = (entity.prs || [])
309
+ .filter((p) => !p.isDraft && !seen.has(p.number))
310
+ .sort((a, b) => a.number - b.number);
311
+ return candidates[0] || null;
312
+ }
313
+
314
+ function evaluate(condition, entity, prevState, target) {
315
+ if (condition !== 'new-pr') {
316
+ return { triggered: false, message: '' };
317
+ }
318
+ // Baseline tick: prevState is empty AND we have no entity yet, or entity is
319
+ // empty/errored. The engine's checkWatches initializes _lastState by calling
320
+ // captureState first when _lastState is empty — so by the time evaluate runs
321
+ // on tick 1, prevState already contains the current `numbers`. New PRs only
322
+ // appear from tick 2 onward, which is the desired no-backfill behavior.
323
+ if (!entity) return { triggered: false, message: '' };
324
+ if (entity.error) {
325
+ // Errors don't fire — surfaced via _lastState.lastError for the dashboard.
326
+ return { triggered: false, message: '' };
327
+ }
328
+ const newPr = _firstUnseenReadyPr(entity, prevState);
329
+ if (!newPr) return { triggered: false, message: '' };
330
+ const cfg = _resolveCfg(target);
331
+ const slug = cfg ? cfg.slug : '<unknown>';
332
+ return {
333
+ triggered: true,
334
+ message: `New PR on ${slug} #${newPr.number}: ${newPr.title}`,
335
+ };
336
+ }
337
+
338
+ function contextVars(entity, prevState, _newState) {
339
+ const newPr = _firstUnseenReadyPr(entity, prevState);
340
+ if (!newPr) return {};
341
+ const cfg = _resolveCfg(entity && entity._target);
342
+ return {
343
+ prNumber: newPr.number,
344
+ prTitle: newPr.title,
345
+ prUrl: newPr.url,
346
+ prBranch: newPr.branch,
347
+ prAuthor: cfg ? cfg.author : '',
348
+ prHeadSha: newPr.headSha,
349
+ prCreatedAt: newPr.createdAt,
350
+ prIsDraft: false,
351
+ repoSlug: cfg ? cfg.slug : '',
352
+ ghAccount: cfg ? (cfg.ghAccount || ghToken.resolveAccountForSlug(cfg.slug) || '') : '',
353
+ };
354
+ }
355
+
356
+ module.exports = {
357
+ name: 'gh-author-prs',
358
+ spec: {
359
+ label: 'GitHub PRs by Author',
360
+ description: 'Poll a GitHub repo for non-draft open PRs by a specific author. Fires once per newly-observed PR number.',
361
+ conditions: ['new-pr'],
362
+ absoluteConditions: [],
363
+ validateTarget: _validateTarget,
364
+ fetchEntity,
365
+ captureState,
366
+ evaluate,
367
+ contextVars,
368
+ },
369
+ // Exported for tests
370
+ _resolveCfg,
371
+ _targetKey,
372
+ _firstUnseenReadyPr,
373
+ setLister,
374
+ _resetCache,
375
+ };