B1
Durable email metadata column (migration)
DONE
Add client_feedback_items.source_metadata jsonb default '{}' so inbound email threading details have a home. Additive + reversible; no behavior change yet.
bwm-ops-events/migrations/NNN_bob_email_channel_metadata.sql
DONE WHENColumn exists in Supabase; migration file on disk + committed + pushed (SHA); other tables/clients untouched.
Codex · buildApplied Supabase migration 127_bob_email_channel_metadata and added migrations/127_bob_email_channel_metadata.sql. Opus can verify the nullable jsonb default on public.client_feedback_items.source_metadata plus branch feat/d2s-email-channel-B1 commit 2d1fb5d54001d13700f31f87ef3143bbc6cccf8b.
Opus · QAVerified via Management API: source_metadata jsonb, nullable, default '{}'. Migration 127 file committed+pushed (feat/d2s-email-channel-B1 @2d1fb5d), RLS validator clean, additive-only.
B2
Capture threading + loop-prevention at ingest
DONE
bwm-gmail-ingest parses Message-ID / In-Reply-To / References + Gmail threadId, and Auto-Submitted / Precedence (auto-mail markers). Persists source_metadata.email.*. Keeps dedup by RFC Message-ID.
bwm-gmail-ingest/src/gmail.tsbwm-gmail-ingest/src/substrate.tsbwm-gmail-ingest/src/index.ts
DONE WHENtsc clean; a mocked insert carries source_metadata.email.rfc_message_id + references + gmail_thread_id; automated-mail fixture is skipped; duplicate inbox copies still make one row.
Codex · buildCaptured Gmail RFC threading and loop-prevention headers, persisted source_metadata.email on feedback inserts, and added focused mocked tests for metadata, automated-mail skip, and RFC Message-ID dedupe. Opus can verify on branch feat/d2s-email-channel-B2 commit a5892c47ae0c82cf77d441801cd72d2ebf314f33 with npx tsc --noEmit plus the B2 node test command.
Opus · QAtsc clean; diff verified (References/Auto-Submitted/Precedence parsed, automated mail skipped before ingest, source_metadata.email.* persisted, RFC Message-ID dedup intact); 3/3 mocked tests pass. Branch feat/d2s-email-channel @a5892c47.
B3
Hand new client emails to the responder
DONE
After a NEW email feedback row is created for an email-preferred client, ingest POSTs an internal, authenticated payload to bwm-slack-listener /bob/inbound-email (threading + body + ids). POST failure leaves the row pending for the existing sweep; cursor still advances.
bwm-gmail-ingest/src/index.tsbwm-gmail-ingest/wrangler.toml
DONE WHENMocked: new row → exactly one POST; duplicate row → no POST; POST failure logged, row pending, cursor advances.
Codex · buildImplemented new-row email handoff from bwm-gmail-ingest to bwm-slack-listener /bob/inbound-email with internal auth, D2S allowlisting, threading/body/id payload, duplicate skip, and non-blocking failure logging. Opus can verify branch feat/d2s-email-channel commit f463b94a10b516b8919d5bfa003cac8c5a94baac with npx tsc --noEmit plus the temp-compiled node test command.
Opus · QAtsc clean; notifyBobForFeedbackInsert posts only on inserted.created && allowlisted slug; POST failure non-blocking (cursor advances); 6/6 tests incl new-row=1 POST, duplicate=0 POST, failure-no-throw. Branch feat/d2s-email-channel @f463b94a.
B4
Make Bob channel-preference aware
DONE
Read clients.config.comms in bobGateway. Client-facing Slack sends are refused when slack_client_facing_enabled=false — so an email-first client never gets Bob chatter on Slack. Slack-first clients are untouched. (D2S's actual config flip is deferred to B10.)
bwm-slack-listener/src/bobGateway.jsbwm-slack-listener/src/index.jsbwm-slack-listener/tests/bobGateway.test.js
DONE WHENUnit: a fixture email-first client's /bob/intents Slack send returns 403 with no Slack call; Townsend/RM/ASAP still resolve to Slack.
Codex · buildFixed the B4 QA failure by reverting the bwm_internal direct-channel resolver expansion and restoring leadership-team bob_enabled=false, while keeping the comms.bob.slack_client_facing_enabled gate and tests. Opus can verify branch feat/d2s-email-channel commit b42a148d60bab2905bebbe6f28047653e2e97189 with cd ~/bwm-slack-listener && node --test.
Opus · QARefix verified: out-of-scope hunks reverted (leadership-team bob_enabled=false, bwm_internal clause removed); diff scoped to bobGateway.js comms gate + 2 tests; 146/146 pass. Email-first client's client-facing Slack send -> 403; Slack-first clients still resolve.
B5
Share the safety gates across channels
DONE
Extract the autonomy gates (pre-gate, pricing/cost hard-stop, context-gate, reply re-gate, decision normalization) into bobAutonomy.js and make the AI prompt channel-neutral, not Slack-specific. Slack transport unchanged.
bwm-slack-listener/src/bobAutonomy.jsbwm-slack-listener/src/inboundResponder.jsbwm-slack-listener/src/aiResponder.js
DONE WHENExisting Slack responder tests pass unchanged; new tests prove billing/pricing/scope/legal text blocks before the model call for BOTH channels; drafted reply is re-gated before send.
Codex · buildExtracted Bob autonomy pre/context/reply gates and model decision normalization into src/bobAutonomy.js, then made the AI prompt channel-neutral while leaving Slack transport behavior unchanged. Opus can verify branch feat/d2s-email-channel commit 82a4c837ac64b51e1d6f95bff2cf1cbbaaff65c3 with cd ~/bwm-slack-listener && node --test.
Opus · QAPrompt now channel-neutral (no Slack assumption); gates extracted to bobAutonomy.js + imported by inboundResponder; existing Slack tests 16/16 unchanged; full suite 159/159; new tests prove pre/context/reply gates fire for BOTH slack+email and re-gate catches sensitive drafts. @82a4c837.
B6
Autonomous email responder /bob/inbound-email
DONE
New authenticated endpoint: validate + active client + email-autonomy flag → run shared gates → on safe/high-confidence, send a threaded Bob reply (reuse a lower-level bobEmail send so /bob/email's HITL validation stays intact) → emit client_comms.sent(channel=email, autonomous=true) → close the feedback item. Shadow mode (autonomy flag off) drafts + escalates instead of sending.
bwm-slack-listener/src/emailInboundResponder.jsbwm-slack-listener/src/index.jsbwm-slack-listener/src/bobEmail.jsbwm-slack-listener/tests/emailInboundResponder.test.js
DONE WHENSafe payload → one Gmail reply to a CONTROLLED test inbox + one client_comms.sent + item addressed; sensitive → no send, item pending, escalation; low-confidence → no send; shadow mode → draft only; /bob/email still requires approved_by.
Codex · buildAdded the authenticated /bob/inbound-email responder with active-client/email-autonomy gating, shared safety gates, threaded Gmail send, client_comms.sent audit, feedback closeout, shadow-mode escalation, and email-inbound idempotency. Opus can verify on branch feat/d2s-email-channel commit 7a66d9bacca73cb69ec54f0d1a9f3b66c5b67dbf with cd ~/bwm-slack-listener && node --test.
Opus · QACore responder verified: auth(x-bwm-internal-key)+header-injection guard+client-id anti-spoof; full double-gate mirror; shadow mode drafts-not-sends; threads via in_reply_to/references, thread_id only when team inbox==send mailbox; reuses exported sendGmailReply so /bob/email keeps approved_by; closeout_proof_kind=event; tests mock fetch (no real email); 7/7 B6 + 166/166 suite.
B7
Loop guards + zero Slack leakage
DONE
Never auto-respond to automated mail; stamp Bob's outbound with Auto-Submitted: auto-generated + X-BWM-Bob-Auto: true; ignore BWM-origin mail; assert the email responder never calls a Slack post API.
bwm-slack-listener/src/emailInboundResponder.jsbwm-gmail-ingest/src/index.ts
DONE WHENAuto-reply fixture → feedback row but no responder POST; test fails if chat.postMessage is called from the email path; outbound MIME includes the loop-prevention headers.
Codex · buildFixed the B7 QA failure by deleting the duplicated autonomous Gmail send/auth/MIME stack from emailInboundResponder.js and routing autonomous replies through shared sendGmailReply with opt-in loop-prevention headers. Opus can verify on branch feat/d2s-email-channel commit 94e2773 with cd ~/bwm-slack-listener && node --test, cd ~/bwm-gmail-ingest && npx tsc --noEmit, and the bundled gmail-ingest B2 node fixture.
Opus · QARefix verified: duplicated Gmail/JWT/MIME stack removed, responder uses shared sendGmailReply(...,{autoGenerated:true}); buildRfc822 extended with extraHeaders -> Auto-Submitted + X-BWM-Bob-Auto on outbound; /bob/email approved_by intact. Ingest: auto-mail + bwm-origin create row+task but suppress responder POST (bobResponderSuppressionReason); email path asserts 0 Slack posts. 166/166 + 8/8.
B8
Human escalation + fast auto-ack
DONE
When Bob can't safely answer: route a human escalation through the existing task/Telegram surfaces. For NON-sensitive uncertain emails, send a short 'we've got it, the team is on it' ack (fast UX). Hard-stop sensitive → escalation only, NO ack, no substantive reply.
bwm-slack-listener/src/emailInboundResponder.js
DONE WHENNon-sensitive uncertain → human escalation surfaced + one conservative ack email (controlled recipient); hard-stop sensitive → escalation only, no email send at all.
Codex · buildAdded autonomous email auto-ack for non-sensitive uncertain decisions after human escalation, while sensitive/model-sensitive hard-stops still send no email and leave the feedback item pending. Opus can verify branch feat/d2s-email-channel commit c9f35bf2b3e9206adf7d240088bebad60547fc04 with cd ~/bwm-slack-listener && node --test.
Opus · QAAuto-ack copy voice-clean (we/the team, no vendor, no timing promise); ack ONLY on non-sensitive uncertain (isNonSensitiveUncertainDecision excludes sensitive category + pre/context/re-gate reasons + shadow); separate :ack idempotency key; threaded via shared sendGmailReply autoGenerated; sensitive/hard-stop send nothing. 167/167.
B9
Full tests, safe live end-to-end, deploy, docs
DONE
Codex: final unit suites green BOTH repos + `wrangler deploy --dry-run` both + write scripts/e2e-inbound-email.mjs (CONTROLLED-recipient live test, never the real client) + update Brain C4 doc + wrangler comments. Opus then performs the privileged steps: merge integration branches to main, deploy both Workers, set gmail-ingest env (BOB_INBOUND_EMAIL_URL; EMAIL_RESPONDER_CLIENT_SLUGS stays empty until B10), and run the controlled live E2E.
bwm-slack-listener/*bwm-gmail-ingest/*buildwise-brain/reference/Client-Feedback-C4-Inbound-Email-Ingest.md
DONE WHENnode --test green (listener); tsc green (ingest); /health green both; live E2E proof captured (threaded, voice-clean, no vendor names, sensitive escalates); Brain SHA + memory updated.
Codex · buildAdded the controlled /bob/inbound-email E2E harness, kept ingest auto-handoff gated until B10, and documented B9 readiness in Brain. Opus can verify branch feat/d2s-email-channel with listener f9d7143755a6b30edec281f7b6c7f89eb7b00b69, ingest 6ce66551df4ea4acda14ce05b5011905703cd28c, and Brain 00cf8f0d4d109b9c8cc2518e327f974d2a23fd4b; run node --test, npx tsc --noEmit, the compiled B2 node fixture, both wrangler dry-runs, and the controlled live E2E script.
Opus · QAMerged all 3 repos to main; deployed bwm-slack-listener (d1bb074c) + bwm-gmail-ingest; 167/167 + tsc green + dry-runs clean. LIVE E2E PASS: real Gmail send 19ee0270e7505f14 to controlled robert@ (threaded, client_comms.sent logged, feedback addressed); sensitive case escalated with NO send. Test rows fully cleaned (0 residue). Bob's real autonomous reply was voice-clean. Brain C4 updated.
B10
Go live for D2S (autonomous email)
DONE
Robert pre-approved all gates (2026-06-19) — no signoff park. After Opus QA is green on B1–B9, flip D2S config ONLY: preferred_client_channel=email, bob.email_enabled+autonomous=true, slack_client_facing_enabled=false, slack.bob_enabled=false (+ MONITORED_CHANNELS). Verify the live pipeline. Rollback = single config revert.
clients.config (Supabase, D2S row)
DONE WHENConfig flipped; live pipeline verified end-to-end (autonomy ON) with a controlled proof; next real D2S email will get a correct autonomous Bob reply; rollback verified as a one-line config change.
Codex · buildRobert pre-approved all gates 2026-06-19 — go-live proceeds automatically once B9 is QA-green.
Opus · QAD2S config LIVE: preferred_channel=email, email_enabled+autonomous=true, slack_client_facing_enabled=false, slack.bob_enabled=false. gmail-ingest allowlist=design2sell + listener-matched BWM_INTERNAL_KEY, redeployed (c939b0c9). Final controlled E2E PASS against PRODUCTION config (real send 19ee02bc1b3515e6 + sensitive escalated). All test rows cleaned (0 residue). Rollback = one config flip.
2026-06-19sysGO-LIVE COMPLETE 2026-06-19: D2S now answered by Bob via autonomous email. Next real D2S email gets a fast threaded reply from bob@buildwisemedia.com; money/legal/scope auto-escalate to a human.
2026-06-19OpusB10 QA PASS — D2S config LIVE: preferred_channel=email, email_enabled+autonomous=true, slack_client_facing_enabled=false, slack.bob_enabled=false. gmail-i
2026-06-19CodexStarting B10 — Go live for D2S (autonomous email).
2026-06-19OpusB9 QA PASS — Merged all 3 repos to main; deployed bwm-slack-listener (d1bb074c) + bwm-gmail-ingest; 167/167 + tsc green + dry-runs clean. LIVE E2E PASS:
2026-06-19CodexB9 built — controlled inbound-email E2E harness, gated ingest config, Brain doc, memory note, and local dry-run/test proof landed on feat/d2s-email-channel.
2026-06-19CodexStarting B9 — Full tests, safe live end-to-end, deploy, docs.
2026-06-19OpusB8 QA PASS — Auto-ack copy voice-clean (we/the team, no vendor, no timing promise); ack ONLY on non-sensitive uncertain (isNonSensitiveUncertainDecision
2026-06-19CodexB8 built — non-sensitive uncertain email escalations now send one conservative threaded ack; sensitive escalations send no email.
2026-06-19CodexStarting B8 — Human escalation + fast auto-ack.
2026-06-19OpusB7 QA PASS — Refix verified: duplicated Gmail/JWT/MIME stack removed, responder uses shared sendGmailReply(...,{autoGenerated:true}); buildRfc822 extende
2026-06-19CodexB7 built — reused shared Gmail sender for autonomous headers and removed duplicated send/auth/MIME code.
2026-06-19OpusB7 QA FAIL — CODE-QUALITY FAIL (tests pass but unacceptable): B7 DUPLICATED the whole Gmail send stack (sendAutonomousGmailReply, getGmailSendToken, buil
2026-06-19CodexB7 built — loop guards suppress automated/BWM-origin responder handoff and stamp autonomous outbound MIME headers.
2026-06-19CodexStarting B7 — Loop guards + zero Slack leakage.
2026-06-19OpusB6 QA PASS — Core responder verified: auth(x-bwm-internal-key)+header-injection guard+client-id anti-spoof; full double-gate mirror; shadow mode drafts-n
2026-06-19CodexB6 built — /bob/inbound-email now gates, sends, audits, and closes safe autonomous email replies on feat/d2s-email-channel.
2026-06-19CodexStarting B6 — Autonomous email responder /bob/inbound-email.
2026-06-19OpusB5 QA PASS — Prompt now channel-neutral (no Slack assumption); gates extracted to bobAutonomy.js + imported by inboundResponder; existing Slack tests 16/
2026-06-19CodexB5 built — shared Bob autonomy gates now cover Slack + email pre/context/reply checks on feat/d2s-email-channel.
2026-06-19CodexStarting B5 — Share the safety gates across channels.
2026-06-19OpusB4 QA PASS — Refix verified: out-of-scope hunks reverted (leadership-team bob_enabled=false, bwm_internal clause removed); diff scoped to bobGateway.js c
2026-06-19CodexB4 built — QA-requested internal Slack defaults restored while preserving the email-first Slack gate.
2026-06-19OpusB4 QA FAIL — Comms gate (comms.bob.slack_client_facing_enabled in resolveClientSlackConfig) is CORRECT — keep it + its 2 tests. REVERT two out-of-scope c
2026-06-19CodexB4 built — /bob/intents now blocks email-first client Slack sends while Slack-first clients still resolve.
2026-06-19CodexStarting B4 — Make Bob channel-preference aware.
2026-06-19OpusB3 QA PASS — tsc clean; notifyBobForFeedbackInsert posts only on inserted.created && allowlisted slug; POST failure non-blocking (cursor advances); 6/6 t
2026-06-19CodexB3 built — new D2S email feedback rows now POST one authenticated /bob/inbound-email handoff on feat/d2s-email-channel.
2026-06-19CodexStarting B3 — Hand new client emails to the responder.
2026-06-19OpusB2 QA PASS — tsc clean; diff verified (References/Auto-Submitted/Precedence parsed, automated mail skipped before ingest, source_metadata.email.* persist
2026-06-19CodexB2 built — Gmail ingest now captures threading metadata, skips automated mail, and preserves RFC Message-ID dedupe on feat/d2s-email-channel-B2.
2026-06-19CodexStarting B2 — Capture threading + loop-prevention at ingest.
2026-06-19OpusB1 QA PASS — Verified via Management API: source_metadata jsonb, nullable, default '{}'. Migration 127 file committed+pushed (feat/d2s-email-channel-B1 @
2026-06-19CodexB1 built — source_metadata jsonb default migration applied and file-bound on feat/d2s-email-channel-B1.
2026-06-19CodexStarting B1 — durable email metadata column (migration).
2026-06-19RobertPre-approved ALL gates — run end-to-end to working + QA'd + D2S live on autonomous email, no stops.
2026-06-19sysTracker live. Codex builds B1→B9, Opus QAs each.
2026-06-19OpusTwo independent plans written (Opus + Codex GPT-5.5 x-high), synthesized into this 10-unit plan. Both AIs treat plan-state.json as the single source of truth.
2026-06-19OpusRecon complete: email send + inbound-ingest pieces already exist but are unwired; responder is Slack-only; D2S is bob_enabled on Slack (the channel they never read).