# Plan — Instagram as a channel: Comments first

**Created:** 2026-07-06 · **Status:** design, awaiting operator go-ahead to build
**Decisions (operator, 2026-07-06):** Comments before DMs · ManyChat stays live
in parallel (platform runs shadow/dry-run until cutover) · live domain is
`channels.silveroakglobal.com`.

## Why comments first, and why it's low risk

Repurpose the platform from WhatsApp-only to omni-channel (WhatsApp + Instagram
now, Facebook later). The agent core is **already channel-agnostic** — this is
the plan's own direction (`plans/kickoff.md` §Channels: "New channels = new
renderer + new inbound normalizer in the gateway; the agent, slots, KB, and
evals are untouched. Do NOT fork the agent per channel."). Today the agent
already accepts `channel: whatsapp|instagram|web`
(`agent-preview.controller.ts`), but ingestion, identity (`waId` everywhere),
and send are WhatsApp-only.

Comments are the cheap entry because they are a **separate domain** from 1:1
messaging — a public moderation stream, not a lead thread. So they get their own
additive table and barely touch the live WhatsApp model. The cross-cutting
`channel` identity migration on `Contact`/`Conversation`/`Message` is **only
needed for DMs** and is deferred to that phase.

This replaces ManyChat's IG comment automation. The behaviour + KBs already
exist in `../manychat-ai/` (behavior.md two-tier logic, CTA-by-post-type,
do-not-reply guardrails incl. appearance/body, no @-tagging; verified KB packs)
— they move in here as the Instagram-comments behaviour + KB.

## Shadow-first (forced by "keep ManyChat live in parallel")

Both bots must not reply to the same comment. So the platform's comment path
ships **dormant/dry-run**: receive → decide → **log + persist decision**, do NOT
post. ManyChat stays the live replier. Prove platform decisions against
ManyChat's live replies from the log, then the operator flips one flag
(`INSTAGRAM_COMMENTS_ENABLED`) to cut over and disables ManyChat. Same
ship-dormant pattern as the whole agent (dry-run form) and the K1 vector leg
(dormant until enabled). Two Meta apps can each subscribe to the same IG
account's webhooks — **verify at wiring time**.

## Base branch — BLOCKER for the operator (do not build until resolved)

Working tree currently holds **uncommitted K1 work** on top of the unpushed
`elevate/ui-consistency-backend-hardening` branch; `main` is behind (no K1).
HANDOFF's "K1 committed to main" is inaccurate vs `git status`. Before Phase 1:
operator decides how to land the elevate branch + the uncommitted K1 so there's
a clean base, then this work branches from it (`feat/instagram-comments`).

## Phases

Each phase: test-driven, per-package vitest (`--pool=forks maxForks=2`, PID cap),
`pnpm typecheck`/`build`, deploy loop per CLAUDE.md, then update HANDOFF.

### Phase 1 — Inbound: route + parse IG comment webhooks (additive)
- `webhooks.controller` POST: branch on envelope `object` — `whatsapp` (existing
  `processEnvelope`) vs `instagram`.
- New IG comment parser in `packages/shared/src/meta` (Zod at boundary): parse
  `entry[].changes` where `field === "comments"` → normalized
  `{ channel:'instagram', kind:'comment', commentId, mediaId, parentId,
  commenterId, commenterUsername?, text, verb }`.
- Loop guard: drop comments authored by our own IG business id.
- Fixtures from a real IG `comments` webhook payload.

### Phase 2 — Persist: new `IgComment` table (additive, no live-model change)
- Migration adds `IgComment` (mediaId, commentId unique, parentId, commenterId,
  username, text, receivedAt, decision enum reply|skip|hide, replyText,
  status enum dry_run|posted|failed, postedAt). Idempotent on commentId (dedup +
  the loop-safe replay guard the standalone scaffold lacked).
- No change to `Contact`/`Conversation`/`Message` — comments are their own
  stream. (DM phase adds the `channel` column to those.)

### Phase 3 — Decide: reuse the agent in "comment mode"
- Invoke the existing agent with `channel:'instagram'` + a **comment behaviour
  module** built from `../manychat-ai/behavior.md`; import the verified KB packs.
- Output contract: `reply` (text) | `skip` | (later) `hide`. Fail-safe to `skip`.
- Extend `tools/agent-eval` with comment scenarios (emoji-only → emoji; intent →
  CTA; "nice figure" → skip; question → brief answer + DM CTA). Operator-gated
  runs only (no unprompted LLM spend — standing rule).

### Phase 4 — Outbound: comment reply + dry-run gate
- New IG send path in `meta-client`: `POST /{commentId}/replies` (no
  `messaging_product`; own IG token from the encrypted `connections` store, not
  env-hardcoded). Comments are plain text — strip `[[buttons]]`/list markers
  (no quick-replies on comments).
- Gate on `INSTAGRAM_COMMENTS_ENABLED` (default off = dry-run: decide + persist,
  don't post). Surface the comment stream + decisions in the inbox/admin.

### Phase 5 — Shadow, prove, cutover
- Run dormant in prod beside ManyChat; compare decisions vs ManyChat replies via
  the `IgComment` log + eval gate. When green: operator sets the IG token +
  subscribes the app to `comments`, flips the flag, disables ManyChat.

## After comments (not now)
- **IG DMs** = the cross-cutting `channel` migration on Contact/Conversation/
  Message + IGSID identity + `/{igid}/messages` send w/ quick-replies; reuses the
  full conversational lead agent. Bound by Meta's 24h reply window.
- **Facebook** = third adapter (Messenger DMs + Page comments), same pattern,
  near-free once IG exists.

## Open / to verify at build time
- Live domain `channels.silveroakglobal.com` for the webhook callback URL
  (`.env.example` `whatapp.silveroakglobal.ae` is stale → correct it).
- Two-apps-one-account webhook subscription confirmed on Meta.
- Whether a commenter who later DMs should link comment history to their
  DM Contact (defer; the `IgComment.commenterId` makes it joinable later).
