commit b8e9d75ef86fa6eec76ba731b31c63f991a5311a
parent aab89d96a00fc5b4799bbab321b67eed9870935f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 28 Aug 2026 12:36:09 -0400
plans: slice 7 planned
One pause: a gate has one definition, one action pair, one control on
every surface that draws a lane, including the runner pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat:
1 file changed, 377 insertions(+), 0 deletions(-)
diff --git a/plans/editor-ia-slice-7.md b/plans/editor-ia-slice-7.md
@@ -0,0 +1,377 @@
+# Editor IA slice 7 — one pause
+
+## Context
+
+**Verified read-only against `494cbee` (clean) on 2026-08-28.** Slice 7 is the IA doc's bullet
+(`plans/editor-operations-ia.md:157-161`): `Pause{Downloads,Transcriptions,Backfill}Button` →
+one button, "the six actions" → one pair, the polarity normalization in `laneState.ts` goes,
+**keep the aria labels**. Its reconciliation (`:87-91`) fixes the definition: *"pause operation
+X" = "pause the root of the tree that dispatches X"*, one action mapping onto the **four legacy
+fields**, **no new settings block** — unified-ops step 6 (slice 9) migrates storage later.
+A pause is a hold, never a stop (`unified-operations-model.md:137`): every gate returns
+`limit() === 0` and `runPool` idle-waits.
+
+**Decided (operator, 2026-08-28):** (a) the button and the action are keyed **by lane** — the
+gate is per lane, three speaker operations share one, and "a per-operation page is not a
+per-operation switch" (`STATE.md:401`); `pauseLaneFor(operationId)` is how an operation page
+finds its lane. (b) The single definition reaches the **dispatch holds too**, not just the
+readers. (c) The runner pages get a pause beside Start/Drain/Stop.
+
+**What the census found (corrects the IA bullet in four places):**
+
+1. **Four gates, three polarities, eight actions in two files.** `transcriptionsPaused`
+ (`settings.ts:152`; plus the LIVE `WorkerPool.pauseAll/resumeAll/isPaused`,
+ `workerPool.ts:283-316`, re-applied at boot by `editor/instrumentation.ts:56-73`),
+ `downloadsPaused` (`:157`), `digest.digestsPaused` (`:474`), `backfill.enabled`
+ (`:289-292`, **inverted**, shared by diarization / attribution-text / attribution-diarized,
+ and deliberately also written by the `LaneSettingsForm` checkbox,
+ `operations/settingsActions.ts:214` — keep that; "two places to set it, and they cannot
+ drift", `jobs/actions.ts:331-342`). Actions: `jobs/actions.ts:150-178` (downloads),
+ `:180-215` (digests), `:329-379` (backfill); `workers/actions.ts:9-23,60-83`
+ (`pauseAllWorkersAction`/`resumeAllWorkersAction`: pool + best-effort persist).
+2. **Three UI copies, one dead.** `PauseTranscriptionsButton.tsx` (2 sites:
+ `jobs/active/page.tsx:37`, `workers/components/WorkersView.tsx:125-129`);
+ `PauseDownloadsButton.tsx` (**zero** importers); four inline `LaneControl` descriptors in
+ `LaneDeck.tsx` (`:126-148,216-238,283-309,437-459`, rendered by `LaneCard` through
+ `LaneActionButton`, the shared primitive); and `SweepLane.tsx:187-211` on ui `Button` with a
+ *composed* aria-label (`"Pause Digest"`, `"Pause Speakers"`) and the **opposite emphasis**
+ (warning tint when NOT held; `LaneActionButton`'s `active` variant is warning-filled when
+ held — "a held lane is visible from across the room"). `CommandPalette.tsx:110-131` calls
+ the workers pair.
+3. **The inversion lives in three readers, and the widget ships it raw.** `LaneDeck.tsx:471`
+ (`gateHeld: !laneEnabled`), `operations/lanes.ts:334-336`, `jobs/active/buildActiveJobs.ts:337`;
+ `api/widget/sync/route.ts:72,96,289,296` sends `digest.paused` and `backfill.enabled`.
+ Readers of those two wire fields: `LaneDeck.tsx:255,403,471,551` only — no spec, no unit
+ test, nothing in `MonitorWidget`/`WidgetControls` (precedent for a wire rename: slice 4's
+ `noDigest → digestReachable`).
+4. **Every existing label survives one canonical table.** Playwright's `getByRole(…, {name})`
+ is case-insensitive substring unless `exact`, and **no pause/resume lookup uses `exact`**
+ (grep verified). So `auto-queue.spec.ts:1186`'s `"Pause Digest"` (scoped to
+ `section[data-lane="digest"]`) matches the canonical `"pause digests"`, and
+ `backfill.spec.ts:809,820` / `widget.spec.ts:803,810` `"pause backfill"` are kept
+ byte-identical. `"Pause Speakers"`, `"Pause Derived"`, `"Resume Digest"`, `"Hold the lane"`
+ are asserted nowhere.
+5. **Transcription's `held` is the live pool on every UI surface, never the flag.**
+ `isGateHeld(settings, "transcription")` answers "will the pool be paused after a restart",
+ not "is it paused now"; the e2e harness rewrites `test-settings.json` wholesale between
+ tests while the pool keeps its `pausedSnapshot`. Surfaces (all live, unchanged):
+ `LaneDeck.tsx:67`, `WorkersView.tsx:119`, `jobs/active/page.tsx:19`, `MonitorWidget.tsx:229`,
+ `buildQueueView.ts:243`, `/api/pulse`, `/api/worker/health`. The flag is read in exactly
+ two places after this slice: the boot hook and the action's no-op check.
+6. **The runner pages have no pause at all** (`RunnerOperationView`/`LaneHeader.tsx:51-84`:
+ Start/Drain/Stop only; `AutoQueueKindStatus` `status.ts:33-53` carries no held field;
+ `railStates.ts:36` hard-codes `gateHeld: false` so the rail can never show a runner
+ holding). Worse, `/operations/transcription` says **"no enabled worker to run it"** while
+ paused, because `pauseAll` disables every worker and `autoRunner.ts:598` reports
+ `no-workers`.
+
+**Hazards carried through every commit:** the runner section's contract
+(`RunnerOperationView.tsx:21-35`) forbids nested `<section>` and reserves `role="status"` —
+`LaneActionButton` is a plain `<button>` plus a `role="alert"` span only on error, fine.
+`laneGuards.test.ts:22` casts a `{digest}`-only object to settings, so `isGateHeld` must
+switch on lane and touch only that lane's field. `pauseControl.tsx` is a client module: import
+`PauseLane` as a **type** only (a value import drags `operations.ts`/`settings.ts` and `fs`
+into the client graph). Resume must always be reachable: `disabled` applies to the pause side
+only (`PauseTranscriptionsButton.tsx:44-47` — and `SweepLane.tsx:191` gets this wrong today).
+
+## Step 0 — the plan on disk
+
+Write this file verbatim to `plans/editor-ia-slice-7.md`; commit alone: `plans: slice 7 planned`.
+
+## Order: five commits after the plan
+
+1. **`common: a pause gate has one definition`** — `pauseGates.ts` + test; the three dispatch
+ holds and the boot hook read it.
+2. **`editor: one action pair holds every lane, and the wire says held`** — the pair in
+ `operations/actions.ts`; eight actions deleted; payload builders and the widget payload
+ read `isGateHeld`; `LaneDeck`'s inversion goes.
+3. **`lanes: one pause control, drawn wherever a lane is`** — `pauseControl.tsx`
+ (`pauseLaneControl` + `PauseLaneButton`); LaneDeck, SweepLane, Workers, Active Jobs,
+ CommandPalette on it; two button files deleted; new e2e.
+4. **`operations: a runner page has its pause beside Start, Drain and Stop`** — `held` on
+ the runner status, the button in `LaneHeader`, the rail shows a runner holding, the
+ `workers-paused` idle reason.
+5. **`plans: slice 7 shipped, and the docs say so`** — CHANGELOG, IA doc, unified-ops doc,
+ STATE, FACTS, memory.
+
+Gates after each of 1–4: `pnpm -C <pkg> exec tsc --noEmit` for `common editor export homepage
+umtool mcp`; `pnpm -C common test` (861 → 861 + the new file's tests); `pnpm -C editor exec tsx
+--test "app/**/*.test.ts"` (90). e2e once after commit 5, detached (memory `e2e-run-detached`).
+
+---
+
+## Commit 1 — `common: a pause gate has one definition`
+
+**New `common/lib/pauseGates.ts`:**
+
+```ts
+export type PauseLane = "transcription" | "download" | "digest" | "backfill";
+// The union of the two id spaces that already exist: AutoQueueKind (the runners) and the
+// editor's SweepLaneId (the sweeps). Asserted here for the first; lanes.ts asserts the second.
+type _RunnerLanesArePauseLanes = AutoQueueKind extends PauseLane ? true : never; // import type from ../jobs/autoQueueState
+
+// Which lane's gate holds this operation. `runner` FIRST: transcode shares
+// TRANSCRIPTION_QUEUE and has no runner, and a queue-key map alone would hand it a
+// transcription pause. Then BACKFILL_QUEUE → backfill; the two digest queues → digest; else null.
+// Walks operationCatalog() (all seven ids), not OPERATION_BY_ID (the four registry entries).
+export function pauseLaneFor(operationId: string): PauseLane | null;
+
+// THE ONLY PLACE POLARITY IS KNOWN. backfill's field is `enabled`, so held = !enabled.
+// Touches only the named lane's field (laneGuards.test.ts casts a {digest}-only settings).
+export function isGateHeld(settings: SiteSettings, lane: PauseLane): boolean;
+
+// The only writer AMONG THE PAUSE CONTROLS (the LaneSettingsForm checkbox writes
+// backfill.enabled too, on purpose). Pure: returns a new settings object, no I/O.
+// Spreads ...settings.backfill / ...settings.digest so sweepEnabled etc. are untouched.
+export function withGateHeld(settings: SiteSettings, lane: PauseLane, held: boolean): SiteSettings;
+```
+
+Header comment states the transcription asymmetry (finding 5): the flag is what the boot
+hook re-applies; "is it paused now" is the pool, and no UI may read the flag for it.
+
+**`common/lib/pauseGates.test.ts`** (node:test, shape of `laneGuards.test.ts:1-12`): round
+trip `withGateHeld`→`isGateHeld` for all four lanes; backfill polarity pinned
+(`enabled: false` ⇒ held); `pauseLaneFor` over all seven catalog ids, **`transcode → null`**;
+`withGateHeld(…, "backfill", true)` leaves `sweepEnabled`, `sweepKinds`, `sweepChannels`,
+`allowRedownload` untouched; `isGateHeld({digest} as …, "digest")` works on a partial object.
+
+**The dispatch holds read it** (each a one-liner; behaviour identical):
+`common/controller/autoRunner.ts:640` (`settings.downloadsPaused` →
+`isGateHeld(settings, "download")`), `common/controller/backfillBatch.ts:734-736`
+(`!live.enabled` → `isGateHeld(liveSettings, "backfill")`),
+`common/controller/laneGuards.ts:129` (`digest.digestsPaused` → `isGateHeld(settings, "digest")`).
+`laneGuards.test.ts:25` still passes (the hold beats every other reason).
+
+---
+
+## Commit 2 — `editor: one action pair holds every lane, and the wire says held`
+
+**`editor/app/operations/actions.ts`** (already `"use server"`, beside `snoozeAutoQueueAction`):
+
+```ts
+async function setLaneHeld(lane: PauseLane, held: boolean): Promise<{ ok: boolean; error?: string }>
+// transcription: the LIVE hold first — getWorkerPool().pauseAll() / resumeAll() — then persist.
+// all lanes: const cur = getSettings(); const next = withGateHeld(cur, lane, held);
+// if (isGateHeld(cur, lane) !== held) await writeSettings(next) — inside try; a failed
+// write returns {ok:false, error} (for transcription AFTER the pool flipped: the button shows
+// the error inline and the label follows the pool — say so in a comment; today
+// persistTranscriptionsPaused swallowed it).
+// revalidatePath("/jobs"); revalidatePath("/workers"); revalidatePath("/operations/[id]", "page").
+// NOT "/": carry workers/actions.ts:60-66's note — "/" reads its pauses from polls
+// (DashboardCockpit.tsx:74-75) and revalidating it wipes the client router cache app-wide.
+export async function pauseLaneAction(lane: PauseLane)
+export async function resumeLaneAction(lane: PauseLane)
+```
+
+**Delete** `jobs/actions.ts:150-215, 329-379` (the three pairs and their `set*` helpers and
+result types; drop the now-unused `writeSettings` import, keep `getSettings` for `:278`);
+`workers/actions.ts:9-23, 60-83` (`persistTranscriptionsPaused` and the workers pair; drop the
+settings import). `common/lib/settings.ts:151` comment "See pauseAllWorkersAction" →
+"See pauseLaneAction".
+
+**Readers lose the inversion:** `operations/lanes.ts:308,336` → `isGateHeld(settings, "digest"
+| "backfill")` (delete the INVERTED comment); `jobs/active/buildActiveJobs.ts:322,337` same;
+`workers/buildWorkers.ts:53` → `isGateHeld(settings, "download")` (the field name
+`downloadsPaused` on the workers payload stays — `dashboard.spec.ts:87` documents that the
+workers poll re-reads it; not a polarity problem); `channels/[slug]/pipelineActions.ts:108`
+and `channels/groupActions.ts:116` → `isGateHeld(…, "download")` (messages unchanged —
+`dashboard.spec.ts:105` pins "Downloads are paused"); `editor/instrumentation.ts:65` →
+`isGateHeld(getSettings(), "transcription")`.
+
+**Widget sync payload** `api/widget/sync/route.ts`: `digest.paused` → `digest.held` (`:72,289`),
+`backfill.enabled` → `backfill.held` (`:96,296`), both via `isGateHeld`; type comments say
+"held — computed by isGateHeld; nothing downstream inverts anything". `LaneDeck.tsx:255,403`
+read `.held`; `:471` becomes `gateHeld: backfillHeld`; `:551` `backfillSweeping && backfillHeld`;
+the `:35-39` comment and `laneState.ts:16-19` rewritten: "every lane's gate reaches the wire
+as `held`, computed once by `isGateHeld`; no reader inverts". A stale pinned widget tab reads
+`undefined ?? false` until reload — harmless; CHANGELOG says so.
+
+**Callers of the deleted actions, rewired in this commit so it is green** (labels untouched
+here): `LaneDeck.tsx:12-22` imports and `:134,145,224,235,294,305,446,457` →
+`pauseLaneAction("…")`/`resumeLaneAction("…")`; `SweepLane.tsx:12-21,193-202` →
+`lane.gateHeld ? resumeLaneAction(lane.id) : pauseLaneAction(lane.id)`;
+`CommandPalette.tsx:114-131` → `pauseLaneAction("transcription")` /
+`resumeLaneAction("transcription")` (entry labels, icons, toasts unchanged);
+`PauseTranscriptionsButton.tsx:4-7,34,48` → the pair. **`git rm` `PauseDownloadsButton.tsx`**
+(zero importers; `:56` was its only reference). `operations/lanes.ts` gains
+`type _SweepLanesArePauseLanes = SweepLaneId extends PauseLane ? true : never`.
+
+---
+
+## Commit 3 — `lanes: one pause control, drawn wherever a lane is`
+
+**New `editor/app/components/lanes/pauseControl.tsx`** (`"use client"`; `import type
+{ PauseLane }`):
+
+```ts
+export function pauseLaneControl({ lane, held, onChange, disabled }: {
+ lane: PauseLane; held: boolean; onChange?: () => void | Promise<void>; disabled?: boolean;
+}): LaneControl
+export function PauseLaneButton(props: Parameters<typeof pauseLaneControl>[0] & { dense?: boolean })
+ // = <LaneActionButton {...pauseLaneControl(props)} dense={dense} /> (LaneCard supplies
+ // dense itself from useSectionFrame; the standalone button takes it as a prop)
+```
+
+The table — **byte-identical to LaneDeck today**, which is the aria contract:
+
+| lane | held | label | ariaLabel | title from |
+|---|---|---|---|---|
+| transcription | no | Pause Transcriptions | Pause Transcriptions | `LaneDeck.tsx:143` |
+| transcription | yes | Resume Transcriptions | Resume Transcriptions | — |
+| download | no | Pause Downloads | Pause Downloads | `:233` |
+| download | yes | Resume Downloads | Resume Downloads | — |
+| digest | no | Hold the lane | pause digests | `:303` |
+| digest | yes | Resume the lane | resume digests | `:292` |
+| backfill | no | Hold the lane | pause backfill | `:444` |
+| backfill | yes | Resume the lane | resume backfill | `:455` |
+
+glyph `❙❙` / `▶`; `variant: held ? "active" : "idle"`; `key: \`${lane}-gate\``;
+`action: () => held ? resumeLaneAction(lane) : pauseLaneAction(lane)`;
+**`disabled: held ? undefined : disabled`** — a Resume is always reachable (a paused pool with
+zero workers must be releasable; this also fixes `SweepLane.tsx:191`, which disables both
+sides on `!lane.available`). Header comment: the runner lanes say *Pause*, the sweep lanes say
+*Hold*, on purpose — CHANGELOG `:34`: the feed **sweeps**, the gate **holds**; the aria-labels
+are the contract `backfill.spec`, `widget.spec`, `dashboard.spec`, `auto-queue.spec`,
+`workers.spec`, `parakeet-partial.spec` select by, and Playwright's name match is
+case-insensitive substring, which is why `"Pause Digest"` finds `"pause digests"`.
+
+**Sites:**
+- `LaneDeck.tsx:126-148,216-238,283-309,437-459` → `pauseLaneControl({ lane, held, onChange })`
+ with `onChange: onWorkersChange` for transcription/download (their `held` comes from the
+ WORKERS payload: `workers.paused` live pool, `workers.downloadsPaused`) and `onSynced` for
+ digest/backfill (`sync.digest.held`, `sync.backfill.held`). The card rules stay in LaneDeck:
+ digest gate only when `sweeping || held` (`:283`), backfill controls only when `anyKind`
+ (`:412`).
+- `WorkersView.tsx:14,125-129` → `<PauseLaneButton lane="transcription" held={paused}
+ disabled={workers.length === 0} onChange={refetch} />`; `jobs/active/page.tsx:8,37` →
+ `<PauseLaneButton lane="transcription" held={paused} />` (no `onChange` ⇒ `router.refresh()`,
+ page is `force-dynamic`). **`git rm` `PauseTranscriptionsButton.tsx`.**
+- `SweepLane.tsx:184-211` → `<PauseLaneButton lane={lane.id} held={lane.gateHeld}
+ disabled={!lane.available} onChange={onRefresh} />`, rendered unconditionally as today
+ (`auto-queue.spec.ts:1186` expects it with no sweep armed). Its aria-label becomes the
+ canonical one and it adopts the dashboard's emphasis (held = warning-filled). Two things
+ change and the comment says so: the pause no longer shares `working` with the sweep button
+ (each has its own pending state; a sweep start no longer disables the pause mid-flight), and
+ `LaneActionButton`'s `px-3 py-1.5 text-sm` sits beside ui `Button size="sm"` — accept; do
+ **not** restyle `LaneActionButton` (it is on the dashboard and the widget). Delete the
+ `:184-186` "normalised in lanes.ts" comment.
+- `CommandPalette` already on the pair from commit 2.
+
+**e2e — new test in `backfill.spec.ts` after `:824`**, "an operation page holds the same lane
+the dashboard does": `resetData("one-transcribe-channel-with-audio")`,
+`writeSettings(backfillSettings())`, `goto("/operations/diarization")`,
+`page.locator('section[data-lane="backfill"]').getByRole("button", { name: "pause backfill" })`,
+`toBeEnabled({ timeout: 30_000 })` (the hydration wait — `awaitHydration` is local to
+`auto-queue.spec.ts`), click, `expect.poll(laneEnabled).toBe(false)`, `sweepFlag()` still
+false, then `goto("/")`, `"resume backfill"` enabled, click, `laneEnabled` true. Helpers
+already in the file: `backfillSettings()` `:60`, `sweepFlag()` `:123`, `SLOW` `:130`.
+
+---
+
+## Commit 4 — `operations: a runner page has its pause beside Start, Drain and Stop`
+
+- `operations/status.ts:33` `AutoQueueKindStatus` gains `held: boolean`; filled in `buildKind`
+ (`:80-92`): transcription from `getWorkerPool().isPaused()` (live — finding 5), download
+ from `isGateHeld(settings, "download")`. Comment: why the two differ.
+- `LaneHeader.tsx:18-30` gains `onChange`; renders `<PauseLaneButton lane={status.kind}
+ held={status.held} onChange={onChange} />` after Stop (`:83`, inside the existing
+ `span.ml-auto`). `RunnerOperationView.tsx:85-91` passes `onChange={onRefresh}`. Contract
+ check: a plain `<button>`; `role="alert"` only on error; no `<section>`, no `role="status"`.
+- `railStates.ts:36` `gateHeld: false` → `gateHeld: status.held` — the operations rail can
+ show a runner lane *Holding* for the first time (note at `:50-53` already handles it).
+- **The honest idle reason** (H4): `common/controller/autoRunner.ts:598` reports `no-workers`
+ while the pool is paused (pauseAll disables every worker), so the transcription page says
+ "no enabled worker to run it" next to a *Resume Transcriptions* button. Add
+ `"workers-paused"` to the idle-reason union (`:123-124`), check `getWorkerPool().isPaused()`
+ before the no-workers branch, map it in `dispatch.ts:108` ("transcriptions are paused") and
+ `buildActiveJobs.ts:399` ("transcriptions paused globally"), mirroring `downloads-paused`.
+- e2e: in `auto-queue.spec.ts` after `:1174` (the `/operations/download` block of "each
+ operation page carries its own lane"): `const download = page.locator('section[data-lane="download"]')`
+ (the runner section carries `data-lane={kind}`, `RunnerOperationView.tsx:82`);
+ `awaitHydration(download)`; `download.getByRole("button", { name: "Pause Downloads" })`
+ visible → click → `expect.poll` on `test-settings.json` `downloadsPaused === true` →
+ `"Resume Downloads"` visible → click → false (so the test leaves the fixture as it found
+ it — the digest block after it does not care, but the next test's `writeSettings` does the
+ real reset). Assert nothing about `role="status"`.
+
+---
+
+## Commit 5 — `plans: slice 7 shipped, and the docs say so`
+
+- `editor/CHANGELOG.md` `[Unreleased]` first bullet, house voice (slice 4's bullet is the
+ model): one pause control on every surface that draws a lane, and now on the runner pages
+ too, beside Start/Drain/Stop; a held runner shows as *Holding* on the operations rail and
+ says "transcriptions are paused" rather than "no enabled worker"; *Resume* is always
+ clickable, even when the lane has no workers or is switched off; the operation page's
+ hold button now emphasises a held lane the way the dashboard does. Every button name is
+ unchanged. **Nothing on disk changes** — the same four settings fields, the Speaker-lane
+ checkbox and the pause button still write the same one. The monitor widget's own data feed
+ renames `digest.paused` / `backfill.enabled` to `held`; a pinned tab shows the lane as not
+ held until reloaded.
+- `plans/editor-operations-ia.md:157-161` → SHIPPED with hashes; new "## Slice 7, as shipped"
+ after the Slice 4 section, recording: keyed by lane, not operation, and why; four gates /
+ three polarities / eight actions (the bullet's "six"); `PauseDownloadsButton` was dead;
+ the canonical-label finding (case-insensitive substring); transcription's live/persisted
+ asymmetry; the runner pages got their pause; what stayed (the `LaneSettingsForm` checkbox
+ as the second writer; the three copies of the lane NOTE strings — slice 8).
+- `plans/unified-operations-model.md:3,124-127` — step 5 **partly**, honestly: one definition
+ (`isGateHeld`/`withGateHeld`) and one writer among the controls; still four settings fields,
+ not tree state; step 6 migrates storage behind two functions.
+- `plans/STATE.md`: "Last updated" prepend; `:364` (the `pause|resumeBackfillAction` names
+ "stay") → gone, aria-labels stayed; "Recommended next" adds a DONE line.
+- `plans/FACTS.md`: `## Verified 2026-08-28 — editor IA slice 7 seams` (census + "As shipped");
+ `:2212` mapping row updated.
+- Memory: `ia-slice-7-shipped.md` + `MEMORY.md` line; amend `ia-slice-4-shipped`'s "next
+ candidates".
+
+---
+
+## Verification
+
+1. After each of commits 1–4: tsc in `common editor export homepage umtool mcp`;
+ `pnpm -C common test` (861 + pauseGates tests); editor units (90).
+2. Grep gates after commit 3, over `editor/ common/` excluding `node_modules`/`.next`:
+ `pauseAllWorkersAction|resumeAllWorkersAction|pauseDownloadsAction|resumeDownloadsAction|pauseDigestsAction|resumeDigestsAction|pauseBackfillAction|resumeBackfillAction|persistTranscriptionsPaused`
+ → 0 in code (plans/ prose is commit 5's); `PauseTranscriptionsButton|PauseDownloadsButton`
+ → 0; `!settings.backfill.enabled|!live.enabled|digestsPaused` in `editor/app` → only
+ `settingsActions.ts:102` (the preserve-on-save) and `LaneSettingsForm`; `\.digestsPaused|\.downloadsPaused|\.transcriptionsPaused|backfill\.enabled` outside `pauseGates.ts`,
+ `settings.ts`, `settingsActions.ts`, `settings/actions.ts` → 0; `INVERTED` → 0.
+3. e2e once after commit 5, detached (`cd editor && setsid nohup sh -c 'pnpm e2e -- <specs>;
+ echo exit=$?' > $CLAUDE_JOB_DIR/tmp/e2e.log 2>&1 < /dev/null & disown`):
+ `backfill.spec.ts widget.spec.ts dashboard.spec.ts auto-queue.spec.ts
+ parakeet-partial.spec.ts workers.spec.ts disk-space.spec.ts operation-settings.spec.ts
+ attribution.spec.ts jobs-active-order.spec.ts queues.spec.ts navigation.spec.ts
+ perf-budget.spec.ts`. Known flakes (`auto-queue.spec.ts:411`) retried individually and
+ reported. Port fallback `PORT=3111 EXPORT_PORT=3110 OLLAMA_STUB_PORT=11535`; never kill
+ anything.
+4. **No editor boot against `transcripts/`, nothing written under it.**
+5. Manual (optional, `PORT=3021 pnpm dev:test`): dashboard band — four gates, labels
+ unchanged; `/operations/attribution-text` *Hold the lane* → `/operations/diarization` shows
+ *Resume the lane* and the rail says Holding; `/operations/transcription` has *Pause
+ Transcriptions* beside Stop, pausing turns the idle line to "transcriptions are paused";
+ `/workers` with zero workers: Pause disabled, Resume (after pausing) enabled.
+
+## Out of scope
+
+- Unified-ops step 6 (tree state; `sweepEnabled`/`sweepKinds`/`sweepChannels`/`weight`) — slice 9.
+- Snooze (`SnoozeControl`, `snoozeAutoQueueAction`), the disk gate, Start/Drain/Stop, the
+ `LaneSettingsForm` checkbox (stays the second writer of `backfill.enabled` on purpose).
+- Folding `buildActiveJobs.ts:346-379`'s copy of `deriveLaneState` precedence and the three
+ copies of the lane NOTE strings (`buildActiveJobs.ts:369-378`, `railStates.ts:44-56`,
+ `SweepLane.tsx:226-233`) — slice 8.
+- Renaming the workers payload's `paused`/`downloadsPaused`, the `/api/widget/sync` path, or
+ any settings key/field `name`. Restyling `LaneActionButton`.
+
+## Handoff — the cadence
+
+On approval, Fable does not implement (memory `plan-then-opus-implements`): write the plan to
+`plans/editor-ia-slice-7.md`, commit it alone, then spawn one `general-purpose` agent,
+`model: "opus"`, with: the plan path, the fish-shell caveats (`git commit -F <file under
+$CLAUDE_JOB_DIR/tmp>`; quote `[slug]`/`[id]` paths and globs; POSIX loops only inside
+`sh -c`), never boot against `transcripts/`, e2e detached, tmp under `$CLAUDE_JOB_DIR/tmp`,
+the two trailer lines, and the report contract (shas + one line each; exact gate outputs;
+e2e per spec with retries; every divergence and why; anything undone). Fable reviews on
+return (`git log --oneline 494cbee..`; `pauseGates.ts` + test, `operations/actions.ts`,
+`pauseControl.tsx`, `LaneDeck.tsx`, `SweepLane.tsx`, `LaneHeader.tsx`, `status.ts`,
+`widget/sync/route.ts`, the new specs; re-runs grep gates + `pnpm -C common test` + editor
+units, not e2e), sends fixes via SendMessage, and reports.