# agent-scope-10 — instruction-field standardization overhaul

> Kickoff: Fable, 2026-07-22, from the operator grill (three locked answers,
> prompt line). This is board Intake ⑦ — "almost an overhaul": the app must
> be operator-extensible, not developer-dependent.

## Grill-locked decisions (operator, 2026-07-22 — ledgered in DECISIONS)

1. **Goal:** instructions are built under **core headings**. The operator
   creates/edits instructions himself; no more minting a bespoke typed
   field per instruction ("Callback number and time", "How it opens a
   chat", …).
2. **Skeleton is fixed:** the set of core headings is defined by design,
   not operator-editable. Headings are **nouns, never questions**
   ("Identity", not "Who is this bot").
3. **Targeting:** an instruction applies **everywhere by default**; the
   operator may optionally select specific channels for it. (No
   per-situation targeting was asked for — don't build it.)

## Orchestrator defaults (flag at gate if any looks wrong)

- **Existing bespoke prose fields migrate** into the standardized shape;
  operator-edited values survive VERBATIM (never clobber an edit; the
  blank-uses-seed convention stays).
- **Safety rails + machine contracts stay fixed code** (boundaries,
  moderation taxonomy, knowledge lookup, output contracts) — visible,
  non-removable, per the standing exception in CLAUDE.md.
- **Surface = the Situations screen** (one concept, one surface — no new
  screen). The heading skeleton becomes how instructions are grouped there.
- **Instructions are removable/disableable** with the explicit REMOVED
  sentinel precedent (field-delete package) — distinct from revert-to-seed.
- **The heading set itself:** executor drafts it by clustering the existing
  modules (candidates: Identity · Tone · What it asks for · Offers & CTAs ·
  Safety limits · How it replies). Parked `?` — the operator reacts to the
  draft at gate glance, not in a design round.

## Fact base (verified this session)

- Every instruction today is a typed module in
  `packages/shared/src/agent/modules.ts` (405 lines): typed schemas, seeded
  editable prose via `MODULE_SCHEMAS`, fixed-text engine modules
  (knowledge, output-contract), volatile live-data modules.
- Labels are hard-mapped in the web app (`Situations.tsx:198-204`) — the
  exact developer-dependence being removed.
- Channel variants + two-gesture editing already exist (scope-4/7); reuse
  that mechanism for the per-channel targeting, don't invent a second one.

## Constraints (red-team folded in)

- **Golden discipline:** WhatsApp goldens byte-identical for untouched
  content. The assembled prompt's SECTION ORDER must be deterministic
  (skeleton order), so operator-added instructions land in a stable slot;
  structural regen of pins is deliberate + logged per commit.
- **Engine parse contracts are untouchable** by operator text: an
  instruction is prose injected under its heading — it can never redefine
  `[[buttons]]`/output-contract semantics. Keep the existing
  channel-conditional syntax handling (DECISIONS 2026-07-22 Option A).
- **Data-driven law:** the skeleton + migrated defaults are seeded data;
  operator instructions live in the same settings store the modules read
  today. Additive migration only.
- **Simplify law:** plain words everywhere; adding an instruction must be
  a short form (heading picker + text + optional channel picks), not an
  engineering UI.

## Acceptance (gate bars)

1. Operator can add a new instruction under a core heading in the UI, and
   it appears in the assembled prompt ("See exact prompt" / playground
   preview) without any code change. Oracle: add via UI → GET the composed
   prompt → the text is there, under its heading, only on its targeted
   channels.
2. Every existing bespoke prose field is reachable and editable in the new
   shape; no orphaned editor remains (grep-proof: the old per-field label
   map is gone).
3. Edit/remove/restore round-trips survive reload (field-delete precedent).
4. WhatsApp goldens byte-identical for untouched content; per-package
   suites green (forks pool, maxForks=2); typecheck/lint/build pass;
   deploy + health green; web published.
5. Migration applies cleanly on prod with operator edits preserved
   (verified by read-back diff before/after).

## Out of scope

Per-situation targeting; operator-editable headings; any change to
moderation passes, rails semantics, or engine contracts; new screens.

## Stage log

- 2026-07-22: kickoff written from the grill; executor dispatched.
- 2026-07-22 · **Stage 1 landed** (commit `8e303c4`) — the fixed core-heading
  skeleton as DATA in shared (`agent/instruction-headings.ts`) + descriptor now
  carries `heading` and a data-driven `label`. **Drafted heading set (nouns,
  operator reacts at gate glance):** Identity · Conversation · Qualification ·
  Offers · Knowledge · Tone · Safety limits.
  > **SUPERSEDED — the live set is FIVE, not seven** (corrected 2026-07-25 under
  > the operator's planning-file authority): `INSTRUCTION_HEADINGS` in
  > `packages/shared/src/agent/instruction-headings.ts:38` is
  > **Role · Goal · Tone · Knowledge · Guardrails**, matching
  > `docs/DECISIONS.md:96-99`. agent-scope-11 Stage 1 consolidated the seven
  > drafted here; Qualification and Offers were retired and their content moved
  > to Parked-for-Flows. Read the code, not this line. Module→heading map: Identity =
  identity, goal; Conversation = triage, never-silent, pivots, operator-steering,
  output-contract, triggers; Qualification = job-seekers, lead-capture,
  lead-temperature, qualification-slots, extra-contact, + the live lead context
  (contact/returning/after-hours/lead-profile); Offers = services,
  offering-choices; Knowledge = knowledge, knowledge-corpus, examples; Tone =
  tone; Safety limits = boundaries, safety-rail, taxonomy. Web PRODUCT_LABEL
  strings relocated verbatim into shared seed data (grep-proof removal comes in
  the web stage). Only label change: job-seekers "Job seekers" → "Job applicants"
  (the former is verbatim the engine registry name — leak-guard forbids it).
  Goldens byte-identical (situations 30 + channel-profile 6 + routing 9); shared
  141 tests green. No prompt/order change.
- 2026-07-22 · **Stage 2 landed** (commit `0984949`) — the operator-authored
  instruction ENGINE in shared. New `instruction` module type; `CustomInstruction`
  ({id, heading, text, channels?}) stored on the situation entry's `added` list;
  normalize + `situationCustomInstructions` resolver (channel targeting =
  everywhere-by-default, or the chosen subset) + descriptor `custom` rows.
  `assembleSituation` splices each instruction at its heading's deterministic slot
  (after that heading's last static seed module, always before the volatile live
  suffix). Empty ⇒ byte-identical → all 3 goldens hold. **Bar-1 engine oracle
  green:** added instruction appears in the assembled prompt only on its targeted
  channels. Full shared agent suite 474 green.
- 2026-07-22 · **Stage 3 landed** (commit `b57b308`) — API wiring. Situations
  controller write-schema accepts `added`; GET surfaces custom rows. All prompt
  paths thread `situationCustomInstructions` (live DM `run-turn`, comment reply +
  moderation via the shared policy builders, and the `GET /agent/prompt` preview)
  so preview == runtime. API situations tests 18 green (added persists verbatim;
  GET custom rows; unknown-heading drops). NOTE: another seat's uncommitted
  `feed/agent-feed.service.ts` currently breaks the local `api` full build
  (`turnKind`/`authorName`) — NOT my files; my api files typecheck clean; verified
  via per-file vitest. Prod deploys from the committed checkout, unaffected.
- 2026-07-22 · **Stage 4 landed** (commit `2fe1a33`) — the Situations SCREEN.
  Blocks now group under their fixed core heading; the operator adds an
  instruction via a short form (heading picker → plain-words text → channel
  ticks, everywhere by default), edits/removes inline, round-trips through
  `added`, and sees them per-channel in the exact-prompt drawer. **Web
  PRODUCT_LABEL map DELETED** (bar 2, grep-proof) — labels are the descriptor's
  data. INSTRUCTION_HEADINGS routed via a new `@channels/shared/instruction-headings`
  subpath aliased to source in vite.config (the rollup-CJS value-export
  workaround the other subpaths already use — hit the "vite-build is the only
  oracle" gotcha en route). Web Situations 40 green (add/target/reload/remove);
  agent routes 158 green; **web build green** (dist ready to publish). NOTE:
  another seat's `Activity.tsx` (FB_DISABLED_REASON) breaks the web full
  typecheck — not my file; Situations.tsx typechecks clean.
- 2026-07-22 · **Stage 5 — verification + bar-1 e2e close** (commit `2eff0d1`,
  RESTARTED executor). Re-verified the four landed stages on the current tree and
  closed the one gate-bar-1 link previously proven only by inspection: a new
  controller-level **end-to-end test** drives BOTH real controllers over one
  settings store — an instruction added via `SituationsController.put` surfaces in
  `AgentAdminController.prompt` (the "See exact prompt" preview) as its own section
  titled by its heading, and ONLY on its targeted channels (everywhere-instruction
  shows on the WhatsApp/external prompt; Instagram-only is absent there; an
  Instagram-targeted instruction shows when Instagram renders in comment mode).
  **Migration finding (bar 5):** there is NO Prisma schema migration in scope-10 —
  the "migrate bespoke fields into the shape" is pure Stage-1 code (heading/label
  data); the `AGENT_SITUATIONS_SETTING_KEY` JSON store is unchanged and `added` is
  additive, so operator edits are preserved by construction. Bar-5's read-back diff
  is therefore a deploy-time VPS check (GET /agent/situations byte-identical
  before/after), not a data migration. **Local evidence:** shared agent suite 477
  green (39 files) incl. all 3 goldens byte-identical; api situations 20 green (18
  + 2 new e2e); web Situations 40 green; shared `tsc` build clean; web `vite build`
  green (dist ready). **Deferred to orchestrator/VPS (this seat is a Windows dev
  box, no pm2/systemd/local api):** api deploy + `curl :3100/health`, web publish,
  and the prod GET-situations read-back diff — none runnable from here.
- 2026-07-22 · **Stage 6 — review fixes** (MAJOR `b362aa2`, MINOR `2c0b43d`).
  Review returned 1 MAJOR + 1 MINOR; both fixed at source, test-first.
  **MAJOR (channel targeting leak + vanish):** both comment passes and the admin
  prompt preview resolved operator instructions against a hardcoded `"instagram"`,
  but the ONE `CommentPolicyService` serves BOTH Instagram and Facebook comments —
  so a Facebook-only instruction never fired (vanish) and an Instagram-only one
  also fired on Facebook (leak), and the "See exact prompt" server view showed what
  runtime silently dropped. Fix: thread the real inbound channel
  (`this.adapter.channel` from the pipeline; the previewed channel from the
  playground) into `situationCustomInstructions` in `decide`/`moderate` (default
  instagram, back-compat); `GET /agent/prompt` now takes a `channel` query for
  comment mode. The prompt BODY still assembles under the instagram comment profile
  — only the instruction FILTER keys off the channel — so pinned IG/FB goldens are
  untouched (proven: situations 30 + channel-profile 6 + routing 9 = 45 byte-
  identical). Repro tests red→green: FB-only fires on FB not IG, IG-only on IG not
  FB (decide + moderate); preview targets the requested channel. **MINOR (Safety
  limits confirm):** an instruction added under the "Safety limits" heading skipped
  the "Change a safety limit?" typed confirm (the gate only saw rail-prose +
  channel rails, never `added`). Fix: a new/changed Safety-limits instruction joins
  the same confirm (removal stays ungated). Tests: adding one prompts + still
  round-trips; other headings don't. Suites: policy service 20, situations
  controller 21, playground 13, web Situations 41 green; api typecheck clean; web
  build green. **Known SCOPE GAP (report-only, for the board):** the kickoff said
  operator instructions are "removable/**disableable**" — only REMOVE exists today;
  there is no disable/toggle (keep-but-mute) state. Not built this scope; flag it
  on the board if disable-without-delete is wanted.

## GATE VERDICT — PASSED (Fable, 2026-07-22)

- Bar 1 (UI add → composed prompt, channel-scoped): e2e test green; channel
  threading fixed after review (b362aa2). Own-run: api situations 21 +
  policy 20 green.
- Bar 2 (label map gone): grep-proof confirmed by review.
- Bar 3 (round-trips): web Situations 41 green (own run).
- Bar 4 (goldens/suites/build/deploy): goldens 45/45 byte-identical (own
  run); deployed, health {ok,db,redis}; web published.
- Bar 5 (migration read-back): no data migration exists; deploy touched no
  settings row (hash check before/after deploy: no stored situations row —
  defaults in effect; sparse operator edits in agentBehaviour untouched).
- Review (process step 3): 1 MAJOR (channel targeting) + 1 MINOR (safety
  confirm) found, both fixed red→green same day; injection/ordering/
  removal/store/migration concerns clean.
- Known gap boarded: "disableable" (mute-without-delete) not built.
- Trailing: operator glance + his first self-added instruction in real use.
