Gmail told me 201. The real number was 35.
Gmail told me a client had sent 201 messages in the window I asked about. The real number was 35. I only found out because I counted them myself.
That gap is why this skill exists, so let me start with the problem underneath it.
Absence does not appear in a list
The state of every client relationship I have lives in Gmail, Calendar and Drive, and nowhere else. There is no CRM field for "Henrik has quietly stopped accepting invites." Reconstructing one client week takes me 20 to 40 minutes of scrolling.
And scrolling misses exactly the things that matter most. An unanswered thread. A stakeholder who went quiet. A decision that was postponed rather than made. All three are absences. Absence does not appear in a list. You cannot scroll past a thing that is not there.
So the failure I care about is not a wrong summary. It is a confident one. A readout that says "quiet week, nothing to report" when a meeting ran and nobody wrote it down is worse than no readout at all, because I will act on it.
Three ways the tooling lied to me
I built the harvest layer on gws, Google's own workspace CLI. Before I wrote a single reference file, I ran the whole surface against my real account. That probe found three lies. Each one returns success. None of them raise.
Lie one: the result count. Gmail's resultSizeEstimate reported 201. I re-ran the identical query, varying only maxResults, and watched it move: 3 gave 201, 10 gave 201, 25 gave 201, 50 gave 35, 100 gave 35. Full pagination returned 35 records. The field is an estimate wearing a count's clothing. A readout quoting it claims 5.7 times the activity that actually happened.
Lie two: the output flag. gws --output ./harvest.json exits 0 and writes nothing. The body goes to stdout instead. What makes this cruel is that the failure mode is inverted from the one you learn first: pointing --output outside the working directory fails loudly with exit 3. So you learn the flag is strict, you trust it, and then a perfectly valid in-directory path fails silently. An agent harvesting this way gets a clean exit and an empty manifest. The harvest uses shell redirection now.
Lie three: unsent drafts. One message in 35 carried labelIds: ["DRAFT"]. That is something I wrote and never sent, sitting in a plain client query looking exactly like correspondence. Citing it produces a fabricated claim with a genuine id, which means the attribution checker cannot catch it. The id resolves. The claim is still false. Two defences ship: -in:drafts in the query, and dropping DRAFT at harvest.

The common thread is the point. All three succeed. The only way any of them surfaced was checking the result against something I knew independently.
Two phases, with a file between them
That discipline is the whole architecture, moved down one layer.
Phase one reads Gmail, Calendar and Drive and writes harvest.json. It does no reasoning. Phase two reasons over that file and reads nothing.
The file uses Google's own field names on purpose: internalDate, threadId, rfc822MessageId, responseStatus, lastModifyingUser. Renaming them to house conventions would feel tidier and would turn any future backend swap into a permanent translation layer.
Because the manifest exists before any thinking happens, every claim in the output resolves mechanically to a record that provably exists. An id that does not resolve is treated as fabricated, not as a lookup miss. Three scripts print PASS or FAIL with numbers rather than asking me to trust the output: verify-time-window, verify-attribution, lint-pii-exposure.
Collapse the seam and there is nothing to check against. Every claim becomes model-attested, which is another way of saying unverifiable.

Google already did the hard part
Building against a real account changed the design twice.
Meet's note-taker already writes structured notes into Drive: a summary, labelled decisions, and action items with named owners in brackets. I parsed five real documents to build the schema from ground truth rather than from a description. 44 action items. 44 owner-tagged.
So the skill parses those notes instead of re-extracting from a transcript, which skips speaker attribution failure entirely. That is a whole class of error I no longer have to defend against.
But only 1 of those 44 carried a due date. That is why "unassigned" ships documented as the correct output rather than a defect to backfill. Inferring a deadline from meeting cadence would be fabrication with a confident face.
The second change matters more for how I work. Gemini quietly gives up on non-English calls. One document came back empty with "not enough conversation in a supported language." My practice runs in seven. So the fallback rungs are the normal path for exactly the meetings I most need read, not an edge case.
The demo is the product
I asked it "what happened with VayaPin last week?" and let it run live.
Zero emails. One meeting, all five invitees accepted. No notes document.
The naive answer is "quiet week, nothing to report." The answer it gave was that a meeting ran and what was decided is not visible from here. It climbed down to calendar metadata, made no decision claims, and said so.
That is the product. It declines to turn silence into an all-clear.

Two limits I am keeping in
Multilingual is design intent, not a demonstration. My account's traffic is 99% English, 148 of 150 messages sampled. Six of the seven declared languages could not be exercised end to end. What I did prove is narrower and more useful: the structural drift signals read responseStatus, timestamps and string length, never body text. I re-ran identical records with Russian, Hebrew and German bodies substituted and the signals came back byte-identical. The floor is language-independent by construction. The full claim is not yet demonstrated.
Read-only is enforced by instruction, not by the credential. The token carries gmail.modify. Three files say "never sends, never files." The credential says nothing. Re-authorising at gmail.readonly would make that structural, and until I do, you should read it as discipline rather than a wall.
One more thing on privacy, because it is the part people get wrong. GDPR Article 9 data is dropped at harvest and blocked from every destination, including private memory. The realistic leak is not a quoted medical record. It is a helpful scheduling note that encodes someone's religion.
Where it lives
@di-atomic/customer-comms-reader is live on the OPVS marketplace. Guidance-only, so there is no backend and no API key to hand over. It reads one authenticated account for a named client over a bounded window, and it never replies, sends, files or archives.
Action-item recall for this class of task measures around 77%. Roughly one item in four is missed, which is why every extract ends with counts per surface, which sources it used, and what it could not reach. Designed to cut a 20 to 40 minute reconstruction down to something I can read in one screen, with each line traceable to a record I can open.