Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Aplans/editor-ia-slice-7.md | 377+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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.