Repository navigation
Description
Problem
The charge-candidates producer's new staged mode (#4641, commit 7757c2e) is supposed to run on the laptop, where the Plaid/Gmail connector CLIs do not live, reading only staged inputs. But the Gmail triage loop falls back to the live CLI for any query whose staged response is missing:
pm-stack/aggregator/scripts/produce-charge-candidates.ts:286-298—stagedResponses[i] ?? (await run(GMAIL_CLI, ["gmail", "+triage", ...])).gmail-triage.jsonis documented as "the two +triage responses, in query order", but nothing checks that both are present.- On the laptop the default
GMAIL_CLI(/opt/hatch/bin/hatch_gws_cli) does not exist, so the fallback throws ENOENT,gmailCandidatesrejects as a whole, andmain's catch (:402, "gmail read failed …; plaid candidates only") swallows it: the run exits 0 and writes a candidates file with the entire Gmail source missing — including messages the staging did deliver. - This contradicts the staged contract the same change documents at
:66-69(stagedText): "a stale or absent staging must fail visibly, never silently produce an empty batch." A short staging is exactly stale/absent staging, and the batch it produces is silently partial instead: the daily collector consumes it as the period's complete candidate set, so that period's Gmail charges simply never match.
The Plaid path does not have this flaw: a missing plaid-transactions.json throws out of plaidCandidates, which main does not catch, so the run fails visibly. The Gmail triage path is the inconsistent one. (Missing gmail-bodies.json is different and correct by design: bodies are requested via gmail-body-requests.json for a later run.)
Evidence (verified this run, laptop probe at origin/main 17610f5)
Probe: ~/git/fleet/probe-2026-11-02-laneB-chargestaged/ on the laptop — the producer extracted verbatim from origin/main (script md5 70bff805), staged inputs for period 2026-11-01..2026-11-02 with one Plaid transaction (Coffee Shop $12.34) and one Gmail receipt ("Your Bakery order receipt $9.99").
- CONTROL —
gmail-triage.json= both responses staged: exit 0, 2 candidates (gmail Bakery $9.99 + plaid Coffee Shop $12.34),gmail skipped 0. - PROBE —
gmail-triage.json= only the first of the two responses staged: stderrproducer: gmail read failed (ENOENT: no such file or directory, posix_spawn '/opt/hatch/bin/hatch_gws_cli'); plaid candidates only, exit 0, output file contains only the Plaid candidate. The staged Gmail message was discarded along with the unstaged query.
Related: #4641 (the staged-mode split this defect ships in), #2448 (a different whole-batch Gmail discard, fixed — same failure shape, different cause).
Acceptance criteria
- In staged mode (
CHARGE_INPUTS_DIRset), the producer never invokes the Plaid/Gmail CLIs, for any input. - A
gmail-triage.jsonthat is missing, unparseable, or holds fewer responses than the producer issues queries fails the run visibly (non-zero exit, no candidates file written or the file clearly marked failed) instead of producing a plaid-only batch with exit 0. - With complete staging, output is unchanged from today (control case above: both sources' candidates present).
- Live mode (env unset) is unchanged.
- A regression test or scripted fixture covers the short-staging case.
Activity
dezren39 commentedon Nov 2, 2026
🤖 Agent note · filed
probe-2026-11-02-laneB-chargestaged@ origin/main 17610f5): with only 1 of the 2 staged Gmail triage responses present, staged mode shells out to the absent live Gmail CLI, the whole Gmail source is dropped, and the producer still exits 0 writing a plaid-only candidates file.creator-b-2026-11-02-1720/packets/packet-4660-triage.json(enqueued to repo-health-code-pilot)10 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up (resume)
Merge pull request #4671 from developing-today/queue/6625-4660-staged…
dezren39 commentedon Nov 3, 2026
Agent note — done (queue worker, lane code-e)
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
17610f5553de6bbb4d3204a29f9e588bd59c5af6(unchanged from the creator probe):pm-stack/aggregator/scripts/produce-charge-candidates.ts:286-298parsesgmail-triage.jsonintostagedResponsesand usesstagedResponses[i] ?? live run(GMAIL_CLI, +triage)with no length check (the queries array has exactly 2 entries);mainat:388-404catches the resulting gmail failure and writes a plaid-only candidates file with exit 0. One sharpening for the fix: in staged mode any gmail error is swallowed by that catch today — missing and unparseable staging included, not just short staging — so the fix must make staged-mode gmail failures fail the run, not only add the length check. The Plaid path is safe only becausestagedTextthrows on a missing file in staged mode, and the gmail body-read path (:334-341) is alreadyINPUTS_DIR-guarded; both are named out of scope in the fix packet.