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