email-communicator
Internal ACE turn step — Send/receive email as ace@ via gog; sends go only through bin/ace- email.
SKILL.mdskills/email-communicator/SKILL.md
GitHubEmail Communicator
Send and receive email through the ACE Gmail account using the GOG CLI. This is a utility skill — other skills delegate to it whenever they need to send or read email rather than reimplementing Gmail access themselves.
The send path is a rail, and approval is procedural. Raw gog gmail send/reply under the ACE
identity is hard-blocked by a deny rail in config/gating.json, enforced by canopy's session gating hook
(agent-core/gating_guard.py --session; canopy#849) — bin/ace-email is the only way email leaves ACE, which also guarantees the
comms-log threadId capture. The rail never prompts; whether a send is appropriate is governed by
the calling context's procedure (a run's pause-point mode, a turn's review posture) — see
docs/superpowers/specs/2026-07-01-agent-operating-model-adoption.md § Gating.
bin/ace-email and bin/ace-mark-read are thin shims over the shared canopy email engine
(canopy email send|mark-read|preflight, canopy ≥ 0.2.255; identity from this repo's
config/agent.json) — fixes propagate fleet-wide, same as hal's shims (ace#826). Requires the
canopy CLI on PATH (/canopy:setup); the shims exit with the remediation when it's missing.
Configuration (preamble)
The identity has ONE source of truth — config/agent.json (email = ace@dimagi-ai.com,
gog_client = canopy); bin/ace-email and the canopy engine read it from there. There are NO
ACE_GMAIL_ACCOUNT / ACE_GMAIL_CLIENT env vars any more: they were RETIRED with ace#1147 /
#1338 (ACE's Canopy vault carries no such keys), and a
$ACE_GMAIL_ACCOUNT in a shell command expands to EMPTY — so the account flag followed by that
var silently ran as -a and failed. Write the literals.
The GOG CLI must be installed (brew install steipete/tap/gogcli) and the configured account must
be authenticated. Preferred: connect via the canopy-web "Connect Google mailbox" button —
self-service, no terminal. Terminal alternative: gog login ace@dimagi-ai.com --client canopy --services gmail,calendar,drive,docs,slides,sheets,forms — pass the FULL scope list every time; a
narrower --services list (e.g. just gmail) replaces the grant and drops the other scopes
(calendar/drive/docs/slides/sheets/forms), it does not add to them.
The gog client is SHARED across the fleet; the mailbox is what's per-agent. Every agent
(ace/eva/hal/ada) authorizes its own address under one of the two interchangeable fleet OAuth
clients (canopy, canopy-web) — identity bleed
means acting as another agent's mailbox, and that is governed by --account, never by the
client. config/agent.json's gog_client is the declared preference the email engine
(canopy email, bin/ace-email) reconciles. The two fleet clients (canopy, canopy-web) are one app — never type a client name; canopy email client picks the one this machine holds a token under. A raw gog call passes
--client "$(canopy email client --repo .)". Do not set it to ace: no credentials-ace.json is
provisioned, so every read/send fails and the printed remedy (gog login --client ace) is an
interactive browser OAuth that a headless turn cannot run (jjackson/ace#1147).
Process
Resolve configuration. The mailbox comes from
config/agent.json(email); the gog client fromcanopy email client --repo .— never from the environment. Abort with a clear error if gog itself is missing.Determine the operation. The calling skill specifies one of: send, reply, search, or read.
For send operations:
- Write the body to a temp file (single-line paragraphs separated by blank lines; bullet lines
one per line —
bin/ace-emailbuilds a reflowing HTML body + plain-text alternative; put the sign-off on its own paragraph, blank-line-separated). - Use:
bin/ace-email --to <recipient(s), comma-separated> [--cc <address>] --subject <subject> --body-file <path> - Approval is procedural, per the calling context: in a turn or review-mode run, present the
composed email (to/cc/subject/body) and get the human's yes before invoking; in auto-mode runs,
the phase's pause-point governance applies. Use
--dry-runto preview the rendered bodies. - Run-page check (ace#2378): a body that links
docs.google.com/drive.google.comand nolabs.connect.dimagi.com/ace/page exits 3 before canopy is called, dry-run included. Lead with the run'sace_web_summary_url; when the recipient is not a reviewer of the run — a thread with no run (sales/BD), or a partner-facing deliverable whose subject IS the Drive pack (Phase 9llo-onboarding, ace#2380) — pass--no-run-page "<why>", which is stripped before canopy and echoed to stderr. - Capture
message_idandthread_idfrom the JSON result.
- Write the body to a temp file (single-line paragraphs separated by blank lines; bullet lines
one per line —
For reply operations:
- Read the original thread first (step 6) to get the thread context and the thread id.
- Prefer
--reply-all:bin/ace-email --reply-all --thread-id <thread_id> --subject "Re: <original>" --body-file <path>— with--thread-idthe engine derives To + Cc from the thread's LATEST non-self message's structured headers, so cc'd people are never silently dropped (§1b rule 3, now tool-backed). Explicit--ccmerges in. Only pass--toyourself when deliberately narrowing the recipient set — and say so in the approval step. - Never
--reply-all --reply-to-message-id <thread id>(ace#2391). A Gmail thread id IS the id of the thread's FIRST message, so that pair takes the engine's message mode and replies to the thread head: on a thread ACE opened it addressesTo: ace@dimagi-ai.comand demotes the counterpart to Cc; on any long thread it targets whoever started it. Enforced:test/skills/reply-all-thread-id.test.ts. - Replies maintain the Gmail thread via the thread id.
For search operations:
- Use:
bin/ace-env gog gmail search "<query>" -a ace@dimagi-ai.com --client "$(canopy email client --repo .)" --json(The two fleet clients (canopy,canopy-web) are one app — never type a client name;canopy email clientpicks the one this machine holds a token under. Thebin/ace-envprefix carries gog's file-keyring password, which a runner-spawned session cannot otherwise see — ace#2844.) - Common queries:
from:<address>,to:<address>,subject:<text>,in:inbox,is:unread,newer_than:1d— combined with spaces. - Returns thread list with IDs, dates, senders, subjects.
- Use:
For read operations (use the shared canopy engine, not raw
gog— canopy ≥ 0.2.346):- Use:
bin/ace-env canopy email read <threadId> --repo . - Returns normalized JSON: per message the headers + decoded
body_text(quoted reply tail trimmed) + each attachment'sattachment_id, PLUS a thread-levelreply_allrecipient set ({to, cc, reply_to_message_id}). Pass a THREAD id — gog reads threads and 404s on a bare message id (the multi-message-thread trap). - Recipient safety is now the engine's job. Take the reply-all set from
reply_all(To/Cc) and pass it tobin/ace-email --to <set>, choosing reply-all vs. direct on purpose. A rawgog gmail readtext dump hidCc:and silently dropped cc'd recipients; the canopy reader is structured, so that can't happen. Download any attachment withcanopy email fetch-attachment <messageId> <attachmentId> --repo . --out <dir>(see inbox-triage's File action). Per canopydocs/agent-operating-model.md § 1breply-quality rule 5 (jjackson/ace#828).
- Use:
Log the operation — the routing contract. For every send/reply, the calling skill MUST record
thread_id+message_id+ recipients + date in the run's comms-log (ACE/<opp>/runs/<run-id>/<N>-<phase>/<skill>_comms-log.md; a send that belongs to no phase logs at the run root instead —release-run_comms-log.md, ace#2780;run-closeout_comms-log.md, ace#2823). Never log a send under the run'scomms-log/subfolder — that holdsobservations.mdanddry-run-*.md, and routing does not search it. This is not optional bookkeeping:inbox-triageroutes inbound replies to their opp/run by matchingthread_idagainst comms-logs. A send without a loggedthread_idproduces an unroutable reply.Mark-read (turn housekeeping, not part of normal skill sends):
bin/ace-mark-read <threadId> …removes the UNREAD label after a turn fully handles a thread.Forward — when asked to forward a message (a receipt, an invoice, someone's mail), forward it:
bin/ace-email forward <thread-id> --to <addr> [--note-file note.txt] [--dry-run]. It sends the original headers, body and attachments (real filenames) underFwd:; the review receipt is keyed to the note file. Do not compose a new message summarising the mail and re-attach with--attach-from-thread— that is not a forward (2026-10-05).
MCP Tools Used
None — this skill uses the GOG CLI and the guarded bin/ace-email wrapper via shell commands, not
MCP tools.
Mode Behavior
- Auto: Execute email operations directly (the phase's pause-point governance decides whether the send-bearing step runs at all), return results to the calling skill.
- Review: For send/reply, present the composed email in-conversation for human approval before
invoking
bin/ace-email. Search/read execute immediately.
Dry-Run Behavior
When --dry-run is active:
- Send/reply:
bin/ace-email … --dry-runprints the rendered plain + HTML bodies without sending (and without the approval gate). Also write the composed email tocomms-log/dry-run-<step>.mdper the skills README contract. Return a synthetic message ID for logging. - Search/read: Execute normally (read-only operations are safe in dry-run).
Terminology
The platform is Connect, never CommCare Connect — in every word this skill
puts in front of a human. The full rule, including the carve-outs for CommCare the
mobile app, CommCare HQ and code-level identifiers, is
skills/_terminology.md. Read it before writing prose; it is
binding on this skill's output.