@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.
- 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 +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- 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 +13 -2
- 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
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Dev composite branch + dev-dashboard workflow
|
|
2
|
-
|
|
3
|
-
This documents the recurring "rebuild the composite branch from all my open PRs and
|
|
4
|
-
relaunch the dev dashboard" workflow. It exists because the flow is otherwise
|
|
5
|
-
reconstructed from `git reflog` every time. The [`rebuild-composite`](../.claude/skills/rebuild-composite/SKILL.md)
|
|
6
|
-
skill and [`scripts/rebuild-composite.ps1`](../scripts/rebuild-composite.ps1) automate it.
|
|
7
|
-
|
|
8
|
-
## What the composite branch is
|
|
9
|
-
|
|
10
|
-
`composite/calebt-prs` is a throwaway integration branch: `origin/main` plus every
|
|
11
|
-
one of an operator's currently-open PRs merged on top, so the whole in-flight set can
|
|
12
|
-
be exercised together in one running dev instance. It is **rebuilt from scratch each
|
|
13
|
-
time** (hard reset to `origin/main`, then re-merge each open PR) — never fast-forwarded
|
|
14
|
-
and never pushed. Because of the hard reset, **anything committed only to
|
|
15
|
-
`composite/calebt-prs` is discarded on the next rebuild**; durable changes (like this
|
|
16
|
-
doc, the skill, and the script) must land on `main` via a normal PR.
|
|
17
|
-
|
|
18
|
-
## The rebuild flow
|
|
19
|
-
|
|
20
|
-
1. Confirm you are on `composite/calebt-prs` and note any dirty tracked files.
|
|
21
|
-
2. Stash dirty tracked edits (see "Rogue dev edits" below) — do **not** discard them.
|
|
22
|
-
3. `git fetch origin --prune`.
|
|
23
|
-
4. Enumerate the operator's open PRs against `main`:
|
|
24
|
-
`gh pr list --repo <owner/repo> --state open --author <login> --base main --json number,headRefName`.
|
|
25
|
-
5. `git reset --hard origin/main`.
|
|
26
|
-
6. Merge each PR's head branch (`origin/<headRefName>`) in ascending PR-number order
|
|
27
|
-
with `git merge --no-edit`. Resolve conflicts, then `git commit --no-edit`.
|
|
28
|
-
7. `node --check` the touched `.js` files.
|
|
29
|
-
8. Relaunch the dev dashboard (see "Relaunching the dev dashboard").
|
|
30
|
-
|
|
31
|
-
## Recurring merge conflicts
|
|
32
|
-
|
|
33
|
-
Two conflicts have recurred on essentially every rebuild. Both are **clean additive**
|
|
34
|
-
conflicts (two branches append to the same spot); the resolution is always "keep both
|
|
35
|
-
sides". The canonical resolutions:
|
|
36
|
-
|
|
37
|
-
### `bin/minions.js` (from the `agent rekey` PR, e.g. #412)
|
|
38
|
-
|
|
39
|
-
`main` adds an `else if (cmd === 'state')` command block; the PR adds an
|
|
40
|
-
`else if (cmd === 'agent')` block at the same location. **Keep both** — the `state`
|
|
41
|
-
block first, then the `agent` block, then continue to the existing `doctor` block.
|
|
42
|
-
Simply delete the three conflict markers (`<<<<<<<`, `|||||||`/`=======`, `>>>>>>>`);
|
|
43
|
-
no lines between the sides need to be dropped.
|
|
44
|
-
|
|
45
|
-
### `dashboard/slim/styles.css` (from the slim-ux Engine-embed PR, e.g. #896)
|
|
46
|
-
|
|
47
|
-
`main` has the `#slim-settings-modal` block; the PR adds `.slim-engine-embed,` to the
|
|
48
|
-
front of the `.slim-*-embed` selector group. **Keep both** — the settings-modal block,
|
|
49
|
-
then `.slim-engine-embed,`, then the existing `.slim-work-embed, …` group.
|
|
50
|
-
|
|
51
|
-
> **Strongly recommended: `git rerere`.** These resolutions are byte-identical every
|
|
52
|
-
> rebuild. Enabling `git config rerere.enabled true` records each resolution once and
|
|
53
|
-
> replays it automatically on every subsequent rebuild, so the conflicts resolve
|
|
54
|
-
> themselves. The rerere cache lives in `.git/` and therefore survives the hard reset.
|
|
55
|
-
|
|
56
|
-
Stop and resolve manually only when a **new** conflict (any other path) appears.
|
|
57
|
-
|
|
58
|
-
## Relaunching the dev dashboard
|
|
59
|
-
|
|
60
|
-
`--dev` mode runs the current checkout **as** the runtime root (`MINIONS_HOME = checkout`)
|
|
61
|
-
with the dashboard on port **7332** (default) so it coexists with a global install on
|
|
62
|
-
7331. See `bin/minions.js` (`--dev` / `--dev-port`).
|
|
63
|
-
|
|
64
|
-
Rebuild is not a compile step — the dashboard SPA is reassembled at startup — so
|
|
65
|
-
"rebuild + relaunch" is just a dev restart:
|
|
66
|
-
|
|
67
|
-
```powershell
|
|
68
|
-
node bin/minions.js --dev restart # or: npm run dev
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Restart-under-load gotchas
|
|
72
|
-
|
|
73
|
-
When the box is saturated (e.g. the dev fleet is running the full test suite), the
|
|
74
|
-
`--dev restart` health verifier can **fast-fail**: the freshly spawned engine can't
|
|
75
|
-
write its pid file inside the verifier's short window, so it is reported
|
|
76
|
-
`state=stopped, pid=none`, the partial stack is torn down, and `engine/stop-intent.json`
|
|
77
|
-
is set — which then blocks the next start, creating a loop. A **stray orphaned
|
|
78
|
-
`dashboard.js`** from an earlier attempt independently trips the verifier's
|
|
79
|
-
duplicate-daemon check.
|
|
80
|
-
|
|
81
|
-
Reliable recovery:
|
|
82
|
-
|
|
83
|
-
1. Stop any stray `engine.js` / `dashboard.js` / `supervisor.js` daemons (leave
|
|
84
|
-
`dispatch-*` agent processes and their worktree children alone — those are detached
|
|
85
|
-
agents that reattach on the next start).
|
|
86
|
-
2. Delete `engine/stop-intent.json`.
|
|
87
|
-
3. Confirm nothing is listening on the dev port.
|
|
88
|
-
4. Start with `node bin/minions.js --dev start` — from a clean state this is more
|
|
89
|
-
reliable than `restart` under load.
|
|
90
|
-
5. Verify: `GET http://localhost:7332/api/status` returns HTTP 200.
|
|
91
|
-
|
|
92
|
-
The engine itself boots fine under load (a foreground `node engine.js` stays healthy);
|
|
93
|
-
the fragility is only in the restart verifier's fast-fail path.
|
|
94
|
-
|
|
95
|
-
## Rogue dev edits
|
|
96
|
-
|
|
97
|
-
Because `--dev` uses the checkout as the runtime root, the dev fleet's own agents
|
|
98
|
-
sometimes edit **tracked** files in place (e.g. `dashboard/js/settings.js`, docs, even
|
|
99
|
-
reverting `README.md`). These show up as dirty tracked files before a rebuild. This is
|
|
100
|
-
expected, not corruption — stash them (with a dated message) rather than discarding, so
|
|
101
|
-
nothing is lost, then rebuild.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# PR visual evidence CI
|
|
2
|
-
|
|
3
|
-
The OPG GitHub workflow captures matched dashboard screenshots for pull requests that change visual dashboard inputs. It compares live base and head checkouts; it never compares a committed demo image with a live PR.
|
|
4
|
-
|
|
5
|
-
## Triggering
|
|
6
|
-
|
|
7
|
-
`Visual evidence capture` runs automatically when a PR changes:
|
|
8
|
-
|
|
9
|
-
- `dashboard/**`
|
|
10
|
-
- `dashboard-build.js`
|
|
11
|
-
- `docs/capture-demos.js`
|
|
12
|
-
- `test/seed-demo-data.js`
|
|
13
|
-
|
|
14
|
-
Add the `visual-evidence` label to opt in a visual change outside that list. Removing the label removes the opt-in. Backend, test-only, prose-only, and documentation-only PRs do not start browsers unless explicitly labeled.
|
|
15
|
-
|
|
16
|
-
## Capture contract
|
|
17
|
-
|
|
18
|
-
The capture workflow:
|
|
19
|
-
|
|
20
|
-
1. Resolves the exact PR, repository, base, and head tuple from the trusted `pull_request_target` event and records it with the workflow run ID and attempt. The request builder comes from the exact `github.workflow_sha` revision, so an older PR base that predates the controller records a `controller-not-on-base` opt-out instead of failing the plan.
|
|
21
|
-
2. Captures and uploads BEFORE in a base-only job.
|
|
22
|
-
3. Starts the separate head job only after BEFORE has been uploaded, then runs the untrusted dashboard in a capability-dropped container with a read-only root/source/controller and one writable marker-owned state mount. The trusted host owns the browser and AFTER output.
|
|
23
|
-
4. Seeds marker-owned state directories with the same deterministic demo fixture.
|
|
24
|
-
5. Starts each checkout on the same port and captures it with the base branch's canonical [`capture-demos.js`](capture-demos.js).
|
|
25
|
-
6. Uses Chromium at 1400x900, dark theme, `en-US`, UTC, reduced motion, and a fixed browser time.
|
|
26
|
-
7. Requires the same ten non-empty PNG names on both sides.
|
|
27
|
-
|
|
28
|
-
Startup, fixture, and capture logs are retained with the images. Partial output is never presented as a successful comparison: a failed AFTER side fails the run, and a failed BEFORE side degrades the run to an explicitly-labelled AFTER-only publication (see [Degraded AFTER-only runs](#degraded-after-only-runs)).
|
|
29
|
-
|
|
30
|
-
## Artifacts and PR summary
|
|
31
|
-
|
|
32
|
-
GitHub Actions stores five artifacts keyed by PR number and full head SHA:
|
|
33
|
-
|
|
34
|
-
- capture-request metadata
|
|
35
|
-
- BEFORE PNG set
|
|
36
|
-
- AFTER PNG set
|
|
37
|
-
- BEFORE logs and `result.json`
|
|
38
|
-
- AFTER logs and `result.json`
|
|
39
|
-
|
|
40
|
-
Artifacts expire after **14 days**. Each upload replaces its same-run name on a rerun, so a later attempt refreshes all five artifacts instead of colliding with the prior attempt. The trusted publisher creates or updates one `github-actions[bot]` comment with BEFORE/AFTER artifact links. All publisher jobs serialize globally through GitHub Actions' multi-entry concurrency queue before mutating PR comments, so a newly queued publisher does not replace an already-pending publication. Each publisher also checks the authoritative workflow-run order before changing comments. Older runs and attempts are suppressed even when a newer opt-out run left no summary. Results for an obsolete head are suppressed when a current result exists; an already-posted result is marked stale when its head no longer matches. If the current PR diff no longer requests visual evidence, the old bot summary is removed.
|
|
41
|
-
|
|
42
|
-
## Degraded AFTER-only runs
|
|
43
|
-
|
|
44
|
-
`capture-before` checks out `github.event.pull_request.base.sha` and runs the **base branch's** seed and capture code, not the pull request's. A defect already merged to `main` therefore breaks the BEFORE side of every PR opened against that base — including the PR that repairs the base, which could otherwise never show a green check.
|
|
45
|
-
|
|
46
|
-
The BEFORE side is consequently non-blocking:
|
|
47
|
-
|
|
48
|
-
- `capture-before` carries `continue-on-error: true` and is named `Capture BEFORE (base, non-blocking)`, the same convention `pr-tests.yml` uses for its non-blocking `perf-tests` job. The failure surfaces as a warning annotation plus a job summary that names the base SHA and states the run is AFTER-only; it does not present a blocking red check on the PR.
|
|
49
|
-
- `Upload BEFORE screenshots` uses `if-no-files-found: warn`, so a crashed capture that wrote zero PNGs is reported once by the capture step instead of raising a second hard error. `Upload BEFORE capture logs` deliberately stays `if-no-files-found: error` — the logs directory is always written and is what makes the failure diagnosable.
|
|
50
|
-
- `capture-after` is gated on `!cancelled() && needs.plan.outputs.capture == 'true'` instead of a successful `capture-before`, so the head-side capture still runs and uploads. The plan gate is re-asserted directly because it is no longer inherited through a green BEFORE job, and `!cancelled()` keeps a run superseded by `cancel-in-progress` from spending a full Docker and Playwright capture.
|
|
51
|
-
|
|
52
|
-
The publisher degrades in step. When the run concludes successfully and the BEFORE PNG set is the *only* missing artifact, `assessArtifacts` returns `status: 'after-only'` and the bot comment is titled **"Visual evidence: AFTER only (BEFORE capture failed)"**. It states that this is not a BEFORE/AFTER comparison, renders the BEFORE cell as `capture failed - no screenshots`, and links the BEFORE logs for diagnosis. Any other gap — a missing AFTER set, missing BEFORE logs, duplicate artifacts, or a genuinely failed run — still reports the full failure summary. A reviewer is never handed a half-capture presented as a matched pair.
|
|
53
|
-
|
|
54
|
-
Nothing about the security posture changes: the controller and base checkout still run from the trusted base ref with `persist-credentials: false`, `Verify exact base checkout` is unchanged, and untrusted head code gains no execution privileges.
|
|
55
|
-
|
|
56
|
-
## Security boundary
|
|
57
|
-
|
|
58
|
-
The workflow definition and capture-request builder run from the exact trusted `github.workflow_sha` revision through `pull_request_target`; detection and capture execution remain pinned to the PR's trusted base SHA. If that historical base does not contain the controller, detection suppresses both browser jobs while the trusted workflow revision still records the opt-out request artifact. Neither planning path loads controller code from the untrusted head. The workflow has only `contents: read` and `pull-requests: read`, checkout credentials are not persisted, and no write-capable token is available. BEFORE and AFTER use separate runners, and AFTER cannot start until the immutable BEFORE artifact has uploaded. The head dashboard runs without the Actions process environment, host process namespace, controller checkout, screenshot output, or Docker socket; its source/controller mounts and container root are read-only. The sole writable bind mount is marker-owned `/state`, and the container runs as the host UID:GID that owns it. Secret-shaped variables and GitHub Actions control-file variables are removed from host capture child processes.
|
|
59
|
-
|
|
60
|
-
Comment publication happens later in `visual-evidence-publish.yml`, triggered by `workflow_run`. That job checks out only the repository default branch and requires a `pull_request_target` run from the trusted base repository. It downloads and parses only the trusted plan job's capture-request JSON, then binds its repository, workflow, stable run ID, PR, base, head repository, base branch, and full head SHA to the live run before writing. A request retained from an earlier attempt is valid for a selective rerun of that same run ID, while metadata claiming a future attempt is rejected. The run's `head_sha` is the captured PR head; publication does not depend on the optional `workflow_run.pull_requests` association. Screenshot and log artifacts remain link-only and are never downloaded or executed by the publisher.
|
|
61
|
-
|
|
62
|
-
## Base-controller failure mode
|
|
63
|
-
|
|
64
|
-
Because the seeder (`test/seed-demo-data.js`) and Playwright controller (`docs/capture-demos.js`) always execute from the base checkout, a break in either one is only observable once it is on `main`: every subsequent BEFORE capture fails, for every PR, and no change on a PR branch can repair it — the PR's own copy is never executed. Both files are also visual paths, so even a PR that fixes them triggers the broken capture. Since `capture-before` is non-blocking, the visible symptom is that every PR degrades to an AFTER-only publication (see [Degraded AFTER-only runs](#degraded-after-only-runs)) until the fix lands. Recovery is to land the fix on `main` (admin bypass) and then push to the affected PRs so a fresh `pull_request_target` event picks up a base SHA that contains it. `test/unit/visual-evidence-ci.test.js` runs the seeder's full seeding body so this class of break fails PR Tests before it can reach `main`.
|
|
65
|
-
|
|
66
|
-
## Local reproduction
|
|
67
|
-
|
|
68
|
-
Start from a trusted checkout of the base branch, never the pull request/head checkout. Resolve the exact full base and head SHAs independently, then use two disposable worktrees and keep output outside both:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
BASE_WORKTREE=../visual-base
|
|
72
|
-
HEAD_WORKTREE=../visual-head
|
|
73
|
-
OUTPUT_ROOT=../visual-output
|
|
74
|
-
git worktree add --detach "$BASE_WORKTREE" <full-base-sha>
|
|
75
|
-
git worktree add --detach "$HEAD_WORKTREE" <full-head-sha>
|
|
76
|
-
test "$(git -C "$BASE_WORKTREE" rev-parse HEAD)" = "<full-base-sha>"
|
|
77
|
-
test "$(git -C "$HEAD_WORKTREE" rev-parse HEAD)" = "<full-head-sha>"
|
|
78
|
-
npm --prefix "$BASE_WORKTREE" ci --ignore-scripts
|
|
79
|
-
npm --prefix "$BASE_WORKTREE" exec -- playwright install chromium
|
|
80
|
-
node "${BASE_WORKTREE}/.github/scripts/run-visual-evidence.js" \
|
|
81
|
-
--dashboard-runtime docker \
|
|
82
|
-
--controller-root "$BASE_WORKTREE" \
|
|
83
|
-
--base-root "$BASE_WORKTREE" \
|
|
84
|
-
--head-root "$HEAD_WORKTREE" \
|
|
85
|
-
--base-sha <full-base-sha> \
|
|
86
|
-
--head-sha <full-head-sha> \
|
|
87
|
-
--output-root "$OUTPUT_ROOT"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The PNGs land in `../visual-output/{before,after}` and logs in `../visual-output/logs`. Docker is the default dashboard runtime so neither dashboard checkout executes in the host Node process. The host still executes the runner, fixture seeder, and Playwright controller, all from the exact base worktree; the runner verifies that controller checkout against `--base-sha` before mutating output. `--dashboard-runtime process` is an explicit opt-in for code you trust. The output root must be new, empty, or carry the runner's `.minions-visual-evidence-output` ownership marker, and it must not overlap either checkout or the controller in either direction. Reruns clear only the known generated subdirectories beneath a valid marker-owned root. Remove the disposable worktrees with the normal `git worktree remove` command after inspection.
|
|
91
|
-
|
|
92
|
-
## Complementary in-description embeds (agent-authored)
|
|
93
|
-
|
|
94
|
-
The CI workflow above is one of **two** complementary visual-evidence surfaces; it is not the only one:
|
|
95
|
-
|
|
96
|
-
- **`Visual evidence capture` (this workflow)** — an automated, sandboxed BEFORE/AFTER comparison posted by `github-actions[bot]`. It is untouched by the agent flow and remains the trusted comparison of record.
|
|
97
|
-
- **Agent-embedded screenshots (additive, W-mrz1yc79)** — when a Minions dispatch makes a meaningful **visual/UI change** (dashboard pages, slim-ux, Settings, work-item/PR/plan views, badges, layout/CSS), the agent proactively captures screenshots and embeds them directly in the PR **description** (``), rather than relying only on the bot. Backend-only, engine-logic, text/prose-only, analysis, and docs changes get no screenshots.
|
|
98
|
-
|
|
99
|
-
The agent embed path does **not** replace or modify the CI workflow — the two coexist. Mechanics the agent uses (full recipe in `playbooks/_pr-description-audit.md` → "Screenshot handling"):
|
|
100
|
-
|
|
101
|
-
- **GitHub:** raw PNGs are saved under `agents/<id>/screenshots/` (never `git add`ed), published as assets on a dedicated per-PR prerelease (`gh release create pr-<num>-visual … --prerelease`, or `gh release upload … --clobber` on re-dispatch), and embedded via each asset's `browser_download_url`. This works on private repos and never commits PNGs to the source branch. The upload uses a per-command `GH_TOKEN` for the repo owner's account (`opg-microsoft` → `yemishin_microsoft`, `yemi33` → `yemi33`) and never runs `gh auth switch`.
|
|
102
|
-
- **Azure DevOps:** `PUT /pullRequests/<id>/attachments/<filename>` with the PNG bytes, then reference the returned URL.
|
|
103
|
-
- **Degrade gracefully:** if the dev server won't start, the route 404s, Playwright is unavailable, or the upload fails, the agent skips without failing the work item and records the reason under `meta.descriptionAudit`.
|
package/engine/bridge.js
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* engine/bridge.js — Constellation-bridge config + marker accessors.
|
|
3
|
-
*
|
|
4
|
-
* Pure helpers for the `minions bridge ...` subcommand and the versioned
|
|
5
|
-
* metadata snapshot consumed by Constellation. Minions remains authoritative:
|
|
6
|
-
* the Constellation agent polls the local dashboard and mirrors only the
|
|
7
|
-
* allowlisted projection returned by buildBridgeSnapshot().
|
|
8
|
-
*
|
|
9
|
-
* Strict semantics: only the literal boolean `true` enables the bridge.
|
|
10
|
-
* Mirror this check on the Constellation reader to avoid truthy coercion
|
|
11
|
-
* silently flipping bridge state on a typo'd `"enabled": "false"`.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const shared = require('./shared');
|
|
16
|
-
|
|
17
|
-
const {
|
|
18
|
-
MINIONS_DIR,
|
|
19
|
-
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
20
|
-
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
21
|
-
safeJson,
|
|
22
|
-
mutateJsonFileLocked,
|
|
23
|
-
} = shared;
|
|
24
|
-
|
|
25
|
-
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
26
|
-
const BRIDGE_SNAPSHOT_PROTOCOL = 'minions.constellation.snapshot';
|
|
27
|
-
const BRIDGE_SNAPSHOT_PROTOCOL_VERSION = 1;
|
|
28
|
-
|
|
29
|
-
const SNAPSHOT_CAPS = Object.freeze({
|
|
30
|
-
id: 128,
|
|
31
|
-
status: 128,
|
|
32
|
-
agent: 128,
|
|
33
|
-
title: 512,
|
|
34
|
-
project: 128,
|
|
35
|
-
url: 2048,
|
|
36
|
-
timestamp: 64,
|
|
37
|
-
shortLabel: 128,
|
|
38
|
-
branch: 255,
|
|
39
|
-
name: 128,
|
|
40
|
-
});
|
|
41
|
-
const INACTIVE_AGENT_STATUSES = new Set(['idle', 'available', 'sleeping']);
|
|
42
|
-
|
|
43
|
-
function _string(value, max, fallback = null) {
|
|
44
|
-
if (typeof value !== 'string') return fallback;
|
|
45
|
-
const trimmed = value.trim();
|
|
46
|
-
if (!trimmed) return fallback;
|
|
47
|
-
return trimmed.slice(0, max);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function _isoTime(value) {
|
|
51
|
-
if (value === null || value === undefined || value === '') return undefined;
|
|
52
|
-
const ms = typeof value === 'number' ? value : Date.parse(String(value));
|
|
53
|
-
if (!Number.isFinite(ms)) return undefined;
|
|
54
|
-
return new Date(ms).toISOString().slice(0, SNAPSHOT_CAPS.timestamp);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function _defined(obj) {
|
|
58
|
-
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function _projectList(items, projector) {
|
|
62
|
-
if (!Array.isArray(items)) return [];
|
|
63
|
-
const projected = [];
|
|
64
|
-
for (const item of items) {
|
|
65
|
-
if (!item || typeof item !== 'object') continue;
|
|
66
|
-
const value = projector(item);
|
|
67
|
-
if (value) projected.push(value);
|
|
68
|
-
}
|
|
69
|
-
return projected;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function _projectWorkItem(item) {
|
|
73
|
-
const id = _string(item.id, SNAPSHOT_CAPS.id);
|
|
74
|
-
if (!id) return null;
|
|
75
|
-
return _defined({
|
|
76
|
-
id,
|
|
77
|
-
title: _string(item.title, SNAPSHOT_CAPS.title, id),
|
|
78
|
-
type: _string(item.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
79
|
-
priority: _string(item.priority, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
80
|
-
status: _string(item.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
81
|
-
project: _string(item.project, SNAPSHOT_CAPS.project) || undefined,
|
|
82
|
-
agent: _string(item.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
83
|
-
createdBy: _string(item.createdBy, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
84
|
-
created: _isoTime(item.created),
|
|
85
|
-
dispatched_to: _string(item.dispatched_to, SNAPSHOT_CAPS.agent) || undefined,
|
|
86
|
-
dispatched_at: _isoTime(item.dispatched_at),
|
|
87
|
-
completedAt: _isoTime(item.completedAt || item.completed_at),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function _projectPullRequest(pr) {
|
|
92
|
-
const id = _string(pr.id, SNAPSHOT_CAPS.id);
|
|
93
|
-
const prNumber = Number(pr.prNumber);
|
|
94
|
-
const url = _string(pr.url, SNAPSHOT_CAPS.url);
|
|
95
|
-
if (!id || !url || !Number.isInteger(prNumber) || prNumber < 0) return null;
|
|
96
|
-
return _defined({
|
|
97
|
-
id,
|
|
98
|
-
prNumber,
|
|
99
|
-
title: _string(pr.title, SNAPSHOT_CAPS.title, id),
|
|
100
|
-
agent: _string(pr.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
101
|
-
branch: _string(pr.branch, SNAPSHOT_CAPS.branch) || undefined,
|
|
102
|
-
reviewStatus: _string(pr.reviewStatus, SNAPSHOT_CAPS.status, 'unknown'),
|
|
103
|
-
status: _string(pr.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
104
|
-
url,
|
|
105
|
-
created: _isoTime(pr.created),
|
|
106
|
-
lastReviewedAt: _isoTime(pr.lastReviewedAt),
|
|
107
|
-
lastPushedAt: _isoTime(pr.lastPushedAt),
|
|
108
|
-
lastDispatchedAt: _isoTime(pr.lastDispatchedAt),
|
|
109
|
-
mergeStatus: _string(pr.mergeStatus || pr.mergeStateStatus, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
110
|
-
mergeable: typeof pr.mergeable === 'boolean' ? pr.mergeable : undefined,
|
|
111
|
-
isDraft: typeof pr.isDraft === 'boolean' ? pr.isDraft : undefined,
|
|
112
|
-
behindBy: Number.isInteger(pr.behindBy) && pr.behindBy >= 0 ? pr.behindBy : undefined,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function _projectPlan(plan) {
|
|
117
|
-
const id = _string(plan.id || plan.file, SNAPSHOT_CAPS.id);
|
|
118
|
-
if (!id) return null;
|
|
119
|
-
return _defined({
|
|
120
|
-
id,
|
|
121
|
-
title: _string(plan.title || plan.summary || plan.plan_summary, SNAPSHOT_CAPS.title, id),
|
|
122
|
-
status: _string(plan.status, SNAPSHOT_CAPS.status, plan.archived ? 'archived' : 'unknown'),
|
|
123
|
-
agent: _string(plan.agent || plan.generatedBy || plan.generated_by, SNAPSHOT_CAPS.agent) || undefined,
|
|
124
|
-
project: _string(plan.project, SNAPSHOT_CAPS.project) || undefined,
|
|
125
|
-
createdAt: _isoTime(plan.createdAt || plan.generatedAt || plan.generated_at),
|
|
126
|
-
updatedAt: _isoTime(plan.updatedAt),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function _projectSchedule(schedule) {
|
|
131
|
-
const id = _string(schedule.id, SNAPSHOT_CAPS.id);
|
|
132
|
-
if (!id) return null;
|
|
133
|
-
return _defined({
|
|
134
|
-
id,
|
|
135
|
-
title: _string(schedule.title, SNAPSHOT_CAPS.title, id),
|
|
136
|
-
status: _string(schedule.status, SNAPSHOT_CAPS.status,
|
|
137
|
-
schedule.enabled === false ? 'disabled' : 'enabled'),
|
|
138
|
-
type: _string(schedule.type, SNAPSHOT_CAPS.shortLabel, 'schedule'),
|
|
139
|
-
project: _string(schedule.project, SNAPSHOT_CAPS.project) || undefined,
|
|
140
|
-
agent: _string(schedule.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
141
|
-
nextRun: _isoTime(schedule.nextRun),
|
|
142
|
-
lastRun: _isoTime(schedule.lastRun || schedule._lastRun || schedule._lastCompletedAt),
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function _projectDispatch(entry, status) {
|
|
147
|
-
const id = _string(entry.id, SNAPSHOT_CAPS.id);
|
|
148
|
-
if (!id) return null;
|
|
149
|
-
const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
|
|
150
|
-
const metaItem = meta.item && typeof meta.item === 'object' ? meta.item : {};
|
|
151
|
-
const metaProject = meta.project && typeof meta.project === 'object' ? meta.project : {};
|
|
152
|
-
return _defined({
|
|
153
|
-
id,
|
|
154
|
-
agent: _string(entry.agent, SNAPSHOT_CAPS.agent, 'unassigned'),
|
|
155
|
-
status: _string(entry.status, SNAPSHOT_CAPS.status, status),
|
|
156
|
-
type: _string(entry.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
157
|
-
project: _string(entry.project || metaProject.name, SNAPSHOT_CAPS.project) || undefined,
|
|
158
|
-
workItemId: _string(entry.workItemId || metaItem.id, SNAPSHOT_CAPS.id) || undefined,
|
|
159
|
-
prId: _string(entry.prId || entry.pr_id, SNAPSHOT_CAPS.id) || undefined,
|
|
160
|
-
startedAt: _isoTime(entry.startedAt || entry.started_at),
|
|
161
|
-
completedAt: _isoTime(entry.completedAt || entry.completed_at),
|
|
162
|
-
outcome: _string(entry.outcome || entry.result, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function _projectDispatchQueue(dispatch) {
|
|
167
|
-
if (!dispatch || typeof dispatch !== 'object') return [];
|
|
168
|
-
const projected = [];
|
|
169
|
-
for (const [bucket, status] of [
|
|
170
|
-
['pending', 'queued'],
|
|
171
|
-
['active', 'active'],
|
|
172
|
-
['review', 'review'],
|
|
173
|
-
['completed', 'completed'],
|
|
174
|
-
]) {
|
|
175
|
-
const entries = bucket === 'completed' && Array.isArray(dispatch[bucket])
|
|
176
|
-
? dispatch[bucket].slice(-20)
|
|
177
|
-
: dispatch[bucket];
|
|
178
|
-
projected.push(..._projectList(entries, entry => _projectDispatch(entry, status)));
|
|
179
|
-
}
|
|
180
|
-
return projected;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function _projectAgent(agent) {
|
|
184
|
-
const id = _string(agent.id || agent.agent, SNAPSHOT_CAPS.agent);
|
|
185
|
-
if (!id) return null;
|
|
186
|
-
return _defined({
|
|
187
|
-
agent: id,
|
|
188
|
-
status: _string(agent.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
189
|
-
project: _string(agent.project, SNAPSHOT_CAPS.project) || undefined,
|
|
190
|
-
currentTaskId: _string(agent.currentTaskId || agent.currentDispatchId, SNAPSHOT_CAPS.id) || undefined,
|
|
191
|
-
currentTaskTitle: _string(agent.currentTaskTitle || agent.currentTask, SNAPSHOT_CAPS.title) || undefined,
|
|
192
|
-
updatedAt: _isoTime(agent.updatedAt || agent.lastActionAt),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Build the complete metadata-only state projection consumed by Constellation.
|
|
198
|
-
* Every field is copied explicitly so free-text task descriptions, prompts,
|
|
199
|
-
* completion reports, diffs, comments, paths, logs, and credentials cannot
|
|
200
|
-
* cross the bridge accidentally.
|
|
201
|
-
*/
|
|
202
|
-
function buildBridgeSnapshot(state = {}) {
|
|
203
|
-
const generatedAt = _isoTime(state.generatedAt) || new Date().toISOString();
|
|
204
|
-
const engine = state.control && typeof state.control === 'object' ? state.control : {};
|
|
205
|
-
const workItems = _projectList(state.workItems, _projectWorkItem);
|
|
206
|
-
const pullRequests = _projectList(state.pullRequests, _projectPullRequest);
|
|
207
|
-
const plans = _projectList(state.plans, _projectPlan);
|
|
208
|
-
const schedules = _projectList(state.schedules, _projectSchedule);
|
|
209
|
-
const agents = _projectList(state.agents, _projectAgent);
|
|
210
|
-
const dispatches = _projectDispatchQueue(state.dispatch);
|
|
211
|
-
const activeAgents = agents.filter(agent => !INACTIVE_AGENT_STATUSES.has(agent.status.toLowerCase())).length;
|
|
212
|
-
const minionsVersion = _string(state.minionsVersion, SNAPSHOT_CAPS.shortLabel);
|
|
213
|
-
const engineState = _string(engine.state, SNAPSHOT_CAPS.status, 'unknown');
|
|
214
|
-
const enginePid = Number.isInteger(engine.pid) && engine.pid > 0 ? engine.pid : null;
|
|
215
|
-
const projects = Array.isArray(state.projects) ? state.projects : [];
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
protocol: BRIDGE_SNAPSHOT_PROTOCOL,
|
|
219
|
-
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
220
|
-
complete: true,
|
|
221
|
-
generatedAt,
|
|
222
|
-
minionsVersion,
|
|
223
|
-
engine: {
|
|
224
|
-
state: engineState,
|
|
225
|
-
pid: enginePid,
|
|
226
|
-
},
|
|
227
|
-
entities: {
|
|
228
|
-
work_item: workItems,
|
|
229
|
-
pr: pullRequests,
|
|
230
|
-
plan: plans,
|
|
231
|
-
schedule: schedules,
|
|
232
|
-
dispatch: dispatches,
|
|
233
|
-
agent_status: agents,
|
|
234
|
-
device_summary: [{
|
|
235
|
-
name: 'Minions',
|
|
236
|
-
status: engineState,
|
|
237
|
-
lastSeen: generatedAt,
|
|
238
|
-
agentCount: agents.length,
|
|
239
|
-
activeAgentCount: activeAgents,
|
|
240
|
-
workItemCount: workItems.length,
|
|
241
|
-
pullRequestCount: pullRequests.length,
|
|
242
|
-
planCount: plans.length,
|
|
243
|
-
scheduleCount: schedules.length,
|
|
244
|
-
dispatchCount: dispatches.length,
|
|
245
|
-
projectCount: projects.length,
|
|
246
|
-
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
247
|
-
minionsVersion,
|
|
248
|
-
snapshotGeneratedAt: generatedAt,
|
|
249
|
-
enginePid,
|
|
250
|
-
}],
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function projectSnapshotHealthForBridge(snapshot) {
|
|
256
|
-
if (!snapshot || typeof snapshot !== 'object') return null;
|
|
257
|
-
if (snapshot.protocol !== BRIDGE_SNAPSHOT_PROTOCOL) return null;
|
|
258
|
-
if (snapshot.protocolVersion !== BRIDGE_SNAPSHOT_PROTOCOL_VERSION) return null;
|
|
259
|
-
if (snapshot.complete !== true || !snapshot.entities || typeof snapshot.entities !== 'object') return null;
|
|
260
|
-
const entities = snapshot.entities;
|
|
261
|
-
const summary = Array.isArray(entities.device_summary) ? entities.device_summary[0] || {} : {};
|
|
262
|
-
const count = key => (Array.isArray(entities[key]) ? entities[key].length : 0);
|
|
263
|
-
return {
|
|
264
|
-
protocolVersion: snapshot.protocolVersion,
|
|
265
|
-
minionsVersion: snapshot.minionsVersion ?? null,
|
|
266
|
-
engineState: snapshot.engine?.state ?? null,
|
|
267
|
-
enginePid: snapshot.engine?.pid ?? null,
|
|
268
|
-
agentCount: count('agent_status'),
|
|
269
|
-
activeAgentCount: summary.activeAgentCount ?? null,
|
|
270
|
-
workItemCount: count('work_item'),
|
|
271
|
-
pullRequestCount: count('pr'),
|
|
272
|
-
planCount: count('plan'),
|
|
273
|
-
scheduleCount: count('schedule'),
|
|
274
|
-
dispatchCount: count('dispatch'),
|
|
275
|
-
projectCount: summary.projectCount ?? null,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Strict check: `true` ⇔ bridge enabled. Any other shape (missing field,
|
|
281
|
-
* non-object, string "true", etc.) returns false.
|
|
282
|
-
*/
|
|
283
|
-
function isBridgeEnabled(config) {
|
|
284
|
-
return config?.engine?.constellationBridge?.enabled === true;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Read the cross-repo marker written by the Constellation agent. Returns
|
|
289
|
-
* `null` when the file is missing, unreadable, or schema-mismatched.
|
|
290
|
-
*
|
|
291
|
-
* Marker shape (see ENGINE_DEFAULTS.constellationBridge docstring):
|
|
292
|
-
* { schemaVersion: 1, lastSeenAt: ISO8601,
|
|
293
|
-
* agentVersion?: string, source?: 'constellation-agent' }
|
|
294
|
-
*/
|
|
295
|
-
function readBridgeMarker(markerPath = CONSTELLATION_BRIDGE_MARKER_PATH) {
|
|
296
|
-
const raw = safeJson(markerPath);
|
|
297
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
298
|
-
if (raw.schemaVersion !== CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION) return null;
|
|
299
|
-
if (typeof raw.lastSeenAt !== 'string') return null;
|
|
300
|
-
return {
|
|
301
|
-
schemaVersion: raw.schemaVersion,
|
|
302
|
-
lastSeenAt: raw.lastSeenAt,
|
|
303
|
-
agentVersion: typeof raw.agentVersion === 'string' ? raw.agentVersion : null,
|
|
304
|
-
source: typeof raw.source === 'string' ? raw.source : null,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Flip `config.engine.constellationBridge.enabled` atomically via
|
|
310
|
-
* mutateJsonFileLocked. Returns `{ previous: bool, current: bool }`.
|
|
311
|
-
* `configPath` override exists for unit tests.
|
|
312
|
-
*/
|
|
313
|
-
function setBridgeEnabled(enabled, configPath = CONFIG_PATH) {
|
|
314
|
-
const next = enabled === true;
|
|
315
|
-
let previous = false;
|
|
316
|
-
mutateJsonFileLocked(configPath, (cfg) => {
|
|
317
|
-
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return cfg;
|
|
318
|
-
cfg.engine = cfg.engine || {};
|
|
319
|
-
cfg.engine.constellationBridge = cfg.engine.constellationBridge || {};
|
|
320
|
-
previous = cfg.engine.constellationBridge.enabled === true;
|
|
321
|
-
cfg.engine.constellationBridge.enabled = next;
|
|
322
|
-
return cfg;
|
|
323
|
-
});
|
|
324
|
-
return { previous, current: next };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Human-readable relative age string (e.g. "12s ago", "3m ago", "2h ago").
|
|
329
|
-
* Caps at "1d+ ago" — anything older than the bridge polling cadence is
|
|
330
|
-
* already actionable as "stale".
|
|
331
|
-
*/
|
|
332
|
-
function formatRelativeAge(isoTimestamp, nowMs = Date.now()) {
|
|
333
|
-
const t = Date.parse(isoTimestamp);
|
|
334
|
-
if (!Number.isFinite(t)) return '(unknown)';
|
|
335
|
-
const deltaSec = Math.max(0, Math.round((nowMs - t) / 1000));
|
|
336
|
-
if (deltaSec < 60) return `${deltaSec}s ago`;
|
|
337
|
-
if (deltaSec < 3600) return `${Math.round(deltaSec / 60)}m ago`;
|
|
338
|
-
if (deltaSec < 86400) return `${Math.round(deltaSec / 3600)}h ago`;
|
|
339
|
-
return '1d+ ago';
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Compose the small stable projection the Constellation bridge consumes
|
|
344
|
-
* from `/api/status`. Kept narrow on purpose: full /api/status is large,
|
|
345
|
-
* unstable, and may surface unrelated local state. New fields go behind
|
|
346
|
-
* a deliberate schema version bump.
|
|
347
|
-
*/
|
|
348
|
-
function projectStatusForBridge(statusJson) {
|
|
349
|
-
if (!statusJson || typeof statusJson !== 'object') return null;
|
|
350
|
-
const dispatch = statusJson.dispatch || {};
|
|
351
|
-
const queueCount = (arr) => (Array.isArray(arr) ? arr.length : 0);
|
|
352
|
-
return {
|
|
353
|
-
engineState: statusJson.control?.state ?? null,
|
|
354
|
-
enginePid: statusJson.control?.pid ?? null,
|
|
355
|
-
minionsVersion: statusJson.version ?? null,
|
|
356
|
-
agentCount: Array.isArray(statusJson.agents) ? statusJson.agents.length : null,
|
|
357
|
-
activeAgentCount: Array.isArray(statusJson.agents)
|
|
358
|
-
? statusJson.agents.filter(a => a && a.status && a.status !== 'idle').length
|
|
359
|
-
: null,
|
|
360
|
-
dispatchPending: queueCount(dispatch.pending),
|
|
361
|
-
dispatchActive: queueCount(dispatch.active),
|
|
362
|
-
dispatchCompleted: queueCount(dispatch.completed),
|
|
363
|
-
projectCount: Array.isArray(statusJson.projects) ? statusJson.projects.length : null,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
module.exports = {
|
|
368
|
-
isBridgeEnabled,
|
|
369
|
-
readBridgeMarker,
|
|
370
|
-
setBridgeEnabled,
|
|
371
|
-
formatRelativeAge,
|
|
372
|
-
buildBridgeSnapshot,
|
|
373
|
-
projectSnapshotHealthForBridge,
|
|
374
|
-
projectStatusForBridge,
|
|
375
|
-
BRIDGE_SNAPSHOT_PROTOCOL,
|
|
376
|
-
BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
377
|
-
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
378
|
-
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
379
|
-
};
|