|
| 1 | +# ECC control-plane live view: `ecc.control-plane.view.v1` |
| 2 | + |
| 3 | +Status: shipped with the control pane (`scripts/lib/control-pane/control-plane-view.js`). Read-only. Advisory only. |
| 4 | + |
| 5 | +The view joins three things the repo already computes separately and serves them as one JSON document shaped as tasks, lanes and events, so another control plane (the Ito ops board, a Hermes or Codex reader, a hook) can consume it without knowing ECC internals. |
| 6 | + |
| 7 | +| Input | Where it comes from | |
| 8 | +|---|---| |
| 9 | +| Sessions | `scripts/lib/control-pane/state.js`, the ECC2 `sessions` table | |
| 10 | +| Pairwise proximity | `scripts/lib/agent-proximity/` (noisy-OR over `x_tree`, `x_overlap`, `x_dep`) via `scripts/lib/control-pane/proximity.js` | |
| 11 | +| 2D projection | `scripts/lib/agent-proximity/projection.js` (rolling z-score, tails clipped at 2.5 / 97.5, PCA) | |
| 12 | +| Coordination inventory | `scripts/lib/coordination-inventory.js` (PR #3028): declared tasks and sessions, heartbeat freshness, lease conflicts | |
| 13 | + |
| 14 | +## Endpoints |
| 15 | + |
| 16 | +Served by `node scripts/control-pane.js` (loopback only, same Host and Origin gate as the rest of the pane): |
| 17 | + |
| 18 | +| Route | Returns | |
| 19 | +|---|---| |
| 20 | +| `GET /control-plane` | Self-contained HTML page: 2D projection canvas, lanes and tasks, event feed. No external scripts. | |
| 21 | +| `GET /api/control-plane` | The full view document below. | |
| 22 | +| `GET /api/control-plane/events` | `{ schemaVersion, generatedAt, thresholds, events, counts }` only, for hooks and pollers. | |
| 23 | + |
| 24 | +The server keeps one projection window per process. Both API routes share a snapshot cached for five seconds, and concurrent refresh requests are coalesced. Reads within that interval do not add samples. After expiry, the next read refreshes the snapshot once; idle intervals do not generate synthetic samples. Failed refreshes return errors rather than healthy empty data. The page rejects failed HTTP responses and invalid view envelopes and shows `offline`. Options on `createControlPaneServer`: `projection` (`windowSize`, `clipPercentiles`), `viewOptions` (`thresholds`, `manifest`, `channelWeights`, `minWindowForZscore`), `proximityOptions` (passed to the scan). |
| 25 | + |
| 26 | +## Document |
| 27 | + |
| 28 | +```json |
| 29 | +{ |
| 30 | + "schemaVersion": "ecc.control-plane.view.v1", |
| 31 | + "generatedAt": "2026-09-11T20:01:00.000Z", |
| 32 | + "source": { "snapshotSchema": "ecc.control-pane.snapshot.v1", "repoRoot": "...", "dbPath": "..." }, |
| 33 | + "thresholds": { "ta": 0.35, "ra": 0.7, "source": "static" }, |
| 34 | + "lanes": [ { "id": "harness:codex", "label": "codex", "kind": "harness", "taskIds": ["session-a"] } ], |
| 35 | + "tasks": [ { "...": "see Task" } ], |
| 36 | + "pairs": [ { "...": "see Pair" } ], |
| 37 | + "events": [ { "...": "see Event" } ], |
| 38 | + "projection": { "...": "see Projection" }, |
| 39 | + "inventory": { "...": "see Inventory" }, |
| 40 | + "counts": { "lanes": 1, "tasks": 1, "agents": 1, "pairs": 0, "events": 0, "advisories": 0, "resolutions": 0 }, |
| 41 | + "limits": [ "..." ] |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +### Task |
| 46 | + |
| 47 | +One task per session. A session with no changed files is still a task; it has no projection point and no pairs. |
| 48 | + |
| 49 | +| Field | Meaning | |
| 50 | +|---|---| |
| 51 | +| `id` | Session id, unchanged. | |
| 52 | +| `lane` | Lane id this task belongs to. | |
| 53 | +| `label` | Session task text, or the id. | |
| 54 | +| `harness`, `agentType`, `state`, `pid` | From the session row. | |
| 55 | +| `worktree` | `{ path, branch, base }` or `null`. | |
| 56 | +| `heartbeatAt`, `updatedAt` | ISO timestamps or `null`. | |
| 57 | +| `workingSet` | `{ fileCount, files }`: the worktree diff against its base. | |
| 58 | +| `projection` | `{ point, pairs, maxRisk }` where `point` is `[x, y]` or `null`. `point` is the risk-weighted centroid of the task's pair points in PCA space. | |
| 59 | +| `inventory` | `{ id, heartbeat, process, authority: "declared-only" }`. `id` is the sanitized identifier used in the inventory manifest; `heartbeat` and `process` are the #3028 observations. | |
| 60 | + |
| 61 | +### Lane |
| 62 | + |
| 63 | +A grouping of tasks. Precedence: `task-group` (session `task_group`), then `project`, then `harness`. Ids are prefixed (`group:`, `project:`, `harness:`) so a consumer can tell the kinds apart without reading `kind`. |
| 64 | + |
| 65 | +### Pair |
| 66 | + |
| 67 | +One row per agent pair from the airspace scan (only sessions with edits participate). |
| 68 | + |
| 69 | +| Field | Meaning | |
| 70 | +|---|---| |
| 71 | +| `a`, `b` | Session ids. | |
| 72 | +| `risk`, `level` | Noisy-OR risk and the scan's level (`clear`, `advisory`, `resolution`) at the scan's thresholds. | |
| 73 | +| `channels` | Raw `{ x_tree, x_overlap, x_dep }` in [0, 1]. | |
| 74 | +| `normalized` | The same after z-score, clip and map-back, or equal to `channels` while the window is cold. | |
| 75 | +| `point` | `[pc1, pc2]` PCA scores. | |
| 76 | + |
| 77 | +### Event |
| 78 | + |
| 79 | +Something an operator or a hook may act on. Ids are deterministic across polls so a consumer can dedupe. |
| 80 | + |
| 81 | +```json |
| 82 | +{ |
| 83 | + "id": "proximity.advisory:session-a|session-b:resolution", |
| 84 | + "kind": "proximity.advisory", |
| 85 | + "level": "resolution", |
| 86 | + "severity": "critical", |
| 87 | + "at": "2026-09-11T20:01:00.000Z", |
| 88 | + "subject": { "a": "session-a", "b": "session-b", "aLabel": "...", "bLabel": "..." }, |
| 89 | + "risk": 1, |
| 90 | + "distance": 0, |
| 91 | + "channels": { "x_tree": 1, "x_overlap": 1, "x_dep": 0 }, |
| 92 | + "threshold": { "ta": 0.35, "ra": 0.7, "crossed": "ra", "source": "static" }, |
| 93 | + "action": { "type": "steer", "steer": "session-b", "hold": "session-a" }, |
| 94 | + "message": "Resolution advisory: session-b steers, session-a holds (risk 100%, static threshold 0.7)." |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +| Kind | Levels | Action types | Source | |
| 99 | +|---|---|---|---| |
| 100 | +| `proximity.advisory` | `traffic` (risk at or above `ta`), `resolution` (at or above `ra`) | `transmit` (both agents share intent), `steer` (`steer` moves, `hold` keeps course) | Every pair link, evaluated against the view's thresholds. Right-of-way: more progress, then earlier start, then stable id. | |
| 101 | +| `inventory.lease-conflict` | `conflict` | `review` | #3028 `leaseConflicts`. Declared-only, never a lock. | |
| 102 | + |
| 103 | +Thresholds are static per view (`source: "static"`). A learned threshold, closure-rate escalation, and the `pause` and `wait` maneuvers are slice (b), see `TCAS-HOOK.md`. |
| 104 | + |
| 105 | +### Projection |
| 106 | + |
| 107 | +```json |
| 108 | +{ |
| 109 | + "method": "pca", |
| 110 | + "channels": ["x_tree", "x_overlap", "x_dep"], |
| 111 | + "weights": { "x_tree": 0.25, "x_overlap": 1, "x_dep": 0.9 }, |
| 112 | + "normalization": "zscore-clipped", |
| 113 | + "window": { "samples": 12, "percentiles": [2.5, 97.5], "channels": [ { "channel": "x_tree", "mean": 0.39, "stddev": 0.42, "clipLow": -0.92, "clipHigh": 1.45 } ] }, |
| 114 | + "pca": { "loadings": [ { "x_tree": 0.12, "x_overlap": 0.87, "x_dep": -0.47 }, { "...": "..." } ], "explainedVariance": [0.6, 0.39] }, |
| 115 | + "agents": [ { "agentId": "session-a", "point": [0.18, 0.41], "pairs": 3, "maxRisk": 1 } ] |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +Pipeline per poll: every pair's channel vector is pushed into a rolling window (default 512 samples). Once the window holds at least 8 samples, each channel is z-scored against the window, clipped to the window's 2.5th and 97.5th percentile (in z units), mapped back to [0, 1], multiplied by the static channel weight, and the weighted matrix goes through PCA (Jacobi on the 3x3 covariance). Below 8 samples the raw channel values are used and `normalization` says `raw`. A channel with zero variance maps to 0.5. Degenerate inputs (fewer than two pairs, zero total variance) give zero scores, never NaN. |
| 120 | + |
| 121 | +The projection is a display. It never changes `risk`, the advisory level, or right-of-way. |
| 122 | + |
| 123 | +### Inventory |
| 124 | + |
| 125 | +The #3028 report with the per-task rows folded into `tasks[].inventory`. Kept at the top level: `status` (`ok` or `unavailable` with `reason`), `truncated` (more than 64 sessions), `observedAt`, `mode: "read-only"`, `activity`, `leaseConflicts`, `warnings`, `coverage`, `limits`. The manifest is built from the live sessions (ids sanitized to the inventory alphabet, paths from the working set, heartbeat from the session row, declared session status `open` for running/pending/idle, `closed` for completed/failed/stopped). An external manifest (`viewOptions.manifest`) can add `goals`, `leases`, `repositories` and extra `tasks`; the inventory then reports lease conflicts and goal activity for them. |
| 126 | + |
| 127 | +## Reuse in the Ito ops control plane |
| 128 | + |
| 129 | +The shape to copy is `task`, `lane`, `event`: |
| 130 | + |
| 131 | +- a **task** has an `id`, a `lane`, a `state`, an optional position, and an observation block whose `authority` says how much to trust it; |
| 132 | +- a **lane** is a named group with ordered `taskIds`; |
| 133 | +- an **event** has a stable `id`, a `kind`, a `level`, a `severity`, an `at`, a `subject`, an `action` with a `type`, and a human `message`. |
| 134 | + |
| 135 | +Nothing in the shape is ECC-specific except the event kinds. An ops board that renders lanes of tasks and a feed of events can render this document as-is, and can emit its own kinds (`deal.stalled`, `bridge.down`) into the same feed. |
| 136 | + |
| 137 | +## What this does not do |
| 138 | + |
| 139 | +- No leases are acquired, no agent is paused or steered. Consumers act; the view reports. |
| 140 | +- No conflict-reduction percentage is claimed. The 85 percent goal in the push plan is measured two weeks before and after slice (b), not here. |
| 141 | +- No semantic, call-graph or frequency channel yet (slice (g)). PCA picks new channels up automatically when they land in the scan. |
0 commit comments