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.
| Step | What happens | Owner |
|---|---|---|
| 1. Intent | Brain emits a client-plane intent forums.create / forums.archive | Atlas brain (separate lane) |
| 2. Decide | Dispatcher sees the tool is confirm: true → returns a confirm decision (never auto-runs) | client-tool-dispatcher.ts |
| 3. Propose | A proposal card renders in the ACP thread; counsel taps Confirm or Cancel | agent-chat-panel.tsx |
| 4. Execute | On confirm, the tool's run fires the real mutation → backend authz + RLS | this Story |
| 5. Reconcile + undo | Invalidate 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
| File | Change |
|---|---|
src/components/settings/forums/use-forums-agent-tools.ts | new pure buildForumsAgentTools + resolveArchivableForumId + the useForumsAgentTools hook (publishes the tools, wires real mutations + localized undo toasts) |
src/components/settings/forums/forums-list-page.tsx | calls useForumsAgentTools({ canWrite, forums }) |
src/components/settings/forums/use-forums-agent-tools.test.ts | new 11 unit tests (shape, create+undo, archive+undo, refusals, no-auto-pick) |
messages/en.json · messages/hi.json | Forums.undo + Forums.agentForumNotFound in both locales |
Acceptance criteria
- met Atlas creates + archives a forum via a confirm-first proposal with undo. Both tools are
confirm: true→ the dispatcher renders the proposal card; on confirm the real mutation runs; a toast offers Undo (restore / archive-back). - met On staging the write round-trips the REAL mutation (no faked rows, no setQueryData). The tools call
useAppCreateForum/useAppUpdateForumwith invalidate-only mutations; nosetQueryDataon the agent path. - met Story walkthrough HTML attached. This document.
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.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| src/components/settings/forums/forums-list-page.tsx | Component | +7 | -0 |
| src/components/settings/forums/use-forums-agent-tools.test.ts | Component | +167 | -0 |
| src/components/settings/forums/use-forums-agent-tools.ts | Component | +239 | -0 |