# agent-scope-5 kickoff — Activity end-state: ONE screen, ONE list

> Fable-authored 2026-07-21 on the operator's "let's surface it now" (which
> also overrides the wait-for-scope-6-witness sequencing — ledgered in
> DECISIONS). The shape is the operator's own ruling (DECISIONS 2026-07-18
> "Activity end-state"): one screen answers "what happened, and what needs
> me" — no second screen, no view toggle.

## The order (operator-ruled shape, not open for redesign)

- **ONE screen** at `/agent/activity`. **ONE list.** No tabs.
- **Two presets on the same list**: **Feed** (newest-first — what happened)
  and **Needs-action** (worst-first, drains to zero — what needs me). A
  preset is a sort+filter preset on the one list, not a second surface.
- **Rows actionable in place in BOTH presets** — approve/edit/send a held
  reply, delete/restore a flagged comment, label — using the existing act
  endpoints and the `cm-btn` primitive. No navigation to act.
- **Count badge on the nav entry** — the needs-action count, accurate, zero
  when drained.
- **DM label checks appear as rows** (the DM-labels-absorb ruling folds in
  here).
- **RETIRE: the Review queue screen AND the `?view=moderation` toggle.**
  Both routes redirect into the one screen with the right preset/filter.
  Their capabilities (approve with draft edit, moderate delete/restore,
  keyboard acts, bulk) survive INSIDE the list rows — capability parity
  before retirement, verified against what ReviewQueue/ModerationView can
  do today.
- Simplify law applies throughout (order item ③): plain words, fewer
  visible concepts; moderation rows read keep/delete/flag.

## Stages

1. **Feed spine (api, test-first).** One activity-feed endpoint returning
   the unified list: agent turns + review items + moderation acts + DM label
   checks as one row shape with `needsAction` + severity ranking (worst
   first) and cursor paging. Reuse existing read services; no new writes.
   The needs-action count rides the same query (one source of truth for the
   badge).
2. **The one screen (web, test-first).** Presets, in-place actions with
   optimistic conflict handling (the act endpoints already return
   row-state-as-data), keyboard support carried over, count badge in the
   nav. 320/375/768 clean.
3. **Retirement.** `/agent/review-queue` and `?view=moderation` →
   redirects; dead components deleted; sub-nav updated (one entry fewer —
   concepts removed ≥ concepts added: net -1 screen, -1 toggle).
4. **Evidence + stage log.** Screenshots of both presets at the three
   widths; grep proves the retired routes are gone.

## Fences

- Engine, prompts, golden pins untouched. No gate flips, no Meta config, no
  LLM spend, no schema change unless additive with a note.
- Situations surface is OUT of scope (its own package, agent-scope-7).
- Shared-tree law: pathspec commits, `git pull --rebase --autostash`, never
  delete index.lock.
- Standing rules: TDD, per-package vitest (forks, maxForks 2), deploy loop +
  health, commit+push per verified step, tokens are law.

## Gate bars

1. Capability parity table: every act possible in ReviewQueue/ModerationView
   today, shown possible in the one list (test or screenshot each).
2. Feed correctness: needs-action preset drains to zero as acts land; badge
   count equals the preset's row count (one query, proven by test).
3. Retired routes redirect; no orphan links to them anywhere in the web app
   (grep).
4. Simplify: net concepts on the Agent nav decreased; copy passes the
   plain-words bar.
5. Operator glance on the live screen (non-blocking witness).

## Gate — interim (Fable, 2026-07-21 late)

Stages 1, 2, 4 MET (own-oracle: feed suite 10/10, Activity+AppShell 27/27,
`/api/agent/feed` live + admin-guarded (401), health green, prod current).
Stage 3 PARTIAL: ReviewQueue fully retired (redirect + producers repointed,
net −1 screen −1 toggle) — but the moderation console's power tools (bulk,
per-post mute, labeled replay, undo/history, act-on-any-comment) are NOT yet
folded; the seat correctly kept `/agent/moderation` standalone rather than
delete capability. **Ruling (Fable default off the operator's ONE-screen
order): Stage 3b — fold those tools into the one list, then retire
`/agent/moderation` behind a redirect.** Bar 1 (capability parity) stays the
gate. Screenshots remain on the operator glance (auth-gated).

## Gate — FINAL, executor half (Fable, 2026-07-21 night)

**Bars 1–4 MET; only bar 5 (operator glance) open.** Stage 3b closed the
parity table completely — every ReviewQueue AND ModerationView capability
lives on the one list; `/agent/moderation` and `/agent/review` both
redirect; components deleted. Own-oracle: ModerationView.tsx confirmed
deleted, Activity+App.routes 57/57, health green, prod at `11b69a8`, live
site 200. The Agent section is at the operator's ruled end-state: ONE
screen, ONE list. **This gate unblocks agent-scope-7 Stages 1–4.**

## Stage log

- 2026-07-21 — kickoff authored (Fable); operator override starts execution
  before the scope-6 witness closes.

### Orientation — capability inventory (before code)

Three data spines feed the one list:
- **agent_turns** (`TurnTraceAdminService`, `GET /api/agent/turns`) — the Feed
  backbone ("what happened"). Read-only.
- **review_items** (`ReviewItemsService`, `GET/POST /api/agent/review`) — the
  canonical needs-action queue. Producers (`ig-dm`, `comment-pipeline`,
  `ig-comment`, `agent.service`) insert `kind: label|approve|moderate` rows;
  held comment ⇒ `approve`, hidden comment ⇒ `moderate`, sampled/DM ⇒ `label`.
  So review_items ALREADY unify "DM label checks + held-reply approvals +
  hidden-comment moderation". Acts ride `POST /api/agent/review/:id/decide`
  (now with the `recordReviewAction` audit spine, source:"review").
- **ig_comments + moderation_actions** (`ConsoleReadService` +
  `ModerationActionService`, `/api/moderation/*`) — the richer console: acts on
  ANY comment (not just ones that spawned a review item), plus bulk / mute /
  replay / undo / history / keyboard / label.

### Capability parity table (source → where it lands in the ONE list)

| Capability | ReviewQueue | ModerationView | Lands in one list as |
|---|---|---|---|
| Approve held reply | ✓ (`decide` approve) | — | Needs row action "Approve & post" (same `decide` endpoint) |
| Edit draft then send | ✓ (`decide` send) | — | Needs row inline edit → "Send edited reply" |
| Reject held reply, never post | ✓ (`decide` reject) | — | Needs row "Reject, never post" |
| Accept/Reject DM label (+reason set, other free-text) | ✓ (`decide` accept/reject) | — | Needs row label actions + reason chips |
| Moderate: delete permanently | ✓ (`decide` delete) | ✓ (`act` delete) | Row "Delete" (review `decide` for review rows; `act` for console rows) |
| Moderate: restore/unhide | ✓ (`decide` restore) | ✓ (`act` unhide) | Row "Restore"/"Unhide" |
| Status filter (waiting/decided…) | ✓ (select) | — | Preset = Needs (waiting) / Feed (all) + status chips |
| Hide a comment | — | ✓ (`act` hide) | Row action (console-backed comment rows) — **DONE 3b** `actComment` → ConsoleActions |
| Reply to a comment (preview→send) | — | ✓ (composer) | Selected-row composer — **DONE 3b** ConsoleActions composer |
| Right/Wrong label (g/b) | — | ✓ (`labelDecision`/`clearLabel`) | Selected-row label buttons — **DONE** (state pill on comment rows added 3b) |
| Action history + undo | — | ✓ (`history`/`undo`) | Selected-row history panel — **DONE 3b** history block + Undo |
| Bulk hide/delete/label | — | ✓ (`actBulk`) | Multi-select bulk bar — **DONE 3b** unified select (export + moderate) |
| Per-post mute lens | — | ✓ (`setPostMute`) | Post-filtered lens control — **DONE 3b** `?mediaId` lens + mute toggle |
| Labeled replay + estimate | — | ✓ (`previewReplay`/`runReplay`) | Replay control — **DONE 3b** Feed-preset replay + estimate/run |
| Keyboard j/k/h/u/d/r/g/b | — | ✓ | Carried onto the one list — **DONE 3b** review + console rows |
| Worst-first severity ordering | — | ✓ (`worstFirst`) | Needs preset default sort |
| Open full trace (TracePanel) | ✓ (details) | via Activity | Selected-row trace |
| Export selected traces (JSON/MD) | — | — (Activity today) | Preserved on Feed preset |

Note: review_items(waiting) is the badge/needs source of truth (one query →
count == rows). Console rows without a review item (posted/shadow comments the
operator wants to act on) surface under the Feed preset with console-backed
actions, preserving ModerationView's "act on any comment" reach.

### Stage 1 — DONE (2026-07-21)

- `GET /api/agent/feed` (`FeedModule` → `AgentFeedService`), two presets:
  `needs` (waiting review items worst-first; `count == rows`, badge SoT) and
  `feed` (agent turns newest-first, createdAt-cursor paged, waiting review
  attached so a feed row is actionable in place). Reuses `TurnTraceAdminService`
  + `ReviewItemsService` (now exported); added a keyset `before` bound to the
  turn read. No new writes.
- Tests: `agent-feed.service.test.ts` (7) + `agent-feed.controller.test.ts` (3)
  green; turn-trace suite (30) still green; web typecheck clean; api build
  clean. Web client `listAgentFeed` added.
- Deployed: VPS `channels-api` rebuilt + restarted; `/health` ok; `/api/agent/
  feed?preset=needs` returns 401 unauthenticated (route wired, admin-guarded).
- Commits: `5827cf2`-range (parity table) + Stage-1 feat commit, pushed.

### Stage 2 — DONE (2026-07-21)

- `Activity.tsx` rewritten: ONE list over `GET /api/agent/feed`, a Feed /
  Needs-action `SegmentedControl` (replaced the activity/moderation toggle).
  Rows actionable in place in both presets via the existing `decideReviewItem`
  endpoint — approve / send-edited / reject / accept / label-with-reasons /
  delete / restore — plus right/wrong labelling (`labelDecision`). Optimistic
  conflict handling (a `ConflictException` → plain toast + refetch). Keyboard
  carried over (j/k move; a/d/u/g/b act). Trace panel on the selected row.
  Export-selected-traces (JSON/MD) preserved on the Feed preset.
- Nav badge: `AppShell` Agent › Activity sub-nav shows `needsActionCount` from
  the SAME `preset=needs` query; zero when drained.
- Tests: `Activity.test.tsx` (8) + `AppShell.test.tsx` (19, incl. badge) green;
  full web suite 603 green; web typecheck clean; web lint introduces no new
  errors (one pre-existing `Button.tsx` rule-config error remains, not this
  scope). Plain-words moderation copy, tokens only.

### Stage 3 — DONE for ReviewQueue; console fold FLAGGED (2026-07-21)

- ReviewQueue fully retired (parity achieved): `/agent/review` → redirect to
  `/agent/activity?preset=needs`; `ReviewQueue.tsx` + test deleted; "Review
  queue" sub-nav entry removed (**net -1 screen**); producer links repointed
  (Overview, Home attention cards via `home.service`, escalation email +
  red-rule → `/agent/moderation`). App.routes/AppShell/Overview/Home/home.service
  /alerts tests updated. The in-Activity `?view=moderation` toggle is gone
  (**net -1 toggle**).
- **FLAGGED FORK — the moderation console fold is NOT complete.** ModerationView's
  power-tools (bulk `actBulk`, per-post mute, labelled replay, undo + action
  history, act-on-ANY-comment incl. posted/shadow) are not yet folded into the
  one list. To avoid deleting working capability, the console is kept reachable
  standalone at `/agent/moderation` (renders `ModerationView`). Fully folding it
  — so `/agent/moderation` can ALSO redirect into the one screen — is the
  remaining work; retiring that route is gated on it. Recommend a follow-on
  package (or operator confirmation that the standalone console is acceptable).

### Stage 3b — console fold COMPLETE + `/agent/moderation` retired (2026-07-21)

The parity table is fully closed — every ModerationView-only capability now lands
in the ONE list, and the standalone console is gone.

- **API (additive read, no new writes):** `AgentFeedService.feed()` joins the
  console identity onto comment turn rows — `igCommentId` (ig_comment db id),
  `commentStatus`, `mediaId`, the operator's `commentLabel`, `commentActionCount`
  — plus a per-post lens (`?mediaId` → resolve the post's comment ids →
  `correlationIds` filter on the turn read, additive on `TurnTraceAdminService`).
  This is what lets the power-tools act on ANY comment in the feed. Feed suite
  11/11 + controller 3/3 + turn-trace 30/30 green; api build clean. Commit on the
  first push of this stage.
- **Web (test-first):** every tool folded onto `Activity.tsx`:
  - **Act on any comment** → `actComment` → `POST /api/moderation/actions`
    (hide/unhide/delete/reply), rendered by the `ConsoleActions` block on a
    comment row with no waiting review (posted/shadow/held). FB honestly disabled.
  - **Bulk** → one unified multi-select over the list: export turns (improvement
    loop) AND bulk hide/delete/label comments (`actBulk`, grouped by status for
    `expectedStatus`; partial failure reported, never averaged).
  - **Per-post mute lens** → `?mediaId` filters the Feed to a post + shows the
    mute/unmute toggle (`getPostMute`/`setPostMute`); "Clear post filter".
  - **Labelled replay** → Feed-preset "Replay labelled decisions" → estimate →
    "Run replay" (`previewReplay`/`runReplay`); diff rendered.
  - **Undo + history** → selected comment row shows `getActionHistory` +
    per-action Undo (`undoAction`), platform-honest (delete = permanent).
  - **Keyboard** → j/k move; review rows a/d/u; console rows h/u/d/r; g/b label.
  - Tests: `Activity.test.tsx` 13/13 (5 new: act-on-any, bulk-hide, undo/history,
    mute, replay); full web suite 589/589; web typecheck clean; web lint adds no
    new errors (the pre-existing `Button.tsx` rule-config error remains). `cm-btn`
    primitives for every new control; plain keep/delete/flag words; tokens only.
- **Retirement:** `/agent/moderation` → `ModerationRedirect` carrying
  `?focus`/`?mediaId`/`?channel` onto `/agent/activity`; `ModerationView.tsx` +
  its test + `moderation.css` deleted; producers repointed (Posts "Moderate →",
  red-rule + escalation email → `/agent/activity`). `App.routes` 44/44 (moderation
  now a redirect + deep-link carry test); api alerts 78/78. Grep evidence:
  `evidence/agent-scope-5/stage3b-retirement-grep.txt` (no orphan links).
- **Deployed:** VPS `channels-api` rebuilt + reloaded, `/health`
  `{"status":"ok","db":true,"redis":true}`; `/api/agent/feed?preset=needs` → 401
  (admin-guarded, live); web rebuilt + published (HTTP/2 200).
- **Not verified by this executor:** the operator-glance screenshots at 320/375/768
  (gate bar 5) — the live app is auth-gated and this seat has no browser session /
  credentials. Both presets + the folded tools are LIVE for the operator glance.

### Stage 4 — evidence (2026-07-21)

- `plans/rethink/evidence/agent-scope-5/`: `retirement-grep.txt` (routes gone)
  + `README.md` (gate-bar mapping + responsive provisions).
- Screenshots at 320/375/768 NOT captured: the live app is auth-gated and this
  executor had no browser session / credentials (`list_connected_browsers` →
  none). That is the operator-glance witness (gate bar 5). Screens are LIVE:
  `/agent/activity` (Feed) and `?preset=needs` (Needs-action).
- Deployed: `channels-api` + web rebuilt on the VPS, api restarted, `/health`
  ok, web published (HTTP 200).

### Review findings fixed (2026-07-21)

Independent review of scope-5/7 raised six findings in this region; fix-seat
closed findings 1–5 test-first:

1. **[MED] Composite keyset cursor.** `TurnTraceAdminService.list` paged on
   `createdAt` alone — same-millisecond rows at a page boundary were silently
   skipped. Now a composite `(createdAt, id)` keyset: an opaque cursor
   (`encodeTurnCursor`) carries both, the where-clause is
   `createdAt < c OR (createdAt = c AND id < cid)`, `id` is the secondary
   orderBy. Feed cursor is opaque end-to-end (controller zod relaxed).
2. **[MED] Activity pagination restored.** The scope-5 rewrite dropped the pager
   (static "paginate by time" copy, no cursor sent). `Activity.tsx` now uses
   `useInfiniteQuery` with a working "Load older" that appends via the fix-1
   cursor; selection + checkbox state survive appended pages.
3. **[LOW] Badge cap honesty.** `needsAction` reported the capped page length as
   the badge; now it reports the true waiting total from the SAME query's
   whole-table `counts` (no extra read) — never understates above NEEDS_CAP.
4. **[LOW] Join channel guard.** `commentInfoForTurns` now scopes the comment
   join to `caseKey: "comment"`, so a non-comment turn can never acquire console
   identity (matches `posts.service`'s comment-join constraint).
5. **[LOW] `label()` busy guard.** Added the same busy check/set decide +
   actComment use, so g/b and rapid clicks can't fire concurrent
   `labelDecision` calls.

Suites green: `agent-feed.service` (14), `turn-trace-admin.service` (33),
`agent-feed.controller` (3), `Activity` (16). Typecheck clean; deployed +
published + health-checked.
