PR: #419: feat(agents): Forums Page Card create/archive — confirm-first + undo [Story-1711] by @ankitmaheshwari-com
View PR →
Story 1711 · Epic-1646 Agentic interaction · RFD-AGT-002 D-11 / D-2 / D-3 / D-10 · ADR-124

Atlas adds and archives a forum — confirm-first, with an undo, on the real backend

RFD-AGT-002 D-11 names the first proving slice of the Surface Operator: Atlas that operates a live page, not a chatbot beside it. Forums leads (the clean Transport-B proof — real generated hooks + invalidation). This Story turns the create and archive operations the Story-1709 Forums Page Card already declared into two executable, confirm-first client tools, each with an undo, wired to the real useAppCreateForum / useAppUpdateForum mutations under settings:write authz.

🛰️ Satellite — why an agent write is the most dangerous button in the app

The stakes are trust, not convenience. A read that Atlas gets wrong is a wrong answer; a write that Atlas gets wrong is a wrong fact in the firm's catalog — a forum that silently appeared, or a venue that silently vanished from every matter picker. Counsel has to be able to see the write before it happens and walk it back after.

The naive fix — "Atlas knows the rows, let it just create/flip them and paint the result" — detonates twice. First, the brain never has write authority: only the backend does (require_permission("settings:write") + RLS in pramaan-functions). Second, if the agent paints an optimistic row it computed itself, the screen and the database can disagree — the exact "fixture lies in your favor" failure the repo doctrine warns about. So the agent path must (a) propose, then wait for an explicit confirm, (b) ride the real mutation, and (c) reconcile the list from the round-trip — never from an agent-computed setQueryData.

✈️ Airplane — the shape of one agent write

The substrate from Story-1656/1658 already carries the parts; this Story is the first real consumer of the write plane. One sustained analogy: think of Atlas as a clerk who fills in a form and slides it across the desk. The clerk never files anything — counsel signs, the registry files, and there's a tear-off "undo" stub on the receipt.

StepWhat happensOwner
1. IntentBrain emits a client-plane intent forums.create / forums.archiveAtlas brain (separate lane)
2. DecideDispatcher sees the tool is confirm: true → returns a confirm decision (never auto-runs)client-tool-dispatcher.ts
3. ProposeA proposal card renders in the ACP thread; counsel taps Confirm or Cancelagent-chat-panel.tsx
4. ExecuteOn confirm, the tool's run fires the real mutation → backend authz + RLSthis Story
5. Reconcile + undoInvalidate the list (no agent setQueryData); a toast offers Undo (itself a real write)this Story

This Story owns steps 4–5: the two CpcClientTools published for the settings.forums surface. Steps 1–3 are the existing dispatcher + dock; the Atlas brain that emits step 1 is a separate agents-lane Story (the minimal eval gate gates its staging deploy).

🔬 Microscopic — the decisions welded into the code

The two tools — and why exactly two

buildForumsAgentTools returns forums.create and forums.archive, both confirm: true. The Page Card also models forums.edit, but the Story scope (and D-11's proving pair) is create + archive. delete is deliberately absent — v2 per RFD §8 — so the tool list has no way to express an irreversible write.

Why archive is the v1 "undo" of create

Every v1 write must be reversible (D-10: "undo is a write"). Archive's inverse is restore (availability: "live") — a clean round-trip. Create's inverse, with no hard delete in v1, is to archive the just-created row: it leaves the live catalog exactly as it was before, and the row is honestly still there (archived), not faked away.

// undo of create — archive the row we just made (v1 has no delete)
deps.notify.created(created.name, () => {
  void deps.archive(created.id)
    .then(() => deps.notify.createUndone(created.name))
    .catch(() => deps.notify.error());
});

The banned shortcut: no agent-computed cache rows

The agent-path mutations are invalidate-only — they carry no onMutate/setQueryData optimistic flip. The list reconciles from the real round-trip; the backend is the only authority (D-3). The direct click-path in forums-list-page.tsx keeps its own optimistic update — but that is the user acting with server-confirmed variables, not the agent computing rows.

// agent path — invalidate-only, NEVER setQueryData (RFD D-3)
const createForum = useAppCreateForum({ mutation: { onSuccess: invalidate } });
const updateForum = useAppUpdateForum({ mutation: { onSuccess: invalidate } });

Never auto-pick the wrong forum

Archive accepts an explicit forum_id (the brain's preferred path, from Page Context visible ids) or an exact name. Name resolution is case-insensitive over live rows only and never auto-picks when more than one matches (D-8: a silent wrong-row pick is forbidden in a legal product). An ambiguous or unknown reference returns null → an honest "couldn't find it", no write.

Honest UX: don't offer what the backend will reject

When the user lacks settings:write (canWrite === false) no tools are published — Atlas cannot even offer the write (proactive filter, D-3). The server gate remains the real authority; this is UX honesty, not the security boundary.

What changed

FileChange
src/components/settings/forums/use-forums-agent-tools.tsnew pure buildForumsAgentTools + resolveArchivableForumId + the useForumsAgentTools hook (publishes the tools, wires real mutations + localized undo toasts)
src/components/settings/forums/forums-list-page.tsxcalls useForumsAgentTools({ canWrite, forums })
src/components/settings/forums/use-forums-agent-tools.test.tsnew 11 unit tests (shape, create+undo, archive+undo, refusals, no-auto-pick)
messages/en.json · messages/hi.jsonForums.undo + Forums.agentForumNotFound in both locales

Acceptance criteria

Boundary honesty. The Atlas brain that emits step-1 intents is a separate agents-lane Story; its staging deploy is gated by the minimal eval gate (D-5). This Story proves the app-side write plane: the proposal executes the authorized round-trip and reconciles from it. The first real end-to-end run against a firm's live (and empty) forum catalog is the system-boundary check that closes the slice.

Verification

pnpm lint · pnpm typecheck · pnpm test (incl. the 11 new tests) · pnpm build. No hardcoded user-facing strings (new copy in en + hi; tool name/description are model-facing, i18n-exempt like the Page Card labels). No color literals.

Files changed

5 file(s), +417 / -0 lines

Path Role + -
messages/en.jsonConfig+2-0
messages/hi.jsonConfig+2-0
src/components/settings/forums/forums-list-page.tsxComponent+7-0
src/components/settings/forums/use-forums-agent-tools.test.tsComponent+167-0
src/components/settings/forums/use-forums-agent-tools.tsComponent+239-0