@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
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/api/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 crypto = require('crypto');
|
|
16
|
+
const shared = require('../core/shared');
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
MINIONS_DIR,
|
|
20
|
+
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
21
|
+
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
22
|
+
safeJson,
|
|
23
|
+
mutateJsonFileLocked,
|
|
24
|
+
} = shared;
|
|
25
|
+
|
|
26
|
+
const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
|
|
27
|
+
const BRIDGE_SNAPSHOT_PROTOCOL = 'minions.constellation.snapshot';
|
|
28
|
+
const BRIDGE_SNAPSHOT_PROTOCOL_VERSION = 1;
|
|
29
|
+
|
|
30
|
+
const SNAPSHOT_CAPS = Object.freeze({
|
|
31
|
+
id: 128,
|
|
32
|
+
status: 128,
|
|
33
|
+
agent: 128,
|
|
34
|
+
title: 512,
|
|
35
|
+
project: 128,
|
|
36
|
+
url: 2048,
|
|
37
|
+
timestamp: 64,
|
|
38
|
+
shortLabel: 128,
|
|
39
|
+
branch: 255,
|
|
40
|
+
name: 128,
|
|
41
|
+
epoch: 64,
|
|
42
|
+
});
|
|
43
|
+
const INACTIVE_AGENT_STATUSES = new Set(['idle', 'available', 'sleeping']);
|
|
44
|
+
|
|
45
|
+
function _string(value, max, fallback = null) {
|
|
46
|
+
if (typeof value !== 'string') return fallback;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (!trimmed) return fallback;
|
|
49
|
+
return trimmed.slice(0, max);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Non-negative integer gauge (counts, byte sizes, PIDs) or undefined. */
|
|
53
|
+
function _count(value) {
|
|
54
|
+
// Strictly a real number: Number(null) is 0 and Number('') is 0, so coercing
|
|
55
|
+
// here would turn a reserved managed spec's `pid: null` into a live-looking 0.
|
|
56
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) return undefined;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Non-negative fractional gauge (milliseconds, seconds) or undefined. Rounded
|
|
62
|
+
* to 3 decimals so float noise cannot make an otherwise-identical sample look
|
|
63
|
+
* like a content change to the entity versioner.
|
|
64
|
+
*/
|
|
65
|
+
function _gauge(value) {
|
|
66
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
|
|
67
|
+
return Math.round(value * 1000) / 1000;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _isoTime(value) {
|
|
71
|
+
if (value === null || value === undefined || value === '') return undefined;
|
|
72
|
+
const ms = typeof value === 'number' ? value : Date.parse(String(value));
|
|
73
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
74
|
+
return new Date(ms).toISOString().slice(0, SNAPSHOT_CAPS.timestamp);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _defined(obj) {
|
|
78
|
+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _projectList(items, projector) {
|
|
82
|
+
if (!Array.isArray(items)) return [];
|
|
83
|
+
const projected = [];
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
if (!item || typeof item !== 'object') continue;
|
|
86
|
+
const value = projector(item);
|
|
87
|
+
if (value) projected.push(value);
|
|
88
|
+
}
|
|
89
|
+
return projected;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _projectWorkItem(item) {
|
|
93
|
+
const id = _string(item.id, SNAPSHOT_CAPS.id);
|
|
94
|
+
if (!id) return null;
|
|
95
|
+
return _defined({
|
|
96
|
+
id,
|
|
97
|
+
title: _string(item.title, SNAPSHOT_CAPS.title, id),
|
|
98
|
+
type: _string(item.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
99
|
+
priority: _string(item.priority, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
100
|
+
status: _string(item.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
101
|
+
project: _string(item.project, SNAPSHOT_CAPS.project) || undefined,
|
|
102
|
+
agent: _string(item.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
103
|
+
createdBy: _string(item.createdBy, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
104
|
+
created: _isoTime(item.created),
|
|
105
|
+
dispatched_to: _string(item.dispatched_to, SNAPSHOT_CAPS.agent) || undefined,
|
|
106
|
+
dispatched_at: _isoTime(item.dispatched_at),
|
|
107
|
+
completedAt: _isoTime(item.completedAt || item.completed_at),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _projectAuthor(author) {
|
|
112
|
+
const a = shared.normalizePrAuthorIdentity(author);
|
|
113
|
+
if (!a) return undefined;
|
|
114
|
+
const projected = _defined({
|
|
115
|
+
provider: _string(a.provider, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
116
|
+
id: _string(a.id, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
117
|
+
descriptor: _string(a.descriptor, SNAPSHOT_CAPS.id) || undefined,
|
|
118
|
+
login: _string(a.login, SNAPSHOT_CAPS.name) || undefined,
|
|
119
|
+
displayName: _string(a.displayName, SNAPSHOT_CAPS.name) || undefined,
|
|
120
|
+
url: _string(a.url, SNAPSHOT_CAPS.url) || undefined,
|
|
121
|
+
});
|
|
122
|
+
return Object.keys(projected).length ? projected : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _projectPullRequest(pr) {
|
|
126
|
+
const id = _string(pr.id, SNAPSHOT_CAPS.id);
|
|
127
|
+
const prNumber = Number(pr.prNumber);
|
|
128
|
+
const url = _string(pr.url, SNAPSHOT_CAPS.url);
|
|
129
|
+
if (!id || !url || !Number.isInteger(prNumber) || prNumber < 0) return null;
|
|
130
|
+
return _defined({
|
|
131
|
+
id,
|
|
132
|
+
prNumber,
|
|
133
|
+
title: _string(pr.title, SNAPSHOT_CAPS.title, id),
|
|
134
|
+
agent: _string(pr.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
135
|
+
author: _projectAuthor(pr.author),
|
|
136
|
+
branch: _string(pr.branch, SNAPSHOT_CAPS.branch) || undefined,
|
|
137
|
+
reviewStatus: _string(pr.reviewStatus, SNAPSHOT_CAPS.status, 'unknown'),
|
|
138
|
+
status: _string(pr.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
139
|
+
url,
|
|
140
|
+
created: _isoTime(pr.created),
|
|
141
|
+
lastReviewedAt: _isoTime(pr.lastReviewedAt),
|
|
142
|
+
lastPushedAt: _isoTime(pr.lastPushedAt),
|
|
143
|
+
lastDispatchedAt: _isoTime(pr.lastDispatchedAt),
|
|
144
|
+
mergeStatus: _string(pr.mergeStatus || pr.mergeStateStatus, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
145
|
+
mergeable: typeof pr.mergeable === 'boolean' ? pr.mergeable : undefined,
|
|
146
|
+
isDraft: typeof pr.isDraft === 'boolean' ? pr.isDraft : undefined,
|
|
147
|
+
behindBy: Number.isInteger(pr.behindBy) && pr.behindBy >= 0 ? pr.behindBy : undefined,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _projectPlan(plan) {
|
|
152
|
+
const id = _string(plan.id || plan.file, SNAPSHOT_CAPS.id);
|
|
153
|
+
if (!id) return null;
|
|
154
|
+
return _defined({
|
|
155
|
+
id,
|
|
156
|
+
title: _string(plan.title || plan.summary || plan.plan_summary, SNAPSHOT_CAPS.title, id),
|
|
157
|
+
status: _string(plan.status, SNAPSHOT_CAPS.status, plan.archived ? 'archived' : 'unknown'),
|
|
158
|
+
agent: _string(plan.agent || plan.generatedBy || plan.generated_by, SNAPSHOT_CAPS.agent) || undefined,
|
|
159
|
+
project: _string(plan.project, SNAPSHOT_CAPS.project) || undefined,
|
|
160
|
+
createdAt: _isoTime(plan.createdAt || plan.generatedAt || plan.generated_at),
|
|
161
|
+
updatedAt: _isoTime(plan.updatedAt),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _projectSchedule(schedule) {
|
|
166
|
+
const id = _string(schedule.id, SNAPSHOT_CAPS.id);
|
|
167
|
+
if (!id) return null;
|
|
168
|
+
return _defined({
|
|
169
|
+
id,
|
|
170
|
+
title: _string(schedule.title, SNAPSHOT_CAPS.title, id),
|
|
171
|
+
status: _string(schedule.status, SNAPSHOT_CAPS.status,
|
|
172
|
+
schedule.enabled === false ? 'disabled' : 'enabled'),
|
|
173
|
+
type: _string(schedule.type, SNAPSHOT_CAPS.shortLabel, 'schedule'),
|
|
174
|
+
project: _string(schedule.project, SNAPSHOT_CAPS.project) || undefined,
|
|
175
|
+
agent: _string(schedule.agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
176
|
+
nextRun: _isoTime(schedule.nextRun),
|
|
177
|
+
lastRun: _isoTime(schedule.lastRun || schedule._lastRun || schedule._lastCompletedAt),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _projectDispatch(entry, status) {
|
|
182
|
+
const id = _string(entry.id, SNAPSHOT_CAPS.id);
|
|
183
|
+
if (!id) return null;
|
|
184
|
+
const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
|
|
185
|
+
const metaItem = meta.item && typeof meta.item === 'object' ? meta.item : {};
|
|
186
|
+
const metaProject = meta.project && typeof meta.project === 'object' ? meta.project : {};
|
|
187
|
+
return _defined({
|
|
188
|
+
id,
|
|
189
|
+
agent: _string(entry.agent, SNAPSHOT_CAPS.agent, 'unassigned'),
|
|
190
|
+
status: _string(entry.status, SNAPSHOT_CAPS.status, status),
|
|
191
|
+
type: _string(entry.type, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
192
|
+
project: _string(entry.project || metaProject.name, SNAPSHOT_CAPS.project) || undefined,
|
|
193
|
+
workItemId: _string(entry.workItemId || metaItem.id, SNAPSHOT_CAPS.id) || undefined,
|
|
194
|
+
prId: _string(entry.prId || entry.pr_id, SNAPSHOT_CAPS.id) || undefined,
|
|
195
|
+
startedAt: _isoTime(entry.startedAt || entry.started_at),
|
|
196
|
+
completedAt: _isoTime(entry.completedAt || entry.completed_at),
|
|
197
|
+
outcome: _string(entry.outcome || entry.result, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _projectDispatchQueue(dispatch) {
|
|
202
|
+
if (!dispatch || typeof dispatch !== 'object') return [];
|
|
203
|
+
const projected = [];
|
|
204
|
+
for (const [bucket, status] of [
|
|
205
|
+
['pending', 'queued'],
|
|
206
|
+
['active', 'active'],
|
|
207
|
+
['review', 'review'],
|
|
208
|
+
['completed', 'completed'],
|
|
209
|
+
]) {
|
|
210
|
+
const entries = bucket === 'completed' && Array.isArray(dispatch[bucket])
|
|
211
|
+
? dispatch[bucket].slice(-20)
|
|
212
|
+
: dispatch[bucket];
|
|
213
|
+
projected.push(..._projectList(entries, entry => _projectDispatch(entry, status)));
|
|
214
|
+
}
|
|
215
|
+
return projected;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _projectAgent(agent) {
|
|
219
|
+
const id = _string(agent.id || agent.agent, SNAPSHOT_CAPS.agent);
|
|
220
|
+
if (!id) return null;
|
|
221
|
+
return _defined({
|
|
222
|
+
agent: id,
|
|
223
|
+
status: _string(agent.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
224
|
+
project: _string(agent.project, SNAPSHOT_CAPS.project) || undefined,
|
|
225
|
+
currentTaskId: _string(agent.currentTaskId || agent.currentDispatchId, SNAPSHOT_CAPS.id) || undefined,
|
|
226
|
+
currentTaskTitle: _string(agent.currentTaskTitle || agent.currentTask, SNAPSHOT_CAPS.title) || undefined,
|
|
227
|
+
updatedAt: _isoTime(agent.updatedAt || agent.lastActionAt),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Extended entity families (Mission Control) ────────────────────────────
|
|
232
|
+
*
|
|
233
|
+
* qa_run / pipeline / managed_process / engine_health widen the projection to
|
|
234
|
+
* the rest of Mission Control's surface. They follow the same rule as every
|
|
235
|
+
* projector above — each field is copied EXPLICITLY, never spread — and their
|
|
236
|
+
* sources are the highest-risk records in the engine: a managed-process record
|
|
237
|
+
* carries `cmd`, `args`, `cwd`, `log_path`, `healthcheck` and free-form
|
|
238
|
+
* `attrs` (credential-shaped env keys included); a QA run carries a free-text
|
|
239
|
+
* `summary` and artifact filesystem paths; a pipeline definition carries a
|
|
240
|
+
* 20 KB free-text `description`, per-stage task prompts and monitored-resource
|
|
241
|
+
* URLs. None of that may cross the bridge, so none of it is copied.
|
|
242
|
+
*
|
|
243
|
+
* OPT-IN, NOT DEFAULT-ON. The Constellation consumer validates `entities` with
|
|
244
|
+
* a `.strict()` Zod object (packages/shared/src/orchestrator-messages.ts), and
|
|
245
|
+
* its poller fails CLOSED on a schema error without falling back to the legacy
|
|
246
|
+
* per-endpoint projection. Emitting an unrequested new kind would therefore
|
|
247
|
+
* take Mission Control dark for every consumer that has not yet widened its
|
|
248
|
+
* schema. Callers ask for kinds explicitly; a caller that asks for nothing gets
|
|
249
|
+
* a byte-identical v1 snapshot. That keeps the additive-field compatibility
|
|
250
|
+
* rule in docs/constellation-bridge.md true for entity KINDS as well as fields,
|
|
251
|
+
* so BRIDGE_SNAPSHOT_PROTOCOL_VERSION still does not move.
|
|
252
|
+
*
|
|
253
|
+
* Every extended kind uses `id` as its identity field. That is the default
|
|
254
|
+
* branch of the consumer's getOrchestratorEntityId(), so adopting these kinds
|
|
255
|
+
* needs no per-kind identity mapping on the Constellation side.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
const EXTENDED_SNAPSHOT_KINDS = Object.freeze([
|
|
259
|
+
'qa_run',
|
|
260
|
+
'pipeline',
|
|
261
|
+
'managed_process',
|
|
262
|
+
'engine_health',
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
// Mirrors packages/shared/src/orchestrator-messages.ts:
|
|
266
|
+
// ORCHESTRATOR_SNAPSHOT_MAX_ENTITIES_PER_KIND = 10_000
|
|
267
|
+
// device_summary: z.array(...).max(1)
|
|
268
|
+
// Enforced here so an oversized list is truncated at the producer instead of
|
|
269
|
+
// making the consumer reject the entire snapshot.
|
|
270
|
+
const SNAPSHOT_MAX_ENTITIES_PER_KIND = 10000;
|
|
271
|
+
const SNAPSHOT_SINGLETON_MAX_ENTITIES = 1;
|
|
272
|
+
const SNAPSHOT_SINGLETON_KINDS = new Set(['device_summary', 'engine_health']);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The exact field list each extended projector may emit, in emission order.
|
|
276
|
+
* This is the machine-readable form of the privacy boundary: a contract test
|
|
277
|
+
* compares it against the live projection in both directions, so widening a
|
|
278
|
+
* projection is always a visible diff in this constant.
|
|
279
|
+
*/
|
|
280
|
+
const SNAPSHOT_PROJECTED_FIELDS = Object.freeze({
|
|
281
|
+
qa_run: Object.freeze([
|
|
282
|
+
'id', 'runbookId', 'targetName', 'status', 'project', 'workItemId',
|
|
283
|
+
'artifactCount', 'createdAt', 'startedAt', 'completedAt',
|
|
284
|
+
]),
|
|
285
|
+
pipeline: Object.freeze([
|
|
286
|
+
'id', 'title', 'status', 'stageCount', 'runCount',
|
|
287
|
+
'lastRunId', 'lastRunStatus', 'lastRunStartedAt', 'lastRunCompletedAt',
|
|
288
|
+
]),
|
|
289
|
+
managed_process: Object.freeze([
|
|
290
|
+
'id', 'status', 'alive', 'healthy', 'pid', 'project', 'agent',
|
|
291
|
+
'workItemId', 'startedAt', 'lastHealthyAt', 'expiresAt',
|
|
292
|
+
]),
|
|
293
|
+
engine_health: Object.freeze([
|
|
294
|
+
'id', 'state', 'pid', 'startedAt', 'lastTickAt', 'heartbeatAt',
|
|
295
|
+
'rss', 'heapUsed', 'heapTotal', 'heapSizeLimit', 'external',
|
|
296
|
+
'eventLoopLagP50', 'eventLoopLagP99', 'eventLoopLagMax',
|
|
297
|
+
'gcCount', 'gcPausesTotalMs', 'lastGcPauseMs', 'lastGcKind',
|
|
298
|
+
'uptimeSeconds', 'sampledAt',
|
|
299
|
+
]),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Allowlist a caller's requested extended kinds. Unknown names, legacy kind
|
|
304
|
+
* names, and prototype-shaped strings are dropped rather than echoed, and the
|
|
305
|
+
* result is ordered by the registry so a given request always produces the
|
|
306
|
+
* same ETag variant and the same `entities` key order.
|
|
307
|
+
*/
|
|
308
|
+
function resolveSnapshotKinds(requested) {
|
|
309
|
+
const raw = typeof requested === 'string'
|
|
310
|
+
? requested.split(',')
|
|
311
|
+
: (Array.isArray(requested) ? requested : []);
|
|
312
|
+
const asked = new Set();
|
|
313
|
+
for (const entry of raw) {
|
|
314
|
+
const name = _string(entry, SNAPSHOT_CAPS.shortLabel);
|
|
315
|
+
if (name) asked.add(name);
|
|
316
|
+
}
|
|
317
|
+
return EXTENDED_SNAPSHOT_KINDS.filter(kind => asked.has(kind));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _projectQaRun(run) {
|
|
321
|
+
const id = _string(run.id, SNAPSHOT_CAPS.id);
|
|
322
|
+
if (!id) return null;
|
|
323
|
+
return _defined({
|
|
324
|
+
id,
|
|
325
|
+
runbookId: _string(run.runbookId, SNAPSHOT_CAPS.id) || undefined,
|
|
326
|
+
targetName: _string(run.targetName, SNAPSHOT_CAPS.name) || undefined,
|
|
327
|
+
status: _string(run.status, SNAPSHOT_CAPS.status, 'unknown'),
|
|
328
|
+
project: _string(run.project, SNAPSHOT_CAPS.project) || undefined,
|
|
329
|
+
workItemId: _string(run.workItemId, SNAPSHOT_CAPS.id) || undefined,
|
|
330
|
+
// Count only. Artifact records carry filesystem paths and agent-authored
|
|
331
|
+
// labels; the consumer fetches artifacts from Minions directly when a
|
|
332
|
+
// human opens the run.
|
|
333
|
+
artifactCount: _count(Array.isArray(run.artifacts) ? run.artifacts.length : 0),
|
|
334
|
+
createdAt: _isoTime(run.createdAt),
|
|
335
|
+
startedAt: _isoTime(run.startedAt),
|
|
336
|
+
completedAt: _isoTime(run.completedAt),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function _projectPipeline(pipeline) {
|
|
341
|
+
const id = _string(pipeline.id, SNAPSHOT_CAPS.id);
|
|
342
|
+
if (!id) return null;
|
|
343
|
+
const stages = Array.isArray(pipeline.stages) ? pipeline.stages : [];
|
|
344
|
+
const runs = Array.isArray(pipeline.runs) ? pipeline.runs : [];
|
|
345
|
+
// The API merges the last-5 runs oldest-first, so the newest is the tail.
|
|
346
|
+
const last = runs.length ? runs[runs.length - 1] : null;
|
|
347
|
+
const lastStatus = last ? _string(last.status, SNAPSHOT_CAPS.status) : null;
|
|
348
|
+
return _defined({
|
|
349
|
+
id,
|
|
350
|
+
// `title` is operator-authored bounded text, exactly like the title already
|
|
351
|
+
// projected for work_item / pr / plan / schedule. It is the one free-text
|
|
352
|
+
// class docs/constellation-bridge.md admits; the 20 KB `description`,
|
|
353
|
+
// per-stage task prompts and monitoredResources URLs are not.
|
|
354
|
+
title: _string(pipeline.title, SNAPSHOT_CAPS.title, id),
|
|
355
|
+
// A pipeline definition has no status of its own — its latest run's status
|
|
356
|
+
// is what an operator means by "how is this pipeline doing". An explicitly
|
|
357
|
+
// disabled pipeline outranks that: startRun refuses it, so a stale
|
|
358
|
+
// `completed` would misrepresent it as merely idle between runs.
|
|
359
|
+
status: pipeline.enabled === false ? 'disabled' : (lastStatus || 'idle'),
|
|
360
|
+
stageCount: _count(stages.length),
|
|
361
|
+
runCount: _count(runs.length),
|
|
362
|
+
lastRunId: last ? (_string(last.runId, SNAPSHOT_CAPS.id) || undefined) : undefined,
|
|
363
|
+
lastRunStatus: lastStatus || undefined,
|
|
364
|
+
lastRunStartedAt: last ? _isoTime(last.startedAt) : undefined,
|
|
365
|
+
lastRunCompletedAt: last ? _isoTime(last.completedAt) : undefined,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function _projectManagedProcess(spec) {
|
|
370
|
+
// The spec's name IS its identity; `id` (rather than `name`) keeps the
|
|
371
|
+
// consumer's default entity-id resolver working with no per-kind mapping.
|
|
372
|
+
const id = _string(spec.name, SNAPSHOT_CAPS.name);
|
|
373
|
+
if (!id) return null;
|
|
374
|
+
const alive = spec.alive === true;
|
|
375
|
+
const healthy = spec.healthy === true;
|
|
376
|
+
return _defined({
|
|
377
|
+
id,
|
|
378
|
+
status: !alive ? 'stopped' : (healthy ? 'healthy' : 'unhealthy'),
|
|
379
|
+
alive,
|
|
380
|
+
healthy,
|
|
381
|
+
pid: _count(spec.pid),
|
|
382
|
+
project: _string(spec.owner_project, SNAPSHOT_CAPS.project) || undefined,
|
|
383
|
+
agent: _string(spec.owner_agent, SNAPSHOT_CAPS.agent) || undefined,
|
|
384
|
+
workItemId: _string(spec.owner_wi, SNAPSHOT_CAPS.id) || undefined,
|
|
385
|
+
startedAt: _isoTime(spec.started_at),
|
|
386
|
+
lastHealthyAt: _isoTime(spec.last_health_at),
|
|
387
|
+
expiresAt: _isoTime(spec.ttl_expires_at),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Singleton health summary for the engine process: liveness plus the memory,
|
|
393
|
+
* event-loop and GC gauges sampled by the memoryBaseline tick phase. All
|
|
394
|
+
* numeric — the only string fields are the engine state and the GC kind, both
|
|
395
|
+
* closed vocabularies capped through _string().
|
|
396
|
+
*/
|
|
397
|
+
function _projectEngineHealth(engine, memory) {
|
|
398
|
+
const sample = memory && typeof memory === 'object' ? memory : {};
|
|
399
|
+
return _defined({
|
|
400
|
+
id: 'engine',
|
|
401
|
+
state: _string(engine.state, SNAPSHOT_CAPS.status, 'unknown'),
|
|
402
|
+
pid: Number.isInteger(engine.pid) && engine.pid > 0 ? engine.pid : null,
|
|
403
|
+
startedAt: _isoTime(engine.started_at || engine.startedAt),
|
|
404
|
+
// The engine has no persisted tick counter; lastTickAt is the observable
|
|
405
|
+
// tick signal, and it deliberately participates in the content hash so a
|
|
406
|
+
// consumer can see the engine ticking via changeVersion.
|
|
407
|
+
lastTickAt: _isoTime(engine.lastTickAt),
|
|
408
|
+
heartbeatAt: _isoTime(engine.heartbeat),
|
|
409
|
+
rss: _count(sample.rss),
|
|
410
|
+
heapUsed: _count(sample.heapUsed),
|
|
411
|
+
heapTotal: _count(sample.heapTotal),
|
|
412
|
+
heapSizeLimit: _count(sample.heapSizeLimit),
|
|
413
|
+
external: _count(sample.external),
|
|
414
|
+
eventLoopLagP50: _gauge(sample.eventLoopLagP50),
|
|
415
|
+
eventLoopLagP99: _gauge(sample.eventLoopLagP99),
|
|
416
|
+
eventLoopLagMax: _gauge(sample.eventLoopLagMax),
|
|
417
|
+
gcCount: _count(sample.gcCount),
|
|
418
|
+
gcPausesTotalMs: _gauge(sample.gcPausesTotalMs),
|
|
419
|
+
lastGcPauseMs: _gauge(sample.lastGcPauseMs),
|
|
420
|
+
lastGcKind: _string(sample.lastGcKind, SNAPSHOT_CAPS.shortLabel) || undefined,
|
|
421
|
+
uptimeSeconds: _gauge(sample.uptime),
|
|
422
|
+
sampledAt: _isoTime(sample.capturedAt),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Route one extended kind to its projector. */
|
|
427
|
+
function _projectExtendedKind(kind, state, engine) {
|
|
428
|
+
switch (kind) {
|
|
429
|
+
case 'qa_run': return _projectList(state.qaRuns, _projectQaRun);
|
|
430
|
+
case 'pipeline': return _projectList(state.pipelines, _projectPipeline);
|
|
431
|
+
case 'managed_process': return _projectList(state.managedProcesses, _projectManagedProcess);
|
|
432
|
+
case 'engine_health': return [_projectEngineHealth(engine, state.memory)];
|
|
433
|
+
default: return [];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* ── Snapshot ordering tokens (additive; NOT a protocolVersion bump) ───────
|
|
438
|
+
*
|
|
439
|
+
* `snapshotVersion` is a counter that is monotonically non-decreasing for the
|
|
440
|
+
* lifetime of one Minions process. It advances only when the *projected*
|
|
441
|
+
* content of at least one entity was added, removed, or modified, so a
|
|
442
|
+
* consumer can order two polls with an integer compare instead of diffing
|
|
443
|
+
* whole snapshots. Each entity additionally carries the `changeVersion` at
|
|
444
|
+
* which its own content hash last changed, so a consumer can skip re-applying
|
|
445
|
+
* entities it already mirrors.
|
|
446
|
+
*
|
|
447
|
+
* `epoch` identifies the process that owns the counter. The counter is
|
|
448
|
+
* in-memory, so it resets with the process; a consumer that sees a new `epoch`
|
|
449
|
+
* must discard its cached versions and take the full snapshot rather than
|
|
450
|
+
* mis-comparing numbers across the reset.
|
|
451
|
+
*
|
|
452
|
+
* All three fields are additive and optional, which per the compatibility rule
|
|
453
|
+
* in docs/constellation-bridge.md ("protocolVersion changes only for a
|
|
454
|
+
* breaking schema change. Additive optional fields do not require a version
|
|
455
|
+
* bump") means BRIDGE_SNAPSHOT_PROTOCOL_VERSION deliberately does NOT move.
|
|
456
|
+
*
|
|
457
|
+
* WHY THIS IS NOT BUILT ON THE `events` TABLE — which already backs this
|
|
458
|
+
* endpoint's HTTP ETag and keeps doing so, unchanged:
|
|
459
|
+
* 1. Its rows are payload-less for exactly the entities projected here. The
|
|
460
|
+
* emitStateEvent calls in the work-item store, the pull-request store and
|
|
461
|
+
* the dispatch queue pass a topic string only — no entity id, no
|
|
462
|
+
* before/after payload — so a per-entity delta cannot be reconstructed
|
|
463
|
+
* from them at all.
|
|
464
|
+
* 2. Emission is best-effort by hard contract: engine/persistence/db-events.js
|
|
465
|
+
* swallows every SQLite failure (logging once per process) so that a state
|
|
466
|
+
* writer can never throw from a cache-invalidation side effect. The id
|
|
467
|
+
* sequence can therefore gap silently, and a consumer would be unable to
|
|
468
|
+
* tell "nothing changed" apart from "the emit failed".
|
|
469
|
+
* Hashing the projected content is immune to both: it is exactly as correct as
|
|
470
|
+
* the projection itself, and it needs no database access.
|
|
471
|
+
*/
|
|
472
|
+
|
|
473
|
+
const SNAPSHOT_EPOCH = [
|
|
474
|
+
process.pid.toString(36),
|
|
475
|
+
Date.now().toString(36),
|
|
476
|
+
crypto.randomBytes(4).toString('hex'),
|
|
477
|
+
].join('-').slice(0, SNAPSHOT_CAPS.epoch);
|
|
478
|
+
|
|
479
|
+
/*
|
|
480
|
+
* Identity + content rules per entity kind. `volatile` fields are stamped from
|
|
481
|
+
* the snapshot's own generation time rather than describing the entity, so they
|
|
482
|
+
* are excluded from the content hash — otherwise every poll would look like a
|
|
483
|
+
* change and the counter would carry no signal at all.
|
|
484
|
+
*/
|
|
485
|
+
const ENTITY_VERSION_RULES = Object.freeze({
|
|
486
|
+
work_item: { identity: 'id', volatile: [] },
|
|
487
|
+
pr: { identity: 'id', volatile: [] },
|
|
488
|
+
plan: { identity: 'id', volatile: [] },
|
|
489
|
+
schedule: { identity: 'id', volatile: [] },
|
|
490
|
+
dispatch: { identity: 'id', volatile: [] },
|
|
491
|
+
agent_status: { identity: 'agent', volatile: [] },
|
|
492
|
+
device_summary: { identity: 'name', volatile: ['lastSeen', 'snapshotGeneratedAt'] },
|
|
493
|
+
qa_run: { identity: 'id', volatile: [] },
|
|
494
|
+
pipeline: { identity: 'id', volatile: [] },
|
|
495
|
+
managed_process: { identity: 'id', volatile: [] },
|
|
496
|
+
engine_health: { identity: 'id', volatile: [] },
|
|
497
|
+
});
|
|
498
|
+
// Fail-safe for a future entity kind whose rule was not registered above: `id`
|
|
499
|
+
// with no volatile fields is correct for every kind projected today, so a miss
|
|
500
|
+
// degrades to a slightly noisier version rather than crashing the snapshot.
|
|
501
|
+
const DEFAULT_ENTITY_VERSION_RULE = Object.freeze({ identity: 'id', volatile: [] });
|
|
502
|
+
|
|
503
|
+
let _snapshotVersion = 0;
|
|
504
|
+
let _entityVersionLedger = new Map();
|
|
505
|
+
|
|
506
|
+
function _canonical(value) {
|
|
507
|
+
if (Array.isArray(value)) return value.map(_canonical);
|
|
508
|
+
if (value && typeof value === 'object') {
|
|
509
|
+
const sorted = {};
|
|
510
|
+
for (const key of Object.keys(value).sort()) sorted[key] = _canonical(value[key]);
|
|
511
|
+
return sorted;
|
|
512
|
+
}
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Stable hash of a projected entity's content, independent of key order. */
|
|
517
|
+
function _contentHash(entity, volatileFields) {
|
|
518
|
+
const subject = { ...entity };
|
|
519
|
+
for (const field of volatileFields) delete subject[field];
|
|
520
|
+
return crypto.createHash('sha256').update(JSON.stringify(_canonical(subject))).digest('hex');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Stamp every projected entity with the version at which its content last
|
|
525
|
+
* changed, and advance the process-wide snapshot version when the ledger of
|
|
526
|
+
* (identity → content hash) pairs differs from the previous poll. Returns a
|
|
527
|
+
* new entities map; the inputs are not mutated. The ledger is rebuilt from the
|
|
528
|
+
* current snapshot on every call, so it stays bounded by the live entity count.
|
|
529
|
+
*
|
|
530
|
+
* A kind that is ABSENT from this snapshot is UNKNOWN, not empty: its ledger
|
|
531
|
+
* entries are carried forward untouched. Extended kinds are opt-in, so the same
|
|
532
|
+
* process serves both narrow polls (`bridge health`, an unwidened consumer) and
|
|
533
|
+
* wide ones. Rebuilding the ledger from only the present kinds would make every
|
|
534
|
+
* extended entity look newly-changed on the next wide poll and would advance
|
|
535
|
+
* snapshotVersion on every alternation — a phantom-change generator driven by
|
|
536
|
+
* who asked last, not by what changed.
|
|
537
|
+
*/
|
|
538
|
+
function _applyEntityVersions(entities) {
|
|
539
|
+
const candidate = _snapshotVersion + 1;
|
|
540
|
+
const ledger = new Map();
|
|
541
|
+
const versioned = {};
|
|
542
|
+
const presentKinds = new Set(Object.keys(entities));
|
|
543
|
+
let changed = false;
|
|
544
|
+
|
|
545
|
+
// Carry forward the kinds this snapshot did not report on.
|
|
546
|
+
for (const [key, entry] of _entityVersionLedger) {
|
|
547
|
+
if (!presentKinds.has(key.slice(0, key.indexOf('\u0000')))) ledger.set(key, entry);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (const [kind, list] of Object.entries(entities)) {
|
|
551
|
+
const rule = ENTITY_VERSION_RULES[kind] || DEFAULT_ENTITY_VERSION_RULE;
|
|
552
|
+
versioned[kind] = _capEntities(kind, list).map((entity, index) => {
|
|
553
|
+
const identity = _string(entity[rule.identity], SNAPSHOT_CAPS.id) || `#${index}`;
|
|
554
|
+
const key = `${kind}\u0000${identity}`;
|
|
555
|
+
const hash = _contentHash(entity, rule.volatile);
|
|
556
|
+
const previous = _entityVersionLedger.get(key);
|
|
557
|
+
const unchanged = previous !== undefined && previous.hash === hash;
|
|
558
|
+
if (!unchanged) changed = true;
|
|
559
|
+
const changeVersion = unchanged ? previous.changeVersion : candidate;
|
|
560
|
+
ledger.set(key, { hash, changeVersion });
|
|
561
|
+
return { ...entity, changeVersion };
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Additions and modifications already flipped the flag above; a shrunk ledger
|
|
566
|
+
// is the remaining way the map can differ. Dropping an entity IS a change —
|
|
567
|
+
// this snapshot is a complete replacement, so a consumer has to unmirror it.
|
|
568
|
+
// Carried-forward entries are on both sides of this comparison, so an absent
|
|
569
|
+
// kind cannot masquerade as a removal.
|
|
570
|
+
if (!changed && ledger.size !== _entityVersionLedger.size) changed = true;
|
|
571
|
+
|
|
572
|
+
if (changed) _snapshotVersion = candidate;
|
|
573
|
+
_entityVersionLedger = ledger;
|
|
574
|
+
return versioned;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Truncate a kind's list to the ceiling the Constellation consumer enforces.
|
|
579
|
+
* Over-cap lists are rejected wholesale on the consumer side, so silently
|
|
580
|
+
* dropping the tail here degrades to "fewer entities" instead of "no snapshot".
|
|
581
|
+
* Callers supply the list already in their preferred order.
|
|
582
|
+
*/
|
|
583
|
+
function _capEntities(kind, list) {
|
|
584
|
+
const cap = SNAPSHOT_SINGLETON_KINDS.has(kind)
|
|
585
|
+
? SNAPSHOT_SINGLETON_MAX_ENTITIES
|
|
586
|
+
: SNAPSHOT_MAX_ENTITIES_PER_KIND;
|
|
587
|
+
return list.length > cap ? list.slice(0, cap) : list;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Build the complete metadata-only state projection consumed by Constellation.
|
|
592
|
+
* Every field is copied explicitly so free-text task descriptions, prompts,
|
|
593
|
+
* completion reports, diffs, comments, paths, logs, and credentials cannot
|
|
594
|
+
* cross the bridge accidentally.
|
|
595
|
+
*
|
|
596
|
+
* `options.extendedKinds` opts into the Mission Control entity families
|
|
597
|
+
* (EXTENDED_SNAPSHOT_KINDS). Omitting it yields a byte-identical v1 snapshot.
|
|
598
|
+
*/
|
|
599
|
+
function buildBridgeSnapshot(state = {}, options = {}) {
|
|
600
|
+
const extendedKinds = resolveSnapshotKinds(options && options.extendedKinds);
|
|
601
|
+
const generatedAt = _isoTime(state.generatedAt) || new Date().toISOString();
|
|
602
|
+
const engine = state.control && typeof state.control === 'object' ? state.control : {};
|
|
603
|
+
const workItems = _projectList(state.workItems, _projectWorkItem);
|
|
604
|
+
const pullRequests = _projectList(state.pullRequests, _projectPullRequest);
|
|
605
|
+
const plans = _projectList(state.plans, _projectPlan);
|
|
606
|
+
const schedules = _projectList(state.schedules, _projectSchedule);
|
|
607
|
+
const agents = _projectList(state.agents, _projectAgent);
|
|
608
|
+
const dispatches = _projectDispatchQueue(state.dispatch);
|
|
609
|
+
const activeAgents = agents.filter(agent => !INACTIVE_AGENT_STATUSES.has(agent.status.toLowerCase())).length;
|
|
610
|
+
const minionsVersion = _string(state.minionsVersion, SNAPSHOT_CAPS.shortLabel);
|
|
611
|
+
const engineState = _string(engine.state, SNAPSHOT_CAPS.status, 'unknown');
|
|
612
|
+
const enginePid = Number.isInteger(engine.pid) && engine.pid > 0 ? engine.pid : null;
|
|
613
|
+
const projects = Array.isArray(state.projects) ? state.projects : [];
|
|
614
|
+
|
|
615
|
+
const entities = _applyEntityVersions({
|
|
616
|
+
work_item: workItems,
|
|
617
|
+
pr: pullRequests,
|
|
618
|
+
plan: plans,
|
|
619
|
+
schedule: schedules,
|
|
620
|
+
dispatch: dispatches,
|
|
621
|
+
agent_status: agents,
|
|
622
|
+
device_summary: [{
|
|
623
|
+
name: 'Minions',
|
|
624
|
+
status: engineState,
|
|
625
|
+
lastSeen: generatedAt,
|
|
626
|
+
agentCount: agents.length,
|
|
627
|
+
activeAgentCount: activeAgents,
|
|
628
|
+
workItemCount: workItems.length,
|
|
629
|
+
pullRequestCount: pullRequests.length,
|
|
630
|
+
planCount: plans.length,
|
|
631
|
+
scheduleCount: schedules.length,
|
|
632
|
+
dispatchCount: dispatches.length,
|
|
633
|
+
projectCount: projects.length,
|
|
634
|
+
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
635
|
+
minionsVersion,
|
|
636
|
+
snapshotGeneratedAt: generatedAt,
|
|
637
|
+
enginePid,
|
|
638
|
+
}],
|
|
639
|
+
...Object.fromEntries(extendedKinds.map(kind => [kind, _projectExtendedKind(kind, state, engine)])),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
protocol: BRIDGE_SNAPSHOT_PROTOCOL,
|
|
644
|
+
protocolVersion: BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
645
|
+
complete: true,
|
|
646
|
+
generatedAt,
|
|
647
|
+
minionsVersion,
|
|
648
|
+
snapshotVersion: _snapshotVersion,
|
|
649
|
+
epoch: SNAPSHOT_EPOCH,
|
|
650
|
+
engine: {
|
|
651
|
+
state: engineState,
|
|
652
|
+
pid: enginePid,
|
|
653
|
+
},
|
|
654
|
+
entities,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function projectSnapshotHealthForBridge(snapshot) {
|
|
659
|
+
if (!snapshot || typeof snapshot !== 'object') return null;
|
|
660
|
+
if (snapshot.protocol !== BRIDGE_SNAPSHOT_PROTOCOL) return null;
|
|
661
|
+
if (snapshot.protocolVersion !== BRIDGE_SNAPSHOT_PROTOCOL_VERSION) return null;
|
|
662
|
+
if (snapshot.complete !== true || !snapshot.entities || typeof snapshot.entities !== 'object') return null;
|
|
663
|
+
const entities = snapshot.entities;
|
|
664
|
+
const summary = Array.isArray(entities.device_summary) ? entities.device_summary[0] || {} : {};
|
|
665
|
+
const count = key => (Array.isArray(entities[key]) ? entities[key].length : 0);
|
|
666
|
+
return {
|
|
667
|
+
protocolVersion: snapshot.protocolVersion,
|
|
668
|
+
minionsVersion: snapshot.minionsVersion ?? null,
|
|
669
|
+
engineState: snapshot.engine?.state ?? null,
|
|
670
|
+
enginePid: snapshot.engine?.pid ?? null,
|
|
671
|
+
agentCount: count('agent_status'),
|
|
672
|
+
activeAgentCount: summary.activeAgentCount ?? null,
|
|
673
|
+
workItemCount: count('work_item'),
|
|
674
|
+
pullRequestCount: count('pr'),
|
|
675
|
+
planCount: count('plan'),
|
|
676
|
+
scheduleCount: count('schedule'),
|
|
677
|
+
dispatchCount: count('dispatch'),
|
|
678
|
+
projectCount: summary.projectCount ?? null,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Strict check: `true` ⇔ bridge enabled. Any other shape (missing field,
|
|
684
|
+
* non-object, string "true", etc.) returns false.
|
|
685
|
+
*/
|
|
686
|
+
function isBridgeEnabled(config) {
|
|
687
|
+
return config?.engine?.constellationBridge?.enabled === true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Read the cross-repo marker written by the Constellation agent. Returns
|
|
692
|
+
* `null` when the file is missing, unreadable, or schema-mismatched.
|
|
693
|
+
*
|
|
694
|
+
* Marker shape (see ENGINE_DEFAULTS.constellationBridge docstring):
|
|
695
|
+
* { schemaVersion: 1, lastSeenAt: ISO8601,
|
|
696
|
+
* agentVersion?: string, source?: 'constellation-agent' }
|
|
697
|
+
*/
|
|
698
|
+
function readBridgeMarker(markerPath = CONSTELLATION_BRIDGE_MARKER_PATH) {
|
|
699
|
+
const raw = safeJson(markerPath);
|
|
700
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
701
|
+
if (raw.schemaVersion !== CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION) return null;
|
|
702
|
+
if (typeof raw.lastSeenAt !== 'string') return null;
|
|
703
|
+
return {
|
|
704
|
+
schemaVersion: raw.schemaVersion,
|
|
705
|
+
lastSeenAt: raw.lastSeenAt,
|
|
706
|
+
agentVersion: typeof raw.agentVersion === 'string' ? raw.agentVersion : null,
|
|
707
|
+
source: typeof raw.source === 'string' ? raw.source : null,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Flip `config.engine.constellationBridge.enabled` atomically via
|
|
713
|
+
* mutateJsonFileLocked. Returns `{ previous: bool, current: bool }`.
|
|
714
|
+
* `configPath` override exists for unit tests.
|
|
715
|
+
*/
|
|
716
|
+
function setBridgeEnabled(enabled, configPath = CONFIG_PATH) {
|
|
717
|
+
const next = enabled === true;
|
|
718
|
+
let previous = false;
|
|
719
|
+
mutateJsonFileLocked(configPath, (cfg) => {
|
|
720
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return cfg;
|
|
721
|
+
cfg.engine = cfg.engine || {};
|
|
722
|
+
cfg.engine.constellationBridge = cfg.engine.constellationBridge || {};
|
|
723
|
+
previous = cfg.engine.constellationBridge.enabled === true;
|
|
724
|
+
cfg.engine.constellationBridge.enabled = next;
|
|
725
|
+
return cfg;
|
|
726
|
+
});
|
|
727
|
+
return { previous, current: next };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Human-readable relative age string (e.g. "12s ago", "3m ago", "2h ago").
|
|
732
|
+
* Caps at "1d+ ago" — anything older than the bridge polling cadence is
|
|
733
|
+
* already actionable as "stale".
|
|
734
|
+
*/
|
|
735
|
+
function formatRelativeAge(isoTimestamp, nowMs = Date.now()) {
|
|
736
|
+
const t = Date.parse(isoTimestamp);
|
|
737
|
+
if (!Number.isFinite(t)) return '(unknown)';
|
|
738
|
+
const deltaSec = Math.max(0, Math.round((nowMs - t) / 1000));
|
|
739
|
+
if (deltaSec < 60) return `${deltaSec}s ago`;
|
|
740
|
+
if (deltaSec < 3600) return `${Math.round(deltaSec / 60)}m ago`;
|
|
741
|
+
if (deltaSec < 86400) return `${Math.round(deltaSec / 3600)}h ago`;
|
|
742
|
+
return '1d+ ago';
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Compose the small stable projection the Constellation bridge consumes
|
|
747
|
+
* from `/api/status`. Kept narrow on purpose: full /api/status is large,
|
|
748
|
+
* unstable, and may surface unrelated local state. New fields go behind
|
|
749
|
+
* a deliberate schema version bump.
|
|
750
|
+
*/
|
|
751
|
+
function projectStatusForBridge(statusJson) {
|
|
752
|
+
if (!statusJson || typeof statusJson !== 'object') return null;
|
|
753
|
+
const dispatch = statusJson.dispatch || {};
|
|
754
|
+
const queueCount = (arr) => (Array.isArray(arr) ? arr.length : 0);
|
|
755
|
+
return {
|
|
756
|
+
engineState: statusJson.control?.state ?? null,
|
|
757
|
+
enginePid: statusJson.control?.pid ?? null,
|
|
758
|
+
minionsVersion: statusJson.version ?? null,
|
|
759
|
+
agentCount: Array.isArray(statusJson.agents) ? statusJson.agents.length : null,
|
|
760
|
+
activeAgentCount: Array.isArray(statusJson.agents)
|
|
761
|
+
? statusJson.agents.filter(a => a && a.status && a.status !== 'idle').length
|
|
762
|
+
: null,
|
|
763
|
+
dispatchPending: queueCount(dispatch.pending),
|
|
764
|
+
dispatchActive: queueCount(dispatch.active),
|
|
765
|
+
dispatchCompleted: queueCount(dispatch.completed),
|
|
766
|
+
projectCount: Array.isArray(statusJson.projects) ? statusJson.projects.length : null,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
module.exports = {
|
|
771
|
+
isBridgeEnabled,
|
|
772
|
+
readBridgeMarker,
|
|
773
|
+
setBridgeEnabled,
|
|
774
|
+
formatRelativeAge,
|
|
775
|
+
buildBridgeSnapshot,
|
|
776
|
+
projectSnapshotHealthForBridge,
|
|
777
|
+
projectStatusForBridge,
|
|
778
|
+
resolveSnapshotKinds,
|
|
779
|
+
BRIDGE_SNAPSHOT_PROTOCOL,
|
|
780
|
+
BRIDGE_SNAPSHOT_PROTOCOL_VERSION,
|
|
781
|
+
EXTENDED_SNAPSHOT_KINDS,
|
|
782
|
+
SNAPSHOT_PROJECTED_FIELDS,
|
|
783
|
+
SNAPSHOT_MAX_ENTITIES_PER_KIND,
|
|
784
|
+
SNAPSHOT_SINGLETON_MAX_ENTITIES,
|
|
785
|
+
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
786
|
+
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
787
|
+
};
|