@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.
- package/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +66 -7
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +186 -45
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +52 -6
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /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
|
+
};
|