# plans-dual-plane Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Runplan-v2 runs appear in the overdeck `/plans` panel by replacing the collector's boot-time harness connection latch with generation-tagged per-poll pointer-file resolution, deployed live.

**Architecture:** Single-file rework of `collector/src/adapters/harness.ts` per the contract in `docs/specs/2026-07-29-plans-dual-plane-design.md` (source of truth — read it first, whole file). Public `HarnessAdapter` interface unchanged; no new files, no new config keys. Then land+deploy (service runs from a deploy clone, not the dev tree) and prove the live path.

**Tech Stack:** TypeScript (bun/vitest workspace `collector/`), existing fetchImpl-stub test patterns.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 | collector/src/adapters/harness.ts, collector/src/adapters/harness.test.ts | single task |
| 2 | Task 2 | none (delivery + live verification) | single task — NOT an engine task: the v2 engine lands (merge→main→push) only at plan end, so Task 2 runs AFTER the engine exits, executed by the orchestrating session |

## File Structure

- Modify: `collector/src/adapters/harness.ts` — connection lifecycle only (`resolveConnection` region ~506-514, `request`/`stream` helpers ~556-582, `poll` ~766-838). Validation, redaction, panel/item construction untouched.
- Modify: `collector/src/adapters/harness.test.ts` — new lifecycle tests; existing tests stay green unmodified (except none should assert the boot-time latch — if one does, update it to the new contract and say so in the commit).

### Task 1: Connection freshness in harness adapter

**Wave:** 1
**Blocks:** Task 2
**Blocked by:** —

**Files:**
- Modify: `collector/src/adapters/harness.ts` — replace boot-time connection latch with generation-tagged resolution
- Test: `collector/src/adapters/harness.test.ts`

**Contract (spec §Design is normative — re-read it; summary):**
- Cache shape: `{ connection: HarnessConnection, gen: number }`; `gen` increments per fresh disk read of `<harnessHome>/control-api.port` + `<harnessHome>/token`.
- `poll()` drops cache at cycle start, resolves ONCE, and passes that cycle-local connection explicitly to every request in the cycle (internal `requestVia(conn, path, init?, validate?)` shape; `streamVia` likewise). Per-run RPC methods resolve per call.
- Generation-checked invalidation: failed request clears cache only if its `gen` matches the currently held one.
- Invalidating failures: fetch rejection (network/timeout), response-body read failure, `Response.status === 401` classified BEFORE body parse. Non-invalidating: any other HTTP status; JSON-parse failure of an `ok` response.
- SSE: handshake network/401 invalidates; established stream never migrates.
- Fixed-endpoint mode (constructor `baseUrl`+`token` both set): freshness logic fully disabled, pointer files never read. `buildAdapters` baseUrl-without-token keeps falling through to disk discovery — unchanged.

**Behavior:** spec §Design + §Error handling. Fail-closed poll semantics, snapshot fallback, redaction byte-identical.

**Acceptance (one executable check):**
- Run: `cd collector && bun run test -- src/adapters/harness.test.ts`
- Expected: PASS, including the 9 new lifecycle cases from spec §Testing (port rewrite between polls; network error + timeout invalidate; 401-with-non-JSON-body invalidates; 404/409/500 and ok-but-malformed-JSON do not; stale-generation failure does not evict newer connection; mid-poll pointer rewrite does not split the cycle across servers; SSE handshake vs established-stream; token rotation; fixed-endpoint never touches disk even after failures; snapshot-fallback poll then healthy poll).

- [ ] Write tests covering the behavior above (tmpdir `harnessHome` fixtures for discovery tests, NOT the fixed-endpoint helper)
- [ ] Run them, verify the lifecycle ones fail against current code
- [ ] Implement to satisfy the contract
- [ ] Full suite green: `cd collector && bun run test`
- [ ] Commit: `git add collector/src/adapters/harness.ts collector/src/adapters/harness.test.ts && git commit -m "collector: re-resolve harness control-api per poll, invalidate on network/401"`

### Task 2: Deliver and prove live

**Wave:** 2
**Blocks:** —
**Blocked by:** Task 1

**Files:** none created/modified — delivery mechanics + live verification only.

**Contract:** The collector service runs from `~/.local/share/overdeck/deploy` (origin/main clone), so Task 1 landed on `main` is NOT yet live. Delivery = Task 1 present on pushed `origin/main` + `packaging/deploy-local.sh` completed (idempotent, fail-closed; restarts `overdeck-collector.service` et al). If the run's own land phase already landed+pushed and ran postland deploy, verify instead of re-running; `deploy-local.sh` is safe to re-run when in doubt.

**Behavior:**
- Confirm Task 1's commit is contained in `origin/main` (`git branch -r --contains <sha>`); if the engine's land phase hasn't pushed, land via the project's frozen `bash .claude/scripts/ship.sh` flow — never raw force-push, never `--no-verify`.
- Run `bash packaging/deploy-local.sh` from the repo root (or confirm postland already ran it: service restart timestamp `systemctl --user show overdeck-collector.service -p ActiveEnterTimestamp` newer than the push).
- Execute the live acceptance from spec §Delivery + acceptance VERBATIM (concrete-runId check with both bearer tokens). Wait up to 3 poll intervals (~15s) after restart before declaring failure.

**Acceptance (one executable check):**
- Run: the two-command acceptance block from spec §Delivery + acceptance (reads `~/.harness/control-api.port` + `~/.harness/token` for the v2 runId, then asserts that runId with `owner=="v2"` in `http://127.0.0.1:31338/state` plans panel using `~/.config/overdeck/token`).
- Expected: `true`

- [ ] Verify Task 1 on `origin/main`; land via ship.sh if not
- [ ] Deploy (or verify postland deploy) via `packaging/deploy-local.sh`
- [ ] Run live acceptance, expect `true`
- [ ] No commit (nothing changed in-tree)

## Self-Review

- Spec coverage: Design/Error-handling → Task 1; Delivery+acceptance → Task 2; Non-goal (no union) has no task by design; Out-of-scope items have no tasks. ✓
- No TBDs; edge cases named via the 9 enumerated test cases; acceptance commands executable. ✓
- Seam consistency: `requestVia`/`streamVia` internal names used consistently; no cross-task symbol drift (single code task). ✓
- Waves: Task 2 semantically depends on Task 1 (deploys its commit) → separate waves; no file overlap anywhere. ✓
- Decision-enumeration: no gated records — no irreversible ops (deploy is the project's standing idempotent script; service restarts reversible), no forks, no missing inputs. Publication mechanics (land/push) resolved by project policy (ship.sh), not user gates. ✓
