Activity + Posts lane · 2026-08-05

Four things nobody has planned yet, drawn so you can rule them

Everything in the lane's build queue is now shipped and live. These four are the remainder: known wants with no plan on disk. Each one is drawn here at the size it would actually ship, with the fact that makes it necessary and the fork only you can settle.

Nothing here is built. Reading this is the decision: say yes, no, or pick a fork, and it becomes a work order. A fifth item, the AI never replying to a comment-reply, was on this list and came off it, because it already works (comment-pipeline.ts:604-626: the reply pass yields on a parent id while moderation still runs above that line, replies are ingested with their parent, and Posts already threads them).
A

The screen is the only thing stopping a forbidden act

a real hole

The registry says which acts a row allows. Both screens obey it. The server only obeys one line of it: a deleted comment is refused. Everything else in commonOverrides() is enforced by the buttons we chose to draw, which is not enforcement at all.

Today

A crafted request, or a stale tab, can post this:

POST /api/moderation/actions
{ igCommentId: "…", action: "hide", expectedStatus: "posted" }

The row is a flow-run. The registry declares no hide path for it. The screen never drew the button, so nobody ever tried, and the server never checked.

Proposed

One guard, in the one place every act already passes through:

moderation-action.service.ts · act()

const allowed = overridesFor(kind, outcome)
if (!allowed.some(o => o.state === "available"
    && ACT_FOR_OVERRIDE[o.id] === action))
  return { ok: false, error: … }

Same shape as the deleted-comment guard added on 2026-08-04, same file, and actBulk delegates to act, so one guard still covers both.

Why it is not already done: the server holds a comment's status, not the composed outcome word the registry keys on. It would have to run computeOutcome at act time, which it now can: that composition moved server-side for Posts on 2026-08-05 and is one import away.

The fork

  1. Refuse, and say why. The act returns the registry's own reason, so a stale tab tells the truth instead of failing blankly. Recommended.
  2. Refuse silently. Cheaper, and indistinguishable from a bug when it fires on a real operator.
  3. Log only, for one week. Learn whether it ever fires before making it fatal. Safer, but leaves the hole open while you watch it.
B

An Instagram DM has no link back to Instagram

honest, and useless

Every kind on Activity offers two Sources: our record, and the real thing. Instagram DMs offer one, because the thread id Instagram addresses a conversation by is never stored. The registry says so in as many words.

Today, on an IG DM row
Open the conversation in Inbox

No second button. The panel says the absolute source is unavailable, which is honest and leaves you retyping a handle into Instagram to see the real thread.

WhatsApp  wa.me/<waId>  → derivable today
Facebook  facebook.com/messages/t/<psid> → derivable today
Instagram instagram.com/direct/t/<threadId> → not stored
Proposed
Open the conversation in Inbox Open the conversation on Instagram ↗

One nullable column on the conversation, written from the webhook envelope that already carries it, and the link composes itself. Additive migration; nothing backfills, so old threads keep saying they cannot link, which stays true for them.

The fork

  1. Store it from now on. New conversations link; old ones keep the honest fallback. Recommended: no backfill, no invention.
  2. Store it and backfill. Needs a Graph read per old conversation, and the thread id may no longer resolve. Spend for a tail.
  3. Leave it. The Inbox link is genuinely the useful one; the absolute source matters most for comments, which already have it.
C

Corrections are kept, counted, and never read

a link with nothing behind it

The Activity sheet's head carries a line you approved: "N corrections kept since DATE. Nothing is added to the agent's instructions automatically." It is not built, because there is nothing to count and nowhere for the link to go. Every time you mark a decision wrong, or reject a draft with a reason, that correction is stored on the row and never gathered.

Agent › Activity › Corrections
WhenWhat came inWhat the agent didYour correction
2 Aug "is this actually 7% or are you just saying that" held for review wrong should have answered, the figure is published Open the row
1 Aug "Influencer email if possible?" replied wrong this is someone pitching us, it belongs hidden Open the row
29 Jul "Which location of Hyatt Place?" answered with the venue right Open the row
Copy the wrong ones into Behaviour Export as markdown Nothing here edits the agent by itself.
The rule this screen exists to keep: corrections are collected, never auto-injected, your ruling of 2026-08-01. So the primary button copies them where you can read and edit them, and the screen says so out loud rather than leaving you to wonder whether marking something wrong quietly changed the bot.

The fork

  1. Since a date you pick. The head counts from a stored "reviewed up to" mark you move when you have read them. Recommended: the count means "owed", not "ever".
  2. Since forever. One number that only grows, and stops meaning anything by October.
  3. Rolling 30 days. No state to keep, but a correction can age out unread.
D

Half the agent's instructions are invisible in Behaviour

your order, 2026-08-05

You said the prompt should all be organised and shown in Behaviour, not hidden. Today Behaviour shows your five headings per situation, and the comment agent is also given a seeded policy that lives in code, which you cannot see or edit. It is not a small part: it carries the two-tier reply logic, the moderation bar and the reply-style examples.

What the comment agent is actually sent
Role
visible in Behaviour
Goal
visible in Behaviour
Tone
visible in Behaviour
Guardrails
visible in Behaviour
Knowledge
visible in Behaviour
Seed policy
in code, invisible, ~2.4k chars
Output contract
in code, invisible
Safety rail
in code, invisible

The seed is what flipped its own CTA when you took comment-to-DM live today. You could not have read that, let alone changed it.

Proposed, Behaviour shows everything, and says what is yours
Goalyours · edited 4 Aug
Reply to one public comment. We have already opened the DM…
Comment policy (seed)from us · editable
Two-tier reply logic, the moderation bar, the reply-style examples. Editing it makes it yours; a Reset returns this text.
Safety railread only
Skip anything about a person's appearance. Never output an @handle. Shown so you know it is there; not editable, on purpose.
The one thing that must not become editable: the output contract (the reply-or-skip JSON the engine parses) and the hard safety rail. Everything else is yours. Showing them read-only is the difference between "hidden" and "not yours to change".

The fork

  1. Show all three, edit two. Seed becomes an ordinary editable field with a Reset; the rail and the contract render read-only. Recommended.
  2. Show all three, edit none of the new ones. You can read the seed but not change it. Solves "hidden", not "operator-extensible".
  3. Fold the seed into your five headings. One surface, no extra card, but it rewrites text you have already recategorised by hand, and the merge is not reversible.