# agent-scope-13 — Situations rebuild (one field per heading)

> **Work order.** Orders: `situations-rebuild-intake-2026-07-25.md`.
> Build spec: `situations-rebuild-mockup-annotations.md` § BINDING — that
> table is the acceptance bar, row by row, and this file does not repeat it.
> Mockup source: `situations-rebuild-mockup-round-2.html`.
> Token law: `design/tokens.md`. Standing laws: `KICKOFF-PROMPT.md`.

## The operator's rulings that unblocked this (2026-07-25)

Asked as one fork, answered "take all recs, start building":

1. **Border parity** — as built. Read-mode boxes 1px `line`; live text controls
   1px `line-strong` per the sheet's input spec. Nothing darker than
   `line-strong` anywhere on the screen.
2. **Four channels** — as built. Four channels exist; the comment tabs render
   the two that have comments. Per-channel additions and per-channel Knowledge
   offer all four.
3. **Tokens** — `design/tokens.md` is **amended** to add a popover/panel shadow
   and a scrim, rather than lose the panel's lift. The amendment lands in this
   package (Stage 4) and the sheet is updated in the same commit.
4. **Global keeps its tab.** `prompt-seed.ts` seeds nothing for it, so it ships
   with five empty heading fields he can type into. That closes annotations
   OPEN 2 for build purposes; what text belongs there is his to write.
5. **The goldens move, and that is signed off.** He accepted that the assembled
   prompt changes. Re-pin them with the diff read line by line — see Stage 1.

Still his, and NOT blocking the build: the knowledge-date question
(annotations OPEN 3 — build keeps the date), the textarea type size
(OPEN 5 — build uses `fs-sm` per the input spec, mono only inside edit mode),
and the quiet-ghost button name (OPEN 6 — build uses `cm-btn--quiet`).

## Why the prompt changes — read this before Stage 1

Today the prompt is assembled by walking a **static module stack** in registry
order (`packages/shared/src/agent/modules.ts:483-495`, `SITUATIONS` at
`:388-392`), splicing custom instructions after the last seed module carrying
each heading (`spliceInstructions`, `:452-475`). DMs is 22 modules. Headings are
a *label* derived from the module id (`instruction-headings.ts:102-104`), not a
structure.

One field per heading inverts that: **the heading becomes the structure and the
module stack stops existing as an operator-facing concept.** Assembly becomes,
for each heading in `INSTRUCTION_HEADINGS` order: the operator's single field,
then that heading's runtime `live` modules.

Two consequences the executor must not paper over:

- **Splice order changes.** Modules that today interleave by stack order now
  group by heading. Every golden snapshot moves. That is expected and approved;
  what is NOT approved is a snapshot moving for a reason nobody read.
- **`live` modules stay in the prompt but leave the screen.** Who they are ·
  Returning contact · After hours · Lead progress are runtime-fed
  (`modules.ts` volatile `live` type, anchored at `staticEnd`). Order 10
  relocates them to the Automation agent node (channels-scope-5), so they are
  absent from Situations but must still assemble, under their existing heading,
  after the operator's field text. Deleting them from the prompt is a defect.

## Stages

Sequential. **Independent review after each stage** (DECISIONS 2026-07-22), and
a fix batch is itself a landed diff that gets reviewed (DECISIONS 2026-07-25).

### Stage 0 — the migration, written and proven before anything renders

- Additive migration: `KnowledgeDocument.channels String[]` (`schema.prisma:1218`
  has no such field — confirmed). Default empty = every channel.
- A one-way converter `SituationsConfig` v1 → **v2**. v2 shape, per situation and
  for global: five heading entries, each `{ text, channels: { <channel>: { text } } }`.
  Nothing else. No `modules`, no `added`, no `restoredParked`, no `replayGate`.
- The converter seeds each heading's `text` from that heading's member modules
  **in current stack order**, resolved through today's precedence
  (`situations.ts:594-608`), joined as markdown with `## <productLabelFor(...)>`
  sub-headings. `live` modules are excluded from the field. Custom instructions
  under a heading append to that heading's text. Channel appends collapse into
  that heading's channel entry.
- **Oracle:** a test that converts the LIVE production config (checked in as a
  fixture, read from the prod `Setting` row) and asserts the v2 text contains
  every non-`live` module's resolved prose, no prose lost, no prose duplicated.
  Character-count equality on the concatenation, not eyeballing.

### Stage 1 — shared: assembly, and the goldens re-pinned

- `assembleSituation` assembles from v2: heading order, field text, then that
  heading's `live` modules.
- Delete `NON_REMOVABLE_SECTION_IDS` (`prompt-seed.ts:285`), `spliceInstructions`,
  the parked machinery (`PARKED_FOR_FLOWS`, `isParkedForFlows`, `restoredParked`),
  the `struck` vocabulary, and the two-gesture channel override path
  (`situations.ts:357-392`) — channel entries are append-only, always below base.
- **Oracle:** `situations-golden.test.ts` (19 tests / 26 snapshots),
  `routing-equivalence.golden.test.ts` (9), `channel-profile.golden.test.ts` (6)
  re-pinned. The stage log records, per moved snapshot, **why** it moved. A
  snapshot whose move cannot be explained blocks the stage.
- `graded-examples-never-in-prompt.test.ts` (4) must stay green untouched.

### Stage 2 — api

- `situations.controller.ts`: `GET` returns the v2 view (five heading fields per
  situation, plus channels); `PUT` takes v2 and validates with Zod.
- **The automatic replay gate is deleted** (intake gap 4, recommendation
  accepted implicitly by "take all recs" — it charges real money per save and
  base-text edits already bypass it, so it protects almost nothing while
  looking like protection). Replaced by an explicit **"Test this against my
  labelled examples"** action that shows the cost and runs only on click.
  ⚠ *This one is worth a sentence to the operator at gate — it is the only
  ruling in this package taken by inference rather than by his words.*
- Delete the rail 400-guard and the typed-confirm path (`Nothing is fixed`).
- `knowledge-admin.controller.ts`: `channels` on create/patch/list DTOs.

### Stage 3 — web: the screen, rebuilt

`Situations.tsx` is 1713 lines / 17 components (`routes/agent/Situations.tsx`,
not `pages/`), `situations.css` 603 lines, both wholly owned by this route.
Rebuild, do not refactor in place. Build against the BINDING table row by row.

- Five heading cards, one `textarea.cm-input` each in edit mode, markdown
  rendered in read mode. Knowledge has no prose field — rows only.
- One page Edit, one Save, sticky save bar with no caption, tabs locked during
  edit with the message shown only on a locked-tab click (that is the dirty
  guard for gap 5 — the `editorKey` remount bug at `:293` dies with the rebuild).
- Knowledge side panel from the right over a scrim, `width: min(440px, 100%)`,
  page headers stay on screen.
- Every control is a `cm-*` primitive. `Input`/`Field`/`Chip` already exist at
  `components/ui/index.ts:15-19` and are used nowhere on this screen today.
- Its 50-test co-located suite (`Situations.test.tsx`) is rewritten with it.

### Stage 4 — tokens amendment

`design/tokens.md` + `apps/web/src/styles/tokens.css`: a popover/panel shadow
token and a scrim token, plus the `cm-btn--quiet` variant in `ui.css`. Sheet and
CSS in one commit — the sheet is law, so it cannot lag the code.

### Stage 5 — Parked for Flows, off the screen

The parked module content moves to a markdown file in the repo (order 5) and
the box goes. The file is the record; the UI never mentions it.

## Definition of done

`corepack pnpm --filter @channels/web build` green (the only oracle for shared
runtime imports), per-package vitest green at `--pool=forks
--poolOptions.forks.maxForks=2`, `corepack pnpm -r --workspace-concurrency=1
typecheck` + `lint` green, the additive migration applied to the live DB, the
v1→v2 converter run once against the prod `Setting` row with the before/after
prompt diff captured, backend rebuilt + restarted + `/health` green, web
published, committed and pushed, `HANDOFF.md` rewritten in place.

## Gate bar

1. Every BINDING row demonstrably built; every Absence demonstrably absent.
2. Every moved golden snapshot has a recorded reason.
3. No prose lost in the migration (Stage 0's oracle, run against prod data).
4. `live` modules still assemble into the prompt though absent from the screen.
5. Clean at 320 / 375 / 768 with no horizontal page scroll.
6. Independent review after each stage, findings fixed or boarded before gate.
7. Operator glance.
