diff --git a/apps/streamdeck/README.md b/apps/streamdeck/README.md index 5fcfbec46..b9cf0481c 100644 --- a/apps/streamdeck/README.md +++ b/apps/streamdeck/README.md @@ -10,8 +10,71 @@ and runs it. The plugin holds no Tower state and never edits files directly. - **Auth**: reads `~/.agent-farm/local-key` and sends it as the `codev-web-key` header (never generates it — Tower owns the key). -Architecture and design decisions live in the pre-migration repo's `PLAN.md` -(see [History](#history)). +The **[Design](#design)** section explains why the plugin is shaped this way; the +[History](#history) section records where it came from. + +## Design + +The rest of this README is the *what*; this section is the *why*. These decisions +shape everything above. + +**A stateless controller, not a second client.** The plugin holds no Tower state +and edits no files. It is a remote: it reads the overview Tower already computes +and POSTs verbs Tower already routes. Everything authoritative lives elsewhere. +Tower owns workspace, builder, and gate state plus the auth key; VSCode owns the +working tree, the review queue, and the composer. The deck only projects that +state onto keys and dials and fires intents back. The payoff is that the device +is disposable: unplug it, restart it, or run two of them, and nothing is lost or +forked, because there was never a second copy of the truth to reconcile. It also +means every surface (deck, VSCode sidebar, web dashboard) reflects one state, so a +change made on any of them shows up on all of them. + +**Canonical verbs over a command relay, not direct editor control.** The plugin +never speaks to VSCode directly. It POSTs a small, fixed vocabulary of canonical +verbs to Tower's command relay (`/api/command`), and the focused VSCode window +maps each verb to a `codev.*` command. The decoupling is deliberate: the deck does +not need to know how many editors are open, which one is focused, or how a command +is implemented. Tower routes the verb to the focused window, and each verb is +stamped with the active workspace, so one Tower serving several workspaces sends +each command to the right place. New editor behavior is a new `codev.*` handler, +and the deck's verb set barely moves. + +**One shared selection, because the board is a single instrument.** Row 1 selects a +builder, Row 2 acts on it, and the dials review it, with all three pointed at the +same builder. That coherence is the design, not a coincidence: a Row 1 press is +select-and-open in one gesture, and focusing a builder's diff or canvas in VSCode +moves the deck's selection back to it (via the `builder-active` activity hook). +Without one binding selection the three zones would drift and every press would +carry a "which builder?" ambiguity. With it, the board reads as one instrument +aimed at one target. + +**Keys commit, dials review, mapped to the hardware.** The interaction model splits +along the SD+'s two physical controls. A key is a discrete, labelled surface, so +keys carry commits: select, approve, run, flush. An encoder is a continuous cursor, +so dials carry review, the one task that is inherently "scan an ordered list and +pick a target." Reviewing a diff walks files, then hunks, then changes; reviewing a +spec walks headings, then blocks. That is a rotate-to-cursor, push-to-act motion, +exactly what a dial is for and what a grid of keys is not. So the dials are +phase-aware review cursors and the keys are the commit buttons: "dials collect, +keys commit." + +**The canvas owns composer state; the deck stays mode-neutral.** A dial press does +not decide what happens to the feedback it submits. It relays a mode-neutral verb +(`feedback-file`, `feedback-hunk`, `feedback-selection`); VSCode forwards it to the +builder now or queues it, per the `codev.diffCodelensMode` workspace setting, and +**Send Feedback** flushes a queue. All of that (the queue, the delivery mode, the +composed text) lives in VSCode, the surface that actually renders the artifact. The +deck renders no artifact content and holds no composer state, so it cannot fall out +of sync with what you are reading; the touch strip only names the live mode so a +press is never a surprise. Keeping composer state where the canvas is, and off the +device, is what lets the deck stay a stateless remote. + +**Gate approval is never silent, by design.** The deck can surface a gate's approval +modal in VSCode, but it can never approve on the device. Approving a spec, plan, or +PR is a human decision with consequences, so it always lands in front of you in the +editor, where the artifact is, rather than behind a one-touch key on a desk +peripheral. Silent one-touch approval is deliberately out of scope for that reason, +not for want of a spare key. ## Hardware @@ -260,17 +323,18 @@ Functional — build/type/unit-verified and validated end-to-end on physical hardware (Stream Deck +, live Tower; versioned in lockstep with the codev workspace since 3.3.0). The dial touch strips render title + a live value via `setFeedback`; a richer SVG/icon render layer (badges, colour by state) is still -out of scope. Also deliberately out of scope (see the pre-migration `PLAN.md`): -silent one-touch gate approval. (Editor scrolling, originally out of scope there, -was since implemented as the Scroll dial.) The `/api/command` route inherits +out of scope. Silent one-touch gate approval is also deliberately out of scope +(see [Design](#design) for the reason). (Editor scrolling, once out of scope, was +since implemented as the Scroll dial.) The `/api/command` route inherits Tower's current auth posture; a Tower-auth follow-up is tracked separately. ## History -This plugin was imported into the monorepo (issue #1347) from -[`cluesmith/codev-integrations`](https://github.com/cluesmith/codev-integrations) -at commit `77be3d0` (`packages/streamdeck`), as part of the #1189 SDK -consolidation: its `@cluesmith/codev-client` dependency was absorbed into -`@cluesmith/codev-sdk`, and the plugin became the sdk's first -outside-the-original-trio consumer. Pre-migration history (including the -original `PLAN.md` design document) lives in that repo. +This plugin was imported into the monorepo under issue #1347 from the +pre-migration repository at commit `77be3d0` (`packages/streamdeck`), as part of +the #1189 SDK consolidation: its `@cluesmith/codev-client` dependency was absorbed +into `@cluesmith/codev-sdk`, and the plugin became the sdk's first +outside-the-original-trio consumer. The design rationale that once lived only in +that repository's planning document now lives in-tree, under [Design](#design) +(issue #1390); the pre-migration repository, which holds the earlier development +history, is slated for retirement after the sdk's first npm publish. diff --git a/apps/streamdeck/src/__tests__/readme-design.test.ts b/apps/streamdeck/src/__tests__/readme-design.test.ts new file mode 100644 index 000000000..ead2a8e23 --- /dev/null +++ b/apps/streamdeck/src/__tests__/readme-design.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * The plugin's design rationale must live in-tree, not depend on the + * pre-migration repository's planning document (issue #1390). That repository is + * slated for retirement after the sdk's first npm publish, so a README that + * defers "why the plugin is shaped this way" to a `PLAN.md` over there is a live + * documentation dependency on a soon-archived source. These pins fail loudly if + * the Design section is dropped or the external design-doc pointer creeps back. + */ +describe('streamdeck README carries its own design rationale', () => { + const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); + const readme = readFileSync(join(root, 'README.md'), 'utf-8'); + + it('has an in-tree Design section', () => { + expect(readme).toMatch(/^## Design$/m); + }); + + it('does not defer design to the pre-migration planning document', () => { + expect(readme).not.toContain('PLAN.md'); + }); + + it('does not name the pre-migration repository in committed prose', () => { + expect(readme).not.toContain('codev-integrations'); + }); + + it('keeps provenance in the History section', () => { + expect(readme).toMatch(/^## History$/m); + expect(readme).toContain('pre-migration repository'); + }); +}); diff --git a/codev/projects/1390-fold-the-stream-deck-plugin-s-/status.yaml b/codev/projects/1390-fold-the-stream-deck-plugin-s-/status.yaml new file mode 100644 index 000000000..7b3e8d492 --- /dev/null +++ b/codev/projects/1390-fold-the-stream-deck-plugin-s-/status.yaml @@ -0,0 +1,22 @@ +id: '1390' +title: fold-the-stream-deck-plugin-s- +protocol: air +phase: pr +plan_phases: [] +current_plan_phase: null +gates: + pr: + status: approved + requested_at: '2026-08-14T00:52:20.798Z' + approved_at: '2026-08-15T02:16:58.623Z' +iteration: 1 +build_complete: false +history: [] +started_at: '2026-08-14T00:27:47.743Z' +updated_at: '2026-08-15T02:16:58.623Z' +pr_history: + - phase: pr + pr_number: 1452 + branch: builder/air-1390 + created_at: '2026-08-14T00:52:10.780Z' +pr_ready_for_human: false diff --git a/codev/state/air-1390_thread.md b/codev/state/air-1390_thread.md new file mode 100644 index 000000000..1c782367a --- /dev/null +++ b/codev/state/air-1390_thread.md @@ -0,0 +1,41 @@ +# air-1390 — Fold the Stream Deck plugin's design rationale in-tree + +**Protocol:** AIR (strict). **Issue:** #1390. **Scope:** docs + a doc-invariant test. + +## What the issue asked + +`apps/streamdeck/README.md` deferred "architecture and design decisions" to the +pre-migration repository's `PLAN.md`, a live dependency on a repository whose +retirement is a tracked follow-up (gated on the sdk's first npm publish). Move the +still-relevant rationale in-tree; keep provenance in the History section only. + +## Decisions + +- **Home = README Design section** (architect's steer; issue left README-vs-arch.md + to me). Co-located with the code, free of the COLD arch.md hot/cold tier discipline. +- **Reconstructed the rationale from what's in-tree** (the README, `store.ts` / + `plugin.ts`, `codev/plans/1347-*`, `codev/reviews/1347-*`, arch.md's Spec 1410 + coherence section), not the external `PLAN.md` — did not assume I could read the + pre-migration repo, and the architect confirmed I likely can't. Wrote the *why* + behind the system as it actually ships: stateless controller, canonical-verb relay, + one shared selection, keys-commit/dials-review hardware mapping, canvas-owns-composer + state, and why silent gate approval is out of scope. +- **Naming:** scrubbed the external repo name/URL (`cluesmith/codev-integrations`, + github.com link) and every `PLAN.md` mention from committed prose, per the standing + "describe in codev's own terms" rule and the architect's naming instruction. + Provenance survives in History described as "the pre-migration repository" (commit + `77be3d0`, `packages/streamdeck`, #1347/#1189 kept as codev's own reference terms). + +## Changes + +- `apps/streamdeck/README.md`: new `## Design` section (the *why*); intro pointer now + points at Design + History; Status/roadmap and History rewritten to drop the three + `PLAN.md` references and the external repo name. +- `apps/streamdeck/src/__tests__/readme-design.test.ts` (new): pins the Design section, + no `PLAN.md` deferral, no external repo name, provenance retained in History. Mirrors + the existing `vendored-ui-lib.test.ts` doc-invariant pattern. + +## Verification + +`pnpm --filter @cluesmith/codev-sdk build && ...streamdeck check-types && ...test`: +check-types clean; 166/166 tests pass across 9 files (4 new).